Source Built from main · latest release v0.4.0

Runtime

Window and panes, daemon, tray, terminal, local API.

On this page
  1. Daemon
  2. App window
  3. Quit
  4. Launch at login
  5. Tray
  6. Local API
  7. Local token
  8. Records
  9. Interface profile
  10. Interrupted
  11. Catch-up
  12. Terminal
  13. Remote screen
  14. Command stream
  15. Pane
  16. Layout
  17. Schema gate

Daemon

A resident TypeScript process on this Mac that runs the tool-call loop itself. Closing the window does not stop it; if the window crashes, turns keep going. On power, while a turn runs or a render is awaited, it holds off idle sleep (the display still goes dark); closing the lid, Sleep from the menu bar, running on battery or shutting down still puts it to sleep, and it picks up again on wake. A killed daemon is relaunched only while the window process is still alive (including when it is hidden in the tray); once the window is gone too, another crash stops it until you open the app again or next log in. Settings has an optional standalone runtime, off by default, which stays disabled in production until the sealed-native gate passes; while it is off, the window process remains the supervisor. The daemon also holds the terminal sessions you open, so their processes keep running after you close the window. When you quit or the daemon stops on its own, those processes stop with it, but where each terminal was open and what was on its screen are kept; on the next launch a new one opens in the same place, and the split layout reconnects to it. Only ending the terminal session forgets it.

Avoid: cloud computer, 24/7 server, treating the UI process as the runtime, keeps running with the lid closed, stops after one kill, standalone KeepAlive login item, closing the window kills terminals

App window

The Tauri desktop window, which holds the Svelte interface (the code and the development guide call it the messenger). Closing the window hides it to the tray rather than quitting; while hidden, it stays in the Dock. At login it goes only to the Dock and the tray, and the window does not pop up. Clicking the Dock icon, Cmd-Tab, Spotlight, left-clicking the tray, or opening the app again brings the window back. Only one window process runs at a time; a second launch just wakes the existing one and connects to the existing daemon. If the window crashes, it does not come back on its own. Before it connects to the daemon, Quit works, Stop does not, and the interface shows "Can't reach the runtime". The width and height you dragged it to, and whether it was maximized, are the same when you reopen after quitting; the position is not remembered, so the window does not land off-screen after a display change. While you drag the border, the app does not ask whether the window is maximized: on macOS that query moves the window, so on release its position would differ from where you were dragging it.

Avoid: browser-only, no tray, hiding removes it from the Dock, window KeepAlive, grabbing the screen at login, multiple windows, treating a window crash as quitting

Quit

Ends this run of the app. Under window supervision (the default), both the window process and the daemon stop, and the tray and Dock icons both disappear. If the standalone runtime is on, quitting closes only the UI and jobs keep going; stopping them takes an explicit stop (written runtime.stop). Turns in progress are marked Interrupted, and the processes in the terminal sessions you opened stop with it; next time you open the app, each terminal that was still open reopens in its original place, and the split layout reconnects to it. It is not closing the window. Only ending the terminal session forgets it. It leaves the Launch at login preference alone. Shutting down and logging out are also treated as Quit for turns. The same rule applies in development mode. In production the standalone runtime is still gated, and the default is still window supervision.

Avoid: treating closing the window as quitting, closing only the window by default and leaving a headless daemon, treating Quit as turning off launch at login, Cmd+Q closing only the window in development mode, treating the gated standalone mode as enabled

Launch at login

Optional, on by default. At login it starts only the window process, into the Dock and the tray, and the window does not pop up; the window process then makes sure the daemon is running. Turning it off affects only the next login and does not kill the current run; at the next login neither the window nor the daemon comes up. Quitting does not clear this switch either. The daemon is not a login item of its own.

Avoid: window popping up at login, turning it off kills the current run immediately, daemon as its own login item, quitting clears launch at login, turning off launch at login but leaving jobs that start themselves

Tray

The menu bar icon of the window process. Left-click brings the window back. Right-click: Show window, Stop (the most recently active turn when the window is not open), Quit. There is no Hide and no Pause daemon. If the window crashes, the tray is gone too.

Avoid: tray badge, a second interface, the daemon running its own tray

Local API

The HTTP + WebSocket interface the daemon serves on 127.0.0.1 for the app window, protected by the local token. Changes go over HTTP; the transcript stream goes over a receive-only WebSocket. Through it the window reads and writes the roster, sends messages and subscribes to the transcript stream.

Avoid: treating the UI process as the runtime, cloud API, two-way command channel, workspace files as HTTP resources, putting the local token in the URL

Local token

A random secret the daemon mints fresh every time its process starts, used to authenticate requests to the Local API. HTTP sends it as Authorization: Bearer; the WebSocket sends it in the first message after connecting, never in the URL. A window crash does not change it; after the daemon exits or crashes, the next process mints a new one. It is not the endpoint API key, and it goes into neither SQLite nor the repository.

Avoid: API key, a long-lived key installed once, writing it into .env, putting it in the WebSocket query

Records

What the app keeps on this machine: the database's messages, turns, tool calls, work log, quality events, plans and tickets, hand-overs and reviews, requirements ledger, organizer runs, judgements, spend, retrospectives, memories and skills, built-in prompt changes, and the daemon's logs. A Bot sees the tables with describe_data, runs one read-only SQL query with query_data, and reads the end of the logs with read_data_log, to answer "which work keeps being sent back" or "which call keeps failing to read" and propose an improvement from it. Read-only, in a child process the daemon starts, stopped at 10 seconds; no approval card, and it works under a hold. What a Bot cannot find is not for Bots: remote access's keys and receipts, push configuration and your terminals are refused, of the data folder only the two logs are read, and anything that looks like a key in them is blanked out. Only when you ask for an analysis or an improvement. Details in Behavior · Records and ADR 0065.

Behavior details

Bots can read what the app keeps on this machine, read-only, for the evidence behind an analysis or an improvement (ADR 0065):

  • Three tools: describe_data lists tables and row counts with a line on the main ones, then a few ready-made queries (recipes), and a table's columns and types for table; query_data runs one read-only SQLite query, or a ready-made one by name with recipe — a single SELECT or WITH … SELECT, ? bound to params in order, 100 rows by default and 1000 at most, long cells cut, blobs shown by size, stopped at 10 seconds, a wrong column answered with the named tables' columns; read_data_log reads the end of daemon.log (or the development build's daemon-dev.stderr.log), with grep. None has a side effect: no approval card, and they work under a hold and in read-only segments. Claude Agent Bots have them too.
  • How it runs: in a child process the daemon starts (--query-data), on a read-only query_only connection, two at most at once; one past its deadline is killed, and the daemon never waits on it.
  • What cannot be read: remote_*, pending_keys, request_receipts (remote access's keys and receipts), notification_push_config, notification_devices (push), terminals (yours), sqlite_*. Such a table reads as "no such table"; reaching one through a view or an index, opening another database or writing is refused from the query plan. Of the data folder only the two logs are read, never through a link, with anything that looks like a key blanked out.
  • What can be read includes other Bots' directs and memories (read-only, never into your context) and anything you ever pasted into a chat — keys belong on approval cards, not in chat.
  • When: only when you ask a Bot to analyze or improve something; what it finds goes into its own skills and memories, or into a built-in prompt through the approval card.

Avoid: a Bot writing the database, letting a Bot read terminals or remote access's keys, reading files of the data folder besides the logs, querying on a background timer, reading another Bot's memories into its own context

Interface profile

Your changes to how the app looks and works, laid over its own: tokens (the design tokens for colours, fonts, corners and shadows, set for the light and dark looks), a fixed list of behaviour (focus mode, where each kind of window opens, Enter or ⌘Enter to send, the time format, motion, code colours, glass content), a stylesheet of your own, and changed words on screen. Every window and a paired phone share one. Say what you want in any conversation with a Bot, and it reads it with describe_interface and changes it with edit_interface: a change under its permission mode, applied at once under auto and allow all, put on a card under ask first (which you can try in a window first), refused read-only; its reply carries an undo. Settings › Interface shows, changes and undoes it too, and every change records who made it and why. Stop, the cards waiting on you and the way to Settings cannot be hidden: a window where they are drops the tokens and CSS, and ?ui=default or ⌥⇧⌘U shows a window's default look for now. Dragged widths and the layout are not in it and stay on the machine. See Behavior · Interface profile and ADR 0106.

Behavior details

How the app looks and works is yours to change, and a Bot changes it when you ask (ADR 0106):

  • What changes: tokens (any --x in tokens.css: colours, fonts, corners, shadows, the chat's width, for the light look, the dark one or both); behaviour (focus mode, where each kind of window opens on the desktop, Enter or ⌘/Ctrl+Enter to send, 24- or 12-hour time, motion as the system says or always reduced, code colours, glass content); a stylesheet of your own (up to 64 KB); words on screen (by where they sit, keeping every placeholder); and the theme and language. Dragged widths and the layout are not part of it and stay on the machine.
  • Your bubble follows the accent: change --accent without --you, and the bubble of your own lines follows it (a step deeper in dark, so white words read); set --you to keep them apart.
  • Asking a Bot: say it in a conversation — "make the accent purple", "a darker sidebar", "send with ⌘Enter". It reads the interface, its hooks and tokens with describe_interface, changes it with edit_interface, and a few seconds later reads the windows' reports. It is a change under the permission modes: under auto and allow all it applies in every window at once and its reply carries "Interface changed · Undo"; under ask first it waits on a ui-edit card you can try in a window first; read-only refuses it.
  • Stable hooks: a stylesheet uses [data-ui="…"] as selectors (the sidebar, a conversation row, the messages, your lines, a Bot's reply, the composer, an approval card, a pane, a tab, Settings, the flow board… some twenty-five), which stay the same across versions; class names may change, and selectors that stop matching after an update show in the report. The root's data-surface="desktop|tablet|phone" splits by device, data-theme by look.
  • The floor: Stop, the buttons of a card waiting on you, a question card, the way to Settings, pausing the custom interface and the undo buttons cannot be hidden. Each window checks after applying: hidden by the profile, nearly transparent, shrunk, not clickable or covered, and that window drops the tokens and CSS, says so once and reports it for the Bot. A button the app itself folded away does not count.
  • Back to the default: ?ui=default in the address, or ⌥⇧⌘U (Ctrl+Alt+Shift+U on Windows and Linux), shows that window as the app ships until pressed again; the switch in Settings › Interface does it for every window, and "Put everything back" clears all four parts.
  • Records: every change records who made it, why, the whole profile before and after and the app's build. Only the latest change can be undone, and only while the interface still looks that way; any revision can be restored. Saves in one sitting of Settings fold into one; typing it back to how it was is a change of its own.
  • Settings › Interface: the switch for all of it on top (with why this window shows the default for now, when it does), then five parts: behaviour, tokens, the stylesheet (saved a second after you stop) with the hooks, the words on screen, and the history (with each kind of window's report, put everything back, and the key back to the default). A wide window shows them as tabs under the switch; the phone lists them, each row saying what that part has changed, and opens one as a page of its own that Back leaves.
  • Code colours: one Shiki theme per look from a fixed list (GitHub, Vitesse, One, Solarized, Catppuccin, Rosé Pine, Gruvbox, Dracula, Nord, Tokyo Night, Monokai…), used by chat's code blocks, command output and the file editor, with its own background and text colour; a look with none chosen keeps the app's own (GitHub in chat, Vitesse in the editor). Code on screen is painted again when it changes. It is behaviour, so pausing the custom interface leaves it in place.
  • Glass content: solid, frosted or clear. On, the content cards let the window's frosted glass through (only on a desktop whose window has a material: macOS, Windows 11), and the conversation (a Bot's or a project session, its header too), a file's preview, the flow board and the file tree on them paint no ground of their own; menus, dialogs, floating panes, the editor and the terminal stay solid. The token --content-glass (how much of the card is kept, such as 60%) tunes it per look. No backdrop-filter on hooks: the window's material does the blur, and it only shuts pictures and menus that pop up inside a pane into it.
  • Out of reach: the PDF and Office previews know only light and dark; a few components define tokens of their own, which only a stylesheet reaches. Font sizes and spacing are not tokens, and there is no whole-window scale yet.

Avoid: a Bot changing the interface unasked, hiding Stop or approval buttons with CSS, counting on class names to match forever, an interface per device, pushing the layout and widths to the phone, changing the app's source to change its look

Interrupted

When the daemon dies, is killed and relaunched, or when you quit, shut down or log out, the turns in progress are marked interrupted. Commands those turns still had running (and everything they started) are asked to exit; any still running when the daemon next starts, because it died or quit before they were done, are stopped then, after checking that the process number still belongs to that same process, so a program that has since been given the number is left alone. In the transcript that line carries a name and avatar like any other Bot message (neither in your direct with it), and "Continue" sits right after the "Interrupted" bubble: it opens a new turn whose trigger is that interrupted line, and does not retry the call that was cut off; that Bot's next turn (in any session) sees "the last turn was cut off", once only. Work a restart cut off does not pick up on its own up to level 3; from level 4 the supervisor decides by the restart's cause whether and when it does. Work that goes on by itself (or that a stop of yours holds) gets nothing more: each turn's own "Interrupted" line says enough. Work that does not (level 3 and below, an external call with no known outcome, development restarts in a row, a turn on no job) gets one line per job once the daemon is back, in the conversation the job belongs to where you are (where you last spoke about it first): why it started again (a deliberate stop, an unexpected exit, a development run restarting), and where each turn stopped; the line carries Continue and Not now, and one notification. Work that was to go on by itself but was cut off again by another start before it could is told by that later start. Details: Behavior · Interrupted. Closing the lid or system sleep is not an interruption: the turn freezes and carries on after wake. Time asleep does not count as being stuck, nor toward a command timeout; a request cut off by sleep waits after wake for the network to come back (about a minute), then is resent in full. An app window crash is not an interruption: the daemon keeps going. Completion timeouts, stalled streams and endpoint 4xx/5xx errors are not interruptions. A health check that fails temporarily while the daemon process is still running is not an interruption either: the window does not launch a second copy, and a second copy that cannot get the port does not touch the database.

Behavior details

When the daemon restarts, every turn in progress is marked interrupted, and that line goes into the conversation the turn ran in. A long job's turns mostly run in Bot↔Bot directs you are not in: on 2026-09-26 a video job cut off by a restart said so only in such a direct, and sat for 7.6 hours until you found it and pressed Continue. So after a restart, as soon as the engine is up and before the scheduler's first tick (so a check-back or routine that fell due while the daemon was down cannot wake a Bot first), each job the restart cut off that will not go on by itself gets one line. Work the supervisor picks up on its own from level 4 (at once after a deliberate stop; once after a minute of steady running after a crash; once after five minutes without starting again after a development restart), and work a stop of yours holds, gets no line and no notification: each turn's own 「中断」 line says enough, and their own notifications are voided and marked read (from 2026-10-04; until then every development restart posted a line and a notification, and on the morning of 2026-10-04 you pressed Leave it on both of the 《一拳超人》 job's lines, the second time setting aside work that would have gone on by itself a minute later). A line goes up for levels 3 and below, a last step with no known outcome, development restarts within five minutes of each other, and turns on no job (the supervisor does not take those up); work that was to go on by itself but was cut off again by another start before its minute (and so no longer will) is told by that later start, saying it was cut by an earlier development restart and the daemon started again before it could pick up. The line goes in the conversation the job belongs to where you are: a plan's turns go to where you last spoke about the plan (as the supervisor's own notices do, while you are still in it), else to the plan's conversation; when you are not in it, to the turn's own conversation, else the conversation up the line that opened the Bot↔Bot direct, else your direct with the Bot. Turns of one job in one place (a plan cut off in two Bot↔Bot directs, say) make one line. It used to go to the plan's conversation first: on 2026-10-04 a job opened in a group and taken up that morning in your direct with the video director was told of in the group at both restarts, while the 「中断」 lines were in the direct beside your last words. It is the app's system line, kept out of every Bot's context and shown as the app's own (see the buttons under Hold):

  • Why it restarted: this is a development run (bun --watch / --hot, or REAL_BOT_DEV=1) — "The development daemon has restarted", however the last run ended: on every save --watch sends SIGTERM and restarts the process in place without waiting for it, and the stop that SIGTERM starts writes "stopped on purpose" first, so there that mark cannot tell a code change from a quit; otherwise, the last run was stopped on purpose (quit, update, a restart you asked for, which wrote last_shutdown = clean) — "The daemon was stopped and has started again"; otherwise "The daemon quit unexpectedly and has started again".
  • Where it stopped: each turn's Bot, its ticket (or the plan, with none), where it ran when that is not this conversation, and its last step (the last command or MCP call in its record).
  • What now: up to level 3 nothing picks up on its own; from level 4 the line says what the supervisor will do (after a deliberate stop, go on at once; after an unexpected exit, once after a minute of steady running; a development run, once after five minutes without starting again, however many restarts came before; a last step that was an external call with no known outcome, or work a stop covers, does not go on by itself), naming each turn's arrangement when they differ — and going on by itself is exactly the Continue below. Continue continues each turn of the job the way its own interrupted line's Continue would: a new turn whose trigger is that line, under your stops like any other — while a stop holds all of those turns the press does not count, and after you lift it you press again; while it holds only some, the rest go on, the row says how many went on and how many the stop still holds, and the line stays unanswered so that pressing again after the lift continues those; a turn already continued, one whose Bot is already at work there, or whose conversation or Bot is gone is skipped, and when every turn is already continued the press counts and does nothing more. Not now keeps it as it is, and the interrupted lines' own Continue comes back, to pick it up later from there (until 2026-10-04 the button read Leave it, in Chinese 「不续」, which read as closing the notice). Each line is pressed once.
  • Notification: with a line, one per job (interrupted); the ones the turns' own interrupted lines made are voided, and in the same conversation an interrupted line the notice covers offers no Continue of its own until you press one of the notice's buttons. With none, the turns' own notifications are voided and marked read (goes_on_by_itself): no system banner, nothing on the Dock badge.

Which turns a restart cut off: whenever the daemon starts ending every turn at once (a quit, a forced drain before an update or a remote restart, the daemon stopping itself), and when a boot tidies up the turns the last run left live, it notes the turns live at that moment; the next boot takes only those still interrupted with their interrupted line not yet continued. Only the boot right after takes the record: when another run opened the database in between without taking it (it died during boot), or an older copy of the app that knows nothing of the record ran and opened turns, the record is stale and is dropped, rather than telling of that earlier shutdown under this restart's cause; those turns keep their own interrupted lines and notifications. An error while writing these lines only adds a daemon.log line, and the daemon starts all the same. A turn the stuck-turn sweep interrupted is not counted; a forced drain you call off is forgotten when you call it off. Every boot writes daemon.restart to the work log (the cause and the turns cut off), and one daemon.log line when anything was cut off. The notice itself does not depend on the engine level; going on by itself by cause is level 4's (supervisor), which checks the effect ledger first and does not go on after a last step with no known outcome, or a last step that submitted a review or a render would be paid for twice. Once every line has been continued (by the supervisor or by you), the notice reads as continued and its notification is resolved. Continue and Not now go through POST /v1/messages/:id/control ({action: "resume" | "leave"}, on the phone too); the line's control is {kind: "restart", cause, notes, offer, acted?}, notes being the turns' interrupted lines; when only some went on, the answer carries partial: {continued, held}.

Avoid: resuming tool calls from the break point, leaving the interruption only in a Bot↔Bot direct you are not in, a Continue / Leave it card for work that goes on by itself, resubmitting a last review or render whose outcome is unknown after a restart without asking you, killing a previous run's process by its number without checking, treating sleep as an interruption, treating shutdown as sleep, treating a window crash as an interruption, keeping "the last turn was cut off" on the profile forever, treating a completion timeout as an interruption, treating an endpoint error as an interruption, treating a health-check timeout as an interruption

Catch-up

After the daemon comes back (on wake, relaunch after a crash, reopening after quitting, or startup), each missed routine runs only its most recent due run once, without running every missed cycle back to back. After waking from sleep it waits a minute before catching up: with the lid closed the system wakes itself for a few seconds, and the network is often not back yet at that point.

Avoid: skipping whatever was missed, running once for every missed run, catching up after sleep but not after quitting

Terminal

A shell session you open yourself on this Mac, held by the daemon; it has the same rights as Terminal.app, with its cwd starting in the workspace by default but not confined to it, and needing no approval. Bots can't see it, can't use it, and don't know it exists; on the desktop one terminal tab is one session, and phones and narrow screens gather all sessions on the Terminal page as its tabs. To stop one, you must explicitly choose "End session" and confirm. Details: Behavior · Terminal.

Behavior details

A shell session you open yourself on this Mac, held by the daemon. It has the same rights as Terminal.app: its cwd starts in the workspace by default, but it is not confined to the workspace and needs no approval. Approval governs Bots' dangerous actions, and you don't need to ask yourself for permission. Open terminal in project folder, in a project's context menu (a directory row's or a project session's), opens one in that project's directory, even with no workspace set. It is for running your own work, including processes that stay up, such as starting a project. Closing the window doesn't stop it. Which directory it is in is whatever the shell itself reports (zsh does this out of the box, once before each prompt): after a cd the tab name follows; if it has never reported, it is the directory it was opened in. When you quit or the daemon itself stops, the processes inside stop with it, but its directory and what was on screen are kept, and the next launch opens a new one in that directory. Only "End session" forgets it. The screen lives in the daemon too: when you reattach, you get it as it is now, full-screen programs included. Questions a program asks the terminal are answered only by the daemon, never by panes, so even when several panes watch one session, each question is answered once. The shell's environment is set up the way Terminal.app sets it up, but only an allowlist gets through; the daemon's other environment variables (including keys) are not passed in. When the desktop and a phone watch one session at the same time, its rows and columns follow whichever end you most recently looked at or typed in. Paired devices have the same rights: you can see it and type into it on your phone. Bots can't see it, can't use it, and don't know it exists. On the desktop, one terminal tab is one session: opening a new terminal tab starts a new shell, and a tab no longer holds a row of sessions to switch between. Closing a tab only removes it from the window; the processes keep running. Sessions not shown in any tab can be brought back with "Reattach" from "+" or from an empty pane. Phones and narrow screens have no panes, and neither does a device on a remote connection at any width (a tablet included); all sessions are gathered on the Terminal page as its tabs. To stop one, you must explicitly choose "End session" and confirm. The daemon's session list is the real one; sessions opened in the same folder are numbered in the order they were opened, and they carry the same names in desktop tabs and on the phone.

Avoid: treating it as a Bot shell, a terminal for Bots, approval cards for terminal commands, killed when the window closes, closing a tab kills the process, pane tabs as the session list, a row of sessions inside one terminal tab, two tabs pointing at the same shell, surviving a daemon restart, cloud computer

Remote screen

A paired device seeing and operating this Mac's screen, on the phone's "Mac screen" page. The picture and input come from macOS's own Screen Sharing; Deskfolk captures nothing and injects nothing itself. It goes direct when the two can reach each other and through the relay otherwise. Off by default, and only the Mac window's settings can turn it on; Screen Sharing needs this Mac's account password, typed on the phone once and kept there after it gets in, so later connections sign in by themselves. It works at the lock screen, not with the lid shut, asleep, or after a restart before anyone has logged in. On Windows the picture and input come from a VNC server the user installs; the page is called "Computer screen" and asks for the VNC password. Details: Behavior · Remote screen.

Behavior details

A paired device sees this Mac's screen and operates it under Tools → Mac screen. The picture and input both come from macOS's own Screen Sharing (local port 5900): Deskfolk captures nothing and injects nothing, so it needs no Screen Recording or Accessibility permission, and no update takes one away. The daemon only ever connects to that one local port; this is not a tunnel that can be pointed elsewhere.

The switch: off by default, and turned on only in the Mac window's Settings → Remote access; a paired device cannot change it. Turning it off ends a connection in progress there and then. Screen Sharing itself is turned on in System Settings → General → Sharing; the card says whether it is on right now, and one click opens that pane when it is not. While the remote screen is off, the phone page says where to turn it on.

Signing in: Screen Sharing asks for this Mac's account name and login password. You type them on the phone once; after they get in, the phone keeps them (where it keeps its pairing keys) and signs in by itself next time, unless you untick Remember on the sign-in form. When Screen Sharing refuses them the phone forgets them and asks again, and a new pairing or another Mac never uses them. The cost, stated: whoever can read this phone's pairing keys can already use your terminal; the kept password also lets them into the Mac's screen and sudo. That is what not typing it each time costs. It works at the lock screen too, where you can type the password to unlock: the lock screen turns the display off after half a minute idle, and Screen Sharing captures a display that is off as solid black, so the Mac turns its display on when a device connects, keeps it on while one is connected, and lets go when the last session ends. It does not work with the lid shut and no external display, asleep, or after a restart before anyone has logged in: the daemon runs per user.

Two routes: the phone tries a direct connection first — a WebRTC data channel whose non-trickle offer and answer travel over the encrypted remote link, so the direct peer is still this paired device; on the Mac real-bot-rtc takes it and connects it to Screen Sharing. The same network, IPv6, or STUN / TURN servers named in the Mac's settings let the two find each other; none are set by default and no third-party server is used. A phone with no candidate address at all goes straight to the relay; one that has some but has not connected within 8 seconds switches to it too: the daemon connects to Screen Sharing itself and the bytes ride type 9 frames on the remote link, neither side running more than 48 KiB ahead of what the other acknowledged. Over the relay the relay's bandwidth budget applies, and a full Retina refresh is slow. The page's top bar says "Direct" or "Via relay".

Using it: a tap is a left click, a two-finger tap a right click, a two-finger drag scrolls. The row at the bottom adds what a soft keyboard lacks: ⌘ ⌥ ⌃ ⇧ apply to the next key, plus esc, ⇥, the arrows and a keyboard toggle; text typed on the soft keyboard goes over character by character, Backspace and Return included. A pinch zooms the picture on the phone, never the Mac: the point between your fingers stays put, up to one Mac pixel across two of the phone's. Zoomed in, two fingers move the view instead of scrolling the Mac; one finger still clicks and drags on the Mac. "Actual size" in the header jumps to one Mac pixel per phone pixel, "Fit to screen" back to the whole page.

Trackpad mode: "Trackpad" in the header makes the whole screen a trackpad (the black bars above and below the picture included) and draws a pointer on the picture; the phone remembers the choice. Sliding one finger moves the pointer by how far the finger travels, not to where it lands: slowly, it moves less than the finger's travel across the picture, for fine positioning; quickly, it crosses the whole screen in one go. A tap clicks where the pointer is, a two-finger tap right-clicks, two fingers slide to scroll; holding still for about half a second and then sliding drags with the left button down, shown by a ring at the pointer's tip, until the finger lifts. A pinch still zooms the picture on the phone. Zoomed in, the pointer always stays where you can see it and the view follows it to the edge; at the picture's own edges there is no margin, so the menu bar and the screen's corners stay reachable. In this mode "Actual size" zooms around the pointer. The pointer starts in the middle of the screen, or where the last tap was, and the Mac's pointer jumps to it at the first slide or tap. noVNC has no way to put the pointer anywhere but under a finger, and its gestures need 50 pixels of travel before a slide counts as a drag, so in this mode the page reads the touches itself and writes RFB pointer events straight into the same channel (after an ARD sign-in the stream is plain RFB).

Smooth mode: "Smooth" in the header has the Mac lower its resolution while you are connected; the phone remembers the choice. Screen Sharing sends every pixel of the framebuffer: a 16" MacBook Pro at "More Space" is 4112×2658, a full-screen change with a photo wallpaper and one window is about 7.5 MB, and the phone decompresses it in JavaScript; at 2336×1510 it is about 2.8 MB, and decoding takes about a third of the time. The mode chosen is the smallest one on the main display with one pixel per point, the current aspect ratio and at least the current size in points, so no window has to shrink; the Mac's own screen just looks soft meanwhile. A screen already at one pixel per point is left alone, and the page says so. Pressing the switch reconnects the page: when the session starts, the daemon has real-bot-rtc display-hold switch the mode before it answers the phone, so Screen Sharing announces the new size when the phone connects; switching it off likewise restores first and answers after. Several devices share one lowering; it is restored 3 seconds after the last session that wanted it ends (room for a reconnect), and at once when you disconnect from the Mac window or turn the remote screen off. The mode is set per process (kCGConfigureForAppOnly): however the helper ends (the daemon closing it, the daemon dying, a crash), macOS puts the previous mode back, and on a clean end it restores first and says so. The Mac window's remote screen card says what the screen is lowered to.

Who is on, and how it ends: while a device is connected, the Mac window's settings card names it and says whether it is direct or via the relay, with a Disconnect button. Revoking that device, turning the switch off, the remote link dropping, the phone page going to the background, or the Mac not hearing the page's keepalive for 45 seconds all end the connection; coming back to the page reconnects.

Windows: Windows has no screen sharing of its own, so the picture and input come from a VNC server the user installs (TightVNC is the one to use, registered as a system service and allowing loopback connections only); the daemon still connects only to local port 5900 (ADR 0059). The phone learns over the remote link that it reached Windows: the page is called "Computer screen", signing in takes only the VNC server's password, the key row reads Ctrl, Alt, Win and Shift, Ctrl+Alt+Del sits in the top bar where Smooth would be, and there is no smooth mode (TightVNC sends JPEG, small already). While a device is connected, real-bot-rtc stay-awake holds off the display and sleep timers, as caffeinate does on a Mac. While no VNC server answers, the computer window's card lists the steps to install and configure one, and its button opens TightVNC's download page. Not yet tried on a real Windows PC.

Interfaces: the phone calls POST /remote/screen/start | offer | tunnel | status | stop over the remote link; these exist only there and loopback cannot reach them. The phone shows the entry only when the /remote/features answer carries screen: "rfb-v1", and host says whether it is a Mac or Windows. The Mac window calls GET / PUT /v1/remote/screen (the switch and ICE servers) and POST /v1/remote/screen/disconnect over the local API; none of these is on the remote allowlist. Protocol details: remote protocol; the trade-offs: ADR 0056. Bots cannot use it.

Avoid: remote desktop, letting Bots drive the screen, turning the switch on from the phone, viewing this Mac from its own window, capturing the screen ourselves, keeping the Mac password on the Mac or the relay, cloud computer

Command stream

The live push of stdout / stderr while a Bot runs a shell, so the call can be seen while it runs. It is visibility only and doesn't change what the call is: stdin is still closed, the timeout is still ten minutes, and paths are still judged per call. It is not a terminal, has no input, and stays out of the main transcript: while running it hangs under that message bubble, when done it folds into one line, and to look back at it you use the turn view in the work dir.

Avoid: input for the command stream, treating it as a terminal session, letting it change approval granularity, putting it in the main transcript, a new message kind

Pane

One content area in the desktop window: a pane is a group of tabs, and a tab can be a session, a terminal, the routine calendar, the workspace, spend, or usage. The window can be split horizontally and vertically into any number of panes; a window split into several panes is still the same window process, and a floating pane is not a second system window either. Only one pane is the current pane at a time; on narrow screens (≤680px) and phones there are no panes, and it is still one screen at a time, stacked layer on layer. Where each kind of window goes when it is not open yet (new tab, replace the same kind, split, neighbouring pane, floating…) is its placement, chosen per kind in the settings and kept on this machine; something already open is only brought forward. Details: Behavior · Pane.

Behavior details

One content area in the desktop window. A pane is a group of tabs; a tab can be a session (its artifact preview and flow board go with it; a header above the conversation says who it is with, what they are doing and how many approvals or questions wait on you there (one click scrolls to the card), and holds pin, flow and settings, the same as the tab's ⋯; its own settings slide out from the right of the conversation in that tab, with an overlay that covers only this pane, and take no tab of their own), or a terminal (one shell session per tab), the routine calendar, the workspace, spend, or usage. The window can be split horizontally and vertically into any number of panes, and both the dividers and the points where they meet can be dragged; tabs can be dragged to reorder within their own tab bar, dragged between panes, dropped on an edge to split in place, or dragged out while holding ⌥ to become a pane floating on top; pressing the empty part of a tab bar drags the whole pane, every tab with it, and while it is carried those tabs all stay where they were, faded, and the chip following the pointer reads "Whole pane · N tabs" (the tab's name when there is only one). Conversations in the sidebar — each row of the list, the pinned avatars, the avatars on the folded rail — drag into the workbench the same way: onto the middle of a pane they open as a new tab there, onto a tab bar at that position, onto an edge in a pane split off there, and let go with ⌥ held they float. A conversation that already has a tab moves that tab rather than opening a second one; let go outside the workbench, nothing changes; a plain click still opens it where its placement says. Right-clicking anywhere in a pane splits off an empty pane in place, up, down, left or right, or offers Float this tab: the tab right-clicked (on the content, the one showing) lifts out into a floating pane of its own, where it was and a little smaller, while the pane's other tabs stay put; a pane left with no tabs closes up, and the only tiled pane stays behind empty; a tab in a floating pane that holds others can be floated on its own too; a tab's ⋯ offers the same; things that already have their own context menu (messages, the code editor) and input boxes keep theirs, and ⌥-right-click gives the web view's native menu. A floating pane works like the original flow board window: drag the bare part of its tab bar to move it, and double-click there to dock it back into the layout; every edge and every corner can be dragged to resize it, the side across staying put, never smaller than what it shows needs and never outside the workbench; it does not split in place, and its right-click menu offers docking it back into the layout and closing the pane. When there are other panes, an empty pane can be closed directly from its top-right corner (not shown on the only tiled pane: closing it would just leave another empty one, and with nothing else on its tab strip the strip folds away); each pane's context menu can also close the whole pane and its tabs, asking first when the workspace or an artifact preview has unsaved edits. Closing the last tiled pane leaves one empty pane. A window split into several panes is still the same window process, and a floating pane is not a second system window either. Only one pane is the current pane at a time — whichever has the keyboard, and clicking or scrolling in a pane also makes it current: Stop hits that pane's turn, and clicking a notification first looks for a pane that already has that session open. When more than one pane is showing, they are set apart the way a Mac sets the front window off the ones behind it: the current pane's card, head and all, stands off the ground on a soft shadow, and every other pane, tiled or floating, steps back in its tab strip alone — tab labels grey and faded, the showing tab sunk into the strip, the card lying flat, and a floating pane that is not the current one on a shorter shadow; nothing a pane shows is dimmed, so a task map or a flow board open in another pane stays clear. With only one pane nothing changes. Something already open is only brought forward when asked for again. Where it goes when nothing shows it yet depends on its kind (conversation, flow, artifacts, workspace, terminal, calendar, spend, usage; a Bot ↔ Bot direct gets no tab and is read in a popup, see Direct) and is chosen in Settings › Behavior › Where windows open (the Behavior tab is there only with the desktop workbench); the choice is kept on this machine, and narrow windows and phones do not have it (ADR 0074). The choices:

  • New tab.
  • Replace the same kind: the same kind of window in front in the current pane, else in the pane nearest the keyboard that shows one; a new tab when that one holds an unsaved edit or none does. "The same kind" is the same row in the settings.
  • New tab in the background: in the current pane, without bringing it forward.
  • Split up, down, left or right: the current pane is divided that way; a new tab when there is no room or the current pane floats.
  • The pane above, below, left or right: into the pane on that side; into the current pane when it is itself on that side; otherwise a pane is split off that way.
  • Floating: a floating pane over the layout. Where this kind of window was last moved or resized while floating, it opens there again at that size (pulled inside a smaller window, and stepped off a float sitting exactly there); one never moved opens in the middle, stepped down and right past the floats already there, back to the middle after six. The places are kept per kind on this machine, and back to defaults forgets them.

The defaults:

  • A conversation replaces the same kind. Going through conversations in the sidebar does not pile up tabs, and in a split window the one replaced is the conversation you were reading, not the artifacts column beside it. A terminal, the workspace and the like are never pushed out.
  • The flow, artifacts and the workspace open in the pane on the right.
  • Terminals open in the pane below.
  • The calendar and spend open in a new tab.

What you pick from a pane's + or an empty pane goes into that pane. Session settings are open on only one session at a time, and opening them elsewhere closes them where they were; there is only one routine calendar at a time. Each session has only one artifact preview and one flow tab (since 2026-10-12, ADR 0086: the rail of tickets, the flow and the plan in the inspector are all in that one tab; before, the trace, board and plan were a tab each): opening another file or another job from this session, from its preview's file tree or from its flow board turns that one over to it rather than opening another. The flow tab opens straight from a right-click: a message's menu has "This job's flow" (landing on the message's card, with the message's ticket picked), and a session's row in the sidebar and a session tab's ⋯ and right-click menu have "Trace" (for that session's latest job); an open tab is brought forward, otherwise a new tab opens in the current pane. A board or plan tab saved before opens as the board opened out or the plan held to its ticket, and asked for again it is the one flow tab. The names of these tabs are fixed and follow the session title, " flow" and "'s artifacts"; they do not change with the open file or the job. Switch away from a flow board tab and back, and it stays where you left it: where you dragged it, the zoom, which turns are folded or unfolded, the filters and the highlighted tickets all stay as they were; the same holds when you switch to another job in its switcher and back; it moves only when a message newly asks it to go to a card. A session pane no longer draws its own top bar: the avatar (with a status dot while a Bot is working, waiting on you or has failed, none when idle) and the name are always on the tab, and the top bar's second line has become the tab's tooltip; ⋯ appears only when the mouse is over the tab and holds pin, trace and settings, and right-clicking a session tab also offers these items ahead of the split options. Right-clicking any tab (also found in a session tab's ⋯) lets you close that tab, the other tabs in the same pane, the tabs to its right, or all tabs; closing all is the same as closing the last tab: the pane closes with it, asking first when the workspace or an artifact preview has unsaved edits. Other tabs (terminal, workspace, routines, spend, flow, artifacts) also always carry their own icon before the name. On narrow screens (≤680px) and phones there are no panes; it is still one screen at a time, stacked layer on layer.

Avoid: multiple windows, second window process, treating a floating pane as a system window, split screen on phones, visible means read, rearranging the layout other than as its placement says, opening a second copy of something already open, leaving empty panes around, settings in their own tab, top bar and tab both announcing the name in a narrow pane

Layout

How the panes are divided: the horizontal and vertical splits, each pane's width and height, which ones float, which one is current, and which session each terminal pane holds. It is kept only in this Mac's localStorage, not in the daemon and not in the URL — the URL says only which session the current pane is showing. After you quit and reopen, it is as it was before; if a session or terminal it refers to is gone, it cleans that up on its own; before resetting to the default single pane, it asks about unsaved changes.

Avoid: layout in the database, layout in the URL, syncing layout across devices, layout that follows you to another machine, reset discarding unsaved changes outright

Schema gate

The check the daemon runs when it opens the database: if the database's schema_min_compatible is above SCHEMA_LEVEL, what this build understands, it refuses to open, writes why to daemon.log, and exits — instead of running on and misreading semantics it does not know. engine_level is the paired integer switch, standing in for a pile of separate feature flags, and only ever moves forward in order: every time the daemon starts it tries to take it up to the highest level this build turns on by itself (ENGINE_LEVEL_BY_DEFAULT, since 2026-10-04 this build's top level, level 8's quality events and lessons), raising the database's floor to what that level needs in the same step (1, for holds; 6 at level 8); a level a later build brings above that starts out experimental and goes up only through a developer's opt-in, whether or not an older installed app shares the data folder. But while an installed app sharing the default data folder does not read the gate yet (or its version cannot be read; off a Mac there is no telling which one is installed, which counts the same), a source run leaves the level where it is, writes why to daemon.log and runs at the level it has — such an older app would open the database whatever its floor says. A developer can accept that on their own machine (engine_gate_optin: POST /v1/capabilities/raise on the local API, or apps/daemon/scripts/engine-level.ts --accept-older-app; never from a phone), taking on that the older app, opened without this daemon running, would ignore holds; the level then goes up at once (at the next start when no daemon runs), and taking the opt-in back never lowers it. The opt-in covers only the level of the build that accepted it (or, past an older installed app, with --level, only as far as asked — level 5 without level 6, say; with no older app in the way --level changes nothing, and the database goes to the default level): a later build bringing a new level asks again; an opt-in an earlier build recorded below the default level never holds a data folder no older app stands in the way of below that default. GET /v1/capabilities reports both numbers, plus which features are on, to whoever calls it — including a phone, since the Mac may be running an older packaged daemon while the messenger's source tree has already moved on.

Avoid: treating schema_min_compatible as the app's version number, raising engine_level before both conditions hold (the gate itself shipped, and the installed app's own version is already new enough) when no developer explicitly accepted an older one, a setting a user would ever see