anvilsign in

collin/browser-terminal-extension

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