| 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 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> |