What Tau core is

Tau core is Pi in a window, plus threads. This document is the list. Anything not on it is an extension, and safe mode (npm run start:safe) must show exactly this list and nothing more.

In core#

Transcript

What a turn's rows are is derived, not decided in a component: src/workbench/transcript-folding.ts turns one turn's tool records into fold, live, group and card rows and holds every rule about them — the fold's label and duration, the trailing-tool exemption, the action classes a summary counts, the tense the live line speaks in. WorkRows.tsx draws those rows and owns only what a reader toggled.

Composer

Threads

Workbench

The workbench and its client#

The workbench is always a client of a host in another process. The window starts that host, watches it and connects the renderer to its socket; the threads belong to the host, so the window may close, crash or reload without stopping one (ADR 0021). Thirteen methods stay on this side — clipboard, image preview, a workspace file shared with the page by URL (tau-ext://files/…, src/main/shared-files.ts: PDFs, images, audio and video inside the open workspace, with byte ranges), the kit bundles the renderer imports, the workbench rebuild, relaunch and update, a system notification, the app icon's badge, a right-click menu the OS draws and the app around the workbench (window-action: menu, quit, release notes) — and a kit that needs the window's process for a native view ships a window half the host calls with callClient.

src/workbench/ is Tau's client without a window: the thread index, the thread on screen, transcript pages, composer scopes and drafts, notices and the host connection, plus WorkbenchStore — the one place a host update becomes client state — and ThreadCommands, everything the workbench does to a thread that is one host call and a notice. It imports no React, no Electron and no browser global, and src/workbench/workbench-boundary.test.ts fails on any of them. Where it needs the view side it names a port, never a class.

src/renderer/ is the React binding of that client, and Platform (src/workbench/platform.ts) is what the client needs of the machine it runs on: clipboard, openExternal, files (only where the host's paths are this machine's), storage, importModule, attention — a notification the OS draws and a count on the app's icon, absent where the client has neither — and contextMenu, a right-click menu the OS draws at a point of the page (src/main/window-context-menu.ts: Menu.popup in the window's process); without it, or when it refuses, the page draws its own. Electron answers it in src/renderer/platform-electron.ts and a browser tab in src/web/platform-web.ts. The sandboxed Electron renderer holds no permission to notify, so its attention asks the window's own process (src/main/window-attention.ts: Notification, app.setBadgeCount); a tab uses the page's Notification API and draws the count into its icon. App.tsx is bootstrap, store wiring and layout; the three facts it cannot work out for itself — which client this is, whether kits were left out, and how to build the platform — arrive as a ClientEnvironment from the entry point.

There are two such entry points in src/. src/renderer/main.tsx is the Electron window; src/web/ is the browser client a listening host serves, which reuses every component and adds only its entry, its platform and the token handling. Below 720 px either of them lays itself out compactly — the composer at the bottom edge, and on a phone the thread list as its home page, with a project filter as an icon in its header and a floating New thread button that starts a draft in the filtered project, else in the one the host last worked in (lastUsedProject, never /), else asks; a tablet's list beside a thread starts it in that thread's project when no filter is set, as ⌘N does everywhere (newThreadProject in src/workbench/new-thread-project.ts). A chat is a screen over the list with Back in its bar. The list, the app pages that claim compact (registerPage, three at most, in their order) and Settings are the phone's main pages, with a bottom navigation (PhoneNav) as their last row (it remembers its pages, so they are there at the next start before the packages that register them have loaded); a chat, a page's view and a Settings section are sub-pages without it. usePhoneNavigation derives the route (src/workbench/phone-route.ts) from the workbench's state, and TouchLayer keeps the browser's history as the path from the list to it (phone-history.ts), so the system's back (Android back, the iOS edge swipe of the native shell) steps out one level and stops at the list; the address names the route (?thread=, ?page=, ?settings=) for a reload or a link — through body[data-profile] and src/renderer/profile-compact.css, not a second component tree. A browser on a touch screen claims compact at any width. A compact client on a tablet's screen (shorter side at least 600 px) and at least 720 px wide gets the desktop's arrangement instead (compactFormFor, with a 40 px hysteresis, never from height or content, and not while the page is hidden): the thread list as a sidebar, the chat, the panels that claim compact as stage tabs beside the chat (one of the two, with "Show chat" in the strip, where there is no room for both), no bottom navigation. The touch pieces live in src/renderer/touch/, a chunk only a compact layout loads: the thread list with a swipe to settle and a long press for every action (TouchThreadList, SwipeRow, ActionSheet), the list's header with search and More as popovers and a floating New thread button (TouchThreadBrowser), on a phone panels that claim compact as sheets over the thread (PanelSheet, opened from the phone's bar — the first two by their glyphs, from three on the rest from its More menu — and by actions.openPanel/closePanel, which reach the sheet there), sheets that close on a pull down (sheet-drag.ts), and TouchLayer — the height the on-screen keyboard leaves (visualViewport), a tap that shows a message's actions, and the open thread in the address (?thread=<id>, which a push notification opens; on a tablet back moves between the threads it held). A touch keyboard's return key writes a newline; the send button sends. A hardware keyboard on a touch screen (no on-screen keyboard over the page, body[data-keyboard] unset) sends on Return and writes a newline on Shift+Return, as the desktop does. Settings on a phone is stacked: the section list, then a page. The hello tells a device paired Read only (ADR 0024) so, and useHostCapabilities().readOnly carries it to kits, which disable a write with that reason instead of offering one the host refuses. Core's own writes follow it: the composer is a note, setting rows are inert with the reason, the palette, the title menu, the compact list and a file tab disable what changes the host (a command or palette row stays enabled with access: "read"; the palette leaves out a group or a source with no such row), a chord for such a command says why instead of running it, thread commands refuse with the reason, and preferences stay on the device.

mobile/ is a third entry point, the native app (Capacitor), which puts the compact client in a shell of its own: saved hosts, pairing by QR code or Bonjour, and a choice of address each time it connects (mobile/src/endpoints.ts). It asks two things of the workbench and nothing of the host: SocketTransportOptions.createSocket, so every socket is a native one that pins the host's key (or checks an address the host flagged for a CA), and ClientEnvironment.shell, the host's name above the thread list and the shell's own actions (Hosts) in its More menu. Each host gets its own view of the page's store (mobile/src/storage.ts), so drafts and the bootstrap cache of one never show up for another.

The model picker (src/renderer/components/ModelPicker.tsx) opens as a popover at the control that opened it (the composer's model chip, a settings field), without a scrim; on a phone or an iPad it is a bottom sheet with the same logic. Escape gives focus back to that control, and a choice made from the composer hands it to the prompt. It keys everything on what the catalog says, in three columns under one search field:

"Recent" (the client's own list) sits at the bottom, one click each, beside a link to pin models in Settings. The search runs across every runtime and groups a model's offerings under it. One menu in the search field holds the sorts (relevance; price, every plan before every API offering, each group by its API price; context; newest, by models.dev's releasedAt), the filters (billing, what a model can do), "Show hidden models" and adding a provider. Keys: ↑↓ move in a column, ← and → move between the columns (from the search field at its start and end), ↵ chooses, ⌥↵ pins, ⌘1–9 reach the first nine favourites of any runtime, ⌘⇧↑↓ step through the runtimes, and typing anywhere goes to the search. A row hides its model (with a pointer; a phone shows only the star). What a runtime hides and the order it lists its models in is modelPreferences.<runtime> in Tau's config, a record of the config levels that Settings → Providers → Models edits per runtime and project; favourites stay one list across runtimes (offeringKey: provider/id for Pi, <runtime>:provider/id otherwise). The host keeps every runtime's catalog, Pi's too, whether or not a thread of it ran (src/main/runtime-catalogs.ts, stale-while-revalidate, on disk across restarts, with price, context, inputs and billing filled from Pi's model data), and the picker lists another runtime's models from it (src/workbench/runtime-catalog-store.ts); a runtime that cannot run says why (not installed, not signed in) instead of listing nothing. A runtime whose program changed since its answer (programKey, the executable's fingerprint) is asked again at once, and the picker's Refresh asks every runtime; Tau's signed model catalog adds models Pi does not know yet (src/main/model-catalog.ts), and src/main/runtime-tool-updates.ts keeps the agent CLIs current from Settings → Runtimes (runtimes.md). Picking another runtime's model binds a new thread's draft to that runtime, and the draft keeps model, level and mode per runtime (runtimeSelections), so switching back and forth loses nothing; for a thread that exists it starts a new thread in the same project on that model, because a thread keeps its runtime, and offers the runtime-switch commands kits register beside it (Handoff Kit's "Continue in"). A draft bound for a runtime other than the thread on screen chooses from that runtime's own catalog (runtime-catalog, which a backend answers through newThreadCatalog without opening a thread): the composer's model and thinking pickers work as they do for Pi, the draft keeps the choice with the runtime it was made for (src/workbench/runtime-catalog-store.ts), and newSession hands model and level to the new thread through catalogWrite before its first prompt. A runtime that can only name its models inside a session (Antigravity before its first one) says so, and the thread starts on its default. One rule picks the marks everywhere (providerMarks in src/renderer/runtime-marks.ts), at most two, and never the model's maker: a runtime with a provider it owns shows its own mark alone (Codex with OpenAI, the Agent SDK runtime with Anthropic, Grok with xAI, Antigravity with Google). The runtime says which providers those are (homeProviders on its backend, published on runtimeBackends); core knows only that a runtime owns the provider of its own name (Cursor's cursor). Any other pair, Pi included, shows the access mark and the runtime's side by side, the runtime's a shade quieter (ProviderIconStack); a subscription plan wears its product's mark (Pi's openai-codex is Codex's; an Anthropic plan login the Agent SDK runtime's), unless the runtime says its plans are its own (ownPlan: Cursor, Antigravity), where Claude on the Cursor plan stays "Cursor via Anthropic". The tooltip names both. A list that is one runtime's (Pi's settings, the Pi-providers list, a runtime's own list in the picker) leaves the runtime's mark out. What the catalog does not say — which generations are legacy, which model wears a "new" badge for a while — lives in src/renderer/model-manifest.ts, hand maintained and dated; an unmatched model is current. A model behind a subscription login the runtime performs (UiModel.login, billing: "subscription") reads "incl." in the price column (the row's accessible name says Plan); whether that is worth a warning is a policy, and policies reach the picker and the composer through registerModelBadge and registerComposerGate. In a new thread's picker, Shift-click hands the model to the set an extension keeps (registerModelSelection) instead of choosing it; without one it chooses.

The stage#

The stage is the column right of the conversation, starting at the window's top edge with its tab strip (the design's 40 px on the side surface, the tab in front joined to its content, an "M" on a changed file, a dot on unsaved work, a panel's count, "All tabs" once some scroll out of view, then the tools, the stage-bar region and the maximize). Core owns the tab strip, the placement, the preview and pin rules and the strip's own gestures — double-click pins a preview, the middle button and Escape close, mod+w closes the active tab, ctrl+tab and ctrl+shift+tab move through them, and the right-click menu offers close, close others, close to the right and pin/unpin. Two kinds of tab are core's own, and a third belongs to whoever registered it.

A file tab shows source or the working-tree diff; core owns the tab strip and the placement, and whoever registered the document source loads the content. openFile(path, { line }) opens the source scrolled to a line and marks it; the tab keeps the line with a request count, so asking for it again scrolls there again. The header draws the commands extensions offer on the file-tab surface (Files Kit's "Edit file"); they read the tab from actions.activeStageTab(). A thread tab shows another thread's transcript, read-only, drawn with the same VirtualTranscript the conversation uses and headed by the title, status and cost the thread index carries. It exists so a sub-agent's chat can be read without becoming the thread the composer talks to; "Take over" is the one button that does switch — and even then the child stays out of the rail, which never lists a thread with a parent. It reloads when the index republishes that thread's entry — which is what the host does when a background turn settles — and polls every two seconds only while the index says the thread is streaming. A tab whose session the index no longer knows shows an empty state rather than an error. WorkbenchActions.openThread(sessionId) is how an extension opens one; the Agents Kit panel is the caller that motivated it. With { machine } (API 1.15.0) the tab reads a thread of another machine the window knows, over the window's own connection there (RemoteThreadDocument, environment-thread-watch.ts): subscribed only while the tab is open, read again at each change, and "Open on <machine>" in place of "Take over". It reads whether or not a runtime holds the thread: a released Pi thread is projected from its session file, a thread of another backend from the shell that backend keeps for the index, and neither opens a runtime.

An extension tab is a kind a desktop extension registered with registerStageTab: the kind supplies the title, the glyph and the content, core everything else. The tab is { id, kind: "extension", tabKind, params, title } with params plain JSON, so it is the same tab whatever draws it and it survives being written to storage; the content talks back through a handle (setTitle, setDirty, onClose), and StageTabController (src/renderer/stage-tab-controller.ts) is the one door that closes a tab of any kind — it asks about unsaved work, runs that tab's listeners and forgets its handle. A kit that goes away takes its tabs with it. Terminal Kit's "open as tab" is the shipped caller; docs/EXTENSIONS.md is the guide.

The stage belongs to the thread (K72). Switching threads writes the stage being left at once and shows the next one's (useWorkbenchLayoutState over ThreadStages); a draft has its own, which becomes its thread's when the first message makes the thread (the draft's composer scope moving to the thread's is the signal), and a draft moved to another project starts empty. A new thread starts with an empty stage; its header's toggle opens the tool last picked in the project. Hiding a thread's stage unmounts its tabs' content but closes nothing: no onClose runs, handles stay, and what a tab shows lives on — a terminal's shells keep running in the host (they end with their thread), the Preview page is the window's one browser and is neither closed nor reloaded, and a Files Kit buffer with unsaved work stays with its document. Each tab still reads and writes in its own project: the project is the one its thread or draft belongs to (stageWorkspace), as in K68 and K71. The first thread shown after the update to per-thread stages, and otherwise a project's most recent thread, takes the stage its project kept before (tau.stage.v1:<workspace>), with the dock's open state and drawer; no other thread does.

Not in core#

These were extension work still inside core files when Phase 1b in PLAN.md started the move; Phase 6 finished it, and each row below names the kit under kits/ that owns it now.

Feature Owner today Belongs to
Git status, staging, commit, push, worktrees, file tree, file reading, editors, branch labels and repository names in the thread index Workspace Kit: kits/workspace/, a package Tau ships (ADR 0014). Host commands, its own Git cache and the project facts it supplies through describeProjects (ADR 0007); core lends the title-bar region (where its actions fold to the room they get, title-collapse.ts: labels become icons, then the project actions and "Open in" move under More, the Git action's label goes last), the header's thread-branch slot (the branch menu), the transcript footer and the document source seam for the stage. A thread's own worktree is the kit's too: a new thread's Branch section (in "Run on" and behind the draft header's branch) chooses the mode, the branch and its base before the first turn and the kit creates it through core's beforeNewThread gate, which knows nothing of Git (ADR 0017). Servers writes Git in a project only through it: repo-from-tree, commit-files-to-branch and merge-branch name tau.servers as their only caller (ADR 0020) and run one write per project at a time in git-coordinator.ts Workspace Kit
Turn checkpoints and restore Workspace Kit on both sides: capture, restore, recovery and ref upkeep in kits/workspace/host-lifecycle.ts through the seam's lifecycle hooks and turn observer; status and the restore dialog in kits/workspace/checkpoints.tsx, and the turn's changes as a pill (kits/workspace/turn-changes.tsx): the running or latest turn's centred over the composer beside Jump to latest (region composer-controls), every earlier turn's in the transcript under its answer; hovering or clicking one opens its files, Open diff and Rewind in a popover, and a file opens the turn's diff. Each snapshot keeps HEAD; when a branch switch, pull or reset moved it during the turn, the files that step brought are left out (read from HEAD's reflog, kits/workspace/turn-attribution.ts; the turn's own commits still count), and a turn whose files cannot be split says "Branch changed" instead of a count. Records written before HEAD was kept are read again the same way. The branch menu asks before switching a checkout a turn is running in, and the header's "N files changed" counts the thread's own: a worktree's branch against its base, else the uncommitted files its turns changed (thread-changes.ts). The Git and lease engine it drives lives with it (kits/workspace/workspace-git.ts, workspace-checkpoint-lease.ts, workspace-kit-checkpoints.ts, the turn-checkpoint-* family), and kits/workspace/pi.ts is the half that captures a turn inside a Pi TUI Tau is only attached to. Core keeps only what an anchor needs: a pinned text-empty assistant entry stays in the transcript Workspace Kit
Thread rail: the sidebar, its search, the settled shelf and the project switcher Workspace Kit's kits/workspace/navigation.tsx, filled through registerSidebar. Core owns threads and draws a row with ThreadRow from the tau API; a thread nobody has written to is a draft, drawn with DraftRow above the active threads until its first message makes it a thread; without the kit the window has no sidebar and still shows a composer and a transcript. The store's registerThreadRailOrganizer lets another kit decide the sections, the row menu and what a drag does (Thread Rail, below). A row's hover card (kits/workspace/thread-card.tsx) is the kit's too: one layer on the list opens it after a rest or on keyboard focus, and other kits add lines and sections to it through registerThreadCardSection (Machines Kit the machine, Terminal Kit running programs, Review Kit the pull requests); core only lends ThreadRow's hoverCard, which drops the row's own tooltip Workspace Kit
Review mode, diff viewer Review Kit overlay over the store Workspace Kit publishes as tau.workspace/store; the Files and Changes panels and the turn-changes pills are Workspace Kit's (the Files panel, its documents and the follower also on a phone or tablet); with Review Kit present the Changes entry opens the review (a panel's redirect), which carries the panel's staging and Changes sections Review Kit
Stage tabs, file viewer tabs and placement stay core (the document area); loading, changed markers and editors come from the registered document source. A thread tab is core's own content: it reads the transcript through transcript-page and draws it with core's VirtualTranscript. Any other content is a kit's: registerStageTab is the seam, and core never learns what a kind draws core placement, Workspace Kit file content, kits for their own kinds
Thread title generation Thread Title Generator: kits/thread-titles/, a package Tau ships (ADR 0014) Thread Title Generator
Installing, updating and removing extension packages: /install, /remove, /update and the Settings → Packages page Packages Kit: kits/packages/, a package Tau ships. The installer itself (npm, git, packages.json, signatures) stays core behind the packages permission; the kit owns the verbs, the wording and the page Packages Kit
Signals: host events, live counts and the shell-run presentation Signals: kits/signals/, a package Tau ships (ADR 0014); a Settings page for debugging Tau itself (mod+alt+o), not a tool beside the chat; core lends the page and the useObservatory hook it reads Signals
Access gate (read-only, ask, full) Access Kit: kits/access/, a package Tau ships (ADR 0014); the gate itself is the Pi extension in kits/access/gate.ts, and its thread-level command, which only Agents Kit may call, narrows one thread below the workbench's level; thread-level-of, which only Servers Kit may call, answers the level a thread runs at. The same decision (gateToolCall) gates Tau's tools over MCP (services.mcp.gate), so a tool asks alike whichever runtime calls it; tau_apply_thread_changes asks like an edit. On Settings → Runtimes it adds the "Before a runtime may…" cards: what each level lets a runtime do, with the level to choose Access Kit, on by default; approvals are Pi ctx.ui.confirm questions, over MCP the same confirm in the thread
Preview browser: the panel, the WebContentsView over it and the preview_* tools the agent drives it with Preview Kit: kits/preview/, a package Tau ships (ADR 0014) — host.ts with view.ts and page-script.ts, desktop.tsx with panel.tsx, store.ts and overlay-watch.ts. Core lends the panel slot, one composer region, and src/renderer/reserved-region.ts — the rectangle a native view owns, which core's own floats keep clear of; the kit publishes it through reserveRegion on tau (ADR 0012). The panel's tool row picks an element (page-overlay.ts, run in an isolated world: selector, tag, text, box, trimmed markup and a cut-out image) or annotates the page (numbered rectangles, arrows and notes, sent as the page with them drawn and a numbered list); both land in the composer as a text-excerpt chip through Composer Context's chip service with the image beside the draft's images. It records the view to webm under the kit's stateDir (recorder.ts: a hidden page asks for display media and the session hands it the preview's frame; one-second chunks, 10 minutes or 500 MB at most) and attaches the file as a chip. Named profiles each get their own partition (persist:tau-preview-<name>; the default keeps persist:tau-preview), and the host suggests local dev servers under the address bar (ports.ts: lsof, else netstat, through findCommand; listeners under the workspace, on dev ports or run by dev programs, probed over HTTP). mod+shift+j toggles the panel. The preview_* tools reach Codex, Agent SDK and Antigravity threads over the host's MCP endpoint too (ADR 0022). While Computer Use is on, the panel switches between Browser and Screen: screen-view.tsx (loaded lazily) draws Computer Use's tau.computer-use/screen stream — the active thread's driven window, live or as its latest screenshot, under the agent's cursor from agent-cursor.tsx. The tool row's cookie button copies chosen sites' cookies from Chrome, Arc, Brave, Edge, Vivaldi, Chromium, Firefox or Safari into a profile (cookie-*.ts, run in the window half; Chromium keys from the keychain only on Import; browser-cookie-import.md has the security rules). The page has a zoom, a viewport (fill, or a fixed CSS size scaled to fit: viewport.ts) and an emulated colour scheme of its own, set from the tool row's menu, by the agent (preview_resize, preview_set_appearance) and by defaults in Settings → Preview; the page's ⌘R, ⇧⌘R, ⌘+, ⌘− and ⌘0 are taken in its before-input-event, ahead of the app menu. The agent's cursor is drawn into the page where each action landed (page-cursor.ts with E24's marks), and a recording (preview_recording_start/_stop too) draws a clean pointer, clicks and keys into the page at the frame rate Settings names. Profiles have names apart from their ids and are renamed or deleted (which clears the partition); recent pages fill the empty panel; a plain click on a web link in a reply opens here when Settings says so. While an agent drives the page or a Computer Use window and the panel is out of sight, a floating preview (mini-player.tsx) shows a picture of it above the composer: display only, larger on hover, moved between corners and resized, with jumps to the app and to the Preview; the host holds who drives (until that turn ends), so every client draws the same, and each device keeps the player's corner and width in its own client storage (mini-prefs.ts). The view lives in one Tau window on the host's machine, pinned through callClient's window option, so the panel, the agent's tools and every device reach the same page. Whatever that window shows (Settings, another thread, no Preview panel, minimized), the page keeps its size and paints for a capture: view-placement.ts sizes a hidden view by showing and hiding it in one task and, while the window is minimized or hidden, keeps the view in a window that is never shown; a view off screen is captured with DevTools' Page.captureScreenshot, since capturePage has no frame of it. A client away from the host (web, compact, a window showing another machine) gets remote-view.tsx instead: frames from the host command live-frame (JPEG at the width the client asks, only an id while unchanged, shared between clients asking at once; live-frames.ts asks only while the view is on screen and paces and sizes itself to the link) and, with Full access, input — a tap as a click, a drag as a scroll, text into the focused field (the device's field turns into a password field for a secret one) and Enter, Tab, Backspace, Escape and the arrows, as trusted DevTools input to the page (remote-input.ts; the user's decision 7). On a phone a strip above the composer (remote-panel.tsx) shows what an agent drives and opens the Preview sheet. A device's frame requests describe its screen (the view's CSS size, pixel ratio, touch; viewer.ts), and the page is laid out for it with DevTools' device metrics and touch emulation while the viewport is Fill (device-layout.ts): a device that watches takes the layout only while the host window does not show the page, takes it any time with "Fit this screen", and the host window takes it back when it shows the page again, from its own note, or 15 s after the device stopped asking; a Read-only device watches at whatever layout the page has. A page laid out for a device is never drawn in the host window (Chromium would size its surface past the panel): it paints in the never-shown stage, and the host's panel shows its pictures under a note that gives it back. The host's state says when no Tau window on its machine can draw the page (noWindow): a device then shows "<machine> has no display", pointing a Linux host without a display to tau service install --display and any other to the Tau app there, and looks again every 5 s. A tab that looks in on a thread of another machine shows that machine's page small and view only (look-in.tsx in the look-in region, through environments.readExtension: state and live-frame only) Preview Kit
Service tier, questionnaire Service Tier is kits/service-tier/ and Questionnaires is kits/questionnaire/, packages Tau ships (ADR 0014); the questionnaire pages through its questions — the ask-user tool's, and a runtime's several questions or form fields — with its own prompt renderer over core's prompt frame separate packages
Computer use: the desktop-automation tools, how a run of them reads in the transcript, and the window the agent drives Computer Use: kits/computer-use/, a package Tau ships (ADR 0014). The host loads the @amaster.ai/pi-computer-use npm package through loadRuntimeExtension — its driver binaries have to stay where npm put them — and the kit contributes it to every runtime unless the user configured the Pi package themselves. A second runtime extension watches every computer_use_* call and result either way (screen-feed.ts): per thread the driven window, its last window-scoped driver screenshots (never a desktop screenshot or a zoom crop) and the agent's recent inputs, placed in screenshot pixels. The desktop half publishes that stream as tau.computer-use/screen; the window half (window.ts) reads the Screen Recording status without prompting, records the one driven window by its id when the system already allows it, and draws the app's icon. A device away from the host drives that window through remote-control.ts: screen-input runs the thread's own click, scroll, type_text or press_key at the pid and window the feed names, then looks again; screen-view-frame answers the live picture or the driver's screenshot scaled down in the window half Computer Use
Spawning sub-agents Agents Kit: kits/agents/, a package Tau ships (ADR 0014). Its host half contributes tau_spawn_thread, tau_get_thread_status, tau_wait_for_thread, tau_send_to_thread (modes auto, queue, steer, restart), tau_cancel_thread, tau_apply_thread_changes and tau_list_threads to every runtime (spawn, send and cancel take a clientRequestId, so a retry does nothing twice; a child that finishes a turn a tool gave it while nobody waits for it wakes its parent with one message carrying its answer, through sessions.send) — to Pi as a runtime extension, to the others over the host's MCP endpoint (ADR 0022); a spawned thread works in a worktree of its own, branched from the parent's state, and the parent takes that work back with one apply (ADR 0017), through the one Workspace Kit module the kit imports; a sub-agent is an ordinary thread in the same project (ADR 0013). Its desktop half owns the Agents stage tab ("Agents N", views Running / Asks / Done: a title and a mono line per agent — model, time, its tool now or what it changed — a held question in amber as "Question", finished agents under "Done · N — from turn X", the parent's prompt count the host records at spawn), the one-line spawn card the transcript shows for a tau_spawn_thread batch ("Started 6 agents · 5 running · 1 question", through registerToolCard), which opens that tab, and publishes the lineage the navigator folds away and counts; a spawned Pi thread's commands run at a lower priority through the shellCommandPrefix option of registerRuntimeExtension, so a sandbox around them does not undo it (below, Out of sight); the kit reads its running budget and that level (priority) from the user's ~/.tau/agents.json and keeps its own link index in <userData>/kit-state/tau.agents/agents-links.json (services.stateDir) so the rail hides agent threads from the first paint. Core keeps the record the index reads: sessions.start({ parent }) writes the link entry and src/main/session-lineage.ts turns it into UiSession.parentThreadId. A project's agent definitions (.tau/agents/*.md, agent-definitions.md) are the kit's too: it reads them, lists them in the panel and in the Inspector, and starts a thread from one with its system prompt, model, tools, access and workspace — on another runtime through sessions.start({ backend }). A child may also run on another machine (machine on the tool or the definition, else Settings → Agents): it is a link of Remote Work's tau.remote-work/threads (ADR 0027), carries a "rex" chip in the panel, counts against that machine's budget (its cores), comes back as a branch through the same apply, and its thread there moves to that machine's trash once settled Agents Kit
What Pi extensions draw through ctx.ui: statuses, the working message and text widgets around the composer Pi UI: kits/pi-ui/, a package Tau ships (ADR 0014); core lends the presentUi seam, the status line and the two composer regions Pi UI
Claude Code backend Claude Code: kits/claude-code/, a package Tau ships (ADR 0014), registering a runtime backend through registerRuntimeBackend and driving the installed claude through the Agent SDK; core knows only Pi and offers the backend-neutral routes onEvent and ask (ADR 0005 amendments) Claude Code kit, shipped by default
Model for a kit's small job HostExtensionServices.complete (src/main/host-completion.ts): one short answer on the user's own ~/.pi/agent model configuration, for a title, a branch name or a commit message. Core neither writes those prompts nor picks the model; the kit names one (smallCompletionModel picks a small one) or takes the user's default Title generator, Worktree Names, Review Kit
Antigravity backend Gemini through Google's own agent: kits/antigravity/ registers a runtime backend that downloads Google's Antigravity ACP server from the official registry URL (or takes TAU_ANTIGRAVITY_ACP_COMMAND), speaks the Agent Client Protocol to it over stdio, lets the user sign in with Google inside that server, forwards the user's own MCP servers and skills, answers the agent's elicitation forms field by field, keeps tool cards across restarts, and fills its card on Settings → Providers; the same onEvent and ask routes Antigravity kit, shipped by default
Project sources: folder browsing, native folder picker, Git clone Workspace Kit host entry (kits/workspace/host.ts) and its two sources in kits/workspace/navigation.tsx; core keeps the sources modal as the placement for registerProjectSource (openProjectSources(source) opens one source's view), and assertAllowedCloneSource stays core's because the package installer clones too. The palette's Add project… (kits/workspace/add-project-menu.tsx) browses the host's folders one level per folder, picks one natively or opens the clone form Workspace Kit
Terminals: a shell per workspace or thread, as a stage tab (or in the drawer, by the kit's setting) on desktop, web and a tablet, and as a sheet on a phone Terminal Kit: kits/terminal/, a package Tau ships (ADR 0014). Its host half holds one node-pty session per terminal (kits/terminal/host.ts), loaded through loadDependency so the native addon stays where npm put it; zsh, bash and fish there get a pi that loads the host's session lock extension (kits/terminal/pi-session-lock.ts, startup files under the kit's state folder, the user's own files untouched), filed under the workspace the host has open and, when a thread asked, started in that thread's worktree; input and resize are commands, output and exit are pushes with byte offsets, so a reloaded client replays without drawing twice; output goes under a per-shell topic only to clients drawing that shell. Each shell keeps its last 5,000 lines for a client that reattaches, and the host names a program running in its foreground so closing can ask first. Terminals die with the workspace (afterWorkspaceClose) or the kit, never with the window. Its desktop half is the Terminal panel over xterm.js, a stage tab from the strip's Terminal button: tabs of split panes (kits/terminal/layout.ts, kept in client storage and reconciled with the host's shells), scoped to the thread on screen: its own and the project's shells are tabs (one row of chrome in the drawer and on the stage: a stage-like strip with an × per tab that scrolls in itself, then the actions, whose split buttons fold into the More menu where it is narrow; on the stage with one tab and no other threads' shells the strip is left out, the tab naming the shell, and the actions float at the corner; a pane has a header of its own only in a split or once its shell exited, and the path is the tab's tooltip), and another thread's shells keep running there behind one "in other threads" button whose menu shows one here on request. Opening the panel (the strip's Terminal button, mod+j, the phone's sheet) starts a shell when the thread and the project have none (kits/terminal/scope.ts). Split dividers drag (or move with the arrow keys), and the shares are kept with the layout. "Open as tab" moves a shell to the stage through registerStageTab; a stage tab splits like a panel tab, and closing it gives its shells back to the panel as one tab, never ending them. The host follows the directory each shell reports with OSC 7 (currentCwd): a pane and a shell without a given name are titled by it, a split starts there, an excerpt names it, and a pane inside the followed project opens it through Workspace Kit's "Open in". The terminal chords (mod+d, mod+shift+d, mod+n, mod+w, mod+], mod+[) are keybindings under terminalFocus, so they reach the command before the shell and can be rebound; mod+j puts the keyboard in the last shell of the thread on screen (showing the panel or bringing it forward, starting a shell when there is none) and, pressed in a shell, hides the panel and hands the keyboard to the composer. The xterm palette is read from the theme's tokens and read again when the theme changes; on a light ground its ANSI colours are ones that read on paper (kits/terminal/palette.ts). A selection goes to the composer as an excerpt through Composer Context's chip service, a localhost URL opens in the Preview through Preview Kit's service, and the font follows the kit's settings, else the user's Ghostty config (read only), else the platform's monospace faces; its tau.terminal/font service lets Appearance Kit show and set it under Typography. Other kits run a command in a shell the user sees through its tau.terminal/run service. A compact client registers its own panel for the same shells (kits/terminal/compact.tsx), which the phone's bar opens as a sheet: one shell at a time with a swipeable strip of tabs to switch (the active one carries its ×; other threads' shells are under ⋯), a key bar (esc, Ctrl and Alt that apply to the next key only, tab, arrows in the program's cursor mode, `~ / -, paste, Ctrl-C, show or hide the keyboard; touch-keys.ts`) whose presses keep focus in the shell, a text size of the client's own, input with autocorrect, capitals and suggestions off, and a drag that scrolls the scrollback (arrow keys in a full-screen program). The sheet follows the visual viewport, so the pty resizes when the on-screen keyboard opens or closes
Markdown export, clipboard, image preview src/main/index.ts, PiHost core (Pi has /export and /copy)
Project scripts: quick actions a repository checks in, the worktree setup, a script's preview Project Scripts: kits/project-scripts/, a package Tau ships (ADR 0014). Its host half reads .tau/project.json per checkout (project-file.md, schema in docs/schemas/), runs a script with /bin/sh -c as a job whose output and exit code are pushes, runs the runOnWorktreeCreate scripts Workspace Kit asks for (worktree-created, callers tau.workspace) and watches the file itself, because core watches only what the host reads. Its desktop half is the bar above the composer with run cards, one script.<id>.run command and chord per script, problems through setProblems, the preview through Preview Kit's tau.preview/browser service and "run in a terminal" through Terminal Kit's host commands Project Scripts
Pull and merge requests: create (generated title and body, draft, template), edit, merge (deleting the branch when asked), auto-merge on and off, revert, status and checks on the rail row Review Kit: kits/review/requests-host.ts asks the source-control provider the remote belongs to (kits/review/provider.ts is the interface, provider-registry.ts finds the provider by the remote's host or the user's choice for a self-hosted server in <stateDir>/source-hosts.json): GitHub through gh, GitLab through glab, Forgejo and Gitea through tea api, Bitbucket Cloud through its REST API with the credential Git's own helper holds, Azure DevOps through az repos; each CLI found with findCommand, no credential stored by Tau. What a provider cannot do is in its capability table (PROVIDERS in protocol.ts) and hidden on the desktop; Settings → Review lists each provider's setup and the self-hosted servers. The Git it needs (push with upstream, branch context, template from the base tree) is Workspace Kit's, reached through commands that name tau.review as caller (ADR 0020); the other way round, Workspace Kit asks branch-request (callers tau.workspace) for a branch's request, with the branch and remote it read, to base the branch diff on it and to count a squash or rebase merge for its cleanup. Auto-merge (GitHub, GitLab, Azure DevOps), revert (GitHub) and deleting the branch on merge (every provider; on GitHub afterwards, never a branch another open request is based on or the default branch) are rows of the capability table like the rest. Settings → Review holds "Delete the branch after merging", the box a merge's question starts with. The desktop half fills the Changes section and the rail-row mark Workspace Kit's store lends; core lends only ThreadRow's accessory Review Kit
Chips in the composer — files by @, pull requests by #, excerpts other kits hand over — file attachments of any type, and large pastes folded into a text file Composer Context: kits/composer-context/, a package Tau ships (ADR 0014). Its desktop half fills core's inline slot (registerComposerInline) and publishes the chip service tau.composer-context/chips for the kits that have context to give; its host half stores attachments in <userData>/kit-state/tau.composer-context/attachments/<thread>/ and drops a thread's folder when the host purges the thread (threadDeleted), reads what a file chip points at and lists files and pull requests (gh, glab through findCommand). A chip becomes text before the prompt when it is sent; an attachment goes as a file to a runtime that opens files and as text or a path to one that does not Composer Context
Usage overview: what the threads Tau ran have used, by period, project, runtime and model; each subscription's limits; the user's model prices Usage: kits/usage/, a package Tau ships (ADR 0014). Its host half runs in a worker and reads only what runtimes wrote down: every response in Pi's session files under services.sessionsDir (a response a fork copied counts once), summed per model and quarter hour and cached per file by size and mtime in services.stateDir (a grown file is read on from where the last read stopped), and the turns Codex, the Agent SDK runtime and Antigravity keep per thread, through a usage command each of them grants to tau.usage (ADR 0020); a thread kept before turns were counts whole by its last activity. Work the CLIs logged outside Tau counts as well: Codex, the Agent SDK runtime and OpenCode name each instance's log folders through usage-logs, and the worker reads Codex rollouts, the CLI's project files and OpenCode's database there (counts only, summed per quarter hour and cached per file in outside-usage.json, grown logs read on from their last offset, a first read in the background; bounded memory, see docs/EXTENSIONS.md); a session a Tau thread ran as counts once, with its thread, and the rest is marked outside Tau. It prices its rows through services.priceUsage, so billed cost and a subscription's API value stay apart, and asks usage-limits of Codex, the Agent SDK runtime and Pi Limits for each plan's windows (read at most every five minutes, never changing the account). A backend that does not answer is shown as not available. Its desktop half is the Usage page (registerPage, in the sidebar's foot as juicebars — a thin upright bar per plan window with what is left in the provider's colour, grouped by account, one account once across machines, its windows chosen per device with "Show in sidebar" on the page, read two seconds after start, every five minutes and after each run, a card on hover and the limits on a click; on a phone or tablet the same bars as a touch-sized strip on top of the thread list (thread-list-head), each account with its lowest rest, chosen with "Show in thread list" — a phone's bottom navigation and behind the "Usage" command; its page sidebar has this month's figure against the month before and at its pace, the filters and links to the sections, which a phone and a tablet draw on top of the page): it draws what it read last at once (kept by the client per machine, last-state.ts) and asks for the last 90 of the client's days (days, so a remote host counts the user's midnights) and the host answers them split by day, thread and model (entries, priced like the rows); the page shows what today, the last 7 and 30 days cost, billed and a plan's value side by side, how much of each plan window is left and when it resets, as Juicebar shows it (the figure and the bar are the rest, the diamond the target: what would be left now at an even pace), and what the last readings say (limit reached, runs out before the reset, reaches the target soon, below it, lasts; the host keeps 24 hours of readings in limit-history.json and the last windows of a source whose read failed), runtimes signed in to one account (by the hashed identity their kits read locally) as one entry with its costs summed per runtime, a 90-day activity calendar beside the days as columns, and the projects, models and threads that used the most, each provider in its own colour (--provider-*), with a filter for work in Tau or outside it; in a desktop window it adds the connected machines' usage and limits (environments.readExtension, one account once across machines) with a filter per machine, with the model-price editor (modelPrices in Tau's config) and the sources as views of the page. Core lends the pricing (UsagePricing: the user's prices, the runtime's, Pi's API list; UiThreadUsage.subscription) that the composer's thread cost and the rail's hover card use as well Usage
Organising the rail: pinned, active, snoozed and settled threads, dragging them between sections and within one, snooze until a time, auto-settle rules (quiet for N days, request merged or closed), archiving and deleting threads with an undo notice (and a question first: deleting asks by default, archiving and unpinning when turned on), the thread commands and chords, starting a new thread in the background (⌘↵) and one prompt to several models Thread Rail: kits/thread-rail/, a package Tau ships (ADR 0014). Its host half keeps the meta per thread in <userData>/kit-state/tau.thread-rail/thread-meta.json, pushes each change and sweeps every five minutes, while no window is open too: a snooze that ran out wakes, and an idle thread settles by the rules in its Settings page, the request state asked of Review Kit's pr-status (callers tau.thread-rail) for a thread that has its worktree to itself and, for every thread, the requests it links (thread-requests): once all of them merged or closed the thread settles, and one still open or unknown keeps it active. Its desktop half is the organizer Workspace Kit's rail lends (registerThreadRailOrganizer), claims a new thread's prompt through claimNewThread for ⌘↵ and for a model set built with registerModelSelection, starts those threads with services.sessions.start in worktrees prepareThreadWorktree makes, and publishes the sibling groups as tau.thread-rail/siblings for the Agents panel. Core's old pin and settle lists in the preferences are handed over once and then mirrored, so the title menu's Pin and Settle keep working. Archive and Delete sit in the row menu and the title menu (the row menu is the OS's own where the client has one, useContextMenu, with the snooze presets in a submenu): an archived thread (archivedAt in the meta; a running one is refused, new work brings it back) leaves the rail and is listed under Settings → Archived by project, and Delete sends the thread to core's trash (sessions.remove), leaving the thread on screen for the newest other thread of its project first. Unpin, settle, snooze, archive and delete leave a toast with Undo on core's stack for five seconds (undo.ts: consecutive actions of one kind are one notice and are undone together; a later action of the kind, or the opposite by hand, spends the earlier undo); thread.undo takes the group back and is bound to mod+z under !terminalFocus && !editableFocus. Settings → Archived also lists the threads in core's trash, to restore or delete for good. Over a settled thread's composer a quiet bar says so and offers Un-settle (composer-above region). Settling or snoozing the thread on screen by hand opens the next active thread in the rail (nextActiveThread in meta.ts); the host's automatic settles move nobody Thread Rail
Prompt tools: stashing a draft, recalling earlier prompts, citing a reply, queue or steer while a turn runs Prompt Tools: kits/prompt-tools/, a package Tau ships (ADR 0014). Its desktop half binds mod+s to "Stash the draft" and draws the Stash control with its count in the composer toolbar: an entry keeps the text, Composer Context's chips (through tau.composer-context/chips) and the images, per project, twenty at most, and restoring one stashes what the composer held first. ↑ in an empty composer recalls the thread's prompts, then the project's other threads' (keyDown on registerComposerInline); "Cite" on an assistant reply (registerMessageAction) puts the selection, or the reply, into the composer as a quote chip, or as a > block without Composer Context; its "While a turn runs" option answers streamingDelivery. Its host half runs in a worker and keeps the stash in <userData>/kit-state/tau.prompt-tools/stash/, and reads the project's prompts from the session files sessions.list names. Core lends those three seams, actions for a composer control and a draft's images (composerImages, setComposerImages) Prompt Tools
Line comments on a diff as context for the next prompt, split or unified diffs, hidden whitespace, files that start collapsed; rewinding to a checkpoint with or without its files Review Kit fills the seams core's ReviewMode lends (lines, layout, ignoreWhitespace, filesStartCollapsed, toolbar, aside; DiffView takes the same line seam): a comment opens under the line its gutter button belongs to, the kit keeps them per workspace in client storage, lists them beside the diffs and hands them to Composer Context's chip service as text-excerpt chips — as text in the draft when that kit is off. Core draws the gutter button and the row under a line and knows no comment; its own review notes are gone, and the kit takes over the ones a user left once. Workspace Kit answers ignoreWhitespace with git diff --ignore-all-space, and its turn-changes pill asks how to rewind: "Keep changes" branches the conversation at the checkpoint's answer through its own rewind command and touches no file, "Revert files too" is the restore above, backup thread first Review Kit, Workspace Kit
The warning before a subscription login a vendor forbids outside its own apps (Anthropic, Google) Subscription Login Warning: kits/subscription-login/, a package Tau ships (ADR 0014) with a desktop half and no host half. A shield before the thread title (thread-title region), a badge and a line in the model picker (registerModelBadge), and one question per provider before such a model is first chosen or sent to (registerComposerGate); the acknowledgements are the kit's own preference value. Core keeps only the UiModel.login fact and the neutral "incl." price Subscription Login Warning
Search: the project's files by content, a file by name, threads and projects from the palette Search: kits/search/, a package Tau ships (ADR 0014). Its host half runs in a worker: ⇧⌘F's content search runs the machine's rg found with findCommand (--json, .gitignore honoured in a Git checkout or not, hidden files but never .git, 500 hits) and stops the running search when the next query arrives, and without ripgrep it walks the project itself, reading each folder's .gitignore; ⌘P ranks the project's file list, cached per project and forgotten when Workspace Kit's store reports a new Git status, with its own fuzzy scorer; the palette's thread search reads the user and assistant text of the newest session files, the thread on screen through services.transcript when it has none, and the threads of Codex, the Agent SDK runtime, Antigravity and OpenCode from an index it keeps of their kits' thread-texts answers (runtime-threads.ts: at most every five seconds, only what changed, four million characters at most, the oldest threads' text forgotten first). Its desktop half draws both dialogs from a title-bar region over the window (a phone's and a tablet's too, with finger-sized rows and a close button), opens a hit with openFile(path, { line }), offers "Go to file" to other kits as tau.search/files, and registers three palette sources: threads by title, threads by what was said in them, projects. Core lends registerPaletteSource, the settings keywords and the file tab's line Search
Notifications: a system notification, a sound and the app icon's badge when a thread finishes, fails or asks something while nobody looks at it; its in-window toasts carry the rail's status mark (done, failed, a question, a permission) Notifications: kits/notifications/, a package Tau ships (ADR 0014). Its host half follows turns through the turn observer and questions through decorateUiPrompt (so runtime:extend, in-process), skips sub-agents, and keeps the threads with unseen news in memory (attention.ts); each client reports whether its window has focus and which thread it shows, a thread on screen in a focused window counts nothing, and otherwise one client hears of it — the one that had focus last. News that finds no client waits for the first one to report; a thread's second piece of news within five seconds notifies nobody. A question answered on any client leaves the list (the decorator's return). Its desktop half draws it through context.attention (core's Platform.attention), synthesises its two sounds with Web Audio, toasts above the composer when opted in and owns Settings → Notifications, whose rows show and reset the level each choice comes from (Tau's config, this machine's) Notifications
Codex backend, and a runtime's CLI version Codex: kits/codex/, a package Tau ships (ADR 0014), registering a runtime backend that drives the installed codex through codex app-server over stdio (JSON-RPC, one process per live thread): threads start and resume by Codex's own thread id, text, reasoning and commands, file changes, MCP calls and web searches stream as runtime events, Codex's approval requests, questions and MCP servers' forms go through ask (a form field by field, paged by Questionnaires; a permission request as the lines it grants), tool cards are kept across restarts in codex-activity/ beside the store, Tau's access levels become Codex's approval policy and sandbox, models and reasoning efforts come from model/list. It refuses a CLI its version policy calls broken — every release older than the protocol it was built against — and warns about one it calls unsafe, keeps its store beside Pi's sessions, answers Usage Kit's usage and usage-logs and fills its card on Settings → Providers, where Claude Code and Antigravity have theirs: a settings page that names a runtime is drawn as that runtime's card there, with the CLI, its version and update, the login and a path override, instead of a nav entry of its own. Any backend may report the program it drives through version() on registerRuntimeBackend; core asks once a day, publishes the answer on runtimeBackends and says in the picker's runtime tab and in Settings → Runtimes when an update is out — Claude Code and Codex compare with npm (npmLatestVersion, cached a day), Antigravity with the release it pins. Codex and the Agent SDK runtime run several instances of their CLI — each with its own executable, home, environment and launch arguments, registered as a backend of its own (codex@work), so a thread keeps its instance and the picker shows one tab per instance; RuntimeInstanceSettings, the version policy and the lazily loaded instance setup, dialog and banner are the seams they share Codex kit, shipped by default
A thread's pull or merge request on the stage: summary, timeline, code with review threads, checks, reviewers and labels, editing the title and description, viewed files, replies and new line comments Review Kit: kits/review/pull-request-host.ts reads and writes one request by its URL through the provider its URL names (the other hosts' shapes are in provider-forgejo.ts, provider-bitbucket.ts and provider-azure.ts; their viewed marks are the kit's own) and, for GitHub and GitLab, gh or glab, found with findCommand — gh pr view --json, gh pr diff, one GraphQL query for review threads and viewed marks, markFileAsViewed, addPullRequestReviewThreadReply, the REST line comment, gh pr comment and gh pr edit, bodies on stdin; GitLab through glab api with JSON on stdin. Reads are cached for a minute per request and dropped by any write; GitLab keeps no viewed marks, so the kit stores them in its stateDir with a fingerprint of each file's change. The desktop half registers the review.pull-request stage-tab kind (registerStageTab), opened from the Changes section's PR #n or "Open the thread's pull request"; it draws the diff with DiffView and its line seam and the text with Markdown, polls only while the tab is on screen, hands comments to Composer Context's chip service and feeds what it read back to the rail row's mark. One composer comments or submits a review (verdict, summary and the line comments held for it in client storage, one POST …/reviews on GitHub; on GitLab the comments, then approve); conversations resolve and reopen, the signed-in account's own comments are edited, reviewers and labels are added and removed. Conversations, viewed marks and GitLab's diffs and discussions are paged past 100, and a diff gh pr diff refuses (over 300 files, too large) falls back to pulls/N/files. The code view's whitespace switch is the kit's "Hide whitespace changes" option, applied to the fetched hunks. The view's header has a merge control (merge with the chosen method, arm auto-merge while checks run, or the armed merge's badge), a menu with the rest (merge now, auto-merge off, the method, link to a thread picked by search, revert) and, for a GitHub stack (/repos/…/stacks, github-stacks.ts), the layer as "2/8" with Merge stack and Rebase stack, each refused when a layer moved since the user confirmed it; pr-action, pr-stack and pr-stack-action are its commands. "Linked from N threads" lists the threads that keep the request (pr-linked-threads). The Remote tab of the Reviews page (below; opened from the sidebar's foot, a phone's bottom navigation and "Pull requests in all projects") lists every project's requests across hosts (pr-list-many, each repository once), grouped by involvement or by project, and opens a row's view as a view of the page; a thread's review.pull-requests tab lists its own project's (pr-list: gh pr list or glab api …/merge_requests) and links to the page. Both put the viewer's own first, sort by merge readiness or "Blocked on me" and narrow by state, involvement, typed qualifiers and a filter menu with the host; an open GitHub row names its stack layer. A thread keeps the requests it links in <userData>/kit-state/tau.review/thread-pull-requests.json: the Changes section lists them, a dialog links one (in the palette a level lists the project's open requests, read once a minute, and takes a typed URL or number too), the rail row counts them, and link_pull_request, unlink_pull_request and list_thread_pull_requests give the agent the same in every runtime (Pi runtime extension and MCP, ADR 0022), with a pull_request_linking section in the system prompt that asks it to link each request it works on (Pi's before_agent_start, the MCP endpoint's instructions elsewhere). Thread Rail asks thread-requests for the linked requests before it settles a thread. With "Proactive panels" on (Settings → Review, off by default), a request the thread on screen links opens as its tab, and a turn that changed at least 3 files or 50 lines opens the Changes view (the review) unless the thread links requests. Over the composer (composer-above, order 80: under the runtime banners, quick actions and Pi's widgets, over Thread Rail's settled note) a strip shows the thread's request, the branch's own or a linked one (open before ended, "+N" for the rest), tinted by its state with the --merged token for a merged one, the host as a mark with a tooltip; it reads only what the rail row already asked for, a click opens the tab and its × hides it in that thread until the request or its state changes (client storage). "Show the pull request above the composer" in Settings → Review turns it off; the strip is its own lazily evaluated module Review Kit
Editing a workspace file on the stage: save, a dot for unsaved work, a change on disk as a conflict; Markdown, HTML and CSV/TSV rendered or as source; PDFs, images, audio and video Files: kits/files/, a package Tau ships (ADR 0014). Its desktop half registers the stage tab kind tau.files.editor, opened by "Edit file" on a file tab (the file-tab command surface), by a double-click in the Files panel (registerFileEditor on Workspace Kit's store) and restored with the stage. The editor is CodeMirror 6 in the kit's own bundle (code-editor.tsx, editor-view.ts), which scripts/build-kits.mjs builds into dist-kits/ and the renderer's budgets do not count; it is evaluated on the first open of a file, and each language mode (languages.ts: TS/JS/JSX/TSX, JSON, CSS, HTML, Markdown, Python, Rust, Go, YAML, shell, TOML) on the first file that needs it. It has line numbers, folding, bracket matching, several cursors, search and replace on mod+f, and a soft-wrap switch in the header that is also the kit's wordWrap option; its colours are Tau's tokens (editor-theme.ts). The buffer lives in the kit (document.ts), so a background tab keeps unsaved work; mod+s is "Save file" under editorFocus and stays Prompt Tools' stash everywhere else; the tab on screen asks the disk every two seconds, on window focus and after the agent's edit, write and bash tools — a clean buffer reloads, a dirty one shows "Reload from disk" or "Keep my version", and a save that names a stale mtime is refused as a conflict. Autosave after a second is an option, off by default. Markdown renders with core's Markdown, HTML in a sandbox="" frame from srcdoc, CSV/TSV as a table of 100 rows and 30 columns; the choice is kept per kind on the client. PDFs, images, audio and video load from actions.shareFile. "Open in" lists every editor Workspace Kit found and reveals in Finder, Explorer or Files, on a client whose host's files are on its own machine. A tablet (a compact client laid out as the desktop) gets the same tab, with its header at a finger's size and the text at 16 px; a phone, which draws no stage, reads a file in Workspace Kit's Files sheet instead (placement: "sheet", core's FileSource) and does not edit. Its host half runs in a worker and only forwards read, stat and write to Workspace Kit's read-file, file-stat and write-file, which name tau.files as caller (ADR 0020) and check every path. A document is keyed by project and path, and its project is the one whose stage the tab is on (render's from), never the host's open one: every call names it, and a tab without one opens and saves nothing Files
Worktree operations: cleanup rules, Settings → Storage, the setup of a new thread's worktree step by step, tau app <path> from a terminal Workspace Kit and Project Scripts. Workspace Kit writes every worktree it creates to <userData>/kit-state/tau.workspace/worktrees.json (worktree-storage.ts) and sweeps them hourly, soon after a thread deletion a rule answers (threadDeleted) and on "Clean up now": four rules — inactive for N days, merged into the default branch, the last thread deleted (a thread in the host's trash still counts), no commits beyond the base — host-wide or per repository (Inherit/Off/Custom) in cleanup-policy.json beside it. It removes only a recorded worktree inside the repository's worktrees folder, through git worktree remove without --force, keeps the branch, and never one with uncommitted or ignored files other than node_modules, unpushed commits, an open thread or the host's own workspace (worktree-cleanup.ts is that judgement, and the Storage page shows it as the dry run: size, age, threads, what the next cleanup does, Remove by hand with a confirmation). Project Scripts tracks a worktree's setup (setup.ts): Workspace Kit reports fetching the base and creating the checkout (worktree-setup-begin/-step/-failed, callers tau.workspace), every runOnWorktreeCreate script is a step with its last four lines, and the card under the transcript (setup-card.tsx) cancels the scripts or starts the thread now. bin/tau.mjs app [path] reads <userData>/host.json, says hello with the host's token as an auxiliary client and asks Workspace Kit's app-open: an attached window opens the project with a new thread's draft (newSession({ workspace })) and the kit's window half brings it to the front; without a window the request waits for the app the command line starts Workspace Kit, Project Scripts
Appearance: a theme for the light and the dark scheme, density, contrast, panel animations, the text size, the interface, prompt and code faces, the timestamp format, a theme editor and VS Code theme import Appearance Kit: kits/appearance/, a package Tau ships (ADR 0014). Text size (Small, Default, Large) is this device's, in client storage, and moves every role of core's type scale (--text-xs to --text-title, and code) a pixel down or up through data-text-size on <html> (--text-step, on top of the system's text size, ADR 0029); zoom (View → Zoom In, Zoom Out, Actual Size) scales the whole window. Its desktop half is Settings → Appearance (scope: "both", so density can be a project's own): the mode as three tiles that draw the window in miniature with the theme each would paint (System split in half), every theme as a card with a swatch per scheme it has — a swatch gives it that scheme, the name every scheme it has, Tau's own card resets (previews.tsx; Tau's colours are read with the kit's own sheets switched off for the moment) — the panel animation duration (0–400 ms, 0 by default) with a preview that replays it, and a Terminal font row drawn from Terminal Kit's tau.terminal/font while that kit is on, and applies the values to <html>: data-density (its stylesheet turns it into --density), data-timestamps, the --prompt-font-*, --code-font-* and --panel-motion properties core's rules read, and one stylesheet with the chosen user themes per scheme and the contrast (hairlines and quiet inks mixed toward --ink on <body>, from the tokens <html> keeps under another name). The theme editor floats over the window from a title-bar region and paints its draft there: three colours derive a palette (palette.ts), every token can be set on its own, and Save writes <themesDir>/<id>.css through the host half (a worker, save-theme), which Tau then lists like any user theme. A VS Code colour theme is read as JSONC and mapped onto the tokens with fallbacks (vscode-import.ts). Core lends themesDir, userThemes, the rows and the font properties Appearance Kit
First start: which runtimes (every registered runtime backend, instances included) and, as an optional group, which pull-request CLIs the machine has, adding the folders the agent CLIs worked in as projects, importing their earlier conversations as threads Onboarding: kits/onboarding/, a package Tau ships (ADR 0014). Its host half runs in a worker: it finds gh and glab with findCommand and asks their version and login, asks each backend kit for the sessions its CLI kept (import-scan) and hands an import on in batches of ten (import-sessions, callers tau.onboarding), pushing progress; reading a CLI's files, checking a path against its home and writing the threads are the backend kit's. A runtime's own state comes from its kit's status (tau.<kind>, { instance } for a <kind>@<id> backend; probe when status has no login), so a path override counts. Its desktop half is a wizard overlay (registerOverlay) in three steps — Agents, Projects, Conversations — which an empty title-bar region opens on a first start (no thread, setup never finished) and /welcome or "Set up Tau…" opens again; a chosen folder is admitted with services.workspaceRef and opened with actions.openWorkspace, and "Add a folder…" is the project sources. The scan groups clones by their origin remote and leaves out linked worktrees, home, temporary folders, ~/Downloads, Codex's scratch folders and TAU_WORKTREES_DIR (folders.ts, reading .git without spawning Git). An install or a sign-in (gh, glab, a runtime's CLI, a kit's terminal sign-in step) runs through Terminal Kit's tau.terminal/run; since the overlay replaces the workbench, the wizard closes for it, a title-bar button leads back, and it reopens and asks again when the shell ends. Core lends nothing new Onboarding
Plan mode: the Build/Plan chip, a proposed plan as a card, Plan ready with Implement Plan Kit: kits/plan/, a package Tau ships (ADR 0014). Its host half gives Pi threads the plan mode through a runtime extension (registerRuntimeExtension(…, { modes })): while a thread plans, its system prompt asks for exploration and a proposed_plan block and edit and write are refused; it also starts the thread "Implement in a new thread" hands a plan to (sessions.start). Its desktop half draws the chip where the runtime offers the mode, draws proposed_plan blocks as cards (registerMessageBlock) and shows Plan ready above the composer once a plan-mode turn settled on a plan; Implement switches the thread back to default (actions.setMode) and sends the plan (actions.submitPrompt). Codex and the Agent SDK runtime implement the mode in their own kits. Core lends the mode itself: the mode capability, the catalog's mode and modes, set-mode and the new-thread configuration's mode Plan Kit
Continuing a thread on another runtime, and bringing a fork or a sub-agent back to its parent Handoff: kits/handoff/, a package Tau ships (ADR 0014). "Continue in…" (the title menu, the palette) opens a runtime menu before the thread title; the fork is a new thread with a lineage link, so a thread never changes its runtime (ADR 0005). Where the runtime forks its own history (Pi, the fork capability) the kit asks core to duplicate the thread and links the copy in afterFork; anywhere else it opens a draft on the chosen runtime whose handoff is a chip, and the summary is written by a small model (smallCompletionModel, never the user's default) only when the first prompt is sent, from the conversation as it stood when it was continued, and goes before that prompt as a handoff_context block. "Bring back to parent" writes what a fork or a sub-agent did since it started or was last brought back (done, decisions, the files its edits and writes named) into the parent's composer as a merge_back_context block for the user to review and send. Both blocks are drawn as folded cards in user messages (registerMessageBlock({ roles: ["user"] })); the links, the pending handoffs and the files live in <userData>/kit-state/tau.handoff/lineage.json, and the parent and the forks show before the title (thread-title region). "Continue in…" and the palette also offer the machines this host's agents may work on with Full access (machines), disabled with the reason where the thread's runtime is not ready there (readiness): the thread goes on there as an ordinary thread through Remote Work Kit's tau.remote-work/threads, from the working copy's state including uncommitted work — Pi with its history (sessions.import there), another runtime with a handoff block before the composer's draft. The window stays where it is and a toast says where the thread went; the thread here stays usable, and a banner above its composer shows the other machine's status with "Open" (the window shows that machine, ADR 0025), "Bring back" (its branch here, and the merge-back block the kit there writes on a small model, remote-merge-back over services.machines.call, into this composer) and "Merge" (only when clean) Handoff
Git odds and ends: submodules in a new worktree, keeping the default branch current, a base folder for new projects and clones that show progress and can be cancelled, publishing a repository, the user's own instructions for commit and request text, diff colours and wrapping Workspace Kit and Review Kit. Settings → Source control (Workspace Kit, source-control-page.tsx, a project may override) chooses how a new worktree fills its submodules — the setting, else worktreeSubmodules in the checked-out branch's .tau/project.json, else recursive (worktree-submodules.ts; a failure is a failed step on the setup card and keeps the worktree) — and whether the default branch is kept current: on a project switch, a window focus and every five minutes the host fast-forwards the project and its main checkout when each is on the default branch, has an upstream, no changed or untracked file and no local commit, through merge --ff-only and nothing else (default-branch-pull.ts, once a minute per checkout at most). A clone is a host job (clone-jobs.ts: git clone --progress, stages and percent as clone-progress pushes, cancel) that writes only into a destination that did not exist and removes it again after a failure or cancel when it holds nothing but git's own files; its toast (clone-toasts.ts) cancels it and opens the project, and the folder browser and the clone start in the base folder the page sets. Review Kit's Changes section offers "Publish repository" for a checkout without a remote (publish-host.ts, publish-form.tsx): gh repo create or GitLab's API through glab, only after the form's last step sends the confirmation the command requires, then Workspace Kit's add-remote (the first remote only, callers tau.review) and a push. Settings → Review holds the commit format, the user's own instructions (added to commit and request prompts), whether a request follows the repository's template, and the diff display: data-diff-colors="blue-orange" on <html>, which the kit's stylesheet turns into diff tokens mixed from the theme's --info and --working, and wrapping through ReviewMode's wordWrap and DiffView's wrap Workspace Kit, Review Kit
OpenCode backend OpenCode: kits/opencode/, a package Tau ships (ADR 0014), registering a runtime backend that starts opencode serve for each live thread on 127.0.0.1 with a password of its own (or connects to a server the user runs, by URL and password) and speaks its HTTP API: one OpenCode session per thread, created on the first turn and resumed by id, text, reasoning and tool parts from the event stream as runtime events until the session is idle, a steer joining the running turn, permission requests and questions (also those of OpenCode's sub-agents) through ask, Tau's access levels as the session's permission rules, the plan mode as OpenCode's plan agent, tool cards kept in opencode-activity/. The models of every provider OpenCode reports connected fill its catalog, with price, context and how each is paid for; Tau's tools reach it as a remote MCP entry in the thread server's config. Instances (opencode@work) take a home that becomes OpenCode's four XDG folders; the version is judged by a policy; Onboarding imports OpenCode's own sessions through a server over the user's data OpenCode kit, shipped by default
Signing in from the window: Pi's model providers, the way Pi's /login does, and every runtime's own login on its Providers card Pi Providers: kits/pi-providers/, a package Tau ships (ADR 0014). Its host half lists Pi's providers through services.modelAuth (core hands a login or a key straight to Pi's auth.json, reads none back, and asks Pi's catalog again afterwards) and runs each provider's sign-in with registerSignIn: Pi's questions, its consent page and its device code become the flow's steps. Its desktop half is Pi's card on Providers, first in the runtime order: the providers Pi reaches now and from where, the ones it can sign in to or take a key for, each row opening the account rows core lends (loadSignInUi). A provider without a mark of Tau's own gets a Logo row there: its site's icon, which the host half fetches once it is set up (only the origin of its base URL, http(s) only, 5 s and 128 KiB at most, images typed by their bytes, SVG only when it only draws, no private address unless the provider is local, cached per origin in the kit's state folder), or a picture the user chooses; both are drawn into a 64 px PNG, kept in the kit's values and published with setProviderIcons. The runtime kits register the same sign-in commands for their programs: Codex over its app server (browser, device code, API key) or codex login in a terminal, the Agent SDK runtime's CLI login in a terminal (Terminal Kit's tau.terminal/run), Antigravity's four methods through the agent's authenticate Pi Providers; each runtime kit for its own program
Cursor backend The Cursor CLI: kits/cursor/, a package Tau ships (ADR 0014), registering a runtime backend that runs cursor-agent acp for each live thread and speaks the Agent Client Protocol through kits/_acp/, the ACP client it shares with Antigravity: one Cursor session per thread, created on the first turn and loaded by id after a restart, the CLI's own login, model and reasoning effort set before each turn, the plan mode and Tau's access levels as Cursor's plan, ask and agent modes (full access answers approvals itself, read-only refuses them), Cursor's questions as a questionnaire, a created plan as a plan card, to-do lists and sub-agent tasks as tool cards, tool cards kept in cursor-activity/, Tau's tools as an ACP http MCP server. The catalog comes from cursor/list_available_models, every offer billed to the Cursor plan. Instances (cursor@work) take a home that stands for ~/.cursor with a file login; a CLI older than 2026.04.08 is never started with acp Cursor kit, shipped by default
Quota windows Pi's subscription providers report Pi Limits: kits/pi-limits/, a package Tau ships (ADR 0014), host half only. A Pi runtime extension reads the rate-limit headers Pi hands to after_provider_response (the ChatGPT backend's x-codex-primary-*/-secondary-*, Anthropic's anthropic-ratelimit-unified-5h-*/-7d-*), keeps the latest windows per provider in services.stateDir and answers them to Usage's usage-limits, each with the hashed account its login belongs to (Pi's auth.json, Anthropic's anthropic-organization-id header). No request of its own Pi Limits
Grok backend Grok Build's CLI: kits/grok/, a package Tau ships (ADR 0014), registering a runtime backend that runs grok agent stdio for each live thread and speaks the Agent Client Protocol through kits/_acp/ (client, thread backend and session store shared with Antigravity and Cursor): one Grok session per thread, created on the first turn and loaded by id after a restart, the CLI's own login or an XAI_API_KEY from Tau's environment, model and reasoning effort set with session/set_model before each turn, xAI's prompt_complete notification as a second way a prompt ends, Tau's access levels as the permission mode the agent starts with (full access --always-approve; ask and read-only --permission-mode default, read-only and plan mode refusing every request; a change restarts the agent and loads the session), plan mode as an instruction with the prompt and Grok's own exit_plan_mode as a plan card, its questions as a questionnaire, each turn's turn_completed usage kept per model, tool cards in grok-activity/, Tau's tools as an ACP http MCP server. The catalog comes from initialize without a sign-in (grok models for the login), billed to the Grok plan or to the API key. Instances (grok@work) take a home that becomes GROK_HOME; the plan's credit window reaches the Usage page Grok kit, shipped by default
Evidence: pictures of what a turn did — the Preview's page and the window the agent drives — under the turn's answer, a viewer that plays them and saves a short video, and attach_evidence Evidence: kits/evidence/, a package Tau ships (ADR 0014). Its host half follows every thread's turns (registerTurnObserver): the Preview's page at a turn's start and end, after each Preview call and every ten seconds (Preview Kit's evidence-frame, granted by callers; nothing while a password field has focus), and each new driver screenshot of the driven window (Computer Use's screen-state/screen-frame). Never the whole screen. The window half (window.ts) shrinks each picture to a 960 px JPEG, a thumbnail and a grey copy the host compares, so an unchanged picture is dropped and a turn that changed nothing keeps none. Frames live under the kit's stateDir, 60 a turn and a size per thread at most, swept after the retention a project sets (services.settings) and deleted by threadDeleted. The kit offers them to other kits through core's turnAttachments, gives the agent attach_evidence for Pi and over MCP, and lends tau.evidence/capture with pause/resume for a hand-over Evidence
SnapShots: a global shortcut captures the window in front — its picture, app, title and accessibility tree — into the composer as a chip SnapShots: kits/snapshots/, a package Tau ships (ADR 0014). The window half (window.ts) registers the shortcut with Electron's globalShortcut (off until the user turns it on in Settings → SnapShots; the host half drops it when the kit goes), reads the Screen Recording and Accessibility status without asking, finds the window in front with @crowecawcaw/xa11y loaded through the window context's loadDependency, records that one window by its id with the capture it shares with Computer Use (kits/_window-capture/) and reads its accessibility tree with bounds in the picture's pixels (limits: 10,000 nodes, 32,000 characters, 3 s). The host half keeps unsent captures under the kit's stateDir (a week at most) and hands each to the first client that shows a composer; the desktop half draws it as an inline chip with a thumbnail and sends the picture as an image and the tree as data the model is told not to obey. macOS only for now SnapShots
The local pull request: a branch against its base before a request exists, with the pictures of the turns that did the work, a description written on request, and pictures uploaded when the request is created Review Kit: the stage tab review.local-pull-request (kits/review/local-request-*.tsx, loaded on first open), opened from the Changes section or the palette. It reads the branch through Workspace Kit's review-request-context (commits with id and time, and when the branch left its base), the checkout's turn pictures through services.turnAttachments and gives each turn to the first commit made after it ended; the checks are Project Scripts' own commands. The description is written only on a click, with the user's Review model or a small one near the thread's (smallCompletionModel), kept per checkout and branch in client storage, and holds chosen pictures as tau-evidence:// placeholders. pr-create and pr-attach-evidence upload them only after the user saw where they go: GitLab through its uploads API (glab api --form), a public GitHub repository as files on refs/tau/evidence/<branch> written through the Git data API and linked by commit (GitHub has no upload API for descriptions); a private GitHub repository and every other host keep them on this machine and they leave the text (kits/review/evidence-upload.ts) Review Kit
Handing over to the user: a sign-in, a two-factor code or a captcha the agent cannot do, and control back afterwards Takeover: kits/takeover/, a package Tau ships (ADR 0014). Its host half gives Pi and every MCP runtime request_takeover({ reason, target?, url? }) and keeps one waiting request per thread until the user presses Done or Cancel (30 minutes at most); it publishes the list to every client (state event and command), asks Evidence Kit to pause the thread first (EVIDENCE_PAUSE_CALLERS), and blocks every Computer Use and Preview tool of any thread and runtime while one waits (Pi's tool_call hook, services.mcp.gate). Its desktop half draws the Your turn card above the composer — one button for the target (Preview Kit's jump, the Preview moved onto the stage; the driven app raised by its driver; the page in the user's own browser), a key icon with the two ways to a password (type it in the Preview, or sign in in the user's browser and bring the site's cookies over through tau.preview/cookie-import), Done and Cancel — a mark on the rail row, and a notification or toast where the card is out of sight. Away from the host's machine the card shows a live picture of the target (Preview Kit's watch) and "Take over here" opens the Preview on that device to tap and type; a Read-only device sees the card but cannot answer it Takeover
Reviewing on a phone or tablet: a recorded turn's diff, the uncommitted changes and the branch's, a comment on a line or a range into the composer, and commit, push or a new pull request only after a question Review Kit: a panel for compact only (kits/review/compact-review.tsx, evaluated when its sheet first opens), opened from the phone's bar as a sheet (on a tablet as a stage tab), or from the pill over the composer that sums up the latest turn that changed files (kits/review/turn-pill.tsx, region composer-controls beside Jump to latest, hidden while a turn runs), which opens the sheet on that turn's files. A picker in its header chooses the latest turn (the default when the thread has one that changed files), any earlier turn, the uncommitted changes or the branch against its base; turns come from Workspace Kit's checkpoints, turn-files and turn-file-diff, the rest through the kit's own changes and file-diff. A file opens alone in DiffView, unified and wrapped, with previous and next; the line seam's button covers the whole row, so a tap selects a line, a second tap in the same file a range, and a bar names the selection and opens the comment. The comment goes into the composer as text (the compact composer has no chip strip) and the sheet closes through actions.closePanel; an unsent comment, a commit message and a request's text survive leaving the page or the sheet. Commit (and push), push and a new request (with a title and description written from the commits on request) each ask in a dialog that says what happens; a device paired Read only sees the diffs, no line can be selected, and the writes are disabled with the reason (useHostCapabilities().readOnly) Review Kit
Other machines: their threads among this machine's in the rail, each with its machine's icon before the provider marks, "Run on" for a new thread's draft, Settings → Machines (add by pairing link, address or a Bonjour search, rename, remove, show the last one again at start), the shown machine in the thread header with the way back Machines: kits/environments/, a package Tau ships (ADR 0014), in-process. Its host half (machines, ADR 0027) reports which machines this host's agents reach and how each connection is doing, and answers whoami for another machine's agents; Settings → Machines has a switch per machine, "This computer's agents may work on …", and one in the add form, and under this computer and each machine its agents reach shows cores, CPU, free memory, running turns, each runtime's mark with its readiness, Git, free space and display (host-resources, readiness), asked when the page opens and on "Check again". choose-machine picks where new work goes when it is left to Tau (plan H §7: weight × cores × idle CPU × free memory, then limits, readiness and a turn per core), with a weight per machine under Settings → Machines → Automatic (this computer 5, others 50, 0 = never); Agents Kit asks it for machine: "auto", and "Run on: Automatic" asks it when a new thread's first prompt is sent and carries the prompt to the chosen machine, which sends it there. The desktop half draws what context.environments gives — the window's process pairs, pins, connects and moves the page (ADR 0025) — lists their threads in Workspace Kit's rail through registerRailThreads on tau.workspace/store, mounts its arrival there through registerRailSection, and on arrival opens the thread or draft the page was sent for. A search's hosts come in core's NearbyMachineList; "Run on" disables a machine that paired this window Read only. In the phone app (K106) context.environments is the app's own list of paired hosts (mobile/src/machines.ts: the shown one live, the others in short visits every two minutes and on coming to the front); the kit lists their threads in the compact thread list (registerThreadListSource), says over it which is out of reach (thread-list-head, Retry or Pair again) and offers "Run on" as a sheet Machines
Tailscale HTTPS: finding Tailscale, tailscale serve for Tau with consent, the MagicDNS address with a certificate browsers trust, the warning that the machine's name becomes public Tailscale: kits/tailscale/, a package Tau ships (ADR 0014), in a worker. Its host half finds tailscale with findCommand (then the macOS app's and the Windows installer's paths; TAU_TAILSCALE_COMMAND names one outright), reads status --json (state, MagicDNS name, whether CertDomains covers it) and, only once HTTPS can work or a mapping exists, serve status --json. On the owner's word (commands with access: "owner") it runs serve --bg --https=<port> http://127.0.0.1:<proxyPort>, never over a port another program is served on, and serve --https=<port> --set-path=/ off to remove only Tau's path; while the mapping stands it keeps the proxy listener (keepProxy, so the host opens it at its next start before any kit runs, a service host included) and publishes https://<machine>.<tailnet>.ts.net/ through services.network, remembered in its stateDir so both come back at start without running the CLI. The CLI's output is matched, never shown or logged. Its desktop half is a section on Settings → Connections (registerSettingsSection): the state and what to do next, a switch that asks first, a consent step that names the machine that goes into the public Certificate Transparency logs with a link to rename it, and the machine name. fixtures/fake-tailscale.mjs stands in for the CLI and Serve in tests Tailscale
Push notifications to the phone app: a thread that finished or failed, a question, an agent's "your turn", while nobody is at a Tau client Push: kits/push/, a package Tau ships (ADR 0014). The host sends them itself with the user's own keys — APNs over HTTP/2 with an ES256 provider token, FCM's HTTP v1 API with a service account's OAuth token — no relay. A paired device registers its token with register (the command learns the device from HostCommandCall.device); a revoked or expired device loses its registration through clients.devices() and devicesChanged. It follows turns and questions like Notifications does, takes "your turn" from Takeover (notify, callers tau.takeover), and asks Notifications (attended) whether someone is at a focused client that was used in the last three minutes; then it stays quiet. Content per values.tau.push.content: the title only, or the title with the first line of the agent's last message, the reason or the question. The keys live in <userData>/kit-state/tau.push/keys.json, mode 0600, not encrypted (the host has no keychain); Settings → Push takes them, owner only, and lists the devices with a test push Push
Servers as a work target: a project deployed over SSH/SFTP or FTP, its targets from .vscode/sftp.json or the SSH config, deployments with rollback, server drift as a branch Servers: kits/servers/, a package Tau ships (ADR 0014), in-process (ADR 0028). No agent on the server: the agent works on the local copy and only the user uploads. Its state lives in <userData>/kit-state/tau.servers/targets/<workspaceId of the main checkout>/<targetId>/ (files 0600, written atomically), never in the project, and its settings exist on this machine's level only. Targets are read from .vscode/sftp.json (every variant of the VS Code extension and its forks, profiles chosen per machine in targets/<workspaceId>/profiles.json), ssh -G aliases and typed-in addresses; Settings → Servers lists a project's targets. A server project's agent commands reach only this machine and the package sources (npm, Packagist, GitHub, PyPI, …) plus hosts the user allows, or everything once the user lifts the limit (targets/<workspaceId>/network.json): the kit provides that as services.executionPolicy and runs Pi's bash under @anthropic-ai/sandbox-runtime, loaded through loadDependency (network-policy.ts, pi-network.ts). In a server project's threads the agent reads the server with server_status, server_list, server_read and server_diff, runs commands with server_exec and writes ~/tmp with server_put_tmp behind the stricter of the target's level and the thread's (Access Kit's thread-level-of); Git writes on the server are refused at every level, a local ssh/scp/rsync/lftp/curl to a target counts as server_exec, and server_propose_upload only draws a card whose click uploads (agent-tools.ts, agent-gate.ts, agent-cards.tsx). SSH and SFTP run over the system ssh with a ControlMaster per login under /tmp/tau-<uid>/, a small SFTP v3 client of the kit's own over ssh -s sftp, and an askpass bridge that turns passwords, passphrases, codes and host keys into Tau dialogs (transport-ssh.ts, sftp-client.ts, askpass.ts); FTP and FTPS run over basic-ftp, bundled into the host half, with the certificate pinned per server and plain FTP only after a question (transport-ftp.ts). Every path is checked on the server (below remotePath or ~/tmp, never .git). Passwords follow the VS Code fork: its keychain items (read once the user allows each), a passwordCommand allowed per project, or the kit's own tau-servers item (credentials.ts, keychain.ts). The mirror state is a bare repository in the target's folder (sync/mirror.ts, refs/tau/server); scan, compare and download are in sync/. "From a server…" in Add project downloads a site into a new repository whose first commit is the server state, and a folder with an sftp.json gets that commit without its files being touched (projects.ts, through Workspace Kit's repo-from-tree). Server drift is checked when the project opens, on request and before a thread's first prompt (registerComposerGate), and imported as a commit on server-drift/<date> that only a click merges (drift.ts, through Workspace Kit's commit-files-to-branch and merge-branch). An upload reads every chosen file on the server again, compares three ways against the mirror state, never overwrites a server change unasked, keeps the server's file first (refs/tau/deploy/<n>) and writes through a temporary name and a rename (deploy.ts, sync/deploy.ts); each upload and each rollback is a deployment in deployments.json, rolled back per deployment, marked committed once HEAD holds what went up, and cleaned up by age and count (rollback.ts, commit-mark.ts, cleanup.ts). Files with live credentials start unticked, and an override file the user sets up never goes up (secrets-scan.ts, live-config.ts, override.ts). The desktop half has the stage tab servers.target (not uploaded, changed on the server, history), a title-bar chip, a Changes section, the rail's targets, a mark on threads with a deployment not committed, Settings → Servers, and a compact sheet with the same upload and rollback in 44 px targets (disabled with the reason on a Read-only device) Servers
A project's work on another machine: its state (commits and uncommitted work) to a worktree there, chosen ignored files along, the result back as a branch that merges only when clean Remote Work: kits/remote-work/, a package Tau ships (ADR 0014), in-process, on both machines (ADR 0027, plan-H §2). The sending side drives every step through services.machines; the other machine never reaches back. The checkout's state becomes one commit under refs/tau/transfer/<id> (a private index, as a spawned thread's start); the other machine keeps one bare mirror per project under ~/.tau/remote-work/repos/<key>.git (the key is the normalized origin URL, else the root commit), cloned from origin when it can read it with its own credentials, else empty, and gets a Git bundle of only what its mirror lacks (services.machines.upload, taken with services.blobs, git bundle verify before any fetch). A worktree per transfer under worktrees/<project>/<slug> on tau/<sending machine>/<slug> with tau-base (the slug is the work's name — a thread's title, an agent's name — else the transfer id; numbered when taken), admitted as a workspace under the sending side's project name (rememberProjectName, again at every start: Git alone would name a worktree of the bare mirror after repos), set up by Project Scripts' worktree-created (or the old runOnWorktreeCreate line); no hook runs there (core.hooksPath points at an empty folder) and no checkout of that machine's user is touched. Long steps are operations there, followed by topic and poll. Ignored files never go by default: Settings → Remote work offers small text, .env* and note folders per project and remembers the ticks in <userData>/kit-state/tau.remote-work/ignored-files.json; they travel inside the call. The result comes back as a bundle this side pulls piece by piece into tau/<machine>/<slug>; merge-tree --write-tree checks it and only a clean one merges (Workspace Kit's mergeBranchIntoCheckout), a conflict or uncommitted work in the way leaves the checkout as it was. Nothing is ever pushed. Threads there (H06, service tau.remote-work/threads): thread-start sends the state and starts an ordinary thread in that worktree (a prompt, or a Pi session taken over with sessions.import and its origin); here only a link stays (<userData>/kit-state/tau.remote-work/remote-links.json: machine, thread id there, transfer, base, status, cost). The other machine derives the status from the runtime, turn events and ui_prompt_* and emits it under hosted-thread/<id>; this side watches it and asks once after a reconnect, reads offline while the machine is unreachable, and refuses a machine without the thread commands with both Tau versions named. thread-send/-abort/-wait/-result/-settle (apply merges when clean, then the worktree there goes). Agents Kit (sub-agents there) and Handoff ("Continue on") start their threads through it. A thread there that asks a question raises a toast here with "Open on <machine>" and "Look in" (a stage tab read over the window's connection, openThread(id, { machine })), and a system notification while the window is not focused Remote Work
Resume with less context: an offer above the composer to compact a long thread whose prompt cache has gone cold (100k tokens, 70 minutes idle), "Keep full history", and Settings → Resume compaction to turn it back on for a runtime the user said "Don't ask again" to Resume Compaction: kits/resume-compaction/, a package Tau ships (ADR 0014) with a desktop half and no host half. It reads contextUsage.updatedAt and promptCacheTtlMs from the thread's snapshot, which Pi reports for a Claude model and the Agent SDK runtime for its threads, compacts through actions.compactContext(), keeps its answers in Tau's config, and publishes tau.resume-compaction/opt-out for a runtime kit's own resume question Resume Compaction
Reviews: a finished thread's worktree branch as a local merge request across projects, Ready / Changes requested / Conflicts / Merged with counts and per project, Merge, "Ask thread to rebase" and a note back to the thread, the month's merges and what they cost, remote pull requests under Remote Review Kit: the app page review.reviews (kits/review/reviews-page.tsx, loaded on first open; layout: "fill", desktop and compact, so a phone's bottom navigation reads Threads · Reviews · Usage · Settings). On a desktop its Sidebar takes the thread list's place: "Filter reviews", the states with their counts, the projects with their open reviews and Remote pull requests; the page is the table alone. Without the sidebar (hidden, a tablet, a phone) tabs and a project filter over the table pick what it lists. The branches and the merge are Workspace Kit's Git (kits/workspace/thread-branches.ts, commands thread-branches and merge-thread-branch, callers tau.review): each workspace a thread names that is a linked worktree on a branch is read against the branch its main checkout has out — commits ahead and behind, diff --numstat from the fork point, uncommitted files, merge-tree --write-tree for the conflicts, merged once the target holds commits of its own (the branch's oldest reflog entry tells a merged branch from one that never moved) — and Merge is mergeBranchIntoCheckout from agent-worktrees.ts, the path Remote Work's apply takes: merge-tree first, then merge --no-ff, refused when the tip moved since the page read it or the worktree holds work not committed. Work that came back from another machine (Remote Work's links with a result branch here) is a review too, checked with its preview and merged with its thread-settle. A branch counts once a thread works in its worktree and none there is busy; the window joins the threads (title, model, cost, age) by workspace id, and Project Scripts' last runs in the worktree are the Checks. The kit's host half keeps what Git does not know in <userData>/kit-state/tau.review/local-reviews.json: the asks sent back to a thread (services.sessions.send, or Remote Work's thread-send) — "Changes requested" until the branch moves — and the merges made from the page with what their threads cost, for the footer "Merged · N this month, $X". The page's entry counts ready and conflicting reviews plus the open pull requests of the rail's threads (useBadge); the Pull Requests page is its Remote tab Review Kit, Workspace Kit

Runtime Controls is core, not a kit. The Settings screen with its General, Models, Pi, Keybindings, Connections, Extensions and Inspector pages, its search field and the controls every page is built from, and the contributions that reach core's own actions — the command palette, mod+, for Settings, escape to abort, mod+n, /reload, /tree, /fork, /clone, "Set model…", "Set thinking level…" — are the workbench itself. A window that cannot pick a model is not a usable window, and safe mode has to be one. They live in src/renderer/settings/ (runtimeControls, activated through registry.activateCore, so it is on in safe mode too and carries no switch). The host entry that reads and writes Pi's keybindings.json and its extension shortcuts is the Keybindings kit's kits/keybindings/host.ts, not core; Runtime Controls only keeps the id tau.runtime-settings, the name Pi's keybindings arrive under.

The Pi page draws the settings Pi owns. HostConfigManager splits an update-config patch: the keys Tau applies itself go to ~/.tau/config.json, and the keys Pi reads and applies go to Pi's own ~/.pi/agent/settings.json (or the project's .pi/settings.json), merging into what is already there. A value in Tau's file that Pi owns is therefore never accepted, persisted and then ignored — Pi's file is the one authority for those, and read lets it win. The names differ on one point: Tau calls a model provider/modelId in models.default, Pi wants defaultProvider and defaultModel apart.

The one thing safe mode loses with the kits is /install and the Packages page — the package manager is a kit like any other now. Recovering from a window that cannot install is still npm run start or an edit to ~/.tau/packages.json.

Out of sight on this machine#

Agents that work in the background should neither take the machine nor pop up in front of the user. What Tau does about it on the machine the user sits at:

What macOS cannot keep out of sight, because it has no virtual display a program may use (no Xvfb; CGVirtualDisplay is private API):

For work that must be invisible, move it to another machine: a Linux host runs without a window, and a display there is Xvfb's.

The stylesheet#

src/renderer/tokens.css is the palette, and it is a contract: every colour, font, radius, elevation and motion value Tau draws with is named there and nowhere else, in two sets — light-dark(light, dark), chosen by the color-scheme that data-theme on <html> sets. The theme preference (system, dark, light; system is the default and follows prefers-color-scheme) is core's, in Settings → Appearance (General without that page) and in the palette, and the client writes it onto <html>: no component in the workbench knows a colour. index.html links the file rather than importing it, so the first paint is already themed.

The palette is the workbench design's (K69), with a warm grey in the light scheme and a near-black with a faint blue cast in the dark (K77), on two grounds — the document area and the side surface, the latter the lighter one in the dark scheme — one blue accent for Tau's own actions and selection, green and red for diffs and checks, amber for a question, short light shadows, and Figtree as the interface face. Figtree ships with Tau as local woff2 files (src/renderer/assets/fonts/figtree/, SIL OFL 1.1, listed in the third-party licences) that tokens.css declares, so no font is fetched from a server and the first paint stays offline. The Tau mark is a warm-white τ on that blue (--brand, artwork in assets/icon/): the app icon, the reload curtain and the onboarding mark.

Spacing is a token scale too: --space-1 to --space-8 (2 to 32 px), each multiplied by --density, which is 1 unless a client sets it on <html>. The structural spacing — the thread rail, the thread row, the transcript, the headers, the composer, the Settings screen — reads the scale; detail rules keep their pixels until a visual pass moves them. Core applies no density of its own: Appearance Kit sets it.

The table of those names is in EXTENSIONS.md §8, because it is what a theme package — a manifest with styles and no code — may set. A theme's stylesheet is linked after core's tokens and after every kit, so its names win on order alone. Nothing about a layout class is API.

A kit that draws a surface of its own carries the rules for it: a styles entry in its manifest, kits/<name>/styles.css, linked while the kit is active and gone with it (see EXTENSIONS.md). Workspace, Agents, Preview, Signals, Packages, Questionnaires, Pi UI and Review have one. They name tokens like everything else and define no colour of their own.

src/renderer/styles.css keeps the classes core itself draws, which is the whole of the test: a rule stays if a core component renders the element, even when only a kit ever mounts that component.

Stayed in core Why
The window and its slots: .app-shell, .workbench-center, .instrument-dock, .panel-rail, .panel-stage, .panel-header, .panel-body, .dock-resizer, .stage*, .thread-document Core's layout and the frame a panel contribution is drawn into. Four kits fill it; none of them owns it.
The thread row: .thread-row, .thread-main, .thread-title, .thread-branch, .thread-project-icon, .activity-*, .thread-cost*, .thread-agent-count, .thread-row-actions, .thread-settle, .provider-icon* ThreadRow is core's component, published on tau (ADR 0014): a thread is core's and its row is how core draws one. The rail around it is Workspace Kit's and moved.
The menu: .menu, .menu-anchor, .menu-label, .menu-heading, .menu-scrim, .menu-hint, .menu-sub-anchor, .chev Menu on tau; Workspace Kit, Access Kit and Service Tier all open core's menu.
The other primitives: .tooltip, .popover, .skeleton, .empty-state*, .spinner-* Drawn by TooltipLayer, Popover, Skeleton, Empty and Spinner on tau. The toast stack's rules (.toast-stack, .toast-*) are the one exception to this file: they live in src/renderer/components/ui/toasts.css and load with the stack's chunk on the first toast, because the initial stylesheet budget had no room for them.
Buttons and chips: .chrome-button, .chrome-ghost, .icon-button, .text-button, .mini-button, .control-pill, .chip, .runtime-chip, .switch, .segmented, .primary, .danger, .accent The shared vocabulary of the workbench. Access Kit and Service Tier draw their composer chips entirely with it; Service Tier has no stylesheet at all, and Access Kit's holds only its cards on Settings → Runtimes.
The prompt frame: .extension-prompt*, .extension-option*, .option-row ExtensionPromptFrame and OptionRow on tau. Only Questionnaires' own pager (.extension-pager) moved.
Review and diff: .review-*, .diff-*, .changes-tree-*, .commit-bar*, .source-*, .stat-add, .stat-del ReviewMode, ChangesTree and DiffView are core components published on tau; the rules for the rows DiffView draws load with its chunk (components/diff-view.css), not with the first paint; Review Kit mounts core's overlay rather than drawing one. Workspace Kit's own dock around them (.changed-file*, .commit-box) moved, including the rules that resize core's .stat-add/.stat-del inside it, and so did what Review Kit draws into the line seam (.review-comment-*, .review-comments).
Settings: .settings-screen, .settings-nav*, .settings-topbar, .settings-crumb*, .settings-scope, .settings-content, .settings-section*, .settings-group*, .settings-row*, .setting-origin*, .setting-reset, .settings-search-*, .settings-field, .settings-input, .settings-select, .settings-filter, .settings-label, .settings-note, .settings-page, .install-extension, .extension-grant-box, .inspector-*, .keybinding-row, .provider-card* Core keeps the Settings screen, its rows (which kits build their pages from) and the pages safe mode needs. Only .packages-* — the install form, its log and its actions — moved to Packages Kit.
Terminal Kit's .terminal-* and the xterm.js rules under .terminal-view Terminal Kit's own panel and the stylesheet xterm.js needs, scoped under the kit's surface; its colors come from the tokens (--sunken, --ink) so a theme reaches the shell too.
The status line and the regions: .status-line, .status-item, .status-side, .workbench-region, .region-* Placements core publishes. Only what Pi extensions draw inside them (.pi-ui-*) moved.
Transcript, composer, palette and modals: .transcript*, .message*, .markdown, .hljs-*, .tool-*, .work-fold*, .work-live*, .task-progress*, .composer-*, .command-palette, .model-picker, .approval, .reload-*, .project-picker, .project-modal, .thread-tree* Core's own surfaces, on the list above.
The tokens (now tokens.css), .spinner and the keyframes The palette and the animations every kit's own rules refer to (var(--ink-2), blink, spin). A kit stylesheet uses them and defines none.

The rules for classes nothing renders any more are gone (.approval-mark, .image-placeholder, .palette-group, .reasoning-toggle, .reasoning-body, .typing-mark, .thread-virtual-spacer, .tool-group-header, .turn-activity-stack, .title-auto-toggle, .review-file-read, .review-files-virtual, .tier-mark, .title-generator-actions, .review-file-select, .review-comment-composer, .review-notes).

How to check#

This page is docs/CORE.md in Tau's repository.