Skip to content

Web ​

Documentation in progress

Some sections are still incomplete.

A ThinkTerm multiplexer can serve its terminals to a browser. A phone, or a machine with no ThinkTerm on it, opens the sessions already running on the machine that has one — the same Spaces, Projects and Threads, not a copy of them.

Nothing listens until you ask for it.

Turning it on ​

Three ways, all of them addressing a multiplexer server rather than the GUI's own in-process one:

  • Settings › Web — a switch, the address it is accepting on, and the list of links you have handed out.
  • thinkterm cli web-server on — the same thing from a shell. --bind-address chooses where to listen, status reports where it is accepting, and off closes the port and drops the browsers on it.
  • A web_servers entry in the configuration — read when the server starts, so the port is open every time it runs.

The first two change a running server and last as long as it does. The config entry is the durable form; reloading the config does not open or close a web port. The one thing a reload does change is whether the page may reach other machines, described below.

Local terminals have to be running in a background session server for there to be anything to serve — that setting is described in Remote, and a new installation starts with it on. Without one, Settings › Web says so rather than offering a switch that would do nothing. If the only thing attached is a remote domain, that section is about that machine's port, not this one's.

A browser is admitted by a token, and the token arrives as a URL:

bash
thinkterm cli web-token mint --label phone --ttl 12h

Whoever opens that URL has what you have on that machine: a shell as your user, and every pane's scrollback. With reaching other machines on, the link also reaches every machine that account can SSH to. Treat it the way you treat an ssh key — in particular, do not paste one anywhere you would not paste a private key.

  • --ttl takes 30m, 12h, 7d, or a number of seconds. Without it the token lives until it is revoked. A link copied from Settings expires in eight hours unless you pick something else, up to Until revoked.
  • web-token list shows every token the server knows, and how many browsers are on each.
  • web-token revoke <id>, or --all, cuts them off at once, live browsers included.
  • --url-only prints nothing but the URL, for scripts.

A connected browser appears in thinkterm cli list-clients under the label its token was minted with.

The token travels in the part of the URL after #, which a browser never sends to a server or in a Referer. The page moves it into the browser tab's own session storage and rewrites the address bar without it, so it is not bookmarked, shared or caught in a screenshot by accident. A reload in the same tab keeps working; a new tab, or the bare address, needs the link again and says so.

Who can reach the port ​

Out of the box the listener binds 127.0.0.1:8088 — the machine it runs on, and nothing else.

Reachable from other devices rebinds it to every address the machine has, so turn it on only on a network you trust. Browsers only expose WebGPU to a secure context, and http://localhost is one while a plain http:// to any other host is not, so a listener off loopback needs TLS: without a certificate of your own, ThinkTerm generates a self-signed one naming the machine's hostname and each of its addresses. A browser warns about that certificate once and then draws the page. Before you accept the warning, compare the certificate's SHA-256 fingerprint in the browser with the one Settings › Web shows, or the one the full web-token mint output prints — --url-only leaves it out — and stop if they differ. require_tls_off_loopback, on by default, is what refuses to serve a non-loopback address in the clear — a page served that way would have no WebGPU and show nothing anyway.

For a phone on the same network or tailnet, the QR code in Settings › Web is the short way to get the link across. Over the open internet, forward the loopback port with ssh -L rather than exposing it.

Behind a reverse proxy ​

The listener accepts a page only from its own address. A page reached under any other name — through a reverse proxy, say https://terminal.example.com — has to be listed in allowed_origins, or its connection is refused. Listing any origin replaces that default, and the links web-token mint prints then point at the listed origins.

Reaching other machines ​

A page can also open terminals on machines other than the one that served it. A browser cannot make an SSH connection, so the server does it: the page names a machine over a second WebSocket, /relay, which takes the same token; the server signs in to that machine over SSH and carries the page's connection to the ThinkTerm running there. The machine's Spaces, Projects and Threads then appear in the page beside this server's own, as a ThinkTerm Connect host's do on the desktop, with this server in the middle.

It is off until you turn it on, in either of two places:

  • Settings › Web › Reach other machines on the desktop turns it on for every web listener on that machine.
  • relay = true in a web_servers entry turns it on for that one listener, which is how to turn it on for a server with no desktop. The entry is matched to the listener by its exact bind_address.

Either one is enough. Neither needs a restart: both are read again as the page uses them, so a config reload reaches pages that are already open, and turning both off ends the machines open through the relay within about two seconds. While it is off the Remote Hosts page lists nothing and says that reaching other machines is off on this server and where to turn it on; opening, adding or forgetting a machine is refused.

There is no list of allowed hosts

Whoever holds the link can open every machine the server's user can SSH to, signing in with that user's keys and ~/.ssh/config, and can type in any other host, port and user as well. A link already gives a shell on the server, and through it whatever that server's SSH setup reaches; the relay adds no reach of its own, but it puts all of it behind the link.

The connection is not encrypted end to end. The browser's TLS ends at this server and the SSH connection to the other machine starts there, so the server carries the traffic between them.

Revoking a link closes the machines opened through it along with its other connections.

The Remote Hosts page ​

The globe button at the foot of the sidebar opens it as a tab of its own. Remote Hosts and Add Remote Host… are also among the search palette's commands, and the Space menu offers Add Remote Host…. Under Connected come this server and each machine open in the page, with how far a connection has got; the machines it can reach follow, each tagged with where it came from:

TagWhere the machine comes from
~/.ssh/configThe server's own ~/.ssh/config, in a folded group of its own that opens while you search
DesktopThe hosts saved in the desktop's own host list on the server's machine, passwords saved there included
Added hereHosts added from a page like this one, kept on the server

New Host asks for a Host, and optionally a Port (22), a User, a Name and a Password. Add and connect saves it on the server in web_machines.json, with any password encrypted, and connects. Forget removes a machine that was added here, or whatever the server kept for the others: a saved password, or the go-ahead to install.

While a machine opens, the page shows each step — Connecting…, Signing in…, Checking ThinkTerm there… — and asks what only you can answer:

  • A password, a key's passphrase, or a code the host asks for. A password or passphrase comes with Remember on this server, ticked by default. Ticked, the server keeps it encrypted the way the desktop keeps saved passwords, in the same folder as the host book, so the caveats there apply, and the next connection, and every reconnect, goes through without asking. A code is never kept.
  • Trust this machine? the first time the server meets the host, with the host key's fingerprint to check. A host whose key has since changed fails with Its host key has changed instead of asking again.
  • Install ThinkTerm there? when the machine has none. It goes into ~/.local/bin for that user, without root, and is asked once per machine.

The ThinkTerm on the machine has to be one this server can talk to. An older one is updated without asking, and a running server there hands its sessions to the new one; if it cannot, the page asks Stop the old ThinkTerm server there?, since stopping it ends everything in it. A newer release there is left as it is, since its sessions would not come to an older build, and the page says to update this server; a build with a newer protocol that is not a newer release, such as a development build, is only replaced after Replace the newer ThinkTerm there?. A desktop installation is never replaced from here, and the page says to update that desktop app first. Where ThinkTerm cannot be used on the machine, the page offers Open a plain SSH terminal instead.

With more than one machine open, the Space menu groups the Spaces by machine, each machine other than this server with its own New Space Here, and Add workspace… gains a selector for the machine the Project goes on. Machines open in the page are reopened in the background the next time it loads.

Settings › Web ​

RowWhat it does
Allow browser accessOpens or closes the port on the session server
Reachable from other devicesListens on every address with its own certificate, instead of loopback only
Reach other machinesLets a page open the machines this computer can SSH to, off by default. On, it applies to every web listener on this computer; off, only listeners with relay = true allow it. No restart needed. See Reaching other machines
Access link › Copy linkMints a link and copies it
Link expiresHow long the next copied link lives: 1 hour, 8 hours (the default), 1 day, 7 days or Until revoked
Scan on your phone › Show codeMints a link and shows it as a QR code
Links you have handed outEvery live link, with a Revoke button each
Every link › Revoke allCuts off every browser at once

Each handed-out link is named by the label it was minted with, otherwise by the device that last used it, otherwise Not used yet. Beneath it is when it expires and how many browsers are connected through it. Revoke ends that one link and drops the browsers using it; the others keep working.

The web_servers entry ​

Each entry in web_servers is one port, read when the server starts:

lua
config.web_servers = {
  {
    bind_address = '127.0.0.1:8088',
    token_file = wezterm.home_dir .. '/.local/share/thinkterm/web-tokens.json',
  },
}
FieldDefaultMeaning
bind_address127.0.0.1:8088The address and port to listen on
pem_private_keyunsetA PEM private key. With pem_cert set too, the port serves https and wss
pem_certunsetA PEM certificate
pem_caunsetA PEM CA chain
static_dirbeside the installed programWhere the page's files are
token_fileunsetWhere minted tokens are kept, as digests
allowed_originsthe listener's own addressOrigins allowed to open the connection
require_tls_off_loopbacktrueRefuse to start without TLS on a non-loopback address
relayfalseLet a page opened on this listener reach other machines through the server. See Reaching other machines

relay is the one field that is read again when the config reloads, including for pages already open. A reload does not open or close the port, change its certificate or move it.

token_file matters more than it looks. Unset, tokens live in the server's memory only: when the server restarts — after a reboot, or an update that cannot hand its sessions over — every link stops working and has to be minted again. Set it, and links survive a restart. The file holds digests, never the tokens, so a copy of it lets nobody in.

static_dir is for running a page other than the one installed, such as a development build; the THINKTERM_WEB_STATIC_DIR environment variable does the same without a config change, and static_dir wins when both are set.

What the page can do ​

The browser draws the same workspace: the Space, Project and Thread tree, the tab strip, split panes, a search palette, and the right-hand panel. Threads and panes can be opened and closed from it.

The right-hand panel offers Snippets, Status, and the panels of installed plugins. Snippets and plugins come from the machine that served the page, not the one the browser runs on: a browser pointed at a server sees that server's snippets and that server's plugins. A Run or Paste lands in the pane that had focus when you pressed it. Status is off until you turn it on with the button at the right end of the tab row. Like the desktop's, it shows what a program reports about itself: what it is doing, why it waits, a progress bar, and its sub-tasks folded into one line that opens on a click. Each agent carries the same mark as on the desktop: Claude Code, Copilot CLI and Kimi Code in colour (Kimi in a single colour on a light page), Codex, CodeBuddy Code, Cursor Agent, OpenCode and Pi in a single colour (OMP borrows Pi's), and, for a program with no mark of its own, the icon its pane's tab shows.

Tabs carry the same program icons as the desktop's. The cards are the ones set in the desktop's settings on the machine that served the page, not on the one the browser runs on.

Pictures sent with the Kitty graphics protocol are drawn in the page, animations included. PNG or JPEG pictures sent through the iTerm2 protocol are not yet.

The sidebar ​

The sidebar shows one Space at a time, as the desktop's does: its name at the top, New Thread, pinned Threads under Pinned, then the Projects under Workspaces with their Threads. The … beside the Space's name lists the other Spaces and offers New Space, and renaming or deleting the one on show — never the last one.

From the page you can:

  • create, rename, switch and delete Spaces
  • add a Project by typing a directory on the server, ~/dir or /dir, under Add workspace…; it comes with a main Thread
  • collapse a Project by clicking it, rename it by double-clicking its name, archive it, restore it, remove it
  • start, pin, unpin and delete Threads, rename one by double-clicking its name, and mark one unread
  • drag a Thread within its Project, or a Project within the Space, to reorder them

Archived Projects collect under Archived (N) at the bottom of the Space; click it to show them. A Thread row's delete button and a tab's close button both ask a second time, showing delete? or close?, because they end the programs inside. Windows on the server that no Thread claims are listed under Other windows, so nothing is out of reach.

The sidebar's width is dragged at its edge and remembered by the browser. The button at the start of the tab row hides it; with Reveal the sidebar on hover on, resting the pointer on the left edge brings it back until the pointer leaves.

Right-click offers the desktop's menus, less what the server has no operation for:

OnOffers
A paneCopy, Paste, Split Right, Split Left, Split Down, Split Up, Frontend access › A · Shared (tmux-like) / B · Handoff (exclusive)
A tabClose Tabs to Left, Close Tabs to Right, Close Other Tabs, New Terminal Tab to Right, Zoom Pane
A ThreadPin Thread or Unpin Thread, Rename Thread…, Delete Thread, Mark as Unread
A ProjectRename Project…, New Thread, Collapse / Expand Threads, Archive Project…, Remove Project
An archived ProjectUnarchive Project, Delete Permanently… › Delete Project and Threads

When a Project has panes running, Archive Project… says how many it will close before Archive and Close Panes does it. Renaming a tab, moving a tab between windows and Reset Terminal are desktop-only. Tabs can still be reordered by dragging them along the tab row, and a pane dragged onto the edge of another pane moves there.

Every pane carries a bar with its tabs on the left and, on the right, a new tab in that pane, split down, split right and zoom. A tab in that bar is named by the pane's label when it has one — set by renaming the tab on the desktop, or with thinkterm cli set-pane-label (see Command Line) — and otherwise by the program's title. The tab strip above keeps the program's title, as the desktop's does.

The search palette ​

⌘K on a Mac, Ctrl+K elsewhere, opens the search palette; the shortcut can be changed in the page's settings. It finds, in sections:

  • Find Thread — Threads in every Space
  • Tabs and Panes — those in the window on show
  • Switch Space — the Spaces
  • Commands — New Thread, New Tab, Split Right, Split Down, Zoom Pane, Close Pane, Take Over the Terminal, Follow the Desktop's Focus, Show or Hide the Sidebar, Settings, Increase font size, Decrease font size, Reset font size

With nothing typed, the things you picked recently come first. Arrows move, Enter runs, Esc closes. The magnifier at the foot of the sidebar opens it with Threads only.

Who drives the terminal ​

How a terminal is shared is the server's choice: the pane menu's Frontend access submenu switches it, with a tick by the current one. The server keeps the choice, across restarts too, so it applies to every device attached to it, not only this page.

B · Handoff (exclusive), the default, gives a terminal one driver at a time. Opening a terminal the desktop is holding puts a card over it — Terminal is being used on another device, Click or scroll to continue — and the terminal stays visible underneath. A click, a scroll or a keystroke in it takes it over. The first keystroke is not lost; it is typed once the handoff completes. Clicks on the tab row, the pane bars and the sidebar never take it. Ctrl+Shift+T, or Take Over the Terminal in the palette, takes it explicitly. That is the same handoff described in Remote, with a browser counting as one device identified by its link — two tabs opened from the same link share a turn.

A · Shared (tmux-like) has no driver to hand over: every attached client sees the same terminals and can type into them, and the last one to interact with a tab decides its size.

While the page holds a tab, the tab is reshaped to the browser window. While the desktop holds it, the page keeps the desktop's size and shape, and clips when the tab is larger than the window. Ctrl+Shift+F reshapes the tab to this window on demand.

Follow the Desktop's Focus, on when the page opens, keeps one focus between the two: when the desktop moves to another pane, the page moves with it, and a click on a pane in the page moves the desktop. Run the command again to stop following; it is not remembered across loads.

Clipboard ​

Selecting text with the mouse copies it as soon as you let go. A double click selects a word, a triple click a line. When a program has asked for the mouse, hold Shift to select text yourself. Copy and Paste are also in the pane menu.

A program can set the clipboard with OSC 52, but only from a pane the page is showing: a pane nobody is looking at does not get to write this device's clipboard.

Keyboard shortcuts ​

KeysAction
Cmd+C or Ctrl+Shift+CCopy the selection
Cmd+V or Ctrl+Shift+VPaste
Ctrl+Shift+EnterSplit right
Ctrl+Shift+\Split down
Ctrl+Shift+ZZoom the pane, or unzoom it
Ctrl+Shift+FFit the tab to this window
Ctrl+Shift+TTake the terminal over
Ctrl+Shift with an arrowFocus the neighbouring pane, without moving the desktop's focus
Cmd+=, Cmd+-, Cmd+0The focused pane's font size: larger, smaller, reset
⌘K / Ctrl+KThe search palette, unless another shortcut is chosen

Every other Cmd combination is left to the browser — reload, the address bar, tabs — and never reaches the terminal. Everything else goes to the focused pane, including Ctrl keys, so the default palette shortcut means Ctrl+K does not reach the terminal off a Mac; pick Ctrl+Shift+P in the settings if a program needs it. Input methods work: keys the input method is composing stay with it, and its candidate window follows the terminal cursor. On WebKit browsers — Safari, and every browser on iOS — the Return that picks a candidate is not sent to the terminal as well.

On a phone ​

The page switches to its phone layout when the pointer is a finger, or when the window is narrower than 720 pixels. Reach the server over https first, as described above.

  • The terminal takes the whole screen. The sidebar and Status become drawers over it; a swipe in from the left edge brings the sidebar out, and a swipe back to the left puts it away. Picking a Thread in the drawer closes it.
  • A key bar along the bottom carries Esc, Tab, Ctrl and Alt, the four arrows, Home, End, PgUp, PgDn, and - / | ~. Ctrl and Alt are sticky: tap one, and it applies to the next key from the bar or from the soft keyboard. Holding an arrow repeats it.
  • The soft keyboard appears only when you ask for it with the keyboard button on the key bar, so a tap on the terminal does not cover half the screen. The button stays at the start of the bar while the keys scroll beside it.
  • A finger drag scrolls, and a flick keeps scrolling. A finger put down on a split divider, within a finger's width of it, drags the divider instead. Two fingers scroll too; pinching changes the font size of the pane under them.
  • A tap is a click. A long press, half a second, opens the pane's menu.
  • A tab with a single pane shows no pane bar; one appears when there is a split to tell apart.

Text cannot be selected by touch: a drag in a pane scrolls it.

The page's own settings ​

The gear at the foot of the sidebar opens the page's settings. They belong to this browser, not to the server or your account. On a phone they are two pages, as a phone's own are: the list of sections, then the one you picked, with a way back.

SectionSettingChoices
GeneralLanguageFollow System — the browser's languages — or one of ThinkTerm's languages
GeneralScrollingSmooth (default), following a finger or trackpad by the pixel, or Stepped, a row at a time
GeneralSearch shortcut⌘K (default), ⌘⇧P, Ctrl+Shift+P. Off a Mac, ⌘ means Ctrl
AppearanceColor schemeFollow the desktop (default), using the scheme the server is configured with, or any built-in scheme
AppearanceThemeDark (default), Light or Follow System, for the page around the terminal
AppearanceFont sizeFollow the desktop, so a cell here matches one there, or Fixed size, 6 to 72 in half-point steps
Sidebar & PluginsReveal the sidebar on hoverOn by default
Sidebar & PluginsReset sidebar widthPuts the sidebar back to its default width
Sidebar & PluginsStatus, SnippetsWhether the right-hand panel offers them
Sidebar & PluginsReload plugins, and each pluginA switch, and how long it runs unused. A plugin you have never allowed shows New: it does not run until you allow it and an Allow button instead
AboutCopy InfoA card with the version and Copy Info, which copies the version, build, language and browser; then Version, Build, Host — the address the page came from — Language and License

Color scheme opens a searchable list with each scheme's colours beside it; the terminal previews whichever row is under the pointer or the arrows, Enter keeps it, and Esc puts back what you had. The search box at the top of the settings finds a row in any section.

Where they are kept ​

In the browser's local storage, along with the Space the page was last showing, the palette's recent picks, the sidebar's width, the machines open in Remote Hosts (thinkterm.machines), which are reopened on load, and the picked colour scheme with its colours, so the next load draws in it straight away. Another browser, or a private window, starts from the defaults. Nothing of this goes to the server. A browser that blocks storage keeps the settings for that load only.

URL overrides ​

A URL can override some settings for one load, without changing what is stored:

ParameterEffect
?lang=de-DEThe page's language
?theme=lightlight, dark or system
?font=14A fixed font size, 6 to 72
?glyphfont=The CSS font list used for characters the page's own fonts lack, which also picks the regional shape of Han characters

Put them before the # of a minted link: https://example-host:8088/?theme=light#token=….

When the connection drops ​

The page reconnects by itself, to the same tab: once straight away, then after a second, doubling the wait each time up to fifteen seconds. A note at the bottom right says the connection was lost and that it is reconnecting. After six failed attempts it adds that the link may have expired, or the server may be down — a browser is not told why a connection was refused, so a revoked link, an expired one and a stopped server look the same from the page.

What was on screen stays there while it is down, and the tab is laid out again from the server once it is back. The page gives up only when there is nothing to come back to: the server has no panes, or speaks a different protocol version. It says which.

What the page cannot do ​

  • Files and Notes — desktop-only for now.
  • Live Overview — there is no browser counterpart, and no button for it.
  • Thread references — they are the desktop's own and are not shown. Projects on an SSH host are not shown either: the server could not open them.
  • Notifications and sounds — the bell has nothing in it, and nothing plays.
  • Links — links in the output cannot be clicked.
  • Scrollback search, copy mode and quick select — the palette searches the workspace, not the text in a pane.
  • Selecting by touch — on a phone, dragging scrolls.
  • iTerm2 images — PNG and JPEG pictures sent through the iTerm2 protocol are not drawn yet.
  • Right-to-left text — shown as boxes, since the page does not reorder bidirectional text and correct letters in the wrong order would be harder to notice.
  • Renaming or moving tabs between windows, Reset Terminal — the server has no operation for them.

Characters the page's two bundled fonts lack — CJK, Hangul, emoji — are drawn with the fonts installed on the device the browser runs on. A character none of them has shows as a box. Without WebGPU the page shows nothing but a message saying to open it over https or http://localhost in a browser with WebGPU enabled.