Skip to content

Commit 10f939e

Browse files
sarg3ntclaude
andauthored
feat(console): cluster 1 polish — search, paste guard, shortcut, tab rename (#143)
* feat(console): cluster 1 polish — search, paste guard, shortcut, tab rename Refs #142 (cluster 1). - xterm-addon-search vendored (@xterm/addon-search 0.15.0). - Search bar markup + CSS: input, prev/next, count, close, Esc to close. Ctrl-F / Cmd-F inside the terminal is intercepted via attachCustomKeyEventHandler so it opens the find bar instead of going to the shell. Each session lazy-loads its own search addon on first use. Result count is rendered live via the addon's onDidChangeResults callback ("3 / 12"). - Large-paste confirm modal: pastes with >= 20 newlines pop a modal with a preview (capped at first 30 lines / 1200 chars). Confirm pipes the text into the active session's PTY via the new ConsoleSession.sendText path; Cancel discards. Document-level paste listener checks that an xterm host is focused before intercepting. Bracketed paste itself rides on the remote shell turning DECSET 2004 on — xterm wraps with ESC[200~/ESC[201~ automatically when asked, so no client-side toggle needed. - Ctrl-Shift-` global shortcut opens a console for the current box (reads the box_id cookie set by switchBox). No-op when no box is pinned — palette is the fallback. - Double-click on a tab label swaps the label into an inline text input; blur/Enter commits, Escape reverts. The rename is per-session (lives on session.label); session.baseLabel still carries the canonical box name for tooltips and #N disambiguation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * address Copilot review findings on PR #143 - Global Ctrl-Shift-` shortcut now reads the correct cookie name (gearbox_active_box, not box_id) and wires eagerly on DOMContentLoaded so it works before the user has opened the console once. - Add .console-search-bar.hidden and .console-paste-modal.hidden to the .hidden specificity-override block; Tailwind's .hidden was losing the cascade tie to our component display rules. - Paste threshold now counts actual newline characters (>= 20) rather than split('\n').length, removing the off-by-one and the trailing-newline edge case. Visible "N lines" label reads newlines + 1 to match how users count. - Confirmed paste routes through term.paste(text) so xterm applies its normal paste pipeline including bracketed-paste wrapping (ESC[200~ ... ESC[201~) when the remote shell has DECSET 2004 enabled. Direct sendText was bypassing that. - Drop dead CSS rules targeting .xterm-decoration.console-search-*. The xterm-addon-search uses its own class names (xterm-find-result-decoration / xterm-find-active-result-decoration) and we already pass decorations.{activeMatchBackground,matchBackground} options in JS, so the CSS was unreachable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 334c2e9 commit 10f939e

4 files changed

Lines changed: 526 additions & 3 deletions

File tree

gearbox/internal/framework/templates/components/console.templ

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,65 @@ templ ConsoleDrawer() {
116116
</svg>
117117
</button>
118118
</div>
119+
// Search overlay — hidden until Ctrl-F/Cmd-F triggers it.
120+
// Powered by xterm-addon-search; input wired in console.js.
121+
<div id="console-search-bar" class="console-search-bar hidden" role="search">
122+
<input
123+
type="text"
124+
id="console-search-input"
125+
class="console-search-input"
126+
placeholder="Find in terminal…"
127+
aria-label="Find in terminal"
128+
/>
129+
<span id="console-search-count" class="console-search-count" aria-live="polite"></span>
130+
<button type="button"
131+
id="console-search-prev"
132+
class="console-tool-btn"
133+
aria-label="Previous match"
134+
title="Previous match (Shift-Enter)">
135+
<svg class="w-4 h-4" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
136+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>
137+
</svg>
138+
</button>
139+
<button type="button"
140+
id="console-search-next"
141+
class="console-tool-btn"
142+
aria-label="Next match"
143+
title="Next match (Enter)">
144+
<svg class="w-4 h-4" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
145+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
146+
</svg>
147+
</button>
148+
<button type="button"
149+
id="console-search-close"
150+
class="console-tool-btn"
151+
aria-label="Close search"
152+
title="Close (Esc)">
153+
<svg class="w-4 h-4" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
154+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
155+
</svg>
156+
</button>
157+
</div>
119158
// xterm host. Sessions attach their own xterm <div> into this.
120159
// `console-xterm-pad` adds breathing room so cursor/text isn't
121160
// flush against the panel border.
122161
<div class="console-xterm-pad flex-1 overflow-hidden bg-black">
123162
<div id="console-xterm" class="w-full h-full"></div>
124163
</div>
164+
// Large-paste confirmation modal. Triggered by ConsoleManager
165+
// when clipboard content exceeds the paste-confirm threshold
166+
// (default 20 newlines). Buttons wired in console.js.
167+
<div id="console-paste-modal" class="console-paste-modal hidden" role="dialog" aria-modal="true" aria-labelledby="console-paste-modal-title">
168+
<div class="console-paste-modal-panel">
169+
<h3 id="console-paste-modal-title" class="console-paste-modal-title">Paste <span id="console-paste-modal-lines">?</span> lines?</h3>
170+
<p class="console-paste-modal-hint">Large pastes run many commands in sequence. Review the preview before confirming.</p>
171+
<pre id="console-paste-modal-preview" class="console-paste-modal-preview" aria-label="Paste preview"></pre>
172+
<div class="console-paste-modal-actions">
173+
<button type="button" id="console-paste-modal-cancel" class="console-paste-modal-btn console-paste-modal-cancel">Cancel</button>
174+
<button type="button" id="console-paste-modal-confirm" class="console-paste-modal-btn console-paste-modal-confirm">Paste</button>
175+
</div>
176+
</div>
177+
</div>
125178
</div>
126179
}
127180

@@ -133,5 +186,6 @@ templ ConsoleAssets() {
133186
<link rel="stylesheet" href="/static/css/components/console.css"/>
134187
<script src="/static/js/vendor/xterm.min.js" defer></script>
135188
<script src="/static/js/vendor/xterm-addon-fit.min.js" defer></script>
189+
<script src="/static/js/vendor/xterm-addon-search.min.js" defer></script>
136190
<script src="/static/js/console/console.js" defer></script>
137191
}

gearbox/static/css/components/console.css

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ body.console-dock-open {
6565
.console-tool-btn.hidden,
6666
.console-tab-dot.hidden,
6767
.console-tab-new.hidden,
68-
.console-tab-bar.hidden {
68+
.console-tab-bar.hidden,
69+
.console-search-bar.hidden,
70+
.console-paste-modal.hidden {
6971
display: none;
7072
}
7173

@@ -203,6 +205,20 @@ body.console-dock-open {
203205
cursor: pointer;
204206
padding: 0;
205207
}
208+
209+
/* Inline rename input — sized to match the original label so the tab
210+
* doesn't jump width on edit. */
211+
.console-tab-rename {
212+
background: rgb(2, 6, 23);
213+
color: rgb(226, 232, 240);
214+
border: 1px solid rgb(52, 211, 153);
215+
border-radius: 2px;
216+
font: inherit;
217+
font-size: 12px;
218+
padding: 0 4px;
219+
min-width: 80px;
220+
max-width: 200px;
221+
}
206222
.console-tab-close {
207223
background: none;
208224
border: none;
@@ -231,6 +247,116 @@ body.console-dock-open {
231247
display: none;
232248
}
233249

250+
/* ---------- search overlay ---------- */
251+
252+
/* Floats below the header in drawer/dock; absolute so it doesn't push
253+
* the xterm host. Width matches the panel content area. */
254+
.console-search-bar {
255+
display: flex;
256+
align-items: center;
257+
gap: 4px;
258+
padding: 4px 8px;
259+
background: rgb(15, 23, 42);
260+
border-bottom: 1px solid rgb(51, 65, 85);
261+
}
262+
.console-search-input {
263+
flex: 1 1 auto;
264+
min-width: 0;
265+
background: rgb(2, 6, 23);
266+
color: rgb(226, 232, 240);
267+
border: 1px solid rgb(51, 65, 85);
268+
border-radius: 4px;
269+
padding: 4px 8px;
270+
font-size: 12px;
271+
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
272+
}
273+
.console-search-input:focus {
274+
outline: none;
275+
border-color: rgb(52, 211, 153);
276+
}
277+
.console-search-count {
278+
font-size: 11px;
279+
color: rgb(148, 163, 184);
280+
min-width: 50px;
281+
text-align: right;
282+
font-variant-numeric: tabular-nums;
283+
}
284+
285+
/* ---------- paste-confirm modal ---------- */
286+
287+
.console-paste-modal {
288+
position: absolute;
289+
inset: 0;
290+
z-index: 5;
291+
background: rgba(0, 0, 0, 0.7);
292+
display: flex;
293+
align-items: center;
294+
justify-content: center;
295+
padding: 24px;
296+
}
297+
.console-paste-modal-panel {
298+
background: rgb(15, 23, 42);
299+
border: 1px solid rgb(51, 65, 85);
300+
border-radius: 6px;
301+
max-width: 560px;
302+
width: 100%;
303+
padding: 16px;
304+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
305+
}
306+
.console-paste-modal-title {
307+
font-size: 14px;
308+
font-weight: 600;
309+
color: rgb(226, 232, 240);
310+
margin: 0 0 4px 0;
311+
}
312+
.console-paste-modal-hint {
313+
font-size: 12px;
314+
color: rgb(148, 163, 184);
315+
margin: 0 0 8px 0;
316+
}
317+
.console-paste-modal-preview {
318+
background: rgb(2, 6, 23);
319+
color: rgb(203, 213, 225);
320+
border: 1px solid rgb(51, 65, 85);
321+
border-radius: 4px;
322+
padding: 8px;
323+
font-size: 11px;
324+
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
325+
max-height: 200px;
326+
overflow: auto;
327+
white-space: pre-wrap;
328+
word-break: break-all;
329+
margin: 0 0 12px 0;
330+
}
331+
.console-paste-modal-actions {
332+
display: flex;
333+
justify-content: flex-end;
334+
gap: 8px;
335+
}
336+
.console-paste-modal-btn {
337+
padding: 6px 12px;
338+
border-radius: 4px;
339+
font-size: 12px;
340+
font-weight: 500;
341+
border: 1px solid rgb(51, 65, 85);
342+
cursor: pointer;
343+
}
344+
.console-paste-modal-cancel {
345+
background: rgb(30, 41, 59);
346+
color: rgb(226, 232, 240);
347+
}
348+
.console-paste-modal-cancel:hover {
349+
background: rgb(51, 65, 85);
350+
}
351+
.console-paste-modal-confirm {
352+
background: rgb(52, 211, 153);
353+
color: rgb(2, 6, 23);
354+
border-color: rgb(52, 211, 153);
355+
}
356+
.console-paste-modal-confirm:hover {
357+
background: rgb(110, 231, 183);
358+
}
359+
234360
/* ---------- xterm padding ---------- */
235361

236362
/* xterm.js paints right up to its container's edges by default. The

0 commit comments

Comments
 (0)