anvilsign in

collin/browser-terminal-extension

1<!doctype html>
2<html>
3 <head>
4 <meta charset="utf-8" />
5 <title>terminal</title>
6 <link rel="stylesheet" href="vendor/xterm.css" />
7 <link rel="stylesheet" href="sidebar.css" />
8 </head>
9 <body>
10 <div id="reveal-strip"></div>
11
12 <!-- Two rows of tabs, nested the way tmux itself nests: sessions on top,
13 and under them the windows of whichever session is selected. The
14 session row is where you are; the window row is where your keystrokes
15 go. Both are populated from the daemon's status frames, which describe
16 the whole tmux server and not just the session we are attached to. -->
17 <header>
18 <!-- The rows, and beside them a corner column that spans all of them. The
19 rows are the tmux part of the header; the corner is the panel's own
20 hand-sized control, kept out of every row so it can be as big as a
21 thumb needs rather than as tall as a row allows. -->
22 <div id="rows">
23 <div class="strip" id="session-strip">
24 <!-- The panel's own controls, held together at the left edge so the tab
25 strip owns the rest of the row and grows rightwards into it.
26 Reconnect appears only when it is actually actionable; theme and
27 density live in settings. Inline SVG rather than unicode glyphs,
28 which render at wildly different weights depending on which font the
29 platform falls back to. -->
30 <button id="reconnect" class="icon" title="Reconnect" hidden>
31 <svg viewBox="0 0 16 16" aria-hidden="true">
32 <path d="M13.5 8a5.5 5.5 0 1 1-1.7-4" fill="none" stroke="currentColor"
33 stroke-width="1.6" stroke-linecap="round"/>
34 <path d="M13.5 1.5v3.2h-3.2" fill="none" stroke="currentColor"
35 stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
36 </svg>
37 </button>
38
39 <!-- A chevron rather than a menu glyph, in the same corner and with the
40 same shape as the one Chrome hangs its tab-search popup off: what it
41 opens is a card that floats over the terminal, so the icon says which
42 way it goes and flips over while it is down. -->
43 <button id="settings-toggle" class="icon" title="Settings"
44 aria-haspopup="dialog" aria-expanded="false" aria-controls="settings">
45 <svg viewBox="0 0 16 16" aria-hidden="true">
46 <path d="M4 6.5l4 4 4-4" fill="none" stroke="currentColor"
47 stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
48 </svg>
49 </button>
50
51 <!-- The socket's state in words, and only while there is no session tab
52 to stand in for it: tabs that are live *are* the "connected" light,
53 and a disconnect empties them and puts this back. The ↻ button beside
54 it appears on the same condition. -->
55 <span id="status-text">disconnected</span>
56
57 <!-- One tab per tmux session. Clicking one runs switch-client, which
58 moves the client this panel already has: the terminal below stays
59 connected and nothing is re-spawned. The count is its window count,
60 and the glyph is the loudest thing Claude Code is doing anywhere in
61 it — which is the reason the daemon reports every session and not
62 only the attached one. -->
63 <div id="sessions" role="tablist" aria-label="tmux sessions" hidden></div>
64
65 <button id="session-new" class="icon" hidden
66 title="New session&#10;Drop a window here to give it a session of its own">
67 <svg viewBox="0 0 16 16" aria-hidden="true">
68 <path d="M3.5 8h9M8 3.5v9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
69 </svg>
70 </button>
71
72 <!-- Named inline instead of in a dialog: a modal in this panel would
73 block the socket's message handler for as long as it is up. -->
74 <input id="session-name" type="text" placeholder="new session" spellcheck="false"
75 autocomplete="off" hidden />
76
77 </div>
78
79 <div class="strip" id="window-strip">
80 <!-- The one control in the header that acts on the page rather than on
81 anything tmux owns, and it leads the window row, immediately left of
82 jump: the two are the pair you reach for while working in a pane —
83 one brings the page into the pane, the other takes you to the pane
84 that wants you — so they sit together and away from the tabs. -->
85 <button id="pick" class="icon" title="Pick an element on the page (Esc cancels)">
86 <svg viewBox="0 0 16 16" aria-hidden="true">
87 <circle cx="8" cy="8" r="3.1" fill="none" stroke="currentColor" stroke-width="1.6"/>
88 <path d="M8 .8v2.6M8 12.6v2.6M.8 8h2.6M12.6 8h2.6" stroke="currentColor"
89 stroke-width="1.6" stroke-linecap="round"/>
90 </svg>
91 </button>
92
93 <!-- Second in the window row, right of the picker: it walks the panes an
94 agent is waiting in, loudest first and never the one it last sent you
95 to. Tinted by the state it would take you to, so the button says
96 whether there is anywhere to go before you press it. -->
97 <button id="jump" class="icon" title="No Claude panes" disabled>
98 <svg viewBox="0 0 16 16" aria-hidden="true">
99 <path d="M3.4 3.6l4.2 4.4-4.2 4.4M8.4 3.6l4.2 4.4-4.2 4.4"
100 fill="none" stroke="currentColor" stroke-width="1.6"
101 stroke-linecap="round" stroke-linejoin="round"/>
102 </svg>
103 </button>
104
105 <!-- One tab per window in the selected session, browser-style: the active
106 tab is lifted to the terminal's own background so the two read as one
107 surface, and the dot in its favicon slot is what Claude Code is doing
108 in that window. Clicking a tab runs select-window — the same thing
109 `prefix 2` does in the terminal, and it moves every client watching
110 the session, because a window belongs to the session and not to us. -->
111 <div id="tabs" role="tablist" aria-label="tmux windows" hidden></div>
112
113 <button id="tab-new" class="icon" title="New window" hidden>
114 <svg viewBox="0 0 16 16" aria-hidden="true">
115 <path d="M3.5 8h9M8 3.5v9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
116 </svg>
117 </button>
118
119 </div>
120
121 <!-- Tab group mode only. With every window on the server in one row, the
122 row is the wrong place to look something up — it scrolls, and past a
123 handful of sessions what you want is off the end of it. So the row
124 gets an address bar under it, the way a browser does: type, and the
125 same set the tabs are drawn from is searched by name.
126
127 It is also where a session is made, which is why there is no second
128 "+" beside the window one: a browser has no "new profile" button next
129 to "new tab" either — you type the thing you want and it takes you
130 there, or offers to create it. -->
131 <div class="strip" id="omni-strip" hidden>
132 <!-- An address bar, and the same two jobs: it says where you are, and it
133 is how you go somewhere else. Where you are is the session name, and
134 it is the box's own text rather than a label beside it — focusing
135 selects the whole of it, so typing replaces it and Enter takes you
136 to whatever you typed, or offers to create it.
137
138 Left of the caret is the button a browser puts in that same spot —
139 what was the padlock and is now a pair of sliders — doing the same
140 job: what the box says in a word, said properly. There it is the
141 origin and its certificate; here it is the session, its working
142 directory, and what is running in it.
143
144 The glyph is Chrome's, deliberately: this is the one control in the
145 panel whose position is borrowed outright, and borrowing the position
146 without the mark leaves people hunting for a padlock. The session's
147 colour is not on it — the tab row above already says which group we
148 are in, and a second copy of that hue in the box competes with it for
149 the same glance. It appears again inside the panel, where it names
150 what you have just opened. -->
151 <div id="omni-box">
152 <button id="omni-here" type="button" hidden
153 aria-haspopup="dialog" aria-expanded="false"
154 aria-label="Session info" title="Session info">
155 <svg viewBox="0 0 16 16" aria-hidden="true">
156 <path d="M2.2 5.5h1.6M8.2 5.5h5.6" stroke="currentColor"
157 stroke-width="1.5" stroke-linecap="round"/>
158 <circle cx="6" cy="5.5" r="1.9" fill="none" stroke="currentColor" stroke-width="1.5"/>
159 <path d="M2.2 10.5h5.6M12.2 10.5h1.6" stroke="currentColor"
160 stroke-width="1.5" stroke-linecap="round"/>
161 <circle cx="10" cy="10.5" r="1.9" fill="none" stroke="currentColor" stroke-width="1.5"/>
162 </svg>
163 </button>
164 <!-- A plain text box. Not a combobox: it does not fill itself in and
165 there is no value to commit — what it holds is a query, and the list
166 under it is results, so Enter runs a result rather than accepting a
167 completion. The list announces itself instead.
168
169 A textarea rather than an input, for one reason: what gets typed
170 here is often a sentence — `!claude <a whole prompt>` — and an input
171 answers a long one by scrolling its own text sideways, which hides
172 the beginning of the thing you are still writing. This wraps and the
173 pill grows down instead, up to a few lines. It is still one line of
174 *value*: Enter runs the chosen row and newlines never get in (see
175 the input handler), so the box behaves exactly as it reads. -->
176 <textarea id="omni" rows="1" wrap="soft"
177 aria-label="Jump to a window, name a new session, or run a command with !"
178 placeholder="Jump, create or !run…"
179 title="Jump to a window, session or Claude pane — type a name to create a session, or !command to run one in a new window"
180 spellcheck="false" autocomplete="off"></textarea>
181 <!-- The other half of what an address bar says: not just which session,
182 but where it is. The name alone never tells you that, and it is the
183 thing you want to know before typing a command into the box below.
184 It rides on the right, dimmed, and steps out of the way the moment
185 the box turns into a query — the query needs the width more than a
186 directory you are about to leave does. -->
187 <span id="omni-cwd" hidden></span>
188 </div>
189 </div>
190 </div>
191
192 <!-- The corner, spanning every row there is. One control lives here, and
193 it is the only one in the panel that is *held* rather than tapped: a
194 finger stays on it for the length of a sentence, and sliding off it
195 mid-word is the failure that matters. So it gets a square of its own
196 out of the rows' reach and fills it, which is the only way a touch
197 target in this header gets past a row's height.
198
199 Held, not clicked: it holds the space bar down in the pane for as long
200 as your finger is on it, which is what Claude Code's voice mode listens
201 for. A sidebar has no key to hold — the terminal is the thing with
202 focus — so this is the panel's own. -->
203 <div id="corner">
204 <button id="talk" class="icon big" aria-pressed="false"
205 title="Hold to talk — holds space down in the pane">
206 <svg viewBox="0 0 16 16" aria-hidden="true">
207 <rect x="5.7" y="1.6" width="4.6" height="8" rx="2.3"
208 fill="none" stroke="currentColor" stroke-width="1.5"/>
209 <path d="M3.4 7.4a4.6 4.6 0 0 0 9.2 0" fill="none" stroke="currentColor"
210 stroke-width="1.5" stroke-linecap="round"/>
211 <path d="M8 12.1v2.3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
212 </svg>
213 </button>
214
215 <!-- No Enter button beside the mic: letting go of it is the send. The
216 button used to be here because a voice message that cannot be sent is
217 not worth dictating, and the keyboard is exactly what your hand is off
218 when it is on the mic — but the hold now queues the Return itself, so
219 the second button was pressing a key that had already been pressed. -->
220 </div>
221 </header>
222
223 <!-- Outside the header and positioned over the terminal: the header is a
224 flex column that sizes the terminal, so a list that grew inside it
225 would reflow the terminal on every keystroke. Deliberately not a
226 <dialog> and nothing modal — a modal in this panel would block the
227 socket's message handler for as long as it is up. -->
228 <div id="omni-list" aria-live="polite" aria-label="Matches" hidden></div>
229
230 <div id="picked" hidden>
231 <div class="picked-top">
232 <span id="picked-tag" class="chip"></span>
233 <span id="picked-meta"></span>
234 <button id="picked-copy" class="icon" title="Copy to clipboard">
235 <svg viewBox="0 0 16 16" aria-hidden="true">
236 <rect x="5.2" y="5.2" width="8.6" height="8.6" rx="1.8"
237 fill="none" stroke="currentColor" stroke-width="1.5"/>
238 <path d="M10.8 5.2V4a1.8 1.8 0 0 0-1.8-1.8H4A1.8 1.8 0 0 0 2.2 4v5a1.8 1.8 0 0 0 1.8 1.8h1.2"
239 fill="none" stroke="currentColor" stroke-width="1.5"/>
240 </svg>
241 </button>
242 <button id="picked-close" class="icon" title="Dismiss">
243 <svg viewBox="0 0 16 16" aria-hidden="true">
244 <path d="M4 4l8 8M12 4l-8 8" stroke="currentColor"
245 stroke-width="1.6" stroke-linecap="round"/>
246 </svg>
247 </button>
248 </div>
249
250 <div id="picked-formats" class="segmented" role="tablist"></div>
251 <div id="picked-value"></div>
252
253 <p id="picked-warn" class="hint warn" hidden></p>
254 <button id="picked-grant" hidden></button>
255 <button id="picked-insert">Insert into terminal</button>
256 </div>
257
258 <div id="term"></div>
259
260 <!-- What a dead socket looks like. The word in the header is easy to miss —
261 it sits in a row of controls, in the smallest type in the panel, while
262 the scrollback below it still shows a session that is no longer there.
263 So the state gets said over the terminal instead, where the eye already
264 is, with the two things you would go looking for on it: retry, and the
265 settings that hold the token and the URL.
266
267 Over the terminal rather than in the flex column, for the reason the
268 omnibar list is: a card in the column would resize the terminal, and
269 xterm.js would reflow the scrollback you are still reading. The layer
270 itself is click-through (`pointer-events: none`) so the header keeps
271 working while it is up; only the card takes the pointer. -->
272 <div id="offline" hidden>
273 <div class="offline-card" role="status" aria-live="polite">
274 <!-- A broken link, which is what happened: the two halves are still
275 there and no longer joined. Hidden while connecting — the spinner
276 the card grows in its place says the same thing in motion. -->
277 <svg id="offline-icon" viewBox="0 0 24 24" aria-hidden="true">
278 <path d="M9.5 7.5H7a4.5 4.5 0 0 0 0 9h2.5M14.5 7.5H17a4.5 4.5 0 0 1 0 9h-2.5"
279 fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
280 <path d="M9.6 12h1.2M13.2 12h1.2" stroke="currentColor"
281 stroke-width="1.7" stroke-linecap="round"/>
282 </svg>
283 <div id="offline-spinner" aria-hidden="true"></div>
284 <p id="offline-title">disconnected</p>
285 <p id="offline-msg"></p>
286 <div class="offline-actions">
287 <button id="offline-retry">Reconnect</button>
288 <button id="offline-settings" class="ghost">Settings</button>
289 </div>
290 </div>
291 </div>
292
293 <!-- A card hung off the chevron rather than a drawer at the foot of the
294 panel — the shape Chrome gives the popup on the other end of that same
295 chevron. Positioned out of flow deliberately: as a flex item it sized
296 the terminal, so every open and close re-fit xterm.js and reflowed the
297 scrollback. Nothing modal about it, for the reason a modal here can't
298 be: it would block the socket's message handler while it is up. -->
299 <aside id="settings" role="dialog" aria-label="Settings" hidden>
300 <div class="settings-head">
301 <span class="title">Settings</span>
302 <button id="settings-close" class="icon" title="Close" aria-label="Close settings">
303 <svg viewBox="0 0 16 16" aria-hidden="true">
304 <path d="M4 4l8 8M12 4l-8 8" stroke="currentColor"
305 stroke-width="1.6" stroke-linecap="round"/>
306 </svg>
307 </button>
308 </div>
309
310 <h2>Pair this extension</h2>
311 <p class="hint">Run this once, then restart the daemon:</p>
312 <pre id="pair-cmd">loading…</pre>
313 <button id="copy-origin" class="ghost">copy</button>
314
315 <h2>Token</h2>
316 <p class="hint"><code>termbridge token</code> prints it.</p>
317 <input id="token" type="password" placeholder="64 hex chars" autocomplete="off" spellcheck="false" />
318 <label class="row"><input id="reveal" type="checkbox" /> reveal</label>
319 <p id="token-hint" class="hint warn" hidden></p>
320
321 <h2>Appearance</h2>
322 <div class="row">
323 <select id="theme-select">
324 <option value="auto">Follow system</option>
325 <option value="light">Light</option>
326 <option value="dark">Dark</option>
327 </select>
328 <select id="density-select" title="How much room the header takes">
329 <option value="normal">Normal header</option>
330 <option value="compact">Compact header</option>
331 <option value="hidden">Hide header</option>
332 </select>
333 </div>
334 <p class="hint" id="density-hint"></p>
335
336 <!-- Two ways to lay out the same tmux server. Nested keeps the two rows
337 this panel was built around; groups folds them into one, the way
338 Chrome folds many tabs into a coloured group chip. -->
339 <div class="row" id="tabmode-row">
340 <select id="tabmode-select" title="How sessions and windows are laid out">
341 <option value="nested">Nested tabs</option>
342 <option value="groups">Tab groups</option>
343 </select>
344 </div>
345 <p class="hint" id="tabmode-hint"></p>
346
347 <!-- What a plain click on "+" opens. Both kinds are always in the button's
348 hold menu; this only says which one the click itself is. -->
349 <div class="row" id="newtab-row">
350 <select id="newtab-select" title="What the &quot;+&quot; button opens">
351 <option value="claude">"+" opens claude</option>
352 <option value="window">"+" opens a shell</option>
353 </select>
354 </div>
355 <p class="hint" id="newtab-hint"></p>
356
357 <!-- Following the browser. Right-click a session or a window to pin it to
358 the tab you are on; these three only say whether pins are acted on at
359 all, whether an unpinned localhost port may guess at one, and whether
360 the same pins also read backwards. -->
361 <div class="row">
362 <label class="row"
363 ><input id="follow-tabs" type="checkbox" /> follow browser tabs</label
364 >
365 </div>
366 <div class="row">
367 <label class="row"
368 ><input id="detect-devport" type="checkbox" /> guess by devport port</label
369 >
370 </div>
371 <div class="row">
372 <label class="row"
373 ><input id="lead-tabs" type="checkbox" /> and switch tabs back</label
374 >
375 </div>
376 <p class="hint">
377 A pinned browser tab brings its session up when you switch to it, and
378 switching to the session brings the tab back up. Right-click a session
379 or window tab to pin one.
380 </p>
381
382 <div class="row" id="font-row">
383 <span class="hint">Font size</span>
384 <button id="font-smaller" class="icon" title="Smaller (Ctrl+Alt+- or Ctrl+wheel)">
385 <svg viewBox="0 0 16 16" aria-hidden="true">
386 <path d="M3.5 8h9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
387 </svg>
388 </button>
389 <span id="font-size">12</span>
390 <button id="font-bigger" class="icon" title="Bigger (Ctrl+Alt+= or Ctrl+wheel)">
391 <svg viewBox="0 0 16 16" aria-hidden="true">
392 <path d="M3.5 8h9M8 3.5v9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
393 </svg>
394 </button>
395 <button id="font-reset" class="ghost" title="Reset (Ctrl+Alt+0)">reset</button>
396 </div>
397
398 <h2>tmux session</h2>
399 <p class="hint">Attaches if it exists, creates it otherwise.</p>
400 <div class="row">
401 <input id="session" list="session-list" placeholder="browser" spellcheck="false" />
402 <datalist id="session-list"></datalist>
403 <button id="session-apply" class="ghost">attach</button>
404 </div>
405
406 <h2>Daemon</h2>
407 <input id="url" value="ws://127.0.0.1:7681" spellcheck="false" />
408 <button id="connect">connect</button>
409
410 <div id="trust-hint" hidden>
411 <p class="hint warn">
412 Firefox upgrades <code>ws://</code> to <code>wss://</code>. The
413 daemon's certificate is self-signed, so it needs a one-time exception.
414 </p>
415 <button id="trust-cert" class="ghost">open https:// to trust the cert</button>
416 </div>
417
418 <h2>Log</h2>
419 <pre id="log"></pre>
420 </aside>
421
422 <script src="lib/theme.js"></script>
423 <script src="lib/sanitize.js"></script>
424 <script src="lib/shot.js"></script>
425 <script src="lib/split.js"></script>
426 <script src="lib/devport.js"></script>
427 <script src="lib/tabpin.js"></script>
428 <script src="picker.js"></script>
429 <script src="vendor/xterm.js"></script>
430 <script src="vendor/addon-fit.js"></script>
431 <script src="vendor/addon-web-links.js"></script>
432 <script src="sidebar.js"></script>
433 </body>
434</html>