termuna

Web and phone viewer

Any browser is a full client for your live session: it decrypts locally, renders every pane, and types back into the real shell on your machine.

Three ways in

  • The dashboard: sign in at termuna.com/app, open a session from the live list, and keep typing. Your session keys come from your encrypted vault, unlocked on the device.
  • A share link: paste a link minted for the session into any browser - no account needed. The key travels in the URL fragment, which never reaches the server, and the browser remembers it so the link is pasted once. The link is good for the mode and the lifetime it was minted with, and its owner can stop it at any time (how links are minted and stopped).
  • Your phone's browser: sign in at termuna.com/app on the phone, or install the dashboard to your home screen, and you have a full viewer with the soft-key row below.

Native Android and iOS apps are in the works: follow them on the roadmap.

A share link is minted with a mode and a lifetime, and the page you open it in shows exactly that much.

  • View only: a "view only" chip sits in the header, and the soft-key row, the snippet strip, the agent composer and the session controls are not drawn. Input is inert: nothing typed reaches the session, and the relay would refuse it anyway.
  • Can type: the full viewer, and typing means asking for the session's one keyboard first (who has it, and how asking works).
  • Three end states, three pages: a link that has expired, a link whose owner stopped it, and a link the cloud does not know each open a page that says which one it is, before any connection is made.
  • Stopped while you watch: the frame freezes where it was, the reason is printed over it, and the status line reads disconnected. There is no reconnect to wait for.

Who has the keyboard

A session has exactly one writer slot. Everyone else watches live, and can ask for it.

  • Ask for the keyboard: the chip's resting state. Asking opens a request to the owner, and you keep watching either way.
  • Waiting for the owner - cancel: the ask is open and can be withdrawn at any point. Nothing you type while it stands reaches the session.
  • You have the keyboard: you are the session's one writer.
  • While someone else holds it: a line under the grid names who is typing, and your own keystrokes are dropped the moment you type them, not queued to arrive later in a burst.
  • A decline is quiet: "Not right now" sits in the row for a few seconds. No alert, no modal.
  • A view-only link cannot even ask: the chip is not drawn, and the relay refuses the request anyway.

Giving the keyboard, declining an ask and taking it back are the owner's verbs, enforced on the relay: a share link's holder can never hand it back. The keyboard returns to the desktop when the owner takes it back, and on its own once the holder stops typing.

Built for touch

  • A soft-key row supplies what phone keyboards lack: Esc, Tab, Ctrl, arrows, and common symbols.
  • The layout is keyboard-aware, and the page installs to your home screen as an app.
  • You can open tabs, split panes, and rename tabs from the phone, driving the same session that runs on your desktop.

Pick mode comes along too, with the labels the desktop draws and tokens found by the daemon, not guessed by the viewer. In the Android app, Pick a token in the session menu labels the screen and each label is a tap target: tap one and the selection sheet opens on it with Copy, Copy to my laptop, Insert and Share. In the dashboard and on /s/ links, Ctrl+Shift+Space opens it and a click on a label copies that token. From the keyboard, type a label to narrow to it, Tab cycles the kind filter, Enter copies, Shift+Enter inserts without running (where this viewer may type) and Ctrl+Enter opens a URL.

Typing that does not wait

A browser or phone session has a round trip in it that the desktop app does not: the relay in between, and the trip back to confirm. Typing here predicts locally, Mosh's idea, kept literally, so the wait does not read on the screen the way it otherwise would.

  • What you type paints at once, in the same ink the daemon would use, with a 1px rule under it and nothing else different: no second colour, no glow.
  • The rule disappears the moment the daemon's own frame confirms the line.
  • A wrong guess never sits next to the truth: the whole line repaints from the daemon's cells the instant one cell disagrees.
  • A password prompt states echo off instead of guessing at a hidden field.
  • Vim, htop, less and anything else on the alternate screen predict nothing there: the program owns every cell, not the prompt.

Snippets at your prompt

Commands you keep retyping live as snippets in your encrypted vault, the same vault your SSH hosts sync through. Manage them on the dashboard's snippets page: name, the command (several lines are fine), an optional note on when it is safe to run. In an open session a strip of your freshest snippets sits above the soft keys, or above the composer in an agent conversation, with "all" for the rest. Tapping one types the command into the input and stops there: Enter is yours, a multi-line snippet leaves its last line waiting at the prompt, and nothing ever runs on its own. Snippets are sealed in the browser before they leave it; the relay stores ciphertext only.

The same snippets are on the desktop, where Ctrl+K inserts one and a selection saves one, and on the phone, where the strip above the soft keys reads and inserts them but never creates or edits one. One vault, one list, the same rule everywhere: a snippet types itself and you press Enter.

Rendering and privacy

Decryption happens entirely in the page; the viewer subscribes to sealed frames and renders one terminal per pane, with the full split tree. The server relays ciphertext it cannot read - the same guarantee as the desktop (the security model).

A copy made by a program on the host lands on this tab's clipboard when this is the tab you are using, and the strip under the session bar says so (the clipboard follows you).

Reconnects

Every frame carries a sequence number and the viewer tracks the last one it rendered. When the connection drops - elevator, tunnel, sleep - the viewer asks the relay for everything since that sequence and replays the gap. If the relay can no longer serve that stretch, the viewer does a full resync instead of showing a corrupted in-between state.

That is this browser's own link. An SSH pane inside the session has a link of its own: the dashboard and the standalone viewer echo the focused pane's SSH state in the same words the phone uses, "reconnecting, 3 of 8", "disconnected" or "host key changed", and refuse what you type while that pane is not live (what each one means).

How far back the buffer goes

Scroll to the top of a session in the browser and a hairline row says which limit ended its history, in that limit's own number. The cloud keeps the newest 4096 frames of every session live, on every plan, so a free session's top row reads "history begins here, the last 4096 frames the cloud holds". A session that was never cut says so instead: "session started here, 11:40 today, nothing was cut".

Above that row, a strip says what the session's history is made of: how much is live tail, how much is archive, the depth your plan keeps and how long a dormant session is retained, all in real numbers. On a paid plan the archive segment is a button too: "load earlier" fetches the next older range, puts it in front of what is on screen and leaves your scroll position exactly where it was. When the archive runs out, the row says whether it reached the session's real start or stopped at the plan's depth bound.

The archive is sealed with the session's own key before it leaves your machine, exactly like the live stream, so the cloud stores a range it cannot read. One consequence is visible: if the session's key was rotated and this browser was not there for it, older ranges stay sealed. The viewer says so itself, names when the key rotated and which device last held it, and offers to open that stretch on that device. It never asks the relay to decrypt anything, because the relay could not.

Free accounts keep no archive: what the tail trims is deleted, and the strip says "no archive on Free" rather than implying more. Whatever your plan, nothing here changes what is on your own machine, where the desktop keeps its own copy.

When a session needs you

A pane that rings its bell or emits an OSC 9/777 notification while nobody is typing - typically an agent waiting on a question - pushes a notification to your browser. Enable it per browser or device on the dashboard's Devices page. The text is generic by design: the payload carries only the attention type and the session id, because the relay cannot read the session and so cannot quote it. At most one push per session per minute; tapping the notification opens that session when the browser already holds its key.

This is browser push. Notifications in the native mobile apps are on the roadmap, not shipped yet.

Tell me when this line appears

A pane watch is armed on the pane itself and lives in the daemon, so the same watch stands on the desktop, in this viewer and on the phone. In the viewer or in the dashboard's session view, open watch in the header and pick watch for a line: give the watch a name and something to look for, plain text (case insensitive) or a regular expression, once or keep watching. The daemon reads the pane's own output for that line, on bytes already on the machine. Nothing new crosses the wire in order to match, and the frame that arms it is the same one the desktop sends.

The name is the whole notification. When the line appears, this browser shows a notification with the watch's name, the session, the pane and the time since arming: never the line that matched, never a command, never a path. A keep-watching watch updates that one notification in place with a match count instead of stacking, and the daemon holds its re-fires to one a minute. The sheet lists every watch on the session with a disarm on each row, and what it lists is the daemon's word: a watch armed on the desktop shows up here, one disarmed here is gone on the desktop.

Refused before anything is sent: a regular expression that does not compile, with the reason under the field. Warned about, not refused: a pattern one or two characters long, which would match almost every line. A desktop older than pattern watches cannot arm one and stays silent; after ten seconds the sheet says so instead of showing a control that looks like it worked. The notification comes from the page holding the session open, so it needs that tab open, in the background or not; a push to a closed browser and to the lock screen is the phone's half of the feature, on the roadmap.

The demo session

The homepage's "Try a live session" button asks for a sandbox of your own and opens it at its own page, in this same viewer - the same relay, the same encryption, the same code path as your own sessions. Nothing about it is special-cased, which is the point: what you learn there transfers one-to-one. No session id is ever baked into the homepage; the button asks at the moment you click it.

  • Yours alone: clicking the button provisions a fresh sandbox for you specifically. Nobody else is in it, and nobody else can be.
  • Sandboxed: the demo shell runs as a locked-down user in a container with no network egress and a read-mostly playground directory.
  • Bounded: the sandbox ends after ten minutes and is torn down, nothing kept. Your own sessions have no timer: they survive reboots, closed laptops and lost Wi-Fi.
  • End to end encrypted anyway: the decryption key rides in the link fragment, exactly as it does for a session you share yourself. The relay routes ciphertext it cannot read, demo included.

Next: Teams