Usage¶
Renaming a session in this browser's session list also updates its open tab. The terminal stays connected, and the sidebar shows the new name once the machine's list refreshes. Reconnecting or reloading uses the new name. Renames made directly in tmux or another browser are not matched automatically to this browser's open tabs.
Rename in session list -> same open terminal -> new name
If you rename a machine, the machine menu shows only your chosen name. Hover over that name with a mouse to see the original name in a tooltip.
Rename machine -> chosen name in menu -> hover -> original name
Details in a machine's ⋯ menu shows what roamd knows about it: its original name (handy on a phone, where there's no hover), whether it's online and since when, its roamd version and its identity. If you set it up over SSH from the web app and ticked Save these login details in my vault, the login is there too: address, username and the password or key, which stay hidden until you press the eye button. Each value has a copy button. Forget saved login removes it (you're asked first), and removing the machine removes it too. It isn't on the main screen: you only see it when you open it.
machine -> ⋯ -> Details -> name, status, version, identity
-> saved SSH login (if ticked) -> eye shows the secret
The machine menu remembers the last known sessions in this browser. Reloading or waiting for a report keeps their names visible at normal brightness, with a small hollow grey ring in the existing indicator space. A confirmed report restores the activity indicator and removes sessions that have ended. No new report does not mean no sessions. A browser with no remembered list must receive its first report.
Session status comes from the remote computer, encrypted through the hub. When the hub restarts (an update, say), it keeps each computer's last report, still encrypted, so session lists stay in place instead of waiting for every computer to report again. A steady amber dot means the remote computer reports that input is required. Idle sessions have a filled grey dot; working sessions have green bars. The amber dot stays in the same indicator space in the menu, tabs and session picker. A hollow amber dot means "Session may need you": the remote computer reported that work stopped or an agent turn ended, and you have not viewed it yet. It is a hint, not proof that the agent needs a reply. Viewing the session or marking its notification read clears it across devices when they sync. Working, input-required and unconfirmed status take precedence. Reloading alone does not create this marker.
The browser tab's roamd icon reflects the amber indicators on your open session tabs. Closed, paused or disconnected tabs do not contribute. Solid amber takes priority when an open tab shows input required; otherwise, hollow amber marks an unread completion. Other states leave the normal icon. Viewing a completed session clears its marker when read state syncs; another session may still need attention. Locking roamd restores the normal icon. The installed app's home-screen icon does not change.
The computer alone decides each session's light, and the browser never times it out. A faint hollow grey ring shows only while the computer is offline, or before this page has heard from it. Opening, closing or resizing a terminal doesn't change a light, and an old working snapshot cannot trigger a completion notification. See Notifications for delivery, offline dismissals and tests.
Remote observation -> encrypted report -> hub -> browser status
Remembered names -> hollow rings -> confirmed activity indicators
Unread completion + idle -> hollow amber -> view/read -> grey idle
Open tab lights -> input required > unread completion > normal logo
Finding your way¶
On phones and tablets, open the hamburger menu for the notification and installation offers. They stay there when a session opens. On desktop, these notices stay below the header, above the session. An offline warning stays on the page on every screen. When a new web version has downloaded, its reload icon is in the hamburger menu on phones and tablets, or at the right of the desktop header. This includes the installed iPad and iPhone app. roamd checks when you return to it, when the connection returns, and periodically while it stays open. Tap the icon when ready to reload.
new version downloaded -> menu (phone/tablet) -> reload icon
-> header (desktop) -> reload icon
Notifications can always be managed from Account → Notifications. Turning them on opens a short explanation, then asks for browser permission and shows optional agent setup. Open Notifications in the main menu for recent alerts, shared read state and a delivery test. Updated machines automatically send Session may need you when a busy session stops, and Input required for a recognised prompt. Consuming an alert expires it across your account. The setup guide explains optional hooks that improve detection; those hooks need approval inside the agent.
phone / tablet -> hamburger menu -> notification / install offer
desktop -> below header -> notice above the session
offline -> page warning -> reconnect
The roamd logo appears beside the app name in the header and in the phone's machine menu. A blue background and underline mark your current session and selected menu section. Mobile keeps the same compact terminal row and white floating session button. The session count sits inside that white circle, below its icon, so it stays readable. Enter above the floating button sends Enter to the current terminal and dismisses the on-screen keyboard. Return on the iPad, iPhone or Android keyboard also sends Enter and dismisses the keyboard. Tap the terminal to bring the keyboard back, including after returning to a session whose cursor is already active. This preserves the text you were entering. Scrolling, selecting text and opening links do not request the keyboard.
Return to session -> keyboard closed -> tap terminal -> resume typing
logo + roamd -> choose a section -> blue marks the current section
session tabs -> choose a session -> blue marks the current session
Enter above floating picker -> run current command
The top right of a session keeps only three buttons: Files (the folder), Conversation view and Split view (the grid). Everything else is in the ⋯ menu beside them: how busy the machine is, the session's notifications switch, Pause this session and, in red, End this session. While a session is disconnected, Reconnect shows as a button too, so getting back never needs the menu.
[folder] [Conversation] [grid] [⋯]
|
v
+-----------------------------+
| home-pc: work [pin] |
| Codex · model |
| Context 40k / 200k (20%) |
| Session limit 24% |
|-----------------------------|
| Resources: CPU 12% Mem 40% |
| Turn off notifications |
| Pause this session |
| End this session |
+-----------------------------+
For an AI agent's session, the top of the ⋯ menu shows its usage. Pin stats keeps
the menu open while you type or change sessions; Unpin stats lets a tap outside
close it again. It follows the selected session. Phone figures use a smaller layout.
The figures are short on purpose: the model, how full the context is now (used out of the
window, with a percent), and how much of each usage limit the agent reports is used, named
Session limit, Day limit or Week limit by how long the limit lasts. It never
shows lifetime totals. A dash means the agent did not report that figure, not zero. Figures
update after the agent saves them. Claude Code saves its current context but gives its
window size and limit percentages only to a status line. For a Claude Code session the box
therefore has one switch, Show Claude's limits and window. Turned on, roamd adds a
status line to Claude Code's settings on that computer, and the window and limits appear
after Claude's next reply. A status line you already had keeps showing, and turning the
switch off puts it back exactly as it was. Only an owner can use the switch. Codex already shows
its limits without it, and they match Codex's own /status: roamd asks Codex for the current
figure when it first sees a Codex session, then about every 5 minutes while you are using
it. A session left idle is not asked again. Under the limits the box says how long ago they
were read, such as "Limits updated 4 minutes ago". If Codex cannot answer, the figure from
its last reply is shown instead, with its age. Gemini CLI, Copilot and Cursor have no status-line command to use,
so the box shows what they save and the switch is not offered for them.
The machine needs an updated roamd before it can supply usage. Shared sessions do not show
the owner's usage. Cursor currently supplies no readable live token figures; Copilot's
saved records may omit usage. Codex is matched to its open conversation, including resumed
sessions and sessions sharing a folder. If that file cannot be identified, ambiguous
conversations may show dashes rather than another conversation's numbers. Costs appear only when the app records dollar costs.
current session -> ⋯ -> usage at the top -> pin -> stays open
switch session -> same menu shows the new session's figures
unpin + tap outside -> closes
Opening a terminal¶
One connection to the machine does the whole job, because each one costs a full handshake (several round trips). The machine also keeps one stream to the hub ready in advance, so it does not have to connect back when you arrive.
machine hub browser
------- --- -------
keeps a spare stream -------> holds it, pings it
<----------- open a connection
"use" ----> machine
"ready" --------------------> (alive: splice it to the browser)
connects the next spare handshake + login (encrypted end to end)
list tmux sessions (same connection)
[picker: restore setting asked on it too]
person chooses
open the terminal on that connection
- Spare stream. The machine connects a spare stream, signed in the same way as its other streams. The
hub keeps one per machine, answering pings, and hands it to the next session: it sends
use, and the machine saysreadyand serves SSH on it, then connects another. Thereadyalso proves the connection is alive. If it does not come within a short wait, or the spare had closed, the hub asks the machine to connect back the usual way ({"open": ...}), so a stale spare costs a moment, never a failed session. A hub without the spare path (older) answers 404 and the machine tries again only now and then; an older machine never offers one. A spare counts against the machine's limit on open streams only once used. - One connection per tab.
tmuxSessionsKept(roamd-web) returns aListing: the sessions plus the open connection.connecttakes it and opens the shell on it; if it went stale while the person was choosing,connectquietly makes a fresh one. A listing not used (cancelled, or the session is already open in another tab) is closed in afinally. Other actions in the picker (rename, end, tidy up, change the restore setting) still use short connections of their own. - No Nagle. The hub accepts connections with
TCP_NODELAY(roamd_hub::no_delay), so a typed key and its echo are sent at once. - Tests.
connect-once.spec.ts(one connection per opened tab),crates/hub/tests/nodelay.rs, the hub's spare-stream tests (used with no dial-back; a silent or closed spare falls back; only the machine can offer one) and the daemon's (a spare is kept ready and renewed; an older hub is not hammered).
Roamd's own terminal¶
The terminal is roamd's own. It reads what the machine sends, keeps the screen, and draws every line itself: colours, bold, italic, underline, the cursor, search matches (the one you are on is brighter) and the line you select with a long press. No ready-made terminal component is on the page.
Typing, pasting, the wheel and touch scrolling all work as before. A program that asks for the mouse, such as tmux with its mouse mode on, is told about clicks and the wheel.
To select text, drag across it with the mouse. Double-click takes a word, triple-click a whole line, and dragging past the top or bottom scrolls. If the program on the machine is using the mouse (tmux with its mouse mode on), hold Shift while you drag, or let tmux do the selecting. On a touch screen, press and hold a line, then drag the two round handles to change what is chosen. While something is selected, a solid outlined toolbar shows at the top right of the terminal. The large blue Copy button copies the text; Clear removes the selection and leaves the terminal text in place. Selection follows the letters shown on the screen, including when the pane is scaled.
select text -> top-right toolbar -> Copy -> clipboard
-> Clear -> selection removed
On mobile, long session titles shorten to leave space for the dropdown arrow. Their spoken labels include the short label and full name, so voice control can match the words on screen.
Conversation view beside Files replaces the terminal area with readable, selectable text. It belongs to that session and leaves the terminal running. This view is available for saved sessions on machines you own.
- AI agent output shows the running Claude Code or Codex conversation, with formatted replies and expandable tool details. It follows the agent's own record; replies appear as the agent writes them, not necessarily a letter at a time.
- tmux output shows the screen and recent text kept by the terminal. Scroll up for older text. A program's own history may contain more than the terminal keeps.
- Write in the box at the bottom and press the send arrow. On a desktop, Enter sends and Shift+Enter adds a line. On a touch screen, Enter adds a line; use the arrow to send. Ctrl/Cmd+Enter also sends. The square button stops the current work in the terminal or the Codex conversation you chose.
- Select and copy text normally. New output follows automatically while you are at the bottom. Scroll up to read without being pulled back down; the down-arrow button returns to the newest output.
- The terminal icon returns to the full terminal, including permission questions and other interactive controls. AI mode asks you to use it for a detected prompt that needs a decision. roamd does not approve anything automatically.
Each session keeps its view choice, text draft and reading position while you switch tabs. Closing a session, reloading or locking clears these from the page; drafts are not saved. If a send cannot be confirmed, the draft stays. Check the terminal before sending it again. roamd never retries a message automatically.
Both modes use the encrypted connection already open to the computer. The computer needs a roamd version with Conversation view. Claude identifies its running conversation; Codex uses its roamd notification hooks, or on Linux its open conversation file. If the exact conversation cannot be identified, use tmux output. If Codex uses a shared background server, use Choose conversation in the view's top bar. Pick the open conversation by its name, folder and ID. Opening the picker does not start a conversation or send a message. The caption names where Send and Stop will go; both target that chosen conversation, even if the terminal is displaying another one. tmux output still controls the terminal itself. The picker needs Codex's local background server and a saved conversation record; conversations kept only in memory cannot be opened here. The choice stays with this session across reconnects and browser reloads, until its agent process ends or you choose Use automatic linking. If you switch chats in Codex, choose again here. Shared servers do not report each terminal's displayed chat. On Mac and Windows, automatic Codex linking can use approved roamd hooks when they run under the terminal's agent. Shared-server hooks cannot identify the correct pane. The view does not choose a conversation just because it is recent or in the same folder. Hidden instructions and reasoning are excluded. Visible messages and tool details are shown without the separate History page's secret masking, so select carefully before copying.
session -> Conversation view -> AI / tmux text
choose -> open Codex conversation -> named destination
write -> send arrow -> terminal or chosen conversation
scroll up -> older text; down arrow -> newest text
terminal icon -> full terminal, still running
Links. A web address (http or https) in the output is a link. On a computer, hold Ctrl
(Cmd on a Mac) and the link under the pointer is underlined; click it to open it in a new tab. A
plain click still selects, or goes to tmux. On a touch screen, tap a link to open it. A link that
wraps onto the next line is still one link, and a full stop or comma after it is left out.
Projects¶
A project is a plain label on a session, such as app, server or game-server, whichever
machine the session is on. Use it to see one piece of work together.
session row: [ web ● ] [tag] --> type a name (or pick one in use) --> Save project
empty + Save, or "Remove from project": takes it out
any project exists? no --> nothing extra on screen
yes --> chips above the machines: [All] [app] [server]
tap one: only its sessions show, from every machine
(a machine with none in it is hidden); tap All to go back
- One project per session. There is no group page and no dragging. The tag button on each session row is always there on a touch screen, and appears on a row you point at or focus with a mouse or keyboard. Machine groups (Home, Work…) are separate: they sort machines.
- Kept private. Labels are saved in the encrypted vault under one item
(
session-projects:"<machine id>/<session name>"to the label), so the hub never sees them and every browser of the account shows the same ones. A label is up to 32 characters. - Renaming. Renaming a session in roamd's session list moves its label with it. Renaming in tmux itself leaves the old name behind; label the session again. Plain shells are not listed sessions and cannot be labelled.
- Filtering. The chosen chip is kept only on this page, not saved. If the last session in a project is relabelled, its chip goes and the filter clears.
- Tests.
projects.spec.ts(chips only once one exists; two machines filtered together; stored encrypted and kept across a reload; removal; rename; a phone-width layout).
The machines list¶
On a wide screen the list of machines sits to the left of the terminal. Drag its right edge to make it wider or narrower.
point at the list's right edge --> a thin line shows
drag left / right --> narrower / wider (kept between a
sensible minimum and half the window)
double-click the edge --> back to the usual width
keyboard: Tab to the edge, then
Left / Right (Shift: bigger steps), Home (narrowest), End (widest)
- The width is remembered in this browser only.
- The arrow beside "Machines" folds the list to a slim rail; opening it again keeps your width.
- On phones and tablets (including landscape), tap the hamburger beside the current session to open the machines drawer. Account, Activity, Docs and the other sections are in this drawer too. There is no separate top navigation row while using a terminal. The selected tab keeps its coloured fill and bold underline after tapping or switching sessions, including while its picker is open.
On phones and compact tablets, the four arrow keys sit in a D-pad above the floating session picker. Tap them to move the terminal cursor or navigate command history. They leave the keyboard as it is: closed stays closed, and open stays open. They are disabled while the session cannot accept input. With the keyboard open, the pad moves to the left of Enter so both controls fit. The keyboard row keeps its other keys; on desktop it also keeps its arrows. On phones, the arrows sit closer together and Enter and the session picker are slightly smaller. Tablets keep their usual sizes.
Up
Left Right
Down
Enter
session picker
- On wide desktop screens, session tabs stay together by machine; see Arrange session tabs below. Tablets using compact navigation keep individual session titles. Tap a title to switch; scroll the row sideways if needed. Phones, narrow screens and portrait tablets show several short tabs in the same compact row: the first two machine letters, a colon and the first three session letters (for example AS:cod). Swipe the tabs sideways when needed; a thin line above the titles shows your position in the row. Tap another tab to switch. The picker shows full names and close buttons. Tap Open sessions in the hamburger menu to return straight to your selected session. Opening a terminal also updates the page address, so refreshing stays on sessions instead of returning to Account or another page. Tap the current session title on smaller screens or the half-transparent white round button on the right to see all terminals open in this browser, switch to one or close its tab. The right-hand controls sit closer together on narrow screens to leave more room for the name. Closing the active tab asks you to confirm first. Cancel keeps it open; confirming closes the tab and leaves its tmux session running on the machine. Tick Never ask again before confirming to skip this question in future. The choice stays in this browser or installed app, including after a refresh. Cancelling does not save it. Clearing this site’s browser data brings the question back. End session still asks before stopping the session.
small screen: [menu] [A:co] [M:sh] > [controls]
large tablet: [menu] [session 1] [session 2] [controls]
terminal content
[sessions]
menu -> machines, Account, Activity, Docs, ...
sessions -> choose an open terminal
menu -> Open sessions -> selected terminal -> refresh -> sessions
Plans¶
Account → Plans shows your plan. Free lets you use 2 machines and have 2 terminals open at once. Paid has no limit on either and adds snippets and sharing with limits (watch only, time limits, per-share permissions). Pick monthly, yearly or lifetime and you pay on Stripe's page; roamd never sees your card. Manage plan opens Stripe's page to change your card or cancel. A cancelled plan stays on until the period you paid for ends. On monthly or yearly, Switch to lifetime buys lifetime and your subscription stops renewing.
Choose plan -> Stripe's page -> pay -> back to Plans -> "Confirming your payment…"
-> taking longer? the page says so
and keeps checking
-> Paid
If your plan ends with more than 2 terminals open, the ones you used least recently close. Only the page's connection ends: the tmux session keeps running on your computer, so you can open it again. Machines past the first 2 you paired are not deleted: they stay in the list, and open again when you renew. Activity keeps the last 30 days and newest 100 events on Free, and a year on Paid.
Snippets¶
Snippets are commands you save once and type with one tap. Press Snippets at the foot of the page (behind More on a phone) and they open in a window over the terminal.
+-----------------------------------------------+
| Snippets [x] |
| [ Filter ] | type to narrow the list
| (Claude Code) (Mine) | two tabs: the app's commands, yours
| Permissions [Insert] [Run] (pencil) |
| /permissions |
| Compact conversation [Insert] [Run] (pencil) |
| /compact |
| [+ Add a snippet] |
+-----------------------------------------------+
- Filter matches a snippet's name or its command, in the tab you're on. If only the other tab has a match, it says how many; tap that to switch. With a keyboard, the cursor starts in the filter; press the down arrow to reach the list.
- Insert types the command without pressing Enter. Run types it and presses Enter. Either one closes the window, so you are back at the terminal.
- A command that looks risky (deleting files,
sudoand the like) only has Insert, so you read it before you run it. - When an AI app is running in the session (Claude Code, Codex and others), there are two tabs: the app's commands and Mine, your own. The window opens on the tab you used last (remembered in this browser). Adding a snippet takes you to Mine. With no app running, you just see your own.
- While roamd is still finding out which app is running, or fetching its commands, the window says Loading… beside small moving bars instead of an empty list.
- The pencil changes a snippet. Inside it, Delete removes one of yours, and Hide takes one of the app's commands off the list. Restore (beside the tabs, on the app's tab) puts the app's commands back as they were.
- Escape, the close button or a tap outside the window closes it.
- Snippets are kept encrypted in your account, but they are typed in plain sight, so don't keep passwords in them.
Compose¶
Compose is a big box to write in when the terminal's own line is awkward: a long prompt for an AI app, or a command you want to edit properly. Press Compose at the foot of the page (behind More on a phone) and a sheet slides up from the bottom.
+-----------------------------------------------+
| Compose [x] |
| +-------------------------------------------+ |
| | Write here, then send it to the terminal | | tap or click anywhere to put the
| | | | cursor there, select, paste, edit
| +-------------------------------------------+ |
| [Dictate] [Send] [Send and run] |
+-----------------------------------------------+
- For long dictation with your phone keyboard's microphone, Compose lets you review the whole phrase before sending it. Direct terminal dictation sends each finished phrase once; its preview stays within the terminal width.
- It is an ordinary text box, so you tap or click to place the cursor, select words, paste, and use your keyboard's own spelling help and microphone.
- Send types what you wrote on the terminal without pressing Enter, so you can still check it there. Send and run types it and presses Enter. Either one closes the sheet and clears the box.
- Dictate opens this sheet, from the voice buttons or from the sheet itself. Your words go into the box at the cursor while you talk, not onto the terminal. The last few words are a guess and may change until you pause. Tap Dictate again to stop, then send. Closing the sheet stops listening too; what you'd said is kept in the box.
- Closing the sheet with the X or Escape keeps what you wrote; opening it again shows it.
- AI app running (Claude Code, Codex and others): line breaks are kept, and the text goes in as one paste, so nothing runs until you say.
- Plain shell: a line break would run the line before it, so the lines are joined with spaces, and the sheet says so.
- Risky commands: if you press Send and run on something that looks risky (deleting
files,
sudoand the like) in a plain shell, the sheet asks Run anyway? first. Tick Do not remind me again on this browser and it never asks again in that browser. With Cancel your text stays in the box. An AI app's prompts are never asked about.
Arrange session tabs¶
On wide screens, sessions from the same machine sit next to each other. Drag a tab to change their order. Within a machine, you move one session. Across machines, you move the whole machine group, keeping its sessions together.
With more than five sessions open in total, machines with several sessions fold into one tab. For example, roamy +2 means three open sessions on roamy. Hover over the machine, click it, or focus it and press a vertical arrow key to see its sessions. Hovering does not switch terminals. Choose a session to use it; its usual status and close button are in the list. The selected machine stays highlighted. A session waiting for you still lights up its machine tab.
Up to five: [roamy: one] [roamy: two] [other: work]
Over five: [roamy +2] [other +2]
|
+-- one
+-- two drag to change session order
+-- three
Drag machine tabs -> change machine order
You can also right-click a session or machine tab and choose Move earlier or Move later. With a tab focused, Alt+Shift+Arrow keys move it; the keyboard's menu key or Shift+F10 opens the same move buttons. In a session list, ordinary up/down arrows move focus; Escape closes it.
Reordering keeps the same terminal selected and does not restart anything or discard file edits. The order is saved with this browser's open sessions and restored after a refresh or unlock. If saving is unavailable, it may not be remembered. Plain shell tabs are still not restored after a refresh.
Phones and tablets using compact navigation keep their existing individual tabs and session picker, without dragging or folding into machine tabs.
Side by side¶
On a wide screen with two or more tabs open, the split button on the tab bar shows them side by side. The one you're typing into has a blue edge; the others are dimmed a little, still readable, just less distracting. Click one to type into it. Split view, like the tabs themselves, comes back after a reload.
+----------------------+----------------------+
| home-pc: work | home-pc: build |
| (you type here, | (dimmed a little) |
| blue edge) | |
+----------------------+----------------------+
How busy a machine is¶
Activity has a Resources page, beside Events and History. Pick a computer to see its last 4 hours as charts of CPU, memory and network (in and out), with the computer's disks and how full each is underneath. It updates every 10 seconds while it's open.
The ⋯ menu of the tab you're looking at also says that computer's CPU and memory now. Choose that line to open Resources for it.
+-------------------------------------------+
| home-pc: work x [folder] [Aa] [grid] [⋯]
+-------------------------------------------+
| ⋯ > Resources: CPU 12% Mem 40%
| click or tap (or Activity > Resources)
v
+-------------------------------------------+
| Events History [Resources] |
| [home-pc v] |
| CPU 12% | Memory 3.1 GB of 8 GB|
| ___/\____/\__ | ______________/------ |
| Network in 2 KB/s, out 1 KB/s |
| Disks: / [=====-----] 40 GB of 100 GB |
+-------------------------------------------+
- Your machine takes a reading every 10 seconds, whether you're looking or not, and keeps the last 4 hours. It keeps them in memory only: if roamd restarts, the charts start again.
- Only the tab you're looking at asks for new readings, and the Resources page while it's open. A tab you switch to catches up at once, so the charts have no gaps.
- Only the machine's owners see this. Someone you share a machine with doesn't.
- A machine still on an older roamd shows nothing here until it is updated.
- The figures match the machine's own monitor: Task Manager on Windows, Activity Monitor on a
Mac, and
toporfreeon Linux. Network counts the real network adapters only, so a VPN doesn't count the same traffic twice.
Read and edit files mentioned by an agent¶
Hold Cmd on a Mac, or Ctrl on another computer, and click a file path in
the terminal. On a tablet or phone, tap it. Paths such as docs/README.md open
on the session's own computer. Paths with spaces need quotation marks in the output.
agent prints a file path -> click or tap
desktop -> reader beside the terminal <-> expand icon -> modal
tablet or phone -> full-screen reader -> Close returns
Each session has its own reader. Switching sessions hides the old reader and shows the new session's reader if one was open. Returning restores the file, scroll position, edit or preview mode and unsaved edits. The header names the computer and session. This also applies to the full-screen reader on tablets and phones.
On desktop, drag the reader's inner edge to resize it, or focus that edge and use the left and right arrow keys. Double-click the edge to reset its width. The expand icon opens a larger window over the terminal; the side-panel icon puts it back. Each session's open file, width and desktop layout are remembered after a reload or unlock, in this browser. The file is read again from the computer, so you see its latest saved contents. Closing the reader stops it reopening. Tablets and phones keep the full-screen reader.
The small icon buttons have tooltips showing their actions. The path fits on one line; hover to see it all, or focus it with the keyboard to expand it. Scrolling down reduces the header to the filename and essential controls. Scroll up to bring back the computer, session, path and extra actions. Unsaved-edit and error messages stay visible.
Drag inner edge -> adjust reader width
Expand icon -> modal -> side-panel icon -> beside terminal
Scroll down -> small header -> scroll up -> full header
session A + its reader -> session B + its own reader (if open)
return to A -> restore A's reader and draft
reload / unlock -> reopen remembered file -> read latest saved text
Markdown opens as a document with headings, tables and links. Other text opens with line numbers. Edit switches to a text editor; Preview shows your current draft. Save writes your edits to the computer. Copy and Download also work on unsaved edits. Refresh reads the file again. Closing, refreshing or following another file asks before discarding unsaved edits. A browser refresh warns too; drafts are only held in memory and are cleared on reload or when roamd locks. The remembered file path and layout are encrypted in your vault. Unsaved edits, editing mode and scroll position are not restored after reload or unlock. Switching sessions does not discard edits or ask a question. Closing a session tab with a draft offers Save, Yes, discard edits or Cancel, even if you chose Never ask again for ordinary tab closes. If saving fails, the tab and draft stay open. A successful save closes only that session's tab.
This reader is for saved sessions on computers you own. It opens small text files
anywhere the account running roamd can read, including other folders. Use a full
path, or a relative path such as ../notes.md from the session's current folder.
Links to files in other folders work too. Saving also requires permission to write
the file and its folder. The computer needs an updated roamd daemon for access
outside the session folder; updating the web app alone does not add it.
A relative path printed before the session changed folders may no longer resolve.
Images and embedded web content do not load in Markdown. Links to other documents
open in the reader; web links open a separate tab.
If the agent changes the file before you save, roamd keeps your draft and asks you to copy it, then refresh. Avoid editing the same file at the same time as the agent: a change at the exact moment of saving can still race the final write.
Files¶
The folder button on the tab bar opens the files panel for the machine in that tab. On a computer it slides in at the right, over the terminal; on a phone it fills the screen. It opens in the folder the terminal is in, every time it opens, even if you have since moved to another folder.
+-----------------------------------------------+
| Files on home-pc [x] |
| Transfers: uploaded files and saved paths |
| [ Upload ] [new folder][choose][R] | Upload is the main (blue) button
| Apple: [Files]; other phones: +Photos/Camera |
| [home][term][up] / home / me / project [edit] | tap a folder name to go there
| [ Filter by name ] [ Name v ] [order] |
| 12 items |
| +-------------------------------------------+ |
| | [folder] src ... | | tap a folder to open it
| | [file] notes.md 2 KB, 17 Oct ... | | tap a file to open it
| +-------------------------------------------+ |
+-----------------------------------------------+
- Upload sends files into the folder you're looking at. On a computer you can also drop files on the list. Files dropped on the terminal, or pasted images, go to the machine's usual folder instead.
- Confirming an upload: before sending, roamd lists the files and destination computer and folder. Choose Upload to send them or Cancel to send nothing. Clicking outside the confirmation also cancels without saving the checkbox. One question covers files dropped or selected together, including pasted images. Tick Never ask again before choosing Upload to skip future questions in this browser or installed app. Leaving it unticked keeps the question on. Cancelling never saves the tick. The choice stays on this browser; clearing its site data brings the question back.
choose / drop / paste -> confirm files + destinations
| Cancel -> nothing sent
| Upload -> send files
+ Never ask again -> remember on this browser
- On iPhone and iPad, Files opens Apple's chooser, which already offers files, photos and the camera. Other touch devices keep the separate Photos and Camera buttons.
- Upload progress and confirmation appear at the top, below the Files heading. Switching terminals shows that terminal's transfers. Completed uploads include their saved paths, even after you browse to another folder.
- Closing Files: click outside the panel, or use its close button. Clicking outside a file preview closes the preview and leaves the file list open. The panel and upload confirmation use the same blue and charcoal accents as the navigation.
outside file list -> close Files
outside preview -> close preview, keep Files
outside upload question -> cancel, keep Files
- Getting around: the house goes to your home folder, the terminal button goes back to the terminal's folder, and the arrow goes up one folder. Tap any folder in the path to jump to it, or the pencil to type a path (a folder or a file).
- Opening a file shows a preview (pictures, videos, sounds and text), its size and path, and what you can do with it:
+---------------------------------------------+
| notes.md [x] |
| +-----------------------------------------+ |
| | # Notes | | a preview
| | ... | |
| +-----------------------------------------+ |
| 2 KB, changed 17 Oct 2026 |
| /home/me/project/notes.md [copy] |
| [ Download ] ( Insert path ) Rename Delete |
+---------------------------------------------+
- Insert path types the file's path at the prompt, ready for a command or an AI agent.
- Delete asks first, then moves the file to the machine's Bin (Recycle Bin or Trash), so you can get it back from there. roamd never deletes anything for good.
- A folder's ... button offers Open, Download as zip, Rename and Delete.
- Choosing several: tap the choose button, or press and hold a row on a phone. Then tick what you want and Download them as one zip, or delete them together.
- Long folders scroll smoothly; more is fetched as you reach the end. The filter looks through the whole folder, not only what has come so far.
- A guest's uploads always go to the machine's usual folder, and a guest who can only watch can't rename or delete anything.
Reconnecting¶
Each tab does this on its own.
connection ends without Disconnect or a lock
|
tmux session? --no--> "A plain shell can't be rejoined..."
| yes (Reconnect starts a new one)
v
browser offline? --yes--> "Offline. Reconnects when the network is back."
| no (no tries spent; online: start again)
v
try now, then twice more <-- online / page visible: try now
|
v
list sessions: "work" still there? (one connection: kept, and
no --> "The tmux session ... is no longer running..." used for the terminal)
yes --> join "work" on that same connection
|
refused --+--> warning, stop fails --> next try;
ok --> the line goes away after the third: "Couldn't reach..."
Coming back to the page. A sleeping phone or a hidden tab can lose its connection without the page being told. When you come back, the page asks each open terminal a tiny question that roamd answers itself (no program is started, so a Windows computer answers as quickly as any). If there is no answer in time (about a second, longer for a computer that is always slow to answer, such as one behind a VPN: the page learns each computer's usual speed), it treats the connection as dead and rejoins your tmux session straight away, with no extra pause. Anything typed before then never arrived, so retype it once the screen is live again.
Checks started before the app goes into the background are cancelled. On return, the page makes one fresh check per connection. If it needs to reconnect, the terminal stays clear briefly while recovery runs; the haze appears if recovery continues. Typing stays blocked whenever that terminal is disconnected or reconnecting. A failed recovery still shows its warning.
app hidden -> cancel old checks
app returns -> fresh check -> recover briefly without haze
-> still reconnecting -> show haze
Typing during reconnection. Input is blocked until that panel is connected again, including paste and dictation. Anything entered while reconnecting is discarded, so retype it once connected. Other panels that are connected still accept typing.
connection lost -> input blocked and pending text discarded
connected again -> fresh typing accepted
Not connected. While a tab is disconnected or trying to reconnect, its screen drains of colour and softens, with a faint grain over it and a sheen that crosses it every few seconds, in step with the other moving lights. The old text can still be read, selected and scrolled. It returns to full colour as soon as the tab is connected again. If your device is set to reduce motion, the screen is dimmed and still.
Session status¶
machine hub browser
------- --- -------
look at tmux sessions
judge: unknown/waiting/working/idle
seal to each owner key
sign with the host key
|
+-- sealed envelopes --> keeps the latest --+
(cannot read it) |
v
pushed down the page's line
(or fetched, if it is down)
check: signed by the machine
approved here? newer? recent?
open with the browser key
show on the card, title, sort
The machine sends a report as soon as something changes, and otherwise now and then to show it is still current. The hub passes each one straight down to the open pages allowed to see it (see Keeping your page up to date), so a session that starts waiting for you shows within a moment. The browser never compares the machine's clock with its own: the machine's time only orders that machine's reports (an older one is ignored, even after a reload), and the browser times how long a report has gone unrenewed by its own clock. When it is no longer current, names remain visible with a faint hollow grey ring instead of an activity claim.
The browser saves the encrypted, machine-signed report locally, scoped to its vault's browser key. After unlocking, it verifies the cached report against the currently approved machine before restoring names. Remembered activity is unconfirmed until a current report is received; restoring names cannot generate a completion notification. Removed machines, revoked status access and session-sharing restrictions also apply to remembered entries. Locking clears decrypted names from memory; encrypted reports remain for the next unlock.
The report carries the session names, their state and one short last line of each screen (text that looks like a password or key is hidden first). The hub holds only the sealed bytes, when they arrived, and their size.
Notifications¶
The account-wide inbox tracks automatic busy-to-idle/input transitions and optional completed-turn hook events separately from delivery, read and handled state. See the notification design and setup guide. The existing generic push path below remains for older machines; updated machines supply deduplicated events through the inbox.
machine hub push service browser
------- --- ------------ -------
a session starts
waiting for you
|
+-- "knock" --> owner muted this
machine? no
|
+-- empty, signed --> forwards --> service worker
push (VAPID) shows "Something
needs you."
The hub contacts only public internet addresses for push (looked up, then pinned for the call, never following redirects), because the addresses come from browsers. A push service that says an address is gone has it forgotten.
Language¶
roamd speaks 19 languages: English, Spanish, Simplified and Traditional Chinese, French, Italian, German, Portuguese (Brazil), Japanese, Korean, Russian, Ukrainian, Polish, Dutch, Turkish, Indonesian, Vietnamese, Hindi and Arabic.
- The web page starts in your browser's language. To pick another, use Account → Language, or the list under the login form. The choice is kept on that browser.
- Emails from roamd come in the language you picked last.
- roamd on your computer (its messages and the install script) follows the computer's own
language setting. To choose another, set
ROAMD_LANGUAGE(for exampleROAMD_LANGUAGE=fr). - In Arabic the page reads right to left. Terminals stay left to right.
- Commands, file names and whatever your computers show are never translated. Voice commands ("run it", "cancel") are still said in English.
browser language ──► page language ◄── Account → Language
│
└──► the hub, for your emails
computer's language ──► roamd's messages, install script
roamd commands¶
roamd login [--hub URL] [--name NAME] [--no-keyring]
[--config DIR] [--allow-root]
roamd run [--config DIR] [--allow-root]
roamd keys [list] [--config DIR]
roamd keys revoke FINGERPRINT [--config DIR]
roamd logout [--config DIR]
roamd uninstall [--yes] [--config DIR] [--allow-root]
roamd service install|uninstall|status [--config DIR]
[--allow-root]
roamd tmux-restore [on [--no-screen-text]|off|status]
[--config DIR]
roamd history [on|off|clear|status] [--config DIR]
roamd install-over-ssh [--hub URL] [--name NAME] [--allow-root]
USER@SERVER [-- SSH OPTIONS]
roamd help
roamd version
loginends by runningservice install, so the machine stays connected withoutroamd runby hand. If that step fails, login is still complete: it prints the reason and the commands to retry, and exits normally.uninstallruns, in order:logout(skipped if not registered),service uninstall,tmux-restore off(skipped where the machine's own tmux setup manages those add-ons), deletes the settings folder, then the program. A failed step is reported and the rest still run. It deletes the settings folder only if it contains roamd's own files, so a mistyped--configcan't remove someone else's folder. On Windows a running program can't delete itself, so the last step prints the path to delete by hand.tmux-restoreturns bringing tmux sessions back after a restart on or off (the tmux-resurrect and tmux-continuum plugins), or shows how it's set.historycontrols AI history on this machine:on,off(stops keeping new entries),clear(deletes all of it) orstatus(the default). History is what you asked Claude Code in your tmux sessions and its final answers, copied from Claude Code's own record with anything that looks like a secret hidden, kept for a year in thehistoryfolder of roamd's settings (owner-only files, each kept to a limited size). It is read only by your own browser, over the encrypted connection; the hub never holds it.install-over-sshsets roamd up on another machine through the local OpenSSH client (see Installation).version(also--versionor-V) printsroamdand the installed version.-
--allow-rootis the user's choice to run roamd as root or Administrator: every approved browser then has full control of the machine.service install --allow-rootinstalls a system service that starts at boot (see Installation). -
keys revokerewrites the approved-keys list. The running daemon re-reads it on every login attempt, so the change is immediate, and a connection already logged in with that key can't open new terminals. An owner's terminals already open stay open until closed (known gap); a guest's are cut. logoutasks the hub to delete this machine, then deletes its registration and its list of approved browser keys (the host key is kept). If the hub is unreachable, nothing is changed locally.- A running
roamd runwhose token the hub refuses exits with "this machine was removed from its account", instead of retrying forever. login --no-keyring(Linux) keeps the host key and device token in owner-only files even when a desktop keyring is available. Without it,loginseals them with a key kept in the desktop keyring when one is running and unlocked (see Safety on your machines).
Replacing the browser key¶
browser machine (roamd)
------- ---------------
save old key as "retired" (+ machines)
save new key as the browser key
SSH login with OLD key, pinned host ---> old key approved? yes
key-replacement request, new pubkey ---> replace old with new
in the approved keys
<--- ok (logged: key replaced
old -> new)
drop the machine from the retired list
- A machine only lets a key replace itself, and only while that key is still approved. It gives no power a logged-in key doesn't already have (a shell could edit the file anyway).
- Offline machines are switched when they're next online and a roamd page is open in any of the account's browsers; connecting to one switches it first. The device list marks machines still waiting (offline, switching, or needing an agent update). Removed machines are dropped from the list. The retired key is deleted once no machine is waiting on it.
- If the machine no longer accepts the old key, there's nothing to replace and it's dropped from the list. A machine whose roamd predates this feature refuses the request; the page says to update it, and connecting uses the old key until then.
- That refusal is saved in the vault with a marker of the machine's current connection (the hub gives each connection a new random marker). The machine isn't asked again until the marker changes, i.e. its roamd has restarted, so an old agent isn't logged in to over and over. Other failures are retried periodically while a page is open.
Access log¶
access.log in the settings folder, owner-only, rotated to access.log.1 automatically:
2026-09-30T08:12:44Z login accepted SHA256:5Jx…
2026-09-30T08:13:02Z login refused SHA256:Qm2…
2026-09-30T08:20:10Z connection closed no login within the time limit
2026-09-30T08:21:00Z connection refused too many connections
2026-09-30T08:25:31Z key replaced SHA256:5Jx… -> SHA256:Hk9…
When an agent seems stuck on a Mac¶
With the updated roamd on your Mac, a task that appears active but stops making visible progress can show an amber notice below its session toolbar: No recent progress. Check your Mac for a permission prompt. The session tab also gets an amber marker. The notice stays with that session in both the terminal and Conversation view, including on phones.
Check the Mac's screen, or ask someone there to check it. A permission request may need a response there. A long-running task can produce the same notice: roamd cannot confirm that a Mac permission window is open. Help explains what to check. Dismiss hides this warning for the current stall in this open tab; it does not answer a permission request. Dismissal is forgotten when you reload or lock roamd. Progress or the end of the task clears the warning automatically. A later stall may show a new warning.
Active Mac task stops progressing
-> session notice + amber tab marker
-> check the Mac, or dismiss the notice
Work resumes or task ends -> warning clears
Ordinary idle sessions do not trigger this warning. roamd needs a recognised sign that the agent is busy; some agents may not provide one. Suspected stalls do not send a new push notification. Real Mac permission prompts have not yet been checked in an end-to-end test of this feature.
After a successful session-picker rename, matching tabs on that machine change name in place, including paused tabs. Their connection, tab identity and reader state stay intact. Saved tab references and subsequent reconnects use the new name. A rejected rename leaves tabs unchanged. Other machines' tabs are unaffected. Session matching is name-based; external tmux and other-browser renames are not reconciled automatically.
Confirmed rename -> update matching local tabs -> save renamed references
The machine menu displays the custom name without appending the registered name in brackets. When the two names differ, the registered name is the name element's native hover tooltip. Unchanged names have no additional tooltip; touch-only screens do not provide mouse hover.
Custom name differs -> custom name in row -> hover -> registered name
Names match -> registered name in row
Details (machine ⋯ menu) opens a dialog built from the device list (registered name when
it differs, online since / last seen, roamd version, sharer, host fingerprint) plus, for
your own machines, the vault item holding the SSH login saved at setup, if any. Secrets
render masked until a per-field toggle (aria-pressed) reveals them; every field has a
clipboard button. Forgetting overwrites the item with an empty sealed value at a new
revision (vault items are never deleted); removing the machine does the same. Shared
machines never show a login.
⋯ -> Details -> device fields + vault login item -> masked secrets -> reveal / copy
Forget or Remove -> item sealed empty, new revision
The machine menu remembers the last known sessions in this browser. Reloading or waiting for a report keeps their names visible at normal brightness, with a small hollow grey ring in the existing indicator space. A confirmed report restores the activity indicator and removes sessions that have ended. No new report does not mean no sessions. A browser with no remembered list must receive its first report.
Session status comes from the remote computer, encrypted through the hub. A hub restart keeps the lists: on SIGTERM (systemd stopping or restarting it) the hub writes each machine's latest sealed envelopes, and when they arrived, to its database in one go, and on start reads them back and deletes them, so a crash later never brings back an old list. Nothing is written while it runs, and memory holds only what it held before. The age carries on from when the machine posted, so the browser's usual checks (signed, newer, recent) apply unchanged. After a crash or a forced kill nothing was kept: lists return with each machine's next report.
systemd stop -> hub writes latest envelopes (one transaction) -> exits
hub starts -> reads them back, deletes them -> pages get them at once
hub crashes -> nothing kept -> lists return with the next reports
The browser tab's roamd icon reflects the amber indicators on your open session tabs. Closed, paused or disconnected tabs do not contribute. Solid amber takes priority when an open tab shows input required; otherwise, hollow amber marks an unread completion. Other states leave the normal icon. Viewing a completed session clears its marker when read state syncs; another session may still need attention. Locking roamd restores the normal icon. The installed app's home-screen icon does not change.
The computer alone decides each session's light, and the browser never times it out. A faint hollow grey ring shows only while the computer is offline, or before this page has heard from it. Opening, closing or resizing a terminal doesn't change a light, and an old working snapshot cannot trigger a completion notification. See Notifications for delivery, offline dismissals and tests.
Remote observation -> encrypted report -> hub -> browser status
Remembered names -> hollow rings -> confirmed activity indicators
Unread completion + idle -> hollow amber -> view/read -> grey idle
Open tab lights -> input required > unread completion > normal logo
On phone-width screens, the floating Enter and session picker use smaller circles. The arrow pad keeps full touch targets but brings their centres closer together. Tablet dimensions are unchanged. Keyboard-visible positioning maintains separation between the pad, Enter and the picker.
phone -> tighter arrow cross + smaller Enter / picker
tablet -> existing arrow spacing and button sizes
Agent usage¶
Usage is collected on the machine and carried in its existing signed, encrypted session report. No extra terminal connection or transcript transfer is opened. The hub cannot read the figures. Guests' reports omit usage. Only counts, percentages and bounded model names are included; prompts, responses and credentials are excluded. Only the newest figures are read: the end of the agent's record, looked at wider only when the end holds none, and only every few seconds. A record of hundreds of megabytes costs the same as a small one.
agent's record --> its end (wider only if empty) --> newest line with figures --> status report
Codex supplies the current context and its window from its record. Its limits (the weekly
window, and the 5-hour session where Codex reports one) come from asking Codex itself,
because the record only holds them as of its last reply. The daemon starts the installed
codex app-server for a moment, sends account/rateLimits/read and stops it. Codex uses its
own login for that, so roamd never reads or handles the login. The question runs in the
background with a 15 second limit: once when a Codex session is first seen, then at most
every 5 minutes and only while that session has been used in the last 10 minutes. The
answer is kept with the time it was read, and replaced by a newer answer or by a newer
figure in the record (the record's lines carry their own times). If Codex is missing, logged
out or slow, the record's figures stand. The report carries limitsAt (Unix seconds) so
the box can show the age; Claude Code's status-line figures carry the time they were kept.
Other agents never receive these figures.
codex session first seen, or in use and 5 minutes since the last question
-> (background) codex app-server -> account/rateLimits/read -> limits + time read
newer of (live answer, record figure) -> owner-only status report -> "updated N ago"
no answer -> limits from the record, as of its last reply
Claude Code supplies the context of its newest reply (input plus cached input), ignoring helper agents. Gemini CLI uses the newest message's tokens; OpenCode the newest assistant message. Copilot supplies the context and its limit. Goose supplies the model. Aider's visible report gives the tokens of its last request. Cursor is detected but has no documented readable token feed. Unsupported fields remain unknown; model limits are never guessed.
Codex record selection first uses the conversation file held by its process or native
launcher child on Linux, macOS and Windows. This supports resumed sessions, custom agent
storage and concurrent sessions sharing a project. If the operating system will not reveal
the file, explicit conversation identity or unambiguous project/time matching is required.
Other agents check the process, project and conversation time where available. An ambiguous
match stays unavailable. Platform builds are checked; physical Mac and Windows verification
of this process-file matching remains pending.
Claude Code gives its limits and window only to its status line. The owner's switch calls
the machine's usage service (owners only, over the same encrypted connection as other
machine settings), which edits only the statusLine entry of Claude's settings file
(CLAUDE_CONFIG_DIR is honoured), keeps a backup, and remembers any status line already
there. Claude then runs roamd claude-statusline, which saves only the model, the context
used, the window and each limit's percent and reset time, one small file per conversation,
never cost, paths or prompts, and runs the previous status line with the same input,
printing its output. A limit whose reset time has passed is dropped. The saved figures are
matched to the session by Claude's conversation id and replace the record's figures.
Turning it off restores the previous entry only if roamd's is still the one in place; a
status line changed by the owner since is left alone. The command is quoted for POSIX
shells and encoded for PowerShell on Windows.
switch on -> usage service -> settings.statusLine = roamd (old one remembered)
Claude reply -> roamd claude-statusline -> figures file + old line's output shown
watcher -> figures merged over the record's -> encrypted status -> ⋯ menu
switch off -> old status line restored (unless the owner changed it since)
Usage is independent of conversation-history recording. It sits at the top of the tab bar's
⋯ menu (SessionMenu), shown only for a session whose status names an agent or carries
usage. The menu is a non-modal popover (role="dialog" holding a role="menu" list), follows
the active tab, closes on outside press or Escape unless pinned, and is smaller on phones.
Escape closes an unpinned menu before terminal input sees it, so it cannot cancel a remote task.
The arrow keys, Home and End move between its choices. The notifications line is a
menuitemcheckbox and leaves the menu open, so a failure to save shows inside it.
The bar itself keeps Files, Conversation view and Split; Reconnect appears only while disconnected.
tab bar: Files | Conversation | Split | ⋯ (+ Reconnect while off)
⋯ -> usage (agent sessions) -> Resources | notifications | Pause | End
agent record -> machine counters -> encrypted status -> owner's ⋯ menu
incomplete / ambiguous / unsupported -> dash, never fabricated zero
pin -> menu stays open -> selected tab changes -> new session figures
Web app update detection¶
Registration follows a worker already installing as well as later updatefound events. Visible pages recheck on foreground, focus and online events, and periodically while open. Concurrent checks share a pending update request; failed checks can be retried. Worker requests bypass the HTTP cache. On resume the page also compares controller identity, preserving a reload offer if replacement happened while it was suspended. First installation does not count as an update, and another tab's activation does not force this page to reload. The phone/tablet offer remains in the main drawer; desktop keeps it in the header. No extra machine connection is opened.
resume / focus / online / visible timer -> worker update check
installing -> waiting -> reload offer -> tap -> activate -> reload
changed controller after suspension -> reload offer
Languages (technical)¶
- Page: English is written in the code inside
t()andplural(). It is also the key into one JSON file per language, fetched only for the language in use. The language is the one picked on this browser (local storage), or else the closest match tonavigator.languages, or else English. It is loaded before the rest of the page starts, so every text is in it. A change reloads the page; the tab stays unlocked. - Plurals follow
Intl.PluralRules, so Russian, Polish, Ukrainian and Arabic get all their forms. Dates and numbers useIntlwith the page's language. - Right to left: Arabic sets
dir="rtl"on the page and the layout uses logical CSS properties, so it mirrors. Terminals, paths and code stayltr. - Messages from the hub and roamd arrive in English and are translated in the page. Messages with values in them are matched by their English pattern.
- Emails: the account keeps the last language its browsers reported. Sign-up sends it with the request, so the first code arrives in it. An invite goes out in the invited person's language if they have an account, else in the inviter's.
- roamd uses
ROAMD_LANGUAGEif set (a code such asfrorzh-TW). Otherwise it readsLC_ALL,LC_MESSAGES,LANGandLANGUAGEon Linux and Mac (and the Mac's own language list when those are unset), and the user's UI language on Windows.CandPOSIXmean English. Its translations are built into the program. The install scripts carry their own short tables. - A browser test fails if any language misses a text, keeps a removed one, or changes a
{place}, its markup or a plural form.
t('Saved {name}.', {name})
│ key
▼
locales/fr.json ──► "{name} enregistré." ──► fill {name} ──► page
│ missing
└──► the English
Notice placement¶
Notification and installation offers use the same responsive breakpoint as navigation. Compact layouts render them inside the machine drawer; desktop renders them in the page flow below the header, including terminal view. Opening an offer's dialog closes the drawer first so only that dialog handles focus. Dismissal and permission checks are shared between layouts. Offline status remains in the page flow on every layout. The notification opt-in dialog briefly explains the message content before browser permission.
navigation breakpoint -> compact -> drawer offers -> dialog
-> desktop -> header -> notices -> session
offline state -> page warning in either layout
Navigation colours¶
The header and compact drawer use the existing SVG app icon without recolouring it. The header and compact terminal toolbar use charcoal shading in dark mode and a pale blue tint in light mode. Selected navigation items and session tabs use theme accent tokens for a background and inset bottom indicator. Focus rings remain visible. Terminal output colours, status lights, layout breakpoints and the floating button's white appearance are unchanged.
system theme -> navigation surface + accent tokens
current item -> tinted background + bottom indicator
terminal -> existing terminal colours
Input and connection state¶
Each terminal accepts input only while its own connection is ready and its sharing permissions allow typing. Connecting, disconnected and reconnecting panels set the engine's input option to disabled and the native textarea to read-only. Disabling clears pending keyboard composition and its timers. The send path also checks readiness, so paste, voice, snippets and other direct sends cannot bypass the gate. Nothing is queued for recovery. Delayed command submission stays with the same tab and connection.
keyboard / paste / voice -> panel ready and permitted? -> send
| no
v
discard
connection lost -> cancel pending keyboard commits
Roamd's own terminal¶
The terminal engine is roamd's own code, taken from xterm.js (the parser and the screen it keeps) and trimmed to what a headless terminal needs. Nothing of xterm.js is on the page: no package, no styles, no elements. A pane written for roamd draws the screen.
machine output --> engine (parses escape codes, keeps the screen and scrollback)
| write / scroll / resize / cursor events
v
pane (draws each changed row as text, on the next animation frame)
^
keys, paste, IME --> hidden input box --> engine --> bytes back to the machine
wheel, mouse -----> pane --> scroll the history, or a mouse report if a program asked for it
- The pane sizes itself: it measures one cell in the chosen font and fits as many columns and rows as the box holds.
- Search runs over the whole history and draws every match; the current one is brighter, and the pane scrolls to it.
- A long press selects a line; the pane draws the selection and the engine returns its text.
- Conversation view occupies the originating terminal tile without unmounting or resizing its hidden terminal. AI and tmux modes use bounded requests on the existing encrypted SSH connection. Reads poll only while the tile is visible and connected; native selection pauses live replacements. tmux updates also pause while reading older rows; returning to the bottom refreshes the live capture. Per-tab generation and connection checks discard replies after a source change, closure, reconnection or lock.
-
The daemon resolves the exact tmux session and pane, then its agent process. Claude's process registration, local hook associations, and Linux Codex writable transcript descriptors provide conversation identity. Directory and recency guesses are not used. Codex's shared local server can serve several terminal clients; tmux identifies the client process, not its selected thread. The owner can explicitly select a loaded root conversation with a persisted transcript. The picker reads metadata without resuming or subscribing, excludes child agents, and never auto-selects the newest conversation. Its local association includes the terminal process identity and survives browser reconnect/reload only while that process remains alive. The selected name is shown above the messages. Selected-conversation Send/Stop use Codex's local protocol and the exact thread ID, with the current turn ID for steering or interruption. Closed conversations are rejected; no conversation is resumed implicitly. Permission decisions stay in Codex. Automatic hooks must descend from the terminal's Codex process; inherited pane variables from a detached shared server are not accepted as proof of origin. A hook association includes process start identity so a reused process ID cannot reuse it. Hooks retain only this association locally, not a second copy of the transcript. Unsupported agents or unlinked conversations remain accessible through tmux mode.
terminal pane -> exact agent -> automatic transcript link | owner chooses -> open Codex thread | read / send / stop by ID -
AI mode reads complete records from the agent's own transcript, with byte cursors for older and newly appended records. It shows user and assistant messages and expandable tool calls/results; it excludes hidden instructions, reasoning and duplicate event records. Very large records may be omitted by bounded reads. Updates arrive at record boundaries, which are not a promise of token-by-token streaming. Native macOS and Windows verification remains outstanding.
- tmux mode captures the exact pane's current screen and retained rows; scrolling up expands that bounded window. Application-managed history and tmux history remain separate, and evicted terminal history cannot be recovered. Both modes retain their own reading positions while switching sources or sessions. Content is selectable HTML; Markdown disables raw HTML and embedded images. File links use the session's reader.
- Input carries the identity returned by the read. The daemon checks owner authority again after receiving the request and verifies the current pane/process/conversation. Automatic linking and tmux mode pass text through tmux standard input, never a shell command line. This terminal input is serialised, pasted into that exact pane and submitted once; Stop sends Ctrl+C. The identity is checked again before paste and before Enter. The OS can still schedule an agent exit between a check and input delivery; this is terminal input, not a transactional agent API. Known approval/question states require the terminal in AI mode. The UI keeps a draft on an uncertain result and never replays input automatically.
- Drafts, transcripts, view choice and scroll state stay in page memory while changing tabs; closure or lock clears them, and reload does not restore them. This is separate from the History page's stored, secret-masked archive. Conversation view requires an updated machine daemon and is owner-only; guests keep their permitted terminal view.
browser -> existing encrypted connection -> exact pane/process
AI -> linked transcript -> visible messages and tools
tmux -> retained pane text
Send -> recheck identity -> paste + Enter in that pane
Stop -> recheck identity -> Ctrl+C in that pane
- The selection toolbar uses an opaque theme surface, accent outline and shadow. Copy uses the primary button style; Clear is quiet. Both targets are at least 44 pixels high and 80 pixels wide, and wrap within narrow panels. It appears only with a selection; clearing removes that selection without changing the buffer.
selected cells -> action toolbar -> copy text / clear range
no selection -> toolbar hidden
detail 2 and 3 on the click give a word or a line). Pointer positions are
converted from the visible screen bounds to the canvas's local grid, shared by selection,
links, selection handles and mouse reports. Font advance comes from the canvas font metrics;
ancestor scaling is excluded from cell measurement. WebKit and Chromium checks drag over
painted glyphs in both directions at several scales and pixel densities and check tmux reports.
The owner confirmed selection works on Mac. When a program has
asked for mouse reports the press goes to it unless Shift is held; the cell is sent counted
from 0 and the report adds the 1 the program expects, so tmux gets the very row you pointed at. Mouse events that follow
a touch are ignored, since touch has its own handles.
- Links (engine/links.ts, Terminal.linkAt). Nothing is scanned as output arrives or when the
pane paints. Only when the pointer moves to a new cell with Ctrl or Cmd held, or on a click or tap,
is the one line under it searched: the rows it wraps over are joined (at most 24 either way), a
pattern for http(s):// finds the address, and trailing . , ; : ! ? and a closing bracket the
address did not open are cut off. The pane underlines the hovered address; the paint cost is one
check per row. Ctrl/Cmd mouse-down on a link is swallowed (no selection, no report to the program)
and the click opens it with window.open(url, '_blank', 'noopener,noreferrer'). A touch click
opens it if the finger moved under 10 px and nothing is selected. Tests: e2e/links.spec.ts,
including that linkScans stays 0 while 3000 lines stream.
mouse moves / click / tap
|
Ctrl or Cmd held, or a tap? --no--> nothing (selection / program as before)
| yes
join the rows the line wraps over --> find http(s)://... --> cut trailing punctuation
|
pointer on it? --no--> nothing
| yes
hover: underline the cells click / tap: open in a new tab
The machines list's width¶
pointerdown on the edge (mouse, pen or touch; touch-action: none)
| capture the pointer
v
pointermove --> width = pointer x - list's left edge
clamped to [minimum, half the window]
saved in this browser's local storage
|
pointerup / pointercancel --> stop
- The edge is an ARIA
separatornamed "Resize machines", focusable, with its current width as its value. Arrow keys move it in small steps (larger with Shift); Home and End jump to the limits; a double-click forgets the saved width. - The width is a CSS variable on the list, so the folded rail and the phone or tablet drawer ignore it. On phones and tablets, the header is hidden during terminal use. The hamburger, named "Machines" for screen readers, shares the current session row. The drawer contains the same navigation links as the desktop header, including conditional sections and the update control. Compact selected-tab buttons keep a transparent background in hover and expanded states, preserving their parent’s accent fill and inset underline.
Compact navigation places four 44px arrow buttons in a cross above the session picker, sharing its safe-area and visible-keyboard anchor. Pointer presses preserve input focus without requesting focus or opening a dismissed software keyboard; arrows use the same modifier-aware, guarded terminal input as the key row. They are disabled for paused, disconnected or otherwise read-only sessions. Compact key rows omit the arrows; desktop rows retain them.
arrow D-pad -> armed modifiers -> writable terminal input
keyboard visible -> D-pad stays above picker -> Enter above
A completed touch on the terminal requests keyboard focus directly in that gesture. When the input is already focused but the keyboard is closed, it briefly renews DOM focus while preserving the native text baseline, value and selection. That local recovery does not emit remote focus reports or logical terminal focus changes. Active composition, keyboard motion, a visible keyboard and read-only input suppress recovery. Links, text selection and cancelled or moved touches do not request focus; mouse input retains its existing behaviour. A normal blur still discards the old editing context. Browser regression checks cover focus and transmitted input; software-keyboard appearance still requires a physical-device check.
Completed tap -> writable, no link/selection -> request focus
already focused + keyboard closed
-> preserve edits -> renew focus
On iPad, iPhone and Android, keyboard Return submits once and blurs the terminal input; releasing the key does not focus it again. Native line-break input events are also handled for keyboards that do not emit a keydown. Return during composition confirms the composition without submitting. Desktop keyboard focus is unchanged. An Enter key sits immediately above the floating session picker, sharing keyboard and safe-area positioning. It sends one carriage return through guarded terminal input, commits pending native edits and then blurs the terminal input to dismiss the on-screen keyboard. It is disabled unless the selected session is connected, unpaused and writable. A floating "Open terminals" button is white with dark ink and a subtle shadow, and half-transparent at rest. Its session count uses an explicit line height and inherits the button's ink, arranged below the icon within the circle rather than positioned over its edge. It sits 100 CSS pixels above its usual position over the keyboard tools, including while the on-screen keyboard is open. The hamburger menu’s "Open sessions" link closes the drawer and returns directly to the selected terminal without opening a picker. All transitions into terminal view navigate to the sessions page, so reload restores sessions through the existing saved-tab mechanism. Refreshing Account without leaving it still opens Account. The current session title on smaller screens and the floating button share the session dialog. Below 900 CSS pixels, and on portrait touch tablets up to 1366 pixels wide, the right-hand action buttons are 36 pixels wide and at least 44 pixels high, with no inter-button gaps, leaving more width for the title. With no terminals open, the menu link goes to the home page. Wide desktop screens group session tabs by machine, as described in Arrange session tabs below. Landscape touch tablets using compact navigation retain individual full session titles in a horizontally scrollable row. Narrow screens and portrait touch tablets up to 1366 CSS pixels wide use short titles (two machine characters, a colon and three session characters). Accessible names include the visible short label and full name; hover titles retain the full name. Noncurrent tabs activate directly; the current compact tab exposes and opens the picker. Close controls stay in that picker on compact rows. A resize observer and scroll listener track overflow; a passive 2px track above the titles appears only when tabs overflow. Its thumb shows the visible proportion and moves with horizontal scroll. The selected tab scrolls into view when selection changes. The dialog lists the browser's open terminals, using the same names, status indicators and close controls as desktop tabs. Choosing one activates it and closes the dialog; Escape or tapping outside also closes it. Focus returns to the opener unless the terminal takes focus. Closing the active tab from the toolbar or the picker opens a confirmation with Cancel focused. Cancel or Escape keeps the tab; confirming disconnects that tab without ending its tmux session. The Never ask again checkbox saves a browser-local preference only when the close is confirmed; Cancel and Escape leave it unchanged. It persists across reloads and applies to both toolbar and picker closes, without changing the separate end-session confirmation. No machine or session details are stored with the preference. Clearing site data resets it; unavailable browser storage leaves confirmation enabled. Closing an inactive tab remains immediate. Desktop tabs and navigation stay in place. The terminal refits when its space changes.
large screen -> horizontal tabs -> choose a session
small screen -> AS:cod tabs + top scroll line -> swipe / choose
-> current title -> full-name picker
compact session row -> hamburger -> machines + navigation
-> Open sessions -> selected terminal
title / floating button -> picker -> activate / close tab
Resume presentation. Visibility and page-cache lifecycle events share a recovery window. Hiding the page cancels outstanding link probes and their deadlines; their late replies cannot update latency estimates or drop a resumed connection. Concurrent checks share one probe per live link. Foreground events are coalesced while a check is pending. A bounded recovery window suppresses only the reconnecting terminal's haze; an explicitly disconnected or failed terminal still shows its warning. Input gating follows connection state throughout, independently of presentation. Repeated foreground events do not extend the window. Automated WebKit and Chromium checks cover these paths; physical installed-iPad suspension remains a device check.
hidden -> cancel probes + deadlines
visible -> shared fresh probe -> ready
-> reconnect (input blocked)
-> recovery window ends -> haze
Plans (technical)¶
- Where: the Plans page, reached from Account. It reads the plan, its renewal date and the
limit from the hub, and sends the browser to Stripe Checkout (buy) or Stripe's customer portal
(manage). After returning from Stripe the page checks every few seconds for the signed
webhook to arrive; after a while it says the payment is taking longer and keeps checking,
less often, while it is open. A subscriber is offered lifetime (
interval: lifetime); the hub then stops the subscription renewing. - Limit: counted by the hub from open connections, so it can't be bypassed from the page.
The page checks too, to explain before asking. A refused connection carries the reason; a
connection closed because the plan ended carries
plan-ended, and that tab is not rejoined automatically. - Not on sale: with no Stripe settings on the hub, or with the plan limits switched off, the page says so and nothing can be bought. See Plans and payment under How it works for the webhook checks and what is stored.
The Snippets window¶
Snippets button --> dialog (aria-modal; Tab stays inside; Escape closes)
|
v
session not in the machine's report yet (short wait) --> "Loading…" row (role=status,
| the beat-driven working light)
v
app running? --no--> your snippets only (no tabs)
| yes
v
tabs, opening on the one used last (kept in this browser's local storage):
[app] app's commands (hub list, or the built-in one; your edits on top, hidden ones out)
[Mine] your snippets for this machine + those for every machine + those for this app
|
v filter: name or command contains the words, any case, in the open tab;
| a count of matches in the other tab is a button that switches to it
Insert / Run --> typed into the terminal showing (Run: Enter sent a moment later,
so full-screen apps don't take it for a paste)
|
v
dialog closes; with a keyboard, focus goes to the terminal
- Which AI app is running comes from the machine's status reports, so its commands show a few seconds after it starts. Until the report lists the session, or the hub's command lists are still on their way, the app's tab shows the Loading row rather than "No commands". After a short wait with no report the session is taken as a plain shell.
- The tabs are a standard tab list: the left and right arrow keys move between them, and a screen reader announces which one is selected.
- Snippets live in one encrypted vault item, so every browser of the account sees the same ones and the server sees none of them.
- On a phone the dialog is a sheet from the bottom, and the filter box doesn't take the focus, so the on-screen keyboard stays down until you tap it.
The Compose sheet¶
Compose button --> sheet from the bottom edge (role=dialog; Tab stays inside; Escape closes)
| a text area (the draft is kept in memory until it is sent)
|
Dictate (bar or sheet) --> opens the sheet; words go to the text area at the cursor as they come
(a guess for the phrase being spoken, replaced when it ends)
|
Send / Send and run
|
v
AI app running in this session, and the terminal is taking pastes?
| yes | no (plain shell, or not known yet)
v v
ESC[200~ text ESC[201~ lines joined with spaces, tabs to spaces
(line breaks kept as Enter) |
| |
+--------------------+---------------------+
v
typed on the terminal; Send and run: Enter a moment later
(so full-screen apps don't take it for a paste)
- Control characters other than line breaks and tabs are removed before sending, so pasted text can never carry an Escape that ends the paste early.
- Which AI app is running comes from the machine's status reports, as for Snippets. The page cannot see inside tmux to know whether a program wants pastes, so until a report names an app the text is treated as a plain shell and the lines are joined.
- Send and run on text that looks risky (the same list as Snippets) asks first, unless an AI app is running, where the text is a prompt and not a command. The question has a "do not remind me again" tick, kept in this browser's local storage only (never in your account). Send never asks, as it presses no Enter.
- Direct keyboard dictation goes through the terminal input handler, separately from the Compose draft. Interim composition stays local. Completed input is sent once and removed from the hidden input buffer when screen-reader mode is off; queued mobile keyboard edits are handled together. The preview is clipped to the remaining terminal width and the focused input stays one cell wide, so a long phrase cannot widen it beyond the terminal. These event sequences are tested in Chromium and WebKit; phone keyboards can vary.
- On a phone the sheet follows the visible part of the screen, so it stays above the keyboard.
- Nothing is stored or sent anywhere except the terminal connection itself; the draft lives only in the page's memory.
Arrange session tabs¶
The wide layout groups open tabs by machine identity, never by display name. First-seen machine order and each machine's session order are stable. New sessions join their existing group. Grouping and reorder controls apply on pointer-fine screens at least 900 CSS pixels wide, or any screen at least 1367 pixels wide. Compact navigation on phones and smaller touch tablets retains individual tabs and its existing picker; switching to that layout dismisses desktop menus.
wide layout -> group by machine -> total open tabs
| five or fewer: session tabs
| more than five: machine tabs
machine tab -> hover / click / keyboard -> its sessions + close buttons
reorder -> retain terminal objects -> save browser's encrypted tab order
reload / unlock -> restore that order and the selected session
When the total exceeds five, only groups with multiple sessions collapse. The badge counts sessions beyond the first: +2 means three sessions. The active machine is marked; its active session is marked inside the menu. Group lights prioritise input required and unread completion so folding cannot hide those alerts. Hover opens a menu without activating or focusing a session; click or vertical arrows open it with keyboard focus. Escape, an outside click, focus leaving the menu, selection or a layout change dismisses it. Menus remain outside the horizontally scrolling container so they are not clipped.
Dragging within a group moves one session; crossing a machine boundary moves its whole group. Collapsed machine tabs move entire groups. Drop marks show the insertion side, including right-to-left layouts. Alt+Shift+Arrow keys and the Move earlier / Move later context-menu buttons offer alternatives to dragging. The context-menu key and Shift+F10 expose the buttons from the keyboard. Reordering preserves terminal identities, connections, selected tab and reader drafts. Keyboard switching and split-view order use the resulting tab order.
Changes use the existing per-browser encrypted tab persistence, including its existing save-failure and plain-shell limitations. No additional plaintext list of machine or session names is stored. Chromium and WebKit checks cover dragging, dropdowns, narrow layouts and keyboard movement; physical touch-device interaction has not been tested for these controls.
Side by side (split view)¶
split button --> setSplit(on) --> up to four tiles in a grid; the active one is .current
|
v
saved with the tabs (the per-browser vault item): { tabs, active, split }
|
reload / unlock --> tabs reopened --> active chosen --> split turned back on
- Tiles other than
.currenthave their terminal at lower opacity, faded quickly; with reduced motion the change is instant. - Turning split on or off saves straight away, as opening or closing a tab does.
Resource readings¶
roamd on the machine the browser
-------------------- -----------
a reading every 10 seconds: the tab showing (terminal in view), or
CPU, memory, network, and the <--- the Resources page: every 10 seconds,
home folder's disk (looked at once "the readings after the last one I have"
a minute, on a thread of its own) |
keeps the last 4 hours, in memory ---> new readings + the disks now, end-to-end
encrypted
-
Two ways to ask. The tab showing asks over its own connection, only while the terminal is in view. The Resources page opens a short connection of its own for each ask, as History does, so it works with no terminal open; it needs this browser to have connected to the computer once, to know its identity. Both keep what they get in the same place, in memory, per computer, so either carries on from what the other fetched.
-
Scrolling on a phone. A phone's address bar slides in and out as a page scrolls, which changes how much of the page shows, as the keyboard does. Only while the keyboard is up does roamd bring the box being typed in back into view, so a picked Computer box never pulls the page back up after a flick to the bottom.
-
Owners only. The machine answers an owner's key; a guest's key is refused. Nothing goes through the hub except as encrypted connection traffic it can't read.
- Times are the machine's. Each reading carries the machine's clock, and the browser asks for what is newer than the last it has, so a browser with the wrong time misses nothing. A reading is never stamped earlier than the one before, even if the clock is set back.
- CPU is the share of time not idle across every core since the last reading.
- Linux reads
/proc/stat; time waiting for a disk counts as idle, as intop. - macOS reads the kernel's tick counts.
- Windows reads the same "% Processor Utility" counter Task Manager shows. If that counter is missing or broken, it falls back to the time the processors weren't idle.
- Memory is what can't be handed to a program right now.
- Linux: total less
MemAvailable. Inside a container with its own memory limit, that limit, and the container's use less file cache it can drop, asdocker statsshows. - macOS: app memory, wired and compressed, as Activity Monitor's "Memory Used".
- Windows: total less available.
- Network is bytes per second over the physical adapters only (Ethernet, Wi-Fi), so a VPN, Docker's bridges or a virtual machine's adapter don't count the same bytes twice. Where there are no physical adapters (inside a container), every adapter but loopback counts. An adapter that came, went or restarted between two readings is left out of that reading.
- Disks are local ones only: network file systems (NFS, SMB) are never looked at, since one
whose server has gone can make the look wait for ever. The disk figures are read on their own
thread, so one that doesn't answer stops only the disk figure, never the rest. On Linux, one
line per disk (a disk mounted in several places shows once), and
/bootis left out. On a Mac, the system's own APFS volumes are left out, since they share the space of/. On Windows, fixed drives only, so an empty card reader never asks for a disk. - Cost: a reading reads a few small system files or counters. It needs no administrator rights and starts no programs.
Terminal document reader and editor¶
File paths in terminal output use the same Cmd/Ctrl-click or touch-tap gesture as web links. Detection runs on the pointed-at text, including wrapped lines. A recognised filename has an extension; quoted paths may contain spaces. Line and column suffixes are accepted, and switching to Edit positions the cursor at the indicated line.
path in output -> originating session's encrypted connection
-> resolve path -> machine account's file permissions
-> Markdown preview / source editor
-> explicit Save -> compare revision -> replace file
changed -> keep draft, explain
The desktop reader shares the terminal's available width. A pointer-captured inner separator supports dragging, arrow keys (Shift for larger steps), Home/End for bounds and double-click reset. Its bounds track the terminal area so resizing the browser or machine list keeps room for both panes. The resolved file path, optional line target, width and panel/modal preference are saved with the browser's open tabs in the encrypted vault. Reload and unlock restore each surviving session's reader and read current contents over its owner connection. An offline session retains its reference and retries the read when it reconnects. Closing the reader removes its saved reference. File contents, drafts, editing mode and scroll/selection are not persisted. Width and layout also survive opening another file in a reader.
Icon-only controls switch between the side reader and a centred desktop modal without replacing the document or editor. Buttons have translated names and tooltips. The one-line path expands on keyboard focus and exposes the full path as a hover tooltip. Scroll direction collapses/restores secondary header details; the filename, editing, save, layout and close controls and status warnings remain. Header focus restores its details. Reflow from this change is ignored by scroll-direction tracking.
Reader state -> bounded side width / desktop modal
Content scroll down -> essential header
Content scroll up or header focus -> expanded header
Tablets and phones use a full-screen dialog with trapped focus and an inert background. Keyboard users can close with Escape and save with Cmd/Ctrl-S. Reader state belongs to each open session tab. Only the active tab's reader is rendered; switching restores that tab's document, draft, edit/preview mode, separate source and preview scroll positions, and editor selection. The header identifies the machine and tmux session. A session with no reader uses the full terminal width. Hidden readers retain their state in memory.
active tab -> its reader state -> side panel / full-screen modal
switch tab -> hide old reader -> restore new tab's state
close tab with draft -> Save / discard / Cancel
save fails -> keep tab and draft
Read and save results update only the originating tab. Late replies after closing, replacing or locking a reader are ignored. Closing a dirty tab from either tab list activates its reader and asks how to resolve the draft, regardless of the ordinary close-confirmation preference. Saving must succeed before that tab closes. Browser refresh warnings cover drafts in hidden readers too. Closing a tab clears its reader; lock clears every decrypted reader; unlock reopens the saved references. Absolute paths are independent of the session's current directory. Relative paths resolve from it, including parent paths; symbolic links resolve to their targets. Reads use the daemon account's OS permissions across the filesystem, so a moved or inaccessible file shows a read error. The reader supports bounded UTF-8 text; it refuses binary and oversized files. Reads and edits are owner-only and require a saved tmux session. The daemon must support the reader protocol.
Markdown is rendered as React elements with raw HTML disabled. No embedded images are fetched. Relative document links resolve from the open document's directory; web links require a click and open with no opener or referrer. Source is always text, never an executable page. Refresh and navigation protect dirty drafts; locking clears them. The draft is not stored in browser storage or uploaded to the hub.
Saves require OS write access to the file and its parent directory. Read-only files, hard-linked files and unsupported Unix ownership are refused. Saves compare a content-and-metadata revision, preserve Unix owner/group and mode bits, and replace the file only after writing a complete temporary file in the same directory. CRLF is retained when the original consistently uses it. Concurrent saves through one daemon are serialised. Other programs do not share this lock: a write between the final comparison and replacement can still be lost. This is not a collaborative editor. Replacement is not a promise to preserve extended attributes or custom ACLs. Linux checks cover the reader and editor; macOS and Windows still need native verification.
The files panel¶
folder button --> panel (role "region", name "Files")
computer: fixed drawer at the right, over the terminal; phone (<= 600px): the whole screen
opens at (each time it opens): tmux session? --yes--> files "here <session>" --> #{pane_current_path}
--no / older roamd--> the home folder
Upload (main) --> choose files --> confirm --> put ... <folder shown> (owners)
--> put ... <usual folder> (guests)
non-Apple touch only: Photos (media types), Camera (capture)
heading -> transfer results for this tab -> controls -> folder list
drop on the list --> folder shown; drop on the terminal / paste --> usual folder
list --> "list" requests, a page at a time; only rows in view are drawn (fixed row
height, absolutely placed); near the end the next page is asked for
tap a file --> sheet (role "dialog") --> preview read with "get" from 0, stopped early
--> Download ("get") / Insert path / Rename ("rename") / Delete ("trash")
folder ... --> Open / Download as zip ("zip <path>") / Rename / Delete
choose several --> Download: one file "get", else "zip <path> <path> ..."
--> Delete: "trash" for each, one after another
- Dismissal and styling: a pointer press outside the Files panel closes it; presses inside it, on its toolbar toggle or inside another dialog leave that dialog in control. A file preview closes only when its own backdrop is pressed. The upload confirmation backdrop rejects its batch without storing a preference. File headings and the upload dialog use the shared brand surface and border tokens in both themes.
outside Files -> close panel
preview backdrop -> close preview, keep Files
upload backdrop -> cancel batch, keep Files
inside a card -> keep it open
- Upload consent: every valid
sendFilecall passes through a shared confirmation before opening an upload channel. A pending batch is displayed in one dialog; Cancel, Escape, clicking its backdrop or locking the app rejects it. The destination tab and folder are captured before waiting, so a later tab or folder change cannot redirect the upload. Pasted images only insert their saved path after an approved upload succeeds. Confirming with Never ask again stores only an opt-out flag in local browser storage, never filenames or destinations. An unchecked confirmation and every cancellation leave storage alone. The preference is browser/app-local, not synced to other devices; if storage is unavailable, later uploads keep asking. Clearing site data resets it.
file + captured destination -> confirmation queue
deny / lock -> resolve without sending
approve -> optional local opt-out flag -> existing encrypted transfer
- New requests on the
filessubsystem (one line, fields split by tabs):here <session>answersdir <path>(tmux'spane_current_pathfor exactly that session; a guest held to the upload folder gets that folder);mkdir <folder> <name>answersmade <path>;rename <path> <name>answersrenamed <path>;trash <path>answersbinned ok;zip <path>...answerszip <bytes>(what the files add up to) and then the zip itself. - Names for a new folder or a rename are used as typed or refused, never changed: one
plain name, no slashes, no control or invisible characters, not
.or.., nothing Windows refuses. A rename stays in the same folder and never replaces something already there (on a disk that ignores case, changing only the case is allowed). - Delete never removes anything for good. Windows and Linux use the system's Bin through
the
trashcrate (the Recycle Bin; the freedesktop Trash under$XDG_DATA_HOME). On a Mac the item is moved into~/.Trashby roamd itself, since asking Finder needs a desktop session the background service may not have. The home folder and/are refused. - Zip is made by the
zipcrate as it streams (deflate), on a thread of its own, a piece at a time; the browser stopping ends it. Links are left out, so a zip never reaches outside what was chosen. It is refused before anything is sent if the files are over the download limit or too many. A dropped zip is started again rather than carried on. - Previews are read into memory and shown from a
blob:URL whose type the page picks from the file's name: pictures in<img>, video and sound in<video>/<audio>, everything else as text (if its start has no zero bytes) in a<pre>. Nothing the machine sends is ever shown as a page. The CSP allowsblob:forimg-srcandmedia-srconly; PDFs and other documents are not previewed (the CSP'sobject-src 'none'blocks the browser's PDF viewer), and pictures, videos and sounds over a size cap are not fetched for a preview. - Who may change files: owners, and guests with a terminal whom the owner allowed files.
A watch-only guest, or one given only some sessions, is refused
mkdir,renameandtrash, even in the upload folder. Every change is written to the machine's activity record. - Long press (touch only, half a second, cancelled by moving) starts choosing with that row chosen. A right click with a mouse opens the row's sheet.
- Transfers are filtered by originating tab and appear directly below the Files heading, before controls and the folder list. Saved paths identify the destination after navigation.
- Apple touch devices use one unrestricted input: their system chooser offers all sources.
iPad desktop mode is detected through Mac platform plus touch support. Other devices keep
Photos and Camera inputs shown only when the main pointer is coarse
(
pointer: coarse). Media-only types make Chrome on Android open the photo picker, andcaptureskips the chooser and opens the camera. All three send through the same path.
Suspected Mac permission waits¶
An updated Mac daemon can report a suspected stall when a recognised agent remains active without meaningful screen progress. Activity comes from an agent hook or a recognised busy footer. Changing footer timers and spinners do not count as progress. Ordinary terminal approval prompts retain their existing input-required state. Unknown or inactive activity does not start this warning.
The browser shows the owner a session-local banner and hollow amber tab marker, including in Conversation view and collapsed machine groups. The same marker contributes to the browser tab icon. It is hidden for paused, disconnected, unconfirmed and guest sessions. Dismissal is in memory, per open tab and stall; reload or lock clears it. The daemon clears the episode on progress or task end. No push is triggered by this additional status field.
Mac activity evidence + no meaningful progress
-> encrypted owner status -> session banner and marker
New progress / task end -> updated status -> clear warning
This is a heuristic, not detection of a pending macOS privacy dialog. Long tool calls can trigger it, and unrecognised activity can be missed. It cannot identify the requested folder or grant access. Browser tests use simulated status reports; native Mac permission-dialog behaviour still needs verification.