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 input. 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 <input id="omni" type="text"
169 aria-label="Jump to a window, name a new session, or run a command with !"
170 placeholder="Jump, create or !run…"
171 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"
172 spellcheck="false" autocomplete="off" />
173 <!-- The other half of what an address bar says: not just which session,
174 but where it is. The name alone never tells you that, and it is the
175 thing you want to know before typing a command into the box below.
176 It rides on the right, dimmed, and steps out of the way the moment
177 the box turns into a query — the query needs the width more than a
178 directory you are about to leave does. -->
179 <span id="omni-cwd" hidden></span>
180 </div>
181 </div>
182 </div>
183
184 <!-- The corner, spanning every row there is. One control lives here, and
185 it is the only one in the panel that is *held* rather than tapped: a
186 finger stays on it for the length of a sentence, and sliding off it
187 mid-word is the failure that matters. So it gets a square of its own
188 out of the rows' reach and fills it, which is the only way a touch
189 target in this header gets past a row's height.
190
191 Held, not clicked: it holds the space bar down in the pane for as long
192 as your finger is on it, which is what Claude Code's voice mode listens
193 for. A sidebar has no key to hold — the terminal is the thing with
194 focus — so this is the panel's own. -->
195 <div id="corner">
196 <button id="talk" class="icon big" aria-pressed="false"
197 title="Hold to talk — holds space down in the pane">
198 <svg viewBox="0 0 16 16" aria-hidden="true">
199 <rect x="5.7" y="1.6" width="4.6" height="8" rx="2.3"
200 fill="none" stroke="currentColor" stroke-width="1.5"/>
201 <path d="M3.4 7.4a4.6 4.6 0 0 0 9.2 0" fill="none" stroke="currentColor"
202 stroke-width="1.5" stroke-linecap="round"/>
203 <path d="M8 12.1v2.3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
204 </svg>
205 </button>
206
207 <!-- No Enter button beside the mic: letting go of it is the send. The
208 button used to be here because a voice message that cannot be sent is
209 not worth dictating, and the keyboard is exactly what your hand is off
210 when it is on the mic — but the hold now queues the Return itself, so
211 the second button was pressing a key that had already been pressed. -->
212 </div>
213 </header>
214
215 <!-- Outside the header and positioned over the terminal: the header is a
216 flex column that sizes the terminal, so a list that grew inside it
217 would reflow the terminal on every keystroke. Deliberately not a
218 <dialog> and nothing modal — a modal in this panel would block the
219 socket's message handler for as long as it is up. -->
220 <div id="omni-list" aria-live="polite" aria-label="Matches" hidden></div>
221
222 <div id="picked" hidden>
223 <div class="picked-top">
224 <span id="picked-tag" class="chip"></span>
225 <span id="picked-meta"></span>
226 <button id="picked-copy" class="icon" title="Copy to clipboard">
227 <svg viewBox="0 0 16 16" aria-hidden="true">
228 <rect x="5.2" y="5.2" width="8.6" height="8.6" rx="1.8"
229 fill="none" stroke="currentColor" stroke-width="1.5"/>
230 <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"
231 fill="none" stroke="currentColor" stroke-width="1.5"/>
232 </svg>
233 </button>
234 <button id="picked-close" class="icon" title="Dismiss">
235 <svg viewBox="0 0 16 16" aria-hidden="true">
236 <path d="M4 4l8 8M12 4l-8 8" stroke="currentColor"
237 stroke-width="1.6" stroke-linecap="round"/>
238 </svg>
239 </button>
240 </div>
241
242 <div id="picked-formats" class="segmented" role="tablist"></div>
243 <div id="picked-value"></div>
244
245 <p id="picked-warn" class="hint warn" hidden></p>
246 <button id="picked-grant" hidden></button>
247 <button id="picked-insert">Insert into terminal</button>
248 </div>
249
250 <div id="term"></div>
251
252 <!-- A card hung off the chevron rather than a drawer at the foot of the
253 panel — the shape Chrome gives the popup on the other end of that same
254 chevron. Positioned out of flow deliberately: as a flex item it sized
255 the terminal, so every open and close re-fit xterm.js and reflowed the
256 scrollback. Nothing modal about it, for the reason a modal here can't
257 be: it would block the socket's message handler while it is up. -->
258 <aside id="settings" role="dialog" aria-label="Settings" hidden>
259 <div class="settings-head">
260 <span class="title">Settings</span>
261 <button id="settings-close" class="icon" title="Close" aria-label="Close settings">
262 <svg viewBox="0 0 16 16" aria-hidden="true">
263 <path d="M4 4l8 8M12 4l-8 8" stroke="currentColor"
264 stroke-width="1.6" stroke-linecap="round"/>
265 </svg>
266 </button>
267 </div>
268
269 <h2>Pair this extension</h2>
270 <p class="hint">Run this once, then restart the daemon:</p>
271 <pre id="pair-cmd">loading…</pre>
272 <button id="copy-origin" class="ghost">copy</button>
273
274 <h2>Token</h2>
275 <p class="hint"><code>termbridge token</code> prints it.</p>
276 <input id="token" type="password" placeholder="64 hex chars" autocomplete="off" spellcheck="false" />
277 <label class="row"><input id="reveal" type="checkbox" /> reveal</label>
278 <p id="token-hint" class="hint warn" hidden></p>
279
280 <h2>Appearance</h2>
281 <div class="row">
282 <select id="theme-select">
283 <option value="auto">Follow system</option>
284 <option value="light">Light</option>
285 <option value="dark">Dark</option>
286 </select>
287 <select id="density-select" title="How much room the header takes">
288 <option value="normal">Normal header</option>
289 <option value="compact">Compact header</option>
290 <option value="hidden">Hide header</option>
291 </select>
292 </div>
293 <p class="hint" id="density-hint"></p>
294
295 <!-- Two ways to lay out the same tmux server. Nested keeps the two rows
296 this panel was built around; groups folds them into one, the way
297 Chrome folds many tabs into a coloured group chip. -->
298 <div class="row" id="tabmode-row">
299 <select id="tabmode-select" title="How sessions and windows are laid out">
300 <option value="nested">Nested tabs</option>
301 <option value="groups">Tab groups</option>
302 </select>
303 </div>
304 <p class="hint" id="tabmode-hint"></p>
305
306 <div class="row" id="font-row">
307 <span class="hint">Font size</span>
308 <button id="font-smaller" class="icon" title="Smaller (Ctrl+Alt+- or Ctrl+wheel)">
309 <svg viewBox="0 0 16 16" aria-hidden="true">
310 <path d="M3.5 8h9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
311 </svg>
312 </button>
313 <span id="font-size">12</span>
314 <button id="font-bigger" class="icon" title="Bigger (Ctrl+Alt+= or Ctrl+wheel)">
315 <svg viewBox="0 0 16 16" aria-hidden="true">
316 <path d="M3.5 8h9M8 3.5v9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
317 </svg>
318 </button>
319 <button id="font-reset" class="ghost" title="Reset (Ctrl+Alt+0)">reset</button>
320 </div>
321
322 <h2>tmux session</h2>
323 <p class="hint">Attaches if it exists, creates it otherwise.</p>
324 <div class="row">
325 <input id="session" list="session-list" placeholder="browser" spellcheck="false" />
326 <datalist id="session-list"></datalist>
327 <button id="session-apply" class="ghost">attach</button>
328 </div>
329
330 <h2>Daemon</h2>
331 <input id="url" value="ws://127.0.0.1:7681" spellcheck="false" />
332 <button id="connect">connect</button>
333
334 <div id="trust-hint" hidden>
335 <p class="hint warn">
336 Firefox upgrades <code>ws://</code> to <code>wss://</code>. The
337 daemon's certificate is self-signed, so it needs a one-time exception.
338 </p>
339 <button id="trust-cert" class="ghost">open https:// to trust the cert</button>
340 </div>
341
342 <h2>Log</h2>
343 <pre id="log"></pre>
344 </aside>
345
346 <script src="lib/theme.js"></script>
347 <script src="lib/sanitize.js"></script>
348 <script src="lib/shot.js"></script>
349 <script src="lib/split.js"></script>
350 <script src="picker.js"></script>
351 <script src="vendor/xterm.js"></script>
352 <script src="vendor/addon-fit.js"></script>
353 <script src="sidebar.js"></script>
354 </body>
355</html>