runpane 2.4.188 → 2.4.190
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/docs-index.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
[{"path":"docs/ADDING_NEW_CLI_TOOLS.md","title":"Adding a New CLI Agent Tool","text":"# Adding a New CLI Agent Tool\n\nAn agent tool in Pane is **not** a panel type and **not** a manager subclass. It is a\n`type: 'terminal'` ToolPanel whose `customState.initialCommand` launches the CLI in a\nPTY, plus a handful of data-driven registrations. This guide walks through every\nintegration point, using the Cursor Agent CLI (`cursor-agent`, agent id `cursor`) as\nthe worked example.\n\n> Historical note: the `AbstractCliManager`/`cliToolRegistry`/panel-type-per-tool\n> architecture was removed. Only Claude's deprecated session-level path still uses\n> it. Do not build new tools on it.\n\n## 0. Learn the CLI first\n\nBefore writing code, verify against the real binary (see the Cursor example):\n\n- Launch command and permission-skip flags (`cursor-agent --force --trust`).\n- Whether the prompt can be passed as a positional argument, including slash-prefixed\n input (Cursor: yes, unconditionally → argument delivery).\n- Session/resume mechanics (Cursor: `create-chat` pre-creates an id; `--resume <id>`;\n `--continue` for latest).\n- Screen frames for status detection: idle composer, working/spinner, approval\n prompts, first-run dialogs. Capture raw PTY bytes with a `pty` harness and distil\n them into test fixtures.\n- Whether the TUI uses the alternate screen and whether it repaints while idle.\n\n## 1. Register the agent in the RunPane contract\n\n`contracts/runpane/contract.json`:\n\n- Add the id to `enums.agents`.\n- Add `agentTemplates.<id> = { title, command, description }`.\n- Update `<codex|claude|cursor>` usage strings.\n- Run `pnpm run generate-runpane-contract` and commit all regenerated artifacts.\n- `scripts/test-runpane-contract.js` (`checkAgentTemplateParity`) loops over the agent\n enum, so the new agent is exercised automatically.\n\n## 2. Teach the classifier the new binary\n\n`main/src/services/agents/agentIdentity.ts` is the **single** command→agent\nclassifier (used by terminalPanelManager, sessionManager, and shutdown marking).\n\n- Widen `TerminalPanelState['agentType']` in `shared/types/panels.ts`.\n- Add the id to `CLI_AGENT_TYPES` and its executable name(s) to\n `AGENT_EXECUTABLES`. The classifier parses the command into tokens (it sees\n through `env`, `command`, `exec` and shell `-c` wrappers) and matches the\n executable name exactly.\n\n## 3. Launch/resume branches\n\n`main/src/services/terminalPanelManager.ts` → `resolveCliLaunchCommand` gets a branch\nper agent: fresh launch (optionally with the prompt as a quoted argument), and\ninterrupted-resume (`wasInterrupted` + `agentSessionId`). Keep agent-specific string\nbuilding in a pure module next to `agents/cursorLaunch.ts` so it is unit-testable.\n\nIf the CLI owns its session ids, scrape them from PTY output via\n`captureAgentSessionId` (add an extractor to `extractAgentSessionId`'s dispatch). If\nlaunch readiness cannot key on first PTY byte (e.g. shell traffic precedes the TUI),\nadd a ready detector like `createCursorReadyDetector` and gate `signalCliReady` on it.\n\nOpenCode validates a `ses_*` id and durably persists it before spawning the PTY,\nthen reuses that exact id with `--session`. Its idle composer gates readiness and\nonce-only stdin prompt delivery; wrapped commands remain opaque. RunPane doctor\nchecks the installed executable's session capability before reporting support.\n\n## 4. Status manifest\n\n`main/src/services/agentStatus/manifests.ts`: write a `<TOOL>_MANIFEST` from captured\nfixtures and register it in `MANIFESTS_BY_AGENT`. Unknown agents fall back to\n`GENERIC_MANIFEST` (works, less precise). Keep blocker rules narrow and live-region\ngated so answered prompts in scrollback don't stick. Record the CLI version the\nfixtures came from.\n\n## 5. Restart/auto-resume\n\n`main/src/services/agents/agentResume.ts` → `resolveResumeId` maps the agent to the\nresume id shown in the resume dialog. `sessionManager.resumeInterruptedSessions` and\nthe `index.ts` graceful-shutdown path are agent-generic once the classifier knows the\nbinary.\n\n## 6. RunPane IPC\n\n`main/src/ipc/runpane.ts`:\n\n- `shouldUseArgumentDelivery` — how the initial prompt reaches the CLI.\n- `runAgentDoctor` — add fallback binary paths (`AGENT_FALLBACK_BIN_PATHS`) when the\n install dir is typically off the GUI PATH.\n\n## 7. Frontend\n\n`shared/constants/agentLaunchPresets.ts` is the single list behind the toolbar pills,\nAdd Tool dropdowns (desktop + remote), and `mod+alt+N` hotkeys. Add one entry\n(`platforms` gates unsupported OSes); `agentLaunchPresets.test.ts` pins the list\nagainst the RunPane contract. Add the brand icon to\n`frontend/src/components/ui/BrandIcons.tsx` and register it in `CLI_BRAND_ICONS`\n(`frontend/src/components/ui/brandIconRegistry.ts`), and a search alias in\n`frontend/src/components/settings/catalog.tsx`.\n\n## 8. Worktree file sync\n\nIf the CLI reads a config directory (like `.cursor/`), add it to\n`DEFAULT_WORKTREE_FILE_SYNC_ENTRIES` in `shared/types/worktreeFileSync.ts` so it is\ncopied into new worktrees.\n\n## 9. Pane Chat (optional)\n\nTo offer the agent as a Pane Chat orchestrator: widen `PaneChatAgent` and the panel-id\nmaps in `shared/types/paneChat.ts`, extend `paneChatManager` (title, session-id\nstrategy, bootstrap prompt), and teach `skillCacheManager` to emit the orchestrator\nguide in a format the CLI actually reads (Cursor: `.cursor/rules/*.mdc`).\n\n## 10. Tests are the spec\n\nEvery step above lands test-first: `agentIdentity.test.ts`, `<tool>Launch.test.ts`,\n`terminalPanelManager.test.ts`, `agentResume.test.ts`, `manifests.test.ts` +\n`agentStatusPipeline.test.ts` (real captured bytes), `runpane.test.ts` (agent matrix +\ndoctor), `agentLaunchPresets.test.ts`, `scripts/test-runpane-contract.js`.\n\n## Agents launched through a wrapper\n\nPeople often start a supported agent through their own command, such as\n`agent-farm run free-range` or a shell script. The launch command then says\nnothing about the agent, but Pane still needs to know it, because composer\ndetection, `panels submit` staging, the agent-specific status manifest and the\nagent shown in `panels list` depend on the panel's `agentType`.\n\nManaged agent panels emit `agent.busy`, `agent.ready`, `agent.blocked`,\n`agent.unknown` and `agent.idle` events before identification. These reach the\ndefault agents-only workspace watch even without an `agentType`; ordinary shell\npanels do not emit them. For example, an Agent Farm variant picker can report\n`agent.blocked` before the agent starts. An untyped `panel.exited` is still\nfiltered by the agents-only watch.\n\nPane decides a terminal panel's agent in this order, and records the source in\n`TerminalPanelState.agentDetection`:\n\n1. **`declared`**: `--agent <id>` together with `--tool-command <command>` on\n `panes create`, `panes adopt` and `panels create`. On the wire this is\n `tool: { command, agentType }`. Here `--agent` means \"the agent this command\n runs\".\n2. **`command`**: the launch command's executable word, through\n `resolveAgentTypeFromCommand` in `main/src/services/agents/agentIdentity.ts`.\n3. **`process`**: the PTY's foreground process name, which node-pty reports as\n `pty.process`. The names `claude`, `codex`, `cursor-agent` and `opencode` map to agents.\n Claude Code's native installer runs `~/.local/share/claude/versions/<version>`,\n so it reports a bare version such as `2.1.283`. Pane resolves that name to an\n executable path with `ps` (or `/proc/<pid>/exe` on Linux) before trusting it.\n This source is skipped on Windows, WSL and ptyHost terminals, where node-pty\n cannot name the foreground program. A wrapper that stays in the foreground\n (for example a Node process that spawns the agent in its own process group)\n hides the agent from this check.\n4. **`screen`**: Claude's closed rule/`❯`/rule composer box, or Codex's\n `OpenAI Codex` header with its `›` prompt, or either agent's folder-trust\n screen: Claude's `Claude Code'll be able to read` with\n `Yes, I trust this folder`, or Codex's `Codex can read, edit, and run files here`\n with `Trust and continue`\n (`main/src/services/agents/agentScreenSignature.ts`). Pane adopts it only after\n two consecutive status polls match. It ignores the check while Pane's own\n shell is in the foreground, so an agent's last frame left above a shell\n prompt does not count.\n\nPane runs the process and screen checks on the agent-status poll\n(`TerminalPanelManager.pollAgentStatus`) until the panel's agent is known. Once\nit is known, Pane:\n\n- stores `agentType`, `agentDetection`, `launchCommand`, `isCliPanel: true` and\n `launchMode: 'wrapped'` on the panel's custom state;\n- switches the panel to that agent's status manifest;\n- restates the panel's current status with the identified agent attached.\n\nAgent-specific composer readiness and a starting message key off the identified\nagent, however it was identified. Pre-identification watch events and folder-trust\nidentification do not establish composer readiness:\n\n- `--wait-ready` (and `panels wait --for ready`) on a launched command matches\n only once the panel has an agent and that agent's own ready rule passes:\n Claude's composer, Codex's configured footer, the existing rules for the\n others. A wrapper's first output byte never counts. A command that shows no\n supported agent times out with `readiness.reason.code: no-agent-detected`;\n one that exits back to Pane's shell fails at once with `command-exited`\n (where the foreground process is known). A plain shell with no launch\n command is ready once it exists.\n- Prompts before the agent appears come back as `blocked` with their text:\n Agent Farm's `Trust this repository workspace? [y/N]`, and any `[y/N]` or\n similar last-line question a wrapper or shell asks, even when `--agent` was\n declared. Pane never answers them.\n- With `--wait-ready`, runpane owns the starting message: it claims the\n message at create, waits for readiness, stages the text and presses the\n detected agent's submit key separately (Ctrl+Enter for Codex), and verifies\n it like `panels submit`. Without `--wait-ready`, the terminal manager holds\n the message until the agent is identified and idle at its composer, then\n stages and submits it the same way. If no agent appears within 30 seconds\n (a blocking prompt restarts that wait), it records `initialInputError` and\n never types the message into the shell or the wrapper.\n- `[TerminalPanelManager] Starting message …` and `[Runpane] Starting message …`\n log lines mark each write and its outcome, with the agent, detection source,\n byte count, submit key and any `PANE_AGENT_SESSION_ID` the wrapper printed.\n They never include the message text.\n\n`launchMode: 'wrapped'` means the command is launched exactly as given.\n`resolveCliLaunchCommand` never adds `--session-id`, resume flags or a prompt\nargument to it. On restart, Pane runs the wrapper again, and the wrapper handles\nits own resume. The exceptions are a panel with an explicit custom command resume\nconfig and a recognized `agent-farm run|agent` command, which relaunch through\ntheir resume template (see\n[Resuming custom launchers](SESSIONS.md#resuming-custom-launchers)). For the same\nreason, `--resume` is rejected for wrapper commands.\n\nDirect OpenCode launches use Pane's durable session id and readiness-gated input.\nNative selector insertion accepts literal direct commands and simple assignment or\n`env NAME=value` prefixes, not recognized shell wrappers or `env --` forms. Those\nforms fail before native identity allocation; explicitly wrapped launches remain opaque.\nNative commands reject both `--continue` and `-c`, and reject OpenCode's\n`--prompt` argument because it would replay on every restore. Supply initial\ninput through Pane (or RunPane's `--prompt`) instead.\nWrapped OpenCode launches run unchanged; the wrapper owns session selection and\nresume, just as it does for the other supported agents.\n\nA new built-in agent works with wrappers once its executable name is in\n`AGENT_EXECUTABLES`. Add a screen signature only if the agent's UI has a stable,\ndistinctive frame.\n\n## Delivery state and ghost text\n\n`panels submit`, `panels submit-composer`, `agents send` and create prompts\nreport `delivery: { state, evidence }` for Claude and Codex. The state comes\nfrom the agent's own transcript when Pane can find it\n(`main/src/services/agentTranscript/`):\n\n- Claude: `~/.claude/projects/<cwd, non-alphanumerics as '-'>/<session id>.jsonl`.\n Pane picks the session id (`--session-id`), except for wrapped launches, where\n it reads the worktree's recently written transcripts. A taken turn is a\n `type: \"user\"` entry. A message sent while Claude works is first a\n `queue-operation` `enqueue` entry, then a `user` entry once taken.\n- Codex: the newest rollout under `~/.codex/sessions/YYYY/MM/DD/` whose\n `session_meta.cwd` is the worktree, or the rollout named by the thread id once\n Pane knows it. A taken turn is a `response_item` user message. Codex writes a\n queued message only once it takes it, so `queued` comes from its screen\n (`Messages to be submitted after next tool call` / `↳ <message>`).\n\nThe reader tails each file from a byte offset, so polling a long transcript\nstays cheap. Without a transcript, the screen decides: a steadily empty composer\nis `taken`, text still in it is `in-composer`.\n\nThe terminal model splits each screen into typed cells and ghost cells: dim\n(SGR 2) or placeholder-grey text, which agents use for placeholders, prompt\nsuggestions and queued-message lists (`TerminalStateEmulator.getScreenText`).\nComposer detection reads only typed cells; `panels screen` reports the ghost\ntext in the composer as `composer.ghostText`. A new agent whose placeholder is\nneither dim nor mid-grey needs its own placeholder rule in\n`agentScreenSignature.ts`.\n"},{"path":"docs/ANALYTICS_INVARIANTS.md","title":"Analytics Invariants","text":"# Analytics Invariants\n\nThese rules keep Pane's PostHog funnel person-stitchable and privacy-safe. Pane\nuses default-on product analytics for new and previously undecided installs.\nPrivacy Settings clearly discloses that default and provides the opt-out.\nExplicit choices from older versions must never be overwritten.\n\n## Bundled Executable Code\n\nImport the SDK through `posthog-js/dist/module.no-external` and keep\n`disable_external_dependency_loading` enabled. This is PostHog's\n[supported entry point for Electron](https://posthog.com/docs/libraries/js):\nanalytics requests and JSON configuration remain available, while executable\nconfiguration, session replay, surveys, and other remote extensions cannot load.\nAll executable dependencies must come from the reviewed, lockfile-pinned build.\nDo not add a second import from the default SDK entry point.\n\n## Identity Comes First\n\nResolve analytics identity in the main process before the renderer captures any\nanalytics disclosure or choice event.\n\nRequired ordering:\n\n1. Resolve GitHub CLI email, git email, or stable `install_id`.\n2. Persist the analytics identity in config.\n3. Capture `analytics_default_enabled` for an undecided install entering the\n default-on experiment.\n4. Capture `analytics_opted_in` or `analytics_opted_out` for explicit Settings\n choices.\n5. Capture `app_first_opened` and usage events only after the default or choice\n is applied.\n\nDo not flush queued usage until the initial default or stored choice has been\nresolved. Opting out must capture the choice marker directly, discard queued\nusage, and leave the PostHog SDK opted out.\n\n## Existing Choice Preservation\n\n`analytics_consent_shown=true` records an explicit choice made under the legacy\nopt-in dialog. Its stored `analytics.enabled` value is authoritative. Never\nreplace an explicit `false` with the default-on value. The\n`analytics_default_applied` distinguishes installs already processed by the\nnew experiment from previously undecided installs.\n\n## Required Event Context\n\nEvery first-run funnel event must include:\n\n- `distinct_id`\n- `install_id`\n- `app_version`\n- `platform`\n- `identity_source`\n\nThe event should also set person properties when the identity is known\n(`personProperties` in `frontend/src/services/posthog.ts`):\n\n- `install_id`\n- `identity_source`\n- `github_username`\n- `github_email`\n- `git_email`\n- `git_email_sha256`\n- `git_user_name`\n- `app_version`\n- `platform`\n\nPrivacy Settings must disclose that analytics can be associated with locally\navailable GitHub or Git account details, including a username or email. Do not\ndescribe this telemetry as anonymous while those person properties are sent.\n\n## Opt-Out Is Still Identified\n\nCapture `analytics_opted_out` with identity context before disabling analytics.\nAfter that capture, discard queued usage events and keep analytics disabled.\n\nThis records who opted out without sending their later product usage.\n\n## Config Saves Preserve Analytics Fields\n\nRenderer settings updates must deep-merge analytics config instead of replacing\nit. In particular, do not drop the identity fields in `analytics`\n(`main/src/types/config.ts`):\n\n- `installId`\n- `distinctId`\n- `identitySource`\n- `githubUsername`, `githubEmail`, `gitEmail`, `gitEmailHash`, `gitUserName`\n\n## Attribution and Versioning\n\nWhen web attribution is present, emit attribution events with the same\n`distinct_id` and `install_id` as consent and usage events.\n\nApp version should come from the running app context and be attached to disclosure,\nfirst-open, usage, attribution, and close events. This lets PostHog distinguish\ncurrent users from older installs.\n\n## Remote Pane Privacy\n\nRemote Pane analytics must use explicit sanitized events. Do not rely on\nautocapture for Remote Pane connection-code, token, host, or command surfaces.\nAny UI that renders or accepts `pane-remote://` codes, remote access tokens, or\nremote setup commands must include `ph-no-capture`.\n\nSSH host aliases from the user's SSH config never go into events or logs sent\noff the machine. The SSH hosts sidebar section and the SSH view carry\n`ph-no-capture`.\n\nRemote Pane events must never include:\n\n- connection codes\n- bearer tokens\n- token hashes\n- base URLs\n- hostnames or IP addresses\n- profile labels\n- client labels\n- device labels\n- raw error messages\n- local paths or Pane data directories\n- remote runtime ids\n\nAllowed properties should stay enum-like: `surface`, `role`, `flow`, `result`,\n`tunnel_preference`, `tunnel_kind`, `data_mode`, `client_kind`,\n`connection_mode`, `failure_stage`, `failure_category`, and connected-client\ncount buckets.\n\n## Remote Pane Event Catalog\n\n| Event | Primary source | Funnel role |\n|---|---|---|\n| `remote_pane_host_setup_started` | `main/src/ipc/remoteDaemon.ts` | Host setup started |\n| `remote_pane_host_setup_succeeded` | `main/src/ipc/remoteDaemon.ts` | Host setup completed |\n| `remote_pane_host_setup_failed` | `main/src/ipc/remoteDaemon.ts`, `main/src/daemon/remoteTransportController.ts` | Host setup or transport failed |\n| `remote_pane_setup_terminal_opened` | `main/src/ipc/remoteDaemon.ts` | Setup command opened in terminal |\n| `remote_pane_connection_code_created` | `main/src/ipc/remoteDaemon.ts` | Host code created |\n| `remote_pane_connection_pair_created` | `main/src/ipc/remoteDaemon.ts` | Advanced paired profile created |\n| `remote_pane_connection_code_imported` | `main/src/ipc/remoteDaemon.ts` | Client imported code |\n| `remote_pane_connection_code_import_failed` | `main/src/ipc/remoteDaemon.ts` | Client import failed |\n| `remote_pane_client_connect_started` | `main/src/daemon/client/remotePaneClient.ts` | Desktop client connection started |\n| `remote_pane_client_connected` | `main/src/daemon/client/remotePaneClient.ts`, `main/src/daemon/httpApiServer.ts` | Desktop client connected |\n| `remote_pane_client_connection_failed` | `main/src/daemon/client/remotePaneClient.ts`, `main/src/ipc/remoteDaemon.ts` | Desktop client connection failed |\n| `remote_pane_client_disconnected` | `main/src/daemon/client/remotePaneClient.ts`, `main/src/daemon/httpApiServer.ts` | Desktop client disconnected |\n| `remote_pane_profile_deleted` | `main/src/ipc/remoteDaemon.ts` | Client profile removed |\n| `remote_pane_host_access_cleared` | `main/src/ipc/remoteDaemon.ts` | Host access revoked |\n| `remote_pane_host_clients_disconnected` | `main/src/ipc/remoteDaemon.ts` | Host disconnected clients |\n| `remote_pane_host_transport_started` | `main/src/daemon/remoteTransportController.ts` | Host transport live |\n| `remote_pane_host_transport_stopped` | `main/src/daemon/remoteTransportController.ts` | Host transport stopped |\n| `remote_pane_pwa_client_connected` | `main/src/daemon/httpApiServer.ts` | Browser/PWA client connected |\n| `remote_pane_pwa_client_disconnected` | `main/src/daemon/httpApiServer.ts` | Browser/PWA client disconnected |\n| `remote_pane_remote_runtime_used` | `main/src/daemon/client/remotePaneClient.ts` | Remote runtime actually used |\n\n## Failure Volume and Error Privacy\n\nKeep core desktop activity (`app_opened`, `app_closed`, `session_created`,\n`panel_switched`, `terminal_panel_created`). Neither wrapper emits routine\ncommand starts or successes. Preserve sanitized wrapper failures and download\nlifecycle events; honor explicit local analytics opt-out as well as CI and\n`RUNPANE_TELEMETRY_DISABLED`.\n\nLocal control emits only `runpane_local_control_failed` for thrown failures or\nhandled `ok: false` results. Normal wait deadlines and optional agent\nunavailability are excluded. Its properties are `action`, `status`, `command_ok`,\n`failure_kind`, `error_type`, `error_code`, and `failure_category`, plus app version,\nplatform and Electron version. Each coarse operation/category/code combination\nreports at most once per five minutes, with an overall cap of 20 per hour per\napp process. Limits are in memory and reset on restart. Local logging remains\nindependent of analytics suppression.\n\nProduction ingestion drops legacy `runpane_wrapper_command_started`,\n`runpane_wrapper_command_succeeded`, and `runpane_local_control`. Keep the new\nfailure event name separate so useful failures survive these exclusions.\n\nPostHog `$exception` events are reserved for app errors: renderer global errors\nand unhandled rejections, React error boundaries, unexpected renderer crashes/OOM,\nmain uncaught exceptions, and shutdown errors. Console errors and handled CLI\nfailures never become exceptions. Exceptions have their own five-minute\ndeduplication and 20-per-hour cap. Both error event paths send directly from main\nthrough PostHog's capture API, with at most four concurrent requests and a 1.5s\ntimeout; neither waits for delivery. Errors obey analytics opt-out and use only\nthe existing install UUID identity, without person-profile updates, account\nproperties, SDK URL enrichment or persistent error storage.\n\nException payloads contain coarse source, built-in error type, allowlisted error\ncode/category, version/platform, a fixed message, and up to ten packaged app JS\nframes. Frames contain relative app filenames and numeric line/column offsets,\nnever absolute paths, function names or source context. Raw messages, causes,\nCLI argv, terminal contents, prompts, source code, repository paths, usernames,\nhome paths and environment values are excluded. Development/server URLs and\nexternal dependency frames are omitted. Restricted interactive-element\nautocapture and pageviews stay enabled, with text and attributes masked;\nautomatic exception capture is disabled. Session replay policy is unchanged.\n\nMain fatal capture uses `uncaughtExceptionMonitor`, preserving exit behavior.\nDelivery during process exit is best effort. Native main-process aborts and\nsegfaults cannot be captured by JavaScript; there is no minidump uploader or\nsource-map upload pipeline. Renderer process loss reports a coarse crash reason\nwithout a native stack. Native crash reporting remains a separate follow-up.\nPostHog's [Electron support request](https://github.com/PostHog/posthog/issues/43993)\ndoes not provide native Electron crash support. The payload follows the supported\n[manual exception schema](https://posthog.com/docs/error-tracking/installation/manual)\nfor JavaScript errors; this is not a replacement for native Electron crash reporting.\nSource-map symbolication is not wired or verified: current reports contain\npackaged JavaScript offsets only, and a future source-map upload integration must\nbe verified against a packaged build before claiming original TypeScript locations.\n\n## Test Coverage\n\nChanges to disclosure, analytics config, or first-run event ordering should update\nor add coverage in:\n\n- `main/src/services/analyticsIdentity.test.ts`\n- `tests/analytics-consent.spec.ts`\n\nThe Playwright test should verify both event order and payload shape, including\nSDK identity and interaction events, and reject external script requests even\nwhen remote configuration enables extensions.\n"},{"path":"docs/APPEARANCE.md","title":"Appearance","text":"# Appearance\n\nPane can follow the operating system or stay on a fixed palette. **System** remembers one light palette and one dark palette and selects between them as the OS appearance changes. **Fixed** uses a third, independent palette. Switching modes never replaces any remembered selection.\n\nNew installations default to System with Light (rounded) for the light slot and Dark (sharp) for the dark slot; the remembered Fixed palette is Light (rounded). Existing configurations that only contain `theme` migrate to Fixed so their current palette remains exact. The legacy palette also seeds the matching System slot, while the opposite slot keeps its default.\n\nPalette slots are validated at load, renderer, IPC, and persistence boundaries. Only palettes whose canonical base class is `light` can occupy the light slot, and only palettes whose base is `dark` can occupy the dark slot. Invalid stored fields are repaired independently and reported by the main process.\n\n## First paint\n\nAt startup the persisted config selects Electron's `nativeTheme` source. Main passes the four-field appearance snapshot through an argv argument and selects a cached, theme-specific BrowserWindow background colour. The preload decodes the snapshot synchronously; the HTML bootstrap resolves System against `prefers-color-scheme` and stamps the full class chain before React mounts. The provider then owns live OS updates, persistence, rollback, window controls, terminal, and editor updates. A versioned local cache and legacy `localStorage.theme` are fallback-only.\n\nOn Linux, System depends on the desktop environment and Chromium exposing `prefers-color-scheme`. Desktops that do not publish an appearance preference may remain on their reported default slot.\n\n## App icon\n\n`pnpm generate-icons` renders the general app assets from\n`main/assets/icon-source.svg` and the macOS Dock assets from\n`main/assets/icon-macos-source.svg`. The macOS Dock PNG and bundle ICNS use the\nblack-sky landscape artwork inside a transparent 1024px canvas so its visible size\nmatches neighboring Dock icons. The simpler PNG, ICO, and favicons continue to\nserve Linux, Windows, and small web sizes. `pnpm dev` copies an updated Dock PNG\ninto the main-process build before launching Electron.\n"},{"path":"docs/ARCHITECTURE.md","title":"Pane architecture","text":"# Pane architecture\n\nThis is an index of how Pane fits together. It names the owning files; the code\nis the reference for details.\n\n## Words\n\n- **Repository (project):** a saved base git repo.\n- **Pane (session):** one feature or PR workspace. It normally owns one\n Pane-managed git worktree and branch.\n- **Panel:** a tab inside a pane. Types are `terminal`, `diff`, `explorer`,\n `editor`, `logs`, `dashboard`, `setup-tasks`, `browser` and `notes`\n (`shared/types/panels.ts`). Agent CLIs run in `terminal` panels.\n\n## Processes\n\n```\nrenderer (frontend/, React) ──IPC via preload──▶ main process (main/src)\n │ services, SQLite, git\nremote PWA / mobile ──HTTP + SSE──▶ daemon ◀──socket── runpane CLI\n │\n ptyHost (optional utility process)\n```\n\n- **Main process** (`main/src/index.ts`) owns native integration, git\n worktrees, CLI processes and persistence. Business logic lives in\n `main/src/services/`; IPC handlers live in `main/src/ipc/`.\n- **Preload** (`main/src/preload.ts`) is bundled with esbuild because the\n renderer is sandboxed (`scripts/verify-sandboxed-preload.js` checks it). It\n sends each invoke either to the main process or to the daemon, decided by\n `isDaemonOwnedChannel` in `shared/types/daemon.ts`. Keep ownership lists in\n that module, and run `pnpm build:main` to check the bundle still loads.\n- **Renderer** (`frontend/src`) is React with Zustand stores\n (`frontend/src/stores`). `frontend/index.html` is the desktop app;\n `frontend/remote.html` is the Remote PWA that `mobile/` also bundles.\n- **Daemon** (`main/src/daemon`) serves the command registry over a local\n socket (a `pane-daemon-*` directory under the system temp directory, or a\n named pipe on Windows). The `runpane` CLI talks to it. For Remote Pane it also\n serves HTTP and SSE (`httpApiServer.ts`), and the same server, behind\n `tailscale serve` on port 8443, gives your other machines `runpane workspace`\n and codeless desktop remote mode (`workspaceHost.ts`,\n `services/tailnetMachines.ts`, [RUNPANE_WORKSPACES.md](RUNPANE_WORKSPACES.md)).\n Who it admits is the visibility and optional password in `auth.ts`.\n `pnpm daemon:headless` runs it\n without a window. Remote setup and lifecycle:\n [SELF_HOSTED_REMOTE_DAEMON.md](SELF_HOSTED_REMOTE_DAEMON.md) and\n [remote-daemon-lifecycle.md](remote-daemon-lifecycle.md).\n- **ptyHost** (`main/src/ptyHost`) runs PTYs in an Electron utility process.\n It is on by default on Windows (`usePtyHost` in `configManager.ts`), and\n `PANE_USE_PTY_HOST=1` turns it on elsewhere. Otherwise PTYs run in the main\n process. See [troubleshooting/WINDOWS_APP_HANG.md](troubleshooting/WINDOWS_APP_HANG.md).\n\n## Terminal output\n\nPTY output goes through `terminalPanelManager.ts`, which batches it and applies\nflow control. It feeds a headless xterm emulator (`terminalStateEmulator.ts`),\nwhose serialization restores a terminal when its panel is shown again, and\nstores capped scrollback in the `panel_buffers` table. The renderer draws with\nxterm.js in `frontend/src/components/panels/TerminalPanel.tsx`. The comment\nabove that component describes the activation lifecycle; read it before you\nchange how panels show, hide or refresh.\n\nAgent status (working, idle, blocked) is derived in\n`main/src/services/agentStatus/`.\n\n## File previews\n\n`FileEditorView.tsx` routes editor tabs from the Files inspector, terminal links,\nand `runpane panels open --file` using `shared/utils/filePreview.ts`. Supported\nformats open read-only; ordinary code and text, HTML included, retain the editor\nand auto-save.\nImages, PDFs, fonts, video and audio load through `pane-media`, registered in\n`main/src/services/mediaPreview.ts`. A revocable URL grants access to one file;\neach request rechecks the existing worktree/symlink boundary through\n`file:getPath`. `mediaStream.ts` streams byte ranges from disk. File bodies do\nnot pass through text/base64 IPC. On a remote desktop, the URL is bound to the\nconnected host, and each request streams from the host's authenticated `/media`\nroute with the same boundary check and byte ranges. Archive and SQLite listings\n(`file:preview-list`) run on the host. \"Open with system app\" on a remote desktop\ncopies the file to a temporary folder on that desktop and opens the copy;\n\"Reveal in folder\" appears only on the host.\n\nHTML is not a preview format. Clicking an `.html`/`.htm` file in the Explorer,\nor its Eye button, opens it rendered in a browser tab: the tab already showing\nthat file, or else a new one (a tab showing anything else is never retargeted).\nThe \"Open HTML files\" setting (Settings → General) switches Explorer clicks to\neditable source, and an HTML file's right-click menu offers both \"Open preview\nin browser\" and \"Open as file\". A browser tab showing a worktree file offers\n\"View source\", and an HTML editor tab offers \"Preview\", which works like a click. `panels open --file` always\nopens the source; `--url file://...` renders.\n\nMarkdown, CSV/TSV and structured text fetch at most 1 MiB. Markdown renders\nwith a source toggle.\nTables display at most 500 data rows and 100 columns. JSON/JSONL are formatted;\nYAML/TOML retain source formatting in read-only Monaco with folding. Fonts use\na temporary FontFace specimen. Decode failures offer system open/reveal actions.\n\n`filePreviewListing.ts` lists ZIP central directories (up to 4 MiB) and TAR\nheaders without extraction, with at most 1,000 entries. ZIP64/split ZIP and\ncompressed TAR are unsupported; extended TAR names are not expanded. SQLite\nlists tables/views from a bounded temporary copy (32 MiB), opened read-only and\nremoved afterward. Nonempty WAL and rollback journals are refused before and\nafter copying beside the canonical target; hard-linked databases are refused\nbecause their recovery-file owner path is ambiguous. No source sidecars are\ncreated. Native schema inspection runs in\na child process with a 3-second deadline, a 1 MiB output cap and at most 1,000\ndisplayed entries. No table data queries are executed.\n\n`file:read` samples at most 8 KiB before decoding UTF-8. Binary files show a\nread-only notice, and `file:write` refuses to overwrite binary content as text.\nPreview components never enter the editable auto-save path, even on errors.\n\n## Data\n\n- The data directory is `~/.pane` for an installed app. `--pane-dir` or\n `PANE_DIR` overrides it (`main/src/utils/appDirectory.ts`).\n- `sessions.db` is SQLite in WAL mode. The base schema is\n `main/src/database/schema.sql`; later changes are inline migrations in\n `main/src/database/database.ts` that run at startup.\n- `config.json` holds app settings (`main/src/services/configManager.ts`,\n types in `main/src/types/config.ts`).\n- Worktrees go in `<repo>/worktrees/` unless the project sets another folder\n (`worktreeManager.ts`). A worktree on a remote-tracking base fetches that\n branch first (`worktreeBaseRefresh.ts`: capped at about 10 s, shared by\n concurrent creates, never fails the create). `worktreePoolManager.ts` keeps\n empty reserve worktrees on `_reserve/<hex>` branches so new panes open fast;\n a reserve is only claimed while its base ref still points at its commit.\n- Archive cleanup intents live in SQLite's `archive_cleanup_jobs`, atomically\n committed with the session archive flag. `ArchiveCleanupManager` resumes only\n those intents, validates physical directory/Git identity, and serializes work\n per repository with two global slots. Awaited, cooperative purge batches use\n Electron's `original-fs` and job-level busy retries. There are no detached\n deletion helpers or timed-out deletions left running. Failed jobs remain\n visible through ArchiveProgress; interrupted scripts require explicit skip\n on retry. Create/remove and pool background mutations share FIFO repository\n admission with no waiter timeout; archive scripts and purge run outside it.\n Known process trees are captured before committing intent. Teardown preserves\n PTY retirement cleanup, escalates freshly matched process identities, and\n verifies exit before touching files. Survivors remain in the job across\n restart and Retry; missing captures and snapshot errors fail closed.\n Process-table reads are confined to archive preparation/teardown/retry, each\n subprocess bounded to five seconds. Windows uses kernel start times and\n pinned process handles; POSIX revalidates start times before individual signals.\n Create/remove also check the cleanup path guard;\n Restore is blocked until cleanup completes. Quarantine lives under\n `<git-common-dir>/pane-archive-cleanup`, separate from legacy trash sweeps.\n- Per-repo setup, run and archive scripts come from `pane.json` and friends:\n [CONFIG_FILES.md](CONFIG_FILES.md).\n\n## Notes\n\nLocal Notes panels use `main/src/services/notes.ts` through `main/src/ipc/notes.ts`.\nCanonical documents live in `<PANE_DIR>/notes/notes.json`, outside disposable\nworktrees. A revision check rejects stale edits; writes atomically replace the\nstore. Promotion changes the canonical scope and retains references to earlier\nscopes. The Session view derives associated project notebooks from existing Pane\nassociations. Renderer recovery drafts retain edits when an autosave fails. The borderless\neditor inserts text/drawing blocks through inline plus or slash menus; the\nsettings cog holds promotion and deletion actions. Agent visibility is automatic\nwithin scope, with no sharing controls in the editor. Restoring an archived\nPane refreshes its derived context from current project notes.\n\n`noteExports.ts` derives marked instruction sections and drawing assets from saved\ndocuments. Excalidraw scenes remain editable JSON, with PNG previews and readable\nlabels in agent-facing Markdown. Global destinations are Claude's user\n`CLAUDE.md`, Codex's user `AGENTS.md` (or existing override), and Cursor's local\n`rules/pane-memories.mdc`. Cursor CLI also receives the generated memory through\na user `sessionStart` hook (`cursorNoteHook.ts`), preserving existing hooks.\nScoped content is exported outside repositories to\n`<PANE_DIR>/notes/contexts/<pane-id>.md`. Each new native Pane terminal receives\nits path through `PANE_NOTES_FILE`; the user-level integration tells agents to\nread that file before each turn. Project contexts contain only that project's\nnotes plus the current feature; Session contexts contain only Session notes.\nOrdinary saves leave repositories clean. The existing `managedAgentsMd` opt-in\nadditionally exports scoped notes into repository `AGENTS.md` and `CLAUDE.md`;\nturning it off removes those memory sections.\n\nExports preserve symlinks and permissions and replace files atomically. A store\nownership marker prevents a second Pane data directory from overwriting the\nfirst directory's user memory. Malformed marker pairs produce a visible error.\nChanging notebooks only reads data. Terminals predating this update need reopening\nto gain the context environment variable. After the first scoped note, start a\nnew agent conversation to load the scoped-reading instruction. Global-file changes refresh on Codex's next\nturn, Claude resume, or a new Cursor conversation in the tested CLI versions;\nupdating a file alone cannot erase context already read. Notes support native\nmacOS, Windows, and Linux installations; WSL agent exports report an unsupported\ndestination instead of claiming delivery. There is no cross-machine sync.\n\n## Subsystems\n\n| Area | Owner | Doc |\n| --- | --- | --- |\n| Adding an agent CLI | `main/src/services/agents/` | [ADDING_NEW_CLI_TOOLS.md](ADDING_NEW_CLI_TOOLS.md) |\n| Panels | `panelManager.ts`, `shared/types/panels.ts` | [TOOL_PANEL_SYSTEM.md](TOOL_PANEL_SYSTEM.md) |\n| Renderer updates | IPC events in `main/src/events.ts` | [STATE_MANAGEMENT.md](STATE_MANAGEMENT.md) |\n| Timestamps | `main/src/utils/timestampUtils.ts` | [TIMESTAMP_HANDLING.md](TIMESTAMP_HANDLING.md) |\n| Pane Chat and orchestration sessions | `paneChatManager.ts`, `skillCacheManager.ts`, `paneChatBundle/` | [SESSIONS.md](SESSIONS.md) |\n| SSH hosts view | `sshConfigHosts.ts`, `ipc/sshHosts.ts`, `SshView.tsx` | [SSH_HOSTS.md](SSH_HOSTS.md) |\n| `runpane` CLI contract | `contracts/runpane/` | [RUNPANE_CLI_CONTRACT.md](RUNPANE_CLI_CONTRACT.md) |\n| Analytics | `analyticsManager.ts`, `frontend/src/services/posthog.ts` | [ANALYTICS_INVARIANTS.md](ANALYTICS_INVARIANTS.md) |\n| Themes | `frontend/src/styles/tokens/colors.css` | [APPEARANCE.md](APPEARANCE.md), [scripts/README.md](../scripts/README.md) |\n| Usage and cost | `main/src/services/usage/` | [usage/README.md](../main/src/services/usage/README.md) |\n| Mobile companion | `mobile/` | [NATIVE_MOBILE.md](NATIVE_MOBILE.md) |\n\n## Rules that follow from this design\n\n- Run project commands through the asynchronous `CommandRunner` and\n `CommandExecutor` (`main/src/utils/`). Await results through IPC and the\n lifecycle callers, drop results from stopped or replaced watchers, and\n serialize Spotlight checkouts with restoration.\n- For git status in WSL, watch from inside the distro: prefer `inotifywait`.\n Without it, Pane polls `git status` in the distro every five seconds while\n focused. Don't add Windows-side recursive watchers over `\\\\wsl.localhost` or\n `\\\\wsl$`.\n- Production builds must never include React Scan (`pnpm perf:scan` is dev\n only).\n"},{"path":"docs/BUILDING_ON_WINDOWS.md","title":"Building Pane on Windows","text":"# Building Pane on Windows\n\nThis document explains how to build Pane for Windows.\n\n> **Note:** The primary native modules (`@lydell/node-pty` and `better-sqlite3-multiple-ciphers`) have prebuilt binaries for Windows.\n\n## Quick Start\n\n```bash\n# Build for x64 (recommended for most users)\npnpm run build:win:x64\n\n# Build for ARM64\npnpm run build:win:arm64\n```\n\nOutput files will be in `dist-electron/`:\n- `Pane-{version}-Windows-x64.exe` - x64 installer\n- `Pane-{version}-Windows-arm64.exe` - ARM64 installer\n\n## Prerequisites\n\n1. **Node.js** - v22.18 or later\n2. **pnpm** - v10\n3. **Python** - v3.x (for native module compilation)\n4. **Visual Studio Build Tools** - With C++ workload\n\n### Installing Visual Studio Build Tools\n\n```bash\n# Using winget\nwinget install Microsoft.VisualStudio.2022.BuildTools\n\n# Or download from:\n# https://visualstudio.microsoft.com/visual-cpp-build-tools/\n```\n\nMake sure to install the \"Desktop development with C++\" workload.\n\n## How the Build Works\n\nThe Windows build uses a custom script (`scripts/build-win.js`). It:\n\n1. Downloads the Electron prebuilt for `better-sqlite3-multiple-ciphers` for the target architecture.\n2. When building for a different architecture than the host, installs the matching `@lydell/node-pty-win32-<arch>` prebuilt package.\n3. Builds the frontend and main process, injects build info, and generates notices.\n4. Runs electron-builder with native module rebuild disabled.\n5. Checks the icons in the packaged app.\n\n### Why These Steps\n\n#### better-sqlite3-multiple-ciphers Electron Prebuilt\n\n**Problem:** When using `pnpm install --ignore-scripts`, the `prebuild-install` postinstall script doesn't run, so the package gets the wrong binary (Node.js ABI instead of Electron ABI). This causes \"is not a valid Win32 application\" errors at runtime.\n\n**Solution:** The build script runs `prebuild-install` manually with the correct Electron runtime and version to download the Electron-compatible prebuilt binary.\n\n#### Cross-Architecture node-pty\n\n**Problem:** pnpm only installs the `@lydell/node-pty` platform package that matches the host architecture.\n\n**Solution:** For cross-architecture builds, the build script downloads the target platform package and links it into `node_modules`.\n\n#### Native Module Rebuild\n\n**Problem:** Rebuilding native modules for Electron can fail on Windows due to pnpm path issues.\n\n**Solution:** The build script disables npm rebuild (`--config.npmRebuild=false`) and relies on:\n- Manually downloaded Electron prebuilts for `better-sqlite3-multiple-ciphers`\n- Prebuilt platform packages for `@lydell/node-pty` (no rebuild needed)\n\n## Manual Build Process\n\nIf the build script fails, you can try these manual steps:\n\n### Step 1: Install dependencies without running scripts\n\n```bash\npnpm install --ignore-scripts\n```\n\n### Step 2: Download Electron prebuilt for better-sqlite3\n\n```bash\n# Navigate to the better-sqlite3-multiple-ciphers package directory\ncd node_modules/.pnpm/better-sqlite3-multiple-ciphers@*/node_modules/better-sqlite3-multiple-ciphers\n\n# Download the Electron-compatible prebuilt (replace 41.10.3 with your Electron version)\nnpx prebuild-install --runtime electron --target 41.10.3 --arch x64 --verbose\n\n# Return to project root\ncd -\n```\n\n### Step 3: Build\n\n```bash\npnpm run build:frontend\npnpm run build:main\npnpm run inject-build-info\npnpm run generate-notices\npnpm exec electron-builder --win --x64 --publish never --config.npmRebuild=false\n```\n\n## Troubleshooting\n\n### Error: `node-gyp failed to rebuild`\n\nTry building with `--config.npmRebuild=false` to skip native module rebuild:\n```bash\npnpm exec electron-builder --win --x64 --publish never --config.npmRebuild=false\n```\n\n### Error: `better_sqlite3.node is not a valid Win32 application`\n\nThe better-sqlite3 native module was built for Node.js instead of Electron. Run the prebuild-install step:\n```bash\ncd node_modules/.pnpm/better-sqlite3-multiple-ciphers@*/node_modules/better-sqlite3-multiple-ciphers\nnpx prebuild-install --runtime electron --target 41.10.3 --arch x64 --verbose\n```\n\n### Native modules not working at runtime\n\nIf the app crashes due to native module issues:\n1. Ensure you're running on the same architecture you built for\n2. Verify the better-sqlite3 prebuilt was downloaded for Electron (see above)\n3. Check that Python and Visual Studio Build Tools are properly installed\n\n## Architecture Notes\n\n### x64 vs ARM64\n\n- **x64 build**: Works on both x64 and ARM64 Windows (ARM64 uses emulation)\n- **ARM64 build**: Native performance on ARM64, but requires ARM64 native modules\n\nFor most users, the x64 build is recommended as it works on all Windows machines.\n\n### Native Module Compatibility\n\nThe following native modules are used:\n- `better-sqlite3-multiple-ciphers` - SQLite database (has Windows Electron prebuilts)\n- `@lydell/node-pty` - Terminal emulation (has Windows x64 prebuilts, validate after each Electron major upgrade)\n\nBoth modules have prebuilt binaries that work on Windows without compilation.\n\n## CI/CD\n\nFor CI/CD pipelines, use:\n```bash\nnode scripts/build-win.js x64\n```\n\nThis runs all of the steps above.\n"},{"path":"docs/CONFIG_FILES.md","title":"Config File Detection","text":"# Config File Detection\n\nPane automatically detects workspace configuration files at your repository root and uses them to provide default scripts for session setup, the Play button, and cleanup on archive. This lets teams share workspace configuration via git — commit the file, push, and every teammate gets the same setup.\n\n## Supported Files\n\nPane checks for these files in priority order. The first file found with at least one script defined wins:\n\n| Priority | File | Format | Origin |\n|----------|------|--------|--------|\n| 1 | `pane.json` | JSON | Pane's native format |\n| 2 | `conductor.json` | JSON | [Conductor.build](https://conductor.build) |\n| 3 | `.gitpod.yml` | YAML | [Gitpod](https://gitpod.io) |\n| 4 | `.devcontainer/devcontainer.json` | JSON | [Dev Containers](https://containers.dev) |\n\nIf none of these exist, Pane falls back to checking for `scripts/pane-run-script.js` in the worktree (the legacy auto-generated script).\n\n## pane.json\n\nThis is Pane's native config format. It follows the same schema as [conductor.json](https://docs.conductor.build/core/conductor-json), so teams using Conductor can switch to Pane without changing their config.\n\n### Schema\n\n```json\n{\n \"scripts\": {\n \"setup\": \"pnpm install\",\n \"run\": \"pnpm dev\",\n \"archive\": \"pnpm cleanup\"\n },\n \"runScriptMode\": \"concurrent\"\n}\n```\n\nAll fields are optional.\n\n### Fields\n\n#### `scripts.setup`\n\nRuns automatically when a new worktree/session is created. Use for dependency installation, database setup, environment configuration, etc.\n\n**Examples:**\n```json\n\"setup\": \"npm install\"\n\"setup\": \"pnpm install && pnpm db:migrate\"\n\"setup\": \"bundle install && rails db:setup\"\n```\n\n#### `scripts.run`\n\nRuns when you click the **Play** button in the panel tab bar (or press `Ctrl+Shift+D`). Use for starting dev servers, test watchers, or any long-running process.\n\nPane sets `PANE_PORT` to the first of a block of 10 ports picked from the pane's id (range 3000–8999), so dev servers in parallel panes rarely collide.\n\n**Examples:**\n```json\n\"run\": \"pnpm dev\"\n\"run\": \"npm run dev -- --port $PANE_PORT\"\n\"run\": \"concurrently \\\"npm run server\\\" \\\"npm run worker\\\"\"\n```\n\n#### `scripts.archive`\n\nRuns before a worktree is deleted when you archive a session. Use for cleaning up external resources — dropping test databases, revoking tokens, stopping background services.\n\n**Examples:**\n```json\n\"archive\": \"npm run db:teardown\"\n\"archive\": \"docker compose down\"\n```\n\n#### `runScriptMode`\n\nControls behavior when a new run script starts while one is already running.\n\n| Value | Behavior |\n|-------|----------|\n| `\"concurrent\"` | (Default) New script starts alongside existing ones |\n| `\"nonconcurrent\"` | Existing script is stopped before starting the new one |\n\n> **Note:** `runScriptMode` is detected but not yet enforced. This will be implemented in a future release.\n\n## How Scripts Map to Pane\n\nEach config file format maps to Pane's three lifecycle hooks differently:\n\n| Lifecycle | pane.json / conductor.json | .gitpod.yml | devcontainer.json |\n|-----------|---------------------------|-------------|-------------------|\n| **Setup** (worktree creation) | `scripts.setup` | First task's `init` | `postCreateCommand` |\n| **Run** (Play button) | `scripts.run` | First task's `command` | `postStartCommand` |\n| **Archive** (session deletion) | `scripts.archive` | — | — |\n\n## Override Model\n\nPane follows the same override model as Conductor:\n\n```\nProject Settings (UI) > Config File > Nothing\n```\n\n1. **If you set a value in Project Settings**, it always wins — the config file is ignored for that field\n2. **If Project Settings is empty**, Pane uses the config file value\n3. **If neither exists**, no script runs (except for the Play button, which falls back to Claude-generated setup)\n\nThis means config files provide **team-shared defaults** that individual users can override locally through the UI.\n\n### Per-Field Override\n\nEach script field is independent. You can override `run` in Project Settings while still using `setup` from `pane.json`:\n\n```\nBuild Script: empty → uses pane.json scripts.setup\nRun Commands: \"npm run dev:custom\" → overrides pane.json\nArchive Script: empty → uses pane.json scripts.archive\n```\n\n## Branch-Local Config\n\nConfig files are read from the **session's worktree**, not the project root. This means:\n\n- If a branch modifies `pane.json`, that branch's sessions use the modified config\n- You can test config changes in a worktree before merging to main\n- Different branches can have different setup/run/archive scripts\n\n## Quick Start\n\n### 1. Create a `pane.json` in your repo root\n\n```json\n{\n \"scripts\": {\n \"setup\": \"pnpm install\",\n \"run\": \"pnpm dev\"\n }\n}\n```\n\n### 2. Commit and push\n\n```bash\ngit add pane.json\ngit commit -m \"Add pane workspace config\"\ngit push\n```\n\n### 3. That's it\n\nWhen your teammates pull, Pane will automatically:\n- Run `pnpm install` when they create a new session\n- Run `pnpm dev` when they click Play\n- Show \"From pane.json\" in Project Settings so they know where the scripts come from\n\n### Already using another tool?\n\nIf your repo already has a `conductor.json`, `.gitpod.yml`, or `.devcontainer/devcontainer.json`, Pane will detect and use it automatically. No changes needed.\n\n## Visibility\n\n### Project Settings\n\nWhen Pane detects a config file, Project Settings shows:\n- An info banner identifying which file was detected\n- \"From pane.json\" badges beneath empty script fields, previewing the command that will run\n- Updated tooltips explaining the fallback behavior\n\n### Play Button\n\nThe Play button tooltip shows exactly what will run and where it came from:\n- **With config:** \"Run: pnpm dev\" with \"from pane.json\" subtitle\n- **Without config:** \"Set up run script (via Claude)\" — launches AI-assisted setup\n\n## Examples\n\n### Node.js / TypeScript project\n\n```json\n{\n \"scripts\": {\n \"setup\": \"pnpm install\",\n \"run\": \"pnpm dev\",\n \"archive\": \"rm -rf node_modules/.cache\"\n }\n}\n```\n\n### Python / Django project\n\n```json\n{\n \"scripts\": {\n \"setup\": \"pip install -r requirements.txt && python manage.py migrate\",\n \"run\": \"python manage.py runserver 0.0.0.0:8000\",\n \"archive\": \"python manage.py flush --noinput\"\n }\n}\n```\n\n### Monorepo with multiple services\n\n```json\n{\n \"scripts\": {\n \"setup\": \"pnpm install --frozen-lockfile\",\n \"run\": \"concurrently \\\"pnpm --filter api dev\\\" \\\"pnpm --filter web dev\\\"\",\n \"archive\": \"docker compose -f docker-compose.test.yml down -v\"\n },\n \"runScriptMode\": \"nonconcurrent\"\n}\n```\n\n### Migrating from Conductor\n\nIf you already have a `conductor.json`, it works as-is — the schema is identical. You can optionally rename it to `pane.json`. If both files exist, `pane.json` wins.\n"},{"path":"docs/IMPORTING_FROM_ANOTHER_TOOL.md","title":"Importing from another tool","text":"# Importing from another tool\n\nPane can adopt a git worktree created by Conductor, another orchestrator, or by hand. An adopted Pane points at the existing directory while ownership stays with the tool that created it.\n\n## Adopt a worktree\n\nFirst save the base repository in Pane. Then adopt one of that repository's registered worktrees:\n\n```bash\nrunpane panes adopt \\\n --repo active \\\n --path /absolute/path/to/existing-worktree \\\n --name existing-work \\\n --agent codex \\\n --resume 00000000-0000-0000-0000-000000000000 \\\n --yes \\\n --json\n```\n\nUse `--dry-run` first to validate the path without creating a Pane. `--folder <name>` places the Pane in a top-level sidebar folder, and `--no-pinned` opts out of the default pin. The command resolves symlinks before storing the path and refuses directories that are not registered worktrees of the selected repository or are already registered in Pane.\n\nWhen `--resume` is present, Pane persists the agent session id and stages that agent's resume command in the terminal without pressing Enter. Review the command and press Enter when ready. Pass `--launch` to run it immediately. Do not drive the same transcript from two applications at the same time because both may append to the same transcript.\n\nThe JSON form supports the same fields:\n\n```json\n{\n \"repo\": \"active\",\n \"panes\": [\n {\n \"path\": \"/absolute/path/to/existing-worktree\",\n \"name\": \"existing-work\",\n \"folder\": \"in-review\",\n \"pinned\": true,\n \"tool\": { \"agent\": \"codex\" },\n \"resume\": \"00000000-0000-0000-0000-000000000000\",\n \"launch\": false\n }\n ]\n}\n```\n\nRun it with `runpane panes adopt --from-json panes.json --yes --json`.\n\n## Ownership and lifecycle\n\nPane stores `worktree_ownership: \"external\"` for adopted Panes. The sidebar labels them **External**, and `runpane panes list --json` returns `ownership: \"external\"` (ordinary Pane worktrees return `\"pane\"`). This marker is separate from main-repository mode and from the worktree name.\n\nPane never creates, file-syncs, builds, or recreates an external directory, and removes one only when asked. Archiving or permanently deleting an adopted Pane only removes Pane's record, unless you archive it with `runpane panes archive --pane <id> --remove-worktree`: that runs the same safety check as a Pane-managed worktree (uncommitted, untracked, and unpushed work block the archive without `--force`; untracked files under the root `.codex/`, `.claude/`, and `.agent-farm/` directories are ignored) and then removes the worktree. The local branch is kept. Deleting its saved project also leaves the directory and git worktree registration untouched. Restoring an adopted Pane whose directory has disappeared succeeds and displays `External worktree directory is missing`; Pane does not recreate it. Automatic checkpoint commits default to disabled for adopted Panes.\n\nOtherwise the tool that owns the worktree remains responsible for its archive and deletion lifecycle, and `runpane panes archive --dry-run --pane <id> --json` reports worktree cleanup as not applicable. Restoring an adopted Pane whose worktree was removed does not recreate it; `git worktree add <path> <branch>` does.\n\n## Conductor imports\n\nThe bulk `runpane import conductor` workflow is planned as a follow-up to the core adoption support. Until then, enumerate active Conductor workspaces and call `panes adopt` for each one. Pane already reads `conductor.json` for project scripts, but adoption deliberately does not run setup/build scripts.\n"},{"path":"docs/NATIVE_MOBILE.md","title":"Native Pane mobile companion","text":"# Native Pane mobile companion\n\n`mobile/` is a Capacitor companion that bundles `frontend/remote.html`; it never navigates a WebView to a remote host. Remote hosts are ordinary authenticated HTTP/SSE endpoints, so an arbitrary host never receives a privileged native bridge.\n\n## Development\n\nUse Node 22.18 or newer and pnpm:\n\n```bash\npnpm mobile:sync\npnpm mobile:build:ios\npnpm mobile:build:android\n```\n\nThe first command builds the Remote Pane entrypoint and copies it to `mobile/www/` before syncing checked-in iOS and Android projects. iOS Simulator builds need Xcode. Android builds need `ANDROID_HOME` plus a JDK (Android Studio's bundled JBR is suitable).\n\nFor a first local Android build on macOS:\n\n```bash\nexport JAVA_HOME=\"/Applications/Android Studio.app/Contents/jbr/Contents/Home\"\nexport ANDROID_HOME=\"$HOME/Library/Android/sdk\"\n# Install the complete API 36 platform (including android.jar), build tools,\n# and accept its licenses in Android Studio's SDK Manager before this command.\n# Equivalent command-line setup when cmdline-tools is installed:\n# \"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager\" \"platforms;android-36\" \"build-tools;36.0.0\"\npnpm mobile:build:android\n```\n\n`mobile:build:ios` is a non-signing Simulator build. To run it, select a booted simulator in Xcode or use `xcrun simctl boot <device-udid>` first. It is not a substitute for a signed-device push test.\n\nProfiles and bearer tokens are in Keychain on iOS and EncryptedSharedPreferences on Android. Native profile removal makes a best-effort authenticated per-installation revoke before deleting the local bearer token; if the host is unreachable, rotate that host's pairing credential to revoke it server-side. Browser PWA storage remains browser `localStorage`.\n\nVoice dictation uses the transcription provider configured on the host. In the Expo app (`native/`), tapping the mic on a host without voice keys opens a sheet that asks for the one key the mode needs (live: Deepgram; recorded: fal), with OpenRouter as an optional key that cleans up the transcript. It saves them to the host through `remote:settings:update`, then starts recording. Saved keys show only as \"Set\", and Settings › Voice replaces them; see `native/README.md` (The composer). iOS declares its microphone purpose in `Info.plist`; Android declares `RECORD_AUDIO` and `MODIFY_AUDIO_SETTINGS`, which Capacitor requests when recording starts. Test both allowing and denying microphone access on a device before distributing a mobile build. The launcher icons reuse the existing opaque 1024px Pane artwork in `main/assets/icon.png`.\n\n## Push delivery operator setup\n\nPush is host-originated: killed-app delivery is an APNs alert (iOS) or FCM notification payload (Android), not a local notification or background JavaScript claim. The host sends the Pane or Session name and routing metadata; see the payload details below.\n\nFor iOS, register `com.dcouple.pane.mobile` (or your signed bundle identifier), enable Push Notifications, and add the capability in Xcode. Then give one host the APNs key in Settings → Integrations → iPhone notifications: choose the `.p8` file and enter the team ID. Pane shares it with your other hosts through your paired devices ([Shared integration keys](SHARED_CREDENTIALS.md)). A host can instead read it from its service environment, which wins over the shared key and is never shared:\n\n```bash\nPANE_APNS_TEAM_ID=... PANE_APNS_KEY_ID=... PANE_APNS_KEY_PATH=/secure/path/AuthKey.p8 PANE_APNS_TOPIC=com.dcouple.pane.mobile PANE_APNS_ENVIRONMENT=sandbox\n```\n\nUse `PANE_APNS_ENVIRONMENT=production` only for a production/TestFlight-signed build. The checked-in Debug entitlement uses the APNs sandbox and the Release entitlement uses production. Never place the `.p8` key in this repository, the app bundle, or a mobile build setting.\n\n### Dcouple's credentials\n\nThese are the provider credentials Dcouple uses for `com.dcouple.pane.mobile`. Only paths and IDs are listed here; the files stay outside git.\n\n- **APNs:** team `FBM5YSF467`, key ID `DGM25BV23X` (team scoped, sandbox and production). The key file is `AuthKey_DGM25BV23X.p8`, kept at `~/.config/pane/apns/` (mode 600) on the machine that created it. Load it once in Settings → Integrations on one host, or copy it to a host outside the repo and set:\n\n ```bash\n PANE_APNS_TEAM_ID=FBM5YSF467\n PANE_APNS_KEY_ID=DGM25BV23X\n PANE_APNS_KEY_PATH=/secure/path/AuthKey_DGM25BV23X.p8\n PANE_APNS_TOPIC=com.dcouple.pane.mobile\n PANE_APNS_ENVIRONMENT=production # TestFlight and App Store builds; use sandbox for Expo dev-client builds\n ```\n\n A host launched as a service reads these from its service environment (systemd unit, launchd plist, or the shell that runs `pnpm daemon:headless`). `mobile:push-status` reports `APNs delivery is configured.` once they're valid.\nFor Android, create a Firebase Android app with the same application ID and download its real `google-services.json` (to `native/google-services.json` for the Expo app, or `mobile/android/app/google-services.json` for Capacitor; the checked-in `.example` is only a shape guide). The host then needs a way to call FCM as a sender service account. It supports two ways to do that:\n\n**Keyless (recommended).** The host impersonates a sender service account with the operator's own gcloud login, so no key file exists anywhere. This is the only option where the organization policy `iam.disableServiceAccountKeyCreation` is on. For the Pane project (`pane-pwa-preview`) the sender account already exists and has the Firebase Cloud Messaging API Admin role. Each operator needs two things:\n\n```bash\nSENDER=pane-push-sender@pane-pwa-preview.iam.gserviceaccount.com\n# Once, by a project owner: let this operator mint tokens for the sender, and nothing else.\ngcloud iam service-accounts add-iam-policy-binding \"$SENDER\" --project pane-pwa-preview \\\n --member=user:you@example.com --role=roles/iam.serviceAccountTokenCreator\n# On the host machine, as that operator: writes ~/.config/gcloud/application_default_credentials.json\ngcloud auth application-default login\n```\n\nThen set these in the host service environment:\n\n```bash\nPANE_FCM_IMPERSONATE_SERVICE_ACCOUNT=pane-push-sender@pane-pwa-preview.iam.gserviceaccount.com PANE_FCM_PROJECT_ID=pane-pwa-preview\n```\n\nFor each send, the host exchanges the refresh token from that login for a user access token, then calls the IAM Credentials API `generateAccessToken` for the sender with the `firebase.messaging` scope. It sends the message with that one-hour token. The login file is read from `GOOGLE_APPLICATION_CREDENTIALS` if that is set, otherwise from gcloud's default location. It must be an `authorized_user` login. Revoking the Token Creator binding, or running `gcloud auth application-default revoke`, stops delivery. Pane reports FCM as not configured when the login file is missing or unreadable.\n\nFor your own Firebase project, create a sender service account with `roles/firebasecloudmessaging.admin` on that project. Then grant Token Creator on that service account only, never at the project level.\n\n**Service-account key.** Where key creation is allowed, `PANE_FCM_SERVICE_ACCOUNT_PATH=/secure/path/service-account.json` still works and takes precedence over impersonation.\n\nDo not commit any provider credential. CI gets the Firebase app config from the `PANE_ANDROID_GOOGLE_SERVICES_JSON` secret (see `native/README.md`); that file identifies the app and is not a sending credential.\n\nThe app asks notification permission after a successful paired connection. Notification setup runs independently of the terminal connection, so permission or provider errors leave remote terminal use available. Registration renewals preserve the installation's alert preferences and deduplication history. OS token registration and each provider network request time out after 15 seconds.\n\nPush-state writes merge with the latest host configuration inside the serialized config-write queue, so they cannot restore a concurrently revoked pairing. Hosts without registered mobile clients do not persist push state on every agent transition.\n\nOnly agent panels send alerts, never plain shells. The name is the Session's for a Session's orchestrator, \"<Session> › <Pane>\" for a Pane associated with a Session, and the Pane's otherwise. A newly blocked agent sends \"<name> is blocked\" at once. A finished turn sends \"<name> needs your attention\" only for a turn a person started and only once: input ending in Enter through `terminal:input` (desktop, phone, remote web) or `runpane panels submit|input --source user` arms the panel, and the next finished turn disarms it. Turns an agent starts by itself (watchers, timers, `runpane` input without `--source user`) finish silently. A turn counts as finished when the agent showed its own working signal (title spinner or working chrome) and then stayed idle for 10 s; terminal output alone, such as typing or a redraw, never counts. Each Pane has one APNs collapse id and one FCM `tag`, so a newer alert replaces the older one. iOS stacks alerts by `thread-id`, the Session's workspace pane id (`sessionPaneId` in the payload) or the Pane's own id. Opening a Pane in the app removes its delivered alerts, and opening a Session also removes its workers'. Every send logs `[Pane mobile push]` with the platform, the kind and, on failure, the provider's status and reason. Tap metadata contains the client-local profile ID (including its host label, URL, and token suffix) and pane/panel IDs; no terminal output or full bearer token is sent. A tap is held in encrypted native storage until saved profiles load, then reconnects only to the matching saved profile, including when switching panels inside the selected pane. An unknown/deleted profile or pane produces an error without connecting to an unrecognized host. Push commands derive client identity from the authenticated request, ignoring caller-supplied identity arguments.\n\nThe Expo app in `native/` uses the same host commands and payload. It registers the raw APNs/FCM device token, not an Expo push token, and routes taps and `pane-remote://` / `pane://pane/…` links as described in `native/README.md` (Notifications and links).\n\nProvider credentials, Apple signing, Firebase setup, and physical-device delivery cannot be validated by this repository alone. In Apple Developer, create an App ID matching the final bundle ID, enable Push Notifications, create an APNs auth key, and configure an App Store Connect record/signing team before TestFlight upload. In Google Play/Firebase, create the Android app and give the host a sender identity as described above. iOS Simulator builds do not prove APNs delivery; A Google Play system image in the Android emulator does receive real FCM messages, so the Android path can be checked end to end without a device. Missing host delivery configuration produces an actionable registration error while ordinary remote terminal use continues.\n"},{"path":"docs/PANE_MCP.md","title":"Pane MCP Server","text":"# Pane MCP Server\n\n`runpane mcp` is an MCP server that gives coding agents Pane's `runpane` commands as tools. With it, an agent in any repository can list saved repositories, create Panes, open panels, read terminal screens, and send input. Pane installs a `pane` skill in the user's home skill folders so agents can find the CLI and how to connect.\n\n## Automatic registration\n\nThe Pane desktop app registers the server for you. On launch, Pane adds a `pane` MCP server to your user-level config:\n\n- **Claude Code**: via `claude mcp add pane --scope user …`, which writes `~/.claude.json` (or `$CLAUDE_CONFIG_DIR/.claude.json`).\n- **Codex**: a `[mcp_servers.pane]` table in `~/.codex/config.toml` (or `$CODEX_HOME/config.toml`).\n- **Cursor**: a `mcpServers.pane` entry in `~/.cursor/mcp.json`. Pane detects Cursor from `~/.cursor`, `cursor`, `agent`, or `cursor-agent` on PATH.\n\nPackaged Pane also installs the on-demand `pane-manage-and-message-agents` user skill for detected Claude Code, Codex, and Cursor clients. Cursor uses `~/.cursor/skills/`; Claude Code and Codex use their user skill folders (including `$CLAUDE_CONFIG_DIR` and `$CODEX_HOME`). Pane leaves hand-added skills alone and removes only its own copy when registration is off. The same applies inside detected WSL distros. [Cursor documents `~/.cursor/skills/` as a user-level Agent Skills directory](https://cursor.com/docs/skills).\n\nPane registers only when it detects the corresponding client, and it manages only the entry it wrote:\n\n- It leaves other MCP servers and settings alone and never writes a second `pane` entry.\n- It rewrites its own entry when the app moves or updates.\n- A `pane` entry you added yourself (for example with `npx runpane mcp`) is left untouched, including when you turn the setting off. For Claude Code, Pane's entry is the one that runs `<Pane data dir>/mcp/runpane/dist/cli.js`. For Codex, it's the `[mcp_servers.pane]` table carrying the `# Managed by Pane` comment. For Cursor, it is the entry whose first argument is Pane's bundled runpane copy.\n- Pane edits `config.toml` as text, so your comments and formatting survive. It then parses the result and writes nothing unless the only change is Pane's own entry. Cursor JSON is parsed before editing; invalid JSON is left untouched. Writes are atomic and follow a symlinked config.\n\nOn Windows, Pane also registers inside each WSL distro that has a saved WSL repository. Agents there run the Windows Pane binary through WSL interop.\n\nThe registered command is the Pane executable in Node mode (`ELECTRON_RUN_AS_NODE=1`) running a copy of the runpane CLI at `<Pane data dir>/mcp/runpane/dist/cli.js`. No Node.js or npm install is needed. Only packaged builds register; a development build never touches your agent config.\n\nTo turn this off, open **Settings → AI & Agents** and switch off **Register Pane tools with Claude Code, Codex, and Cursor**. Pane then removes its entry from each config.\n\nRestart a running `claude` or `codex` session to pick up a new registration. In Cursor, start a new chat or use **Developer: Reload Window**. Check with `claude mcp list`, `codex mcp list`, or `agent mcp list`. Cursor CLI may require `agent mcp enable pane` to add the server to its local approved list; Pane leaves that trust decision to you.\n\n## Toolsets\n\nA server that offers dozens of tools makes models, especially smaller ones, worse at choosing the right one. So by default the server serves a lean `core` toolset, and everything else is opt-in:\n\n| Toolset | Tools |\n|---|---|\n| `core` (default) | `agents_start`, `agents_status`, `agents_send`, `panels_create` (a new agent tab in an existing Pane), `panels_input` (exact keys, for menus and prompts), `repos_list`, `repos_add`, `panes_list`, `workspace_state`, `panes_git_status`, `panes_archive`, `panes_restore`, `links_create`, `docs_search`, `docs_read`, `doctor` |\n| `agents` | the three agent tasks, `workspace_state`, `watch` |\n| `panes` | create, adopt, list, archive, restore, pin, unpin, rename, focus, cost, run and stop the run script, move to a folder, `folders_list`, `folders_create` |\n| `panels` | create, list, output, screen, input, submit, submit-composer, wait |\n| `git` | status, commit, push, pull, fetch, rebase onto main, squash-rebase onto main, stash, stash-pop, soft reset (`panes_*`) |\n| `sessions` | the eight `sessions_*` tools and the three named-lock tools (`lock_acquire`, `lock_release`, `lock_list`) |\n| `repos`, `docs`, `links`, `admin` | repository, documentation, deep-link, and diagnostic tools |\n| `all` / `read` | every tool / every read-only tool |\n\nChoose with `runpane mcp --toolsets core,git` (comma-separated). `--read-only` keeps only read-only tools from whatever is selected. The Pane app registers `core` by default; **Settings → AI & Agents → Pane tools to register** switches it to `all`. Each command's toolsets live in the contract's `toolsets` field.\n\n## The three agent jobs\n\nThe core set is built around the three jobs agents most often need, each finished in one call:\n\n- `agents_start`: starts the agent with the task, waits until it is ready, and returns the pane and panel ids and a `pane://` link. Pass `pane` to open it as a new tab in an existing Pane (review, audit, fix, QA); pass `repo` and `name` only for new work, which creates a Pane. 1 feature = 1 worktree = 1 branch = 1 Pane; reviews, audits, fixes and QA for it are tabs in that Pane.\n- `agents_status`: whether the agent is working, ready, blocked (waiting on a person), idle, or exited, plus its current screen.\n- `agents_send`: submits a follow-up and reports whether the agent took it or queued it behind its current turn (`delivery`), read from the agent's transcript where Pane can find it.\n\n## Docs and links\n\n`docs_search` searches Pane's docs, `runpane` help, per-command context, and the Pane Chat skills installed in the Pane data directory. It returns short excerpts with paths. `docs_read` returns one path in full. Both work offline, and the same docs are listed as `runpane-docs:` MCP resources.\n\n`links_create` builds `pane://open?pane=<id>[&panel=<id>]`, `pane://open?repo=<id>`, or `pane://open?session=<id>`. Pane registers the `pane://` scheme on macOS, Windows, and Linux. Opening a link (from a browser, a terminal, or `links_open`) raises Pane and selects what it names. It never changes Pane state, and a link with any unexpected part is rejected.\n\n## Registering by hand\n\nWithout the desktop app, or for another MCP client, run the server from npm: `npx --yes runpane@latest mcp`. Add `--toolsets <names>` to choose tools.\n\n```bash\nclaude mcp add --scope user pane -- npx --yes runpane@latest mcp\ncodex mcp add pane -- npx --yes runpane@latest mcp\n```\n\nCursor (`~/.cursor/mcp.json`):\n\n```json\n{ \"mcpServers\": { \"pane\": { \"type\": \"stdio\", \"command\": \"npx\", \"args\": [\"--yes\", \"runpane@latest\", \"mcp\"] } } }\n```\n\nVS Code (`.vscode/mcp.json`, or run **MCP: Add Server**):\n\n```json\n{ \"servers\": { \"pane\": { \"type\": \"stdio\", \"command\": \"npx\", \"args\": [\"--yes\", \"runpane@latest\", \"mcp\"] } } }\n```\n\nAny other client that can launch a stdio server uses the same command and arguments.\n\n`agents_send` uses the target agent's composer state. For Codex, it queues with Tab while the agent is working and submits with Enter when ready. A send is confirmed only after the text leaves the composer; if it remains, the result includes a command to retry the appropriate key. `panels_submit_composer` with `strategy: auto` uses the same keys.\n\nThe server is stdio only: it runs next to the Pane app on the same machine, so there is no HTTP transport and no OAuth. To drive a remote Pane, run the server on the remote host.\n\nThe Python package (`pipx run runpane`) does not include the MCP server, the docs search, or the agent tasks, because they need Node. From Python, those commands print the npm command and exit with status 2.\n\n## Manual test steps\n\nAutomated tests cover the registration and link logic on every OS. Check these by hand on a packaged build of each platform.\n\n**macOS, Windows, and Linux**\n\n1. Install and launch Pane, with Claude Code, Codex, and Cursor installed.\n2. Run `claude mcp list`, `codex mcp list`, and `agent mcp list`. Each lists `pane`. If Cursor asks for approval, run `agent mcp enable pane`; it should report ready with 15 core tools.\n3. In a fresh `claude` session, ask \"Use Pane to list my repos\". It calls `repos_list`.\n4. Quit and relaunch Pane. The Claude, Codex, and Cursor configs are unchanged.\n5. Turn off **Settings → AI & Agents → Register Pane tools with Claude Code, Codex, and Cursor**. `pane` is gone from all three configs, and the other entries are unchanged.\n\n**pane:// links**\n\n1. Get a link: `runpane links create --pane <pane-id>`.\n2. With Pane running, open it from a browser or a terminal:\n - macOS: `open \"pane://open?pane=<pane-id>\"`\n - Windows: `start \"\" \"pane://open?pane=<pane-id>\"`\n - Linux: `xdg-open \"pane://open?pane=<pane-id>\"`\n3. Pane comes to the front on that Pane, and no second Pane window opens. On Windows and Linux, the extra process hands the link to the running Pane and exits.\n4. Quit Pane and open the link again. Pane starts and opens that Pane.\n5. Open `pane://open?pane=<pane-id>&archive=1`. Pane logs `Unknown pane link parameter: archive` and changes nothing.\n6. On Linux, check the handler: `xdg-mime query default x-scheme-handler/pane` names Pane's desktop file (AppImage and .deb).\n\n**Windows with WSL**\n\n1. Save a repository that lives in a WSL distro, with Claude Code, Codex, and Cursor installed inside that distro.\n2. Relaunch Pane. Inside the distro, `claude mcp list`, `codex mcp list`, and `agent mcp list` show `pane`, whose command is `/mnt/c/.../Pane.exe`.\n3. From a `claude` session inside the distro, ask \"Use Pane to list my repos\". The call reaches the Windows Pane.\n4. Turn the setting off. `pane` is gone from the distro's configs too.\n\n## Example prompts\n\n- \"Use Pane to start Claude on fixing the login redirect in the web repo, and give me a link to watch it.\"\n- \"How is the agent in my fix-login Pane doing? Does it need anything from me?\"\n- \"Tell the fix-login agent to also add a test for the redirect.\"\n- \"Which of my Panes have uncommitted or unpushed work?\"\n- \"How do I archive a Pane and get it back later? Check Pane's docs.\"\n\n## Not supported\n\n- Streaming. `watch --follow` and `watch --until-event` aren't offered; use `agents_status`, or `watch` with `timeoutMs`.\n- App settings. Agents can't read or change Pane's settings.\n- Permanently deleting Panes or removing repositories. Those stay in the app, where the user sees what goes.\n- Progress notifications and pagination (the full list is small).\n- Code or scripts run inside the MCP server itself. Agents run code in Pane terminals through `agents_start` and `agents_send`.\n\n## Protocol\n\nThe server uses the MCP TypeScript SDK v2 over stdio. It speaks MCP revision 2026-07-28, which has no handshake: `server/discover`, with the version and client capabilities carried on each request. It also accepts the 2025 `initialize` handshake that current Claude Code and Codex releases send, negotiating down to 2025-11-25.\n\nOnly JSON-RPC goes to stdout, and the server exits when stdin closes. Cancelling a call (`notifications/cancelled`) stops the `runpane` process running it.\n\n## Tools\n\nTools are generated from [`contracts/runpane/contract.json`](../contracts/runpane/contract.json) when the server starts:\n\n- Every command that has result `jsonSchemas` becomes a tool. Spaces and hyphens in the name become underscores: `repos list` → `repos_list`, `panels submit-composer` → `panels_submit_composer`.\n- The tool's inputs are the flags from the command's `usage` lines and its `agentContext` arguments, in camelCase (`--timeout-ms` → `timeoutMs`). Flags that take a value are strings; flags without one are booleans. Daemon commands also accept `paneDir`.\n- The description combines the command's summary, details, and notes from `agentContext`. The title is `runpane <command>`.\n- The output schema is the command's `*Result` JSON schema. Every call runs `runpane <command> … --json` and returns its JSON output unchanged, both as `structuredContent` and as a text block for older clients.\n- A non-zero exit returns a tool error (`isError: true`). A CLI failure reads as its message, a `Next:` line that names tools instead of CLI commands, and a `Code:` line; a command's own `ok: false` result comes back as its JSON. Bad arguments are tool errors too, so the model can correct them. An unknown tool name is a JSON-RPC Invalid Params error (`-32602`).\n- Annotations come from the contract. `readOnlyHint` is the inverse of `mutates`. A mutating tool is `destructiveHint: true` unless the command is marked `additive`. `idempotentHint` and `openWorldHint` follow the `idempotent` and `openWorld` fields.\n- Values are passed as `--flag=value`, so a value may start with `-` (for example `- [ ] item`).\n\nAdding or changing a command in the contract changes its tool. There is no per-tool code.\n\nA few flags are left out. `--json` is always passed. `--follow` and `--until-event` are omitted because a streaming or open-ended watch cannot return a single result; call `watch` with `timeoutMs` instead. Tools have no stdin, so pass exact terminal bytes in `text` (for example `\\u0003` for Ctrl-C) rather than `inputFile: \"-\"`.\n\n## Confirmation\n\nMutating tools keep the CLI's confirmation rule. Every command whose usage includes `--yes` gets a `yes` input. Without `yes: true`, the call fails with the CLI's own refusal and changes nothing. Tools are also annotated with `readOnlyHint`, so clients can auto-approve read-only tools and prompt for the rest.\n\n## Agent guidance\n\nPane installs a managed `pane` skill under `~/.claude/skills/pane` (or `$CLAUDE_CONFIG_DIR/skills/pane`) and `~/.agents/skills/pane`, including saved WSL distros on Windows. It leaves any existing skill without Pane's marker alone. **Install Pane skill for agents** in Settings → AI & Agents controls the installation; turning it off removes Pane's copy.\n\n**Publish Pane instructions to AGENTS.md** is off by default. If enabled, Pane writes a marked block into the active repository's `AGENTS.md` with CLI and MCP setup instructions. Turning it off removes only Pane's marked block from saved repositories. On upgrade, Pane turns off the old default once and removes those blocks; a later opt-in is remembered. The full command catalog lives in MCP tools and `runpane agent-context`.\n"},{"path":"docs/RUNPANE_BUNDLED_CLI.md","title":"Bundled runpane on terminal PATH","text":"# Bundled runpane on terminal PATH\n\nEvery terminal Pane launches (Session orchestrators, agent panels, plain\nterminals, and run scripts) can call `runpane` without a global install. The\ncommand always matches the running Pane build.\n\n## How it works\n\n- `pnpm build:main` bundles `packages/runpane/src/cli.ts` into\n `main/dist/runpane/runpane.cjs` (`main/build-runpane-cli.js`). It ships\n inside the app with the rest of `main/dist`, and `pnpm dev` builds it\n through `pnpm build:main`. The bundle embeds its version and docs search\n index, so both still work after the single file is copied out of the app.\n- At startup (`createPaneDaemonHost`, which both the desktop app and the\n headless daemon use), Pane copies that file to `<PANE_DIR>/bin/runpane.cjs`\n and writes a `<PANE_DIR>/bin/runpane` shim (plus `runpane.cmd` on Windows).\n The shim runs the CLI with Pane's own Electron binary as Node\n (`ELECTRON_RUN_AS_NODE=1`) and defaults `PANE_DIR` to that Pane data\n directory. Files are rewritten only when their content changes. Pane never\n follows a symbolic link there or overwrites a file it did not write.\n- Terminal launches put `<PANE_DIR>/bin` first on `PATH` and export\n `PANE_RUNPANE_BIN` with the shim's absolute path. User startup files can\n rebuild `PATH`, so supported shells re-apply it afterwards:\n - **zsh:** `ZDOTDIR` points at `<PANE_DIR>/shell/zsh`. Each wrapper file\n sources the user's matching file from their own `ZDOTDIR` (or `$HOME`),\n then the last one moves `<PANE_DIR>/bin` to the front and restores the\n user's `ZDOTDIR`. A `precmd` hook keeps it first when tools such as direnv\n or nvm edit `PATH` before a prompt. History stays in the user's\n `.zsh_history`.\n - **bash:** `--rcfile <PANE_DIR>/shell/bash/rcfile` sources\n `/etc/bash.bashrc` and `~/.bashrc` (what an interactive non-login bash\n normally reads), then moves the directory to the front and adds a\n `PROMPT_COMMAND` hook.\n - **fish:** `--init-command` runs after the user's config and adds a\n `fish_prompt` handler.\n - **Other shells, Windows, and run scripts:** `PATH` is prepended only.\n - **WSL terminals** are unchanged. The Windows Electron binary cannot run\n inside the distro. Install `runpane` in the distro, or use\n `npx --yes runpane@latest`.\n\n## Existing global installs\n\nA globally installed `runpane` (`npm i -g runpane`) still works in other\nterminals. Inside Pane terminals, Pane's shim resolves first because it\nmatches the running build's local-control contract. Use `$PANE_RUNPANE_BIN`\nto call it by absolute path.\n\nSeveral Pane builds that share one `PANE_DIR` (for example a dev build and\nthe installed app, both on `~/.pane`) rewrite the same shim at startup; the\nmost recently started build wins. Use `PANE_DIR=~/.pane_test pnpm dev` to keep\nthem apart.\n"},{"path":"docs/RUNPANE_CLI_CONTRACT.md","title":"Runpane CLI Contract","text":"# Runpane CLI Contract\n\n<!-- Generated by scripts/generate-runpane-contract.js. Do not edit generated sections by hand. -->\n\n`runpane` is a thin installer and configurator for Pane. The npm and PyPI\npackages expose the same command contract and download the real Pane release\nartifact at command runtime.\n\nThe packages must not download, install, or configure Pane during package installation.\nWork starts only when a user runs `runpane ...`.\n\n## Maintainer Rules\n\n- Treat `contracts/runpane/contract.json` as the source of truth for both wrapper packages and generated docs.\n- Every command, flag, platform default, artifact-selection rule, and attribution rule change must be reflected in the contract.\n- The npm and PyPI wrappers must expose the same command behavior unless the contract explicitly documents a package-manager-specific difference.\n- Root `README.md` and package READMEs should lead with one guided quick-start command. Explicit commands, package-manager variants, and flags belong in an Advanced section.\n- Release version bumps must keep root `package.json`, `packages/runpane`, and `packages/runpane-py` versions in sync. Run `pnpm run check:runpane-package-versions` before release.\n- `pnpm run test:runpane-contract` must pass before changing wrapper command parsing, help output, platform defaults, release asset selection, or generated contract artifacts.\n- Token-based npm or PyPI publishing is a temporary fallback. Prefer trusted publishing once the package names are reserved and trusted publishers are configured.\n\n## Compatibility Floors\n\nThe npm wrapper should run on Node.js `>=20.0.0` and newer. The root Electron app\nmay require a newer Node.js version for development and packaging.\n\nThe PyPI wrapper should run on Python `>=3.8` and newer. Keep runtime dependencies\nout of the wrapper unless a compatibility test covers the new dependency.\n\n## Public Terminology\n\n- `repo`: Saved Pane repository/project record\n- `pane`: User-visible Pane session\n- `tool`: Terminal-backed tab\n- `agent`: Built-in agent command template\n\n## Package Manager Entrypoints\n\nRecommended guided quick starts:\n\n```bash\nnpx --yes runpane@latest\nnpm i -g runpane && runpane setup\npipx run runpane\npython -m pip install runpane && python -m runpane setup\n```\n\nCanonical npm and Node commands:\n\n```bash\nnpx --yes runpane@latest\nnpx --yes runpane@latest setup\nnpx --yes runpane@latest install client\nnpx --yes runpane@latest install daemon --label \"My Server\"\npnpm dlx runpane@latest\npnpm dlx runpane@latest install daemon --label \"My Server\"\nnpm i -g runpane && runpane\nnpm i -g runpane && runpane setup\nnpm i -g runpane && runpane install daemon --label \"My Server\"\npnpm add -g runpane && runpane\npnpm add -g runpane && runpane setup\npnpm add -g runpane && runpane install daemon --label \"My Server\"\nyarn dlx runpane@latest install daemon --label \"My Server\"\nbunx runpane@latest install daemon --label \"My Server\"\n```\n\nCanonical Python commands:\n\n```bash\npipx run runpane\npipx run runpane setup\npython -m pip install runpane\nrunpane\nrunpane setup\npython -m runpane setup\nrunpane install daemon --label \"My Server\"\npipx install runpane\nrunpane\nrunpane setup\npipx run runpane install daemon --label \"My Server\"\nuvx runpane@latest\nuvx runpane@latest setup\nuvx runpane@latest install daemon --label \"My Server\"\npython -m runpane install daemon --label \"My Server\"\n```\n\nUse `pnpm dlx` for one-shot pnpm execution and `pnpm add -g` for persistent CLI installation.\nDo not document `pnpm install runpane` as the public CLI install path.\n\n## Commands\n\n```bash\nrunpane\nrunpane setup\nrunpane install\nrunpane install client\nrunpane install daemon\nrunpane update\nrunpane version\nrunpane doctor\nrunpane doctor --json\nrunpane daemon repair --pane-dir ~/.pane_remote --yes --json\nrunpane agent-context\nrunpane agent-context --command \"panes create\" --json\nrunpane mcp\nrunpane repos list --json\nrunpane repos add --path /path/to/repo --name Pane --yes --json\nrunpane panes list --repo active --json\nrunpane panes cost --pane <pane-id> --json\nrunpane panes create --repo active --name issue-252 --agent <agent> --prompt \"Kick off the discussion skill for issue 252\" --source agent --no-focus --wait-ready --yes --json\nrunpane panes create --from-json panes.json --yes --json\nrunpane panes archive --pane <pane-id> --source agent --yes --json\nrunpane panes rename --pane <pane-id> --name issue-393 --yes --json\nrunpane panels list --pane <pane-id> --json\nrunpane panels output --panel <panel-id> --limit 200 --json\nprintf 'Continue\\n' | runpane panels input --panel <panel-id> --input-file - --yes --json\nrunpane sessions list [--json] [--pane-dir <path>]\nrunpane sessions create --from-json <path|-> [--json] [--pane-dir <path>]\nrunpane sessions get --session <id|name> [--json] [--pane-dir <path>]\nrunpane sessions update --session <id|name> --from-json <path|-> [--json] [--pane-dir <path>]\nrunpane sessions pin --session <id|name> [--json] [--pane-dir <path>]\nrunpane sessions unpin --session <id|name> [--json] [--pane-dir <path>]\nrunpane sessions set-agent --session <id|name> --agent <codex|claude|cursor> [--json] [--pane-dir <path>]\nrunpane sessions associate --session <id|name> --pane <pane-id> [--json] [--pane-dir <path>]\nrunpane sessions detach --session <id|name> [--pane <pane-id>] [--json] [--pane-dir <path>]\nrunpane sessions overview --session <id|name> [--json] [--pane-dir <path>]\nrunpane lock acquire --name testing-account --ttl 30m --wait 1800000 --note \"call QA\" --json\nrunpane lock release --name testing-account --json\nrunpane lock list --json\nrunpane agents start --repo active --name fix-login --agent claude --prompt \"Fix the login redirect\" --yes --json\nrunpane agents start --pane <pane-id> --agent codex --prompt \"Review the PR from this branch and report findings\" --yes --json\nrunpane agents status --pane <pane-id> --json\nrunpane agents send --pane <pane-id> --text \"Also add a test\" --yes --json\nrunpane report --state ready --pr 747 --head fc5dce9 --summary-file /tmp/report.md --json\nrunpane panels last-message --panel <panel-id> --json\nrunpane panels resume --panel <panel-id> --wait-ready --yes --json\nrunpane panels resume --session <id|name> --wait-ready --yes --json\nrunpane docs search --query \"archive a pane\" --json\nrunpane links create --pane <pane-id> --json\nrunpane panes git-status --pane <pane-id> --json\nrunpane help\nrunpane <command> --help\nrunpane watch --until-event\nrunpane watch --follow\nrunpane watch --session <id|name> --follow\nrunpane watch --session <id|name> --follow --ready-settle 1m --blocked-settle 15s --min-interval 2m\nrunpane pr watch [--pane <id>] --url <PR URL> [--head <full-sha>] [--json] [--pane-dir <path>]\nrunpane pr unwatch [--pane <id>] --url <PR URL> [--json] [--pane-dir <path>]\nrunpane pr list [--pane <id>] [--json] [--pane-dir <path>]\n```\n\n`runpane` with no arguments and `runpane setup` open an interactive wizard when stdin and stdout are TTYs. The remote-host wizard asks only for a name, then runs interactive Tailscale setup with automatic port selection; explicit install daemon flags remain available for SSH and manual URLs. In non-interactive shells or CI, both forms must print help, common commands, and agent discovery hints, then exit successfully instead of waiting for input.\n\n`runpane install` is an alias for `runpane install client`.\n\n`runpane install client` downloads the selected Pane desktop artifact and installs, opens, or launches it for the current platform.\n\n`runpane install daemon` downloads or installs Pane, resolves a stable Pane executable path, and spawns `<pane executable> --remote-setup <forwarded remote setup args>`.\n\nThe wrapper must stream Pane stdout/stderr without reformatting because `pane --remote-setup` prints the one-time `pane-remote://...` connection code.\n\n`runpane update` defaults to both app and active supported CLI, with independent supervision, staging, durable status, and explicit restoration outcomes. Prerequisites, status fields/exit codes, draft handling and manual recovery are documented in [Update recovery](UPDATE_RECOVERY.md).\n\n`runpane version` prints only wrapper package metadata and does not contact, launch, or focus the Pane app or daemon.\n\n`runpane doctor` checks platform support, release metadata reachability, download URL selection, installed Pane detection, daemon reachability, and remote-daemon hints. Add `--json` for a machine-readable report that agents should run before mutating Pane state. Installed app version detection must be best-effort and must not launch, focus, or configure Pane; macOS wrappers read app bundle metadata instead of executing `Pane --version`.\n\n`runpane daemon repair` rewrites and restarts only the managed remote-daemon launcher/service. It never creates pairing credentials, changes tunnels, or downloads Pane; doctor only recommends it and never runs it automatically.\n\n`runpane agent-context` prints a brief, token-efficient command schema for coding agents without connecting to the Pane daemon.\n\n`runpane agent-context --command \"panes create\"` prints the detailed definition for one command. Add `--json` for machine-readable output. An unknown command exits 2 and lists the closest command names; with `--json` it prints `{ ok: false, code: \"unknown_command\", message, candidates }` on stdout.\n\n`runpane mcp` runs a stdio MCP server whose tools are generated from this contract: every command with result `jsonSchemas` becomes a tool that runs `runpane <command> --json` and returns its output. Only the npm package and the Pane app include it; the Python wrapper prints how to run it with Node and exits non-zero.\n\n`runpane repos list` connects to the running local Pane daemon and prints saved repository records.\n\n`runpane repos add` registers an existing git repository with the running local Pane daemon. It does not create directories or initialize git repositories by default.\n\n`runpane panes list` lists Pane sessions, optionally scoped to one saved repository.\n\n`runpane panes cost` reports estimated token costs per Pane for the last 30 days, including per-model breakdowns and cache efficiency; unscoped output includes an Unattributed bucket that reconciles against workspace totals.\n\n`runpane panes create` connects to the running local Pane daemon, resolves the requested saved base repository, creates user-visible Pane sessions backed by Pane-managed worktrees/branches, opens terminal-backed tool tabs, and optionally sends initial input to the started tool. Built-in agent panes and `--source agent` default to background/no-focus unless `--focus` is passed. New Panes are pinned into the UI's favorite/pin set by default, except when the CLI runs inside a Session orchestrator (`PANE_ORCHESTRATION_SESSION_ID`), where child worktrees default to unpinned. Explicit `--pinned` / `--no-pinned` override creation defaults. First Session association clears an existing pin, including an explicit creation pin; use `panes pin` after association to pin a child. This avoids duplicating Session children in the global pinned list. Manual pins applied afterward are preserved. Panes created interactively in the Pane UI are unaffected. Inside a Session orchestrator (`PANE_ORCHESTRATION_SESSION_ID` set), new Panes are associated with that Session automatically; `--no-associate` opts out. A failed association is reported on the item and never undoes the Pane.\n\nFor `panes create --wait-ready` (and `panels create`, `panes adopt --launch`), ready means an identified agent at its own input box: declared with `--agent`, or detected from the command, its foreground process or its screen. A `--tool-command` that never shows a supported agent is not ready: `readiness.ok` is false with `readiness.reason.code` `no-agent-detected` after the timeout, or `command-exited` as soon as the command exits back to the shell, and nothing is typed into it. `agent-not-ready` means the agent was detected but its input box was not ready in time. CLI readiness is confirmed on consecutive polls. Codex also requires a recognized configured model or context footer below its live composer; its provisional startup composer is not ready. Hidden or unrecognized Codex status lines conservatively time out rather than prove readiness. Startup prompts return blocked with the prompt's own text in blocked.message and a suggestedCommand, and Pane never answers them: first-run trust and daemon dialogs and Agent Farm's `Trust this repository workspace? [y/N]` as blocked.kind=first-run-dialog; Codex's security setup banner and any other prompt, including a wrapper's or shell's `[y/N]` before the agent appears, as agent-prompt. A starting message blocked this way is held and sent once the prompt is answered and the agent's input box is ready. Every create with a starting message returns `initialInput`. Pane stages the text in the composer, presses the agent's submit key separately (Ctrl+Enter for Codex, declared or detected), and checks where it went: `initialInput.delivery` is `taken` or `queued` (from the agent's transcript or its screen), `in-composer`, or `unknown` (`argv` means a prompt was routed at launch; Pane gives the transcript up to 10 seconds before settling on unknown). `initialInput.verifiedSubmitted` is true exactly when it is `taken` or `queued`, and the create's `ok` is false unless a Claude or Codex starting message is verified. Cursor and OpenCode have no acceptance probe: their delivery stays `unknown` with a delivery.message saying so, `verification: unverifiable`, and `ok` follows readiness. Without `--wait-ready` the create returns at once with `initialInput.verifiedSubmitted: false`, a delivery.message saying the call did not wait, and a `panels wait` nextCommand; Pane still sends the message once the agent is identified and at its input box, and gives up without typing it if no supported agent appears within 30 seconds.\n\n`runpane panes archive` refreshes the configured upstream, reports exact unpushed commit evidence, and refuses unsafe archive operations unless `--force` is used. A branch whose upstream is gone counts as pushed when a merged GitHub pull request has HEAD as its head (`safetyCheck.mergedViaPr`). Add `--dry-run` to inspect the same evidence without archiving. `--remove-worktree` applies the same check and removal to an adopted worktree; local branches are always kept. Successful archives report `worktreeCleanup: completed` once the worktree is gone from its path and from git; `trashDeletion: pending` means its files are still being deleted in the background. `runpane panes archive --session <id|name> --merged` archives every Session Pane that is clean and pushed or merged, and reports a reason for each skipped Pane.\n\n`runpane panes rename` trims and updates a Pane's display name without changing its worktree, branch, panels, or focus, and returns the updated pane summary.\n\n`runpane panes focus` raises the Pane window and selects a Pane (and optionally one of its panels) exactly like clicking it in the UI. Because it steals the user's window focus, run it only on an explicit user request to open, focus, show, or switch to a Pane; never focus a Pane proactively, the same doctrine that keeps `panes create` background/no-focus for `--source agent`.\n\n`runpane panels open` opens a URL or a file from the Pane worktree as a tab in an existing Pane (default: the calling panel's Pane from `PANE_SESSION_ID`), in split view beside the agent unless `--tab` is passed. Files open in an editor tab with read-only previews for supported formats, and HTML opens as editable source; an existing tab showing the same target is reused. It brings the tab forward for clients already showing that Pane, and never raises or focuses the Pane window or switches anyone's Pane.\n\n`runpane panels list` lists tool panels inside one Pane session. Each panel with a saved agent session ID reports it as `resumeId`, with `resumeSource` (`agent-farm`, `configured`, or `agent`) naming the rule that reopens the conversation.\n\n`runpane panels output` reads bounded recent terminal output from one panel and strips common terminal control noise for agent use.\n\n`runpane panels input` sends exact input bytes to one terminal panel. Prefer `--input-file` for newlines, Ctrl-C, quotes, or shell-sensitive text.\n\n`runpane panes create --prompt` is an alias for `--initial-input`; request JSON and daemon payloads should use the canonical `initialInput` field.\n\n`runpane panes create --branch <name>` creates the Pane's worktree on exactly that new branch, slashes included (for example `agents/w5a`). The name is checked with `git check-ref-format --branch`, creation fails if the branch already exists, and Pane never renames it to make it unique. `--worktree-name` still names the directory and defaults to `--name`. `--base` is an alias for `--base-branch`, and `--prompt-file` for `--initial-input-file`. `panes create --base <ref> --branch <name> --prompt-file <file>` replaces `git worktree add` plus `panes adopt`. When the base is a remote-tracking branch (the default `origin/main`, or one named with `--base`), Pane fetches it first, waiting at most about 10 seconds, so the Pane starts at the remote's current tip. Each created item reports `base` (`ref`, `commit`, `refresh`); if the fetch fails the Pane still starts, from the local ref, and `base.notice` says so and how to catch up.\n\n`runpane panes adopt --launch` accepts `--prompt`, `--prompt-file`, `--wait-ready`, and `--ready-timeout-ms`, and reports `readiness`, `initialInput`, and `nextCommand` like `panes create`. A prompt without `--launch` is an error rather than being dropped.\n\nIf composer submission cannot be verified without risking a duplicate, the create item is unsuccessful with `initialInput.staged`, `initialInput.attempts`, `initialInput.blocked.kind: submission_unverified`, and an actionable `nextCommand`. The CLI-facing `--prompt` alias maps to this canonical `initialInput` result.\n\nWhen running from WSL while Pane is installed on Windows, the Linux wrapper may look for a missing `/tmp/pane-daemon.../daemon.sock` or resolve to a Windows shim such as Volta. In that case invoke the Windows wrapper through PowerShell from a Windows cwd, for example `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane repos list --json'`.\n\n`runpane watch` waits for agent, Pane, panel, and PR events from the daemon journal without polling. `--follow` keeps watching with defaults tuned for an agent watching unattended (quiet, batched, and it ends itself after 3 hours with nothing happening); individual duration flags override these defaults. Cadence requires a named follow consumer; `--follow --since <generation>` has no consumer name and bypasses settling and batching. `--until-event` waits for one event worth waking for, prints it immediately without follow settling or batching, and exits 0. The defaults are listed in the contract under defaults.watch.\n\nA watch releases its pending slot when its client socket or named pipe disconnects. Re-arming the same named cursor takes over the previous request; the superseded watcher exits with a WATCH ERROR instead of competing for the cursor. Anonymous concurrent watchers retain the eight-wait limit. Each request also has a 125-second lease (slightly longer than the maximum 120-second long poll), so an abandoned request is reclaimed even if no disconnect arrives.\n\n`sessions list` list durable named orchestration Sessions.\n\n`sessions create` create a durable named orchestration Session and its hidden terminal owner.\n\n`sessions get` read one durable named orchestration Session.\n\n`sessions update` update a Session overview from structured JSON.\n\n`sessions pin` and `sessions unpin` set the Session pin star through the existing UI update path. Create JSON accepts `isPinned` (default false); update JSON preserves the pin when the field is omitted.\n\n`sessions set-agent` switch the durable terminal agent for a named Session.\n\n`sessions associate` associate a user-visible Pane with a named Session.\n\n`sessions detach` detach a Pane from a named Session.\n\n`sessions overview` read a live status, activity, git, and pull request overview for a named Session. Each panel in `panes[].panels[]` carries its own latest `runpane report` as `report`, so one call shows what every worker last reported and when (`reportedAt`, next to the panel's `state` and `lastActivityAt`). When a report's summary is a complete JSON object, such as a status file sent with `--summary-file`, `report.data` holds it exactly as the worker gave it; Pane never reads, renames or validates its fields. A summary that is not a JSON object (plain text, an array, a scalar or invalid JSON), is nested more than 64 levels deep, or was truncated (`summaryTruncated`), has no `data`. A panel that never reported has no `report`. `panes[].report` is still the newest report among the Pane's panels.\n\n`runpane lock acquire|release|list` coordinate a resource shared between agents, such as one test account. The caller's Pane and panel own the lock; it is scoped to the owner's Session (or global outside one), renews for the same owner, and is released on TTL expiry, owner panel exit, or owner Pane archive. `--wait` blocks in the daemon until the lock comes free.\n\n`runpane agents start|status|send` finish the three common agent jobs in one call each: start an agent on a task (a new tab in the work's Pane with `--pane <id>`, or a new Pane for new work), check on it, and send it a follow-up.\n\n`runpane report --state ready|blocked|failed|done` is how a worker hands back its result. It stores the latest report on the worker's panel (state, `--pr`, `--head`, up to 16,000 characters of `--summary` or `--summary-file`, and the `--question` a blocked worker needs answered), journals an opt-in `agent.report` watch event (`REPORT <pane-name> pane <pane-id> panel <panel-id> ready pr#747 fc5dce9`; it skips the `--min-interval` batch), records it as Session activity, and shows it in `agents status`, `panels list`, and `sessions overview` (each panel's own report as `panes[].panels[].report`, with a JSON object summary as `data`, and the Pane's newest as `panes[].report`). Inside a Pane terminal the panel comes from `PANE_SESSION_ID` and `PANE_PANEL_ID`; elsewhere pass `--pane` and `--panel`. It prints how many watches were waiting for the report and received it (`Delivered to 1 waiting watcher.`, `liveWatchers` in JSON). Zero means nobody was waiting right now, not that the report is lost: it stays on the panel, and a named-cursor watch receives it when it next waits.\n\nWork sent to an agent through `runpane panels create`, `panes create` (with a prompt), or `panels submit` records that the sender waits for a `runpane report`, unless the request says `--source user` (Pane's own UI does) or `--no-expect-report`; `--expect-report` asks for one anyway. Typing into a tab never does. If the agent then goes ready or its panel exits without a report, Pane journals one opt-in `agent.report.missing` entry (`MISSING REPORT <pane-name> pane <pane-id> panel <panel-id> ready|exited`) whose `lastOutput` is the agent's last transcript reply or last screen lines; it skips the `--min-interval` batch and reaches only watchers that list it in `--kinds`. While the agent's screen shows background work (Claude's `· 1 shell` footer or a running background subagent row, Codex's `background terminal running` line), Pane waits and looks again every 10 seconds instead. `agent.blocked` entries carry `screenLines`, the prompt the agent waits on, and are journalled once a panel has stayed blocked for 2 seconds.\n\nThe orchestrator guide prescribes submit without `--wait`, followed by its named watch; use one completion consumer. As an optional CLI route, `--wait` on `runpane panels submit`, `panels create`, and `panes create` turns the expected report on and blocks on that panel until the first `agent.report`, `agent.report.missing`, `agent.blocked`, `panel.exited`, or `pane.gone`, then prints one line of JSON (`dispatchWaitResult`): the outcome, ids, the report's state, summary, `summaryPath`, PR and head, `screenLines` for a prompt, or `lastOutput` for a missing report, never the transcript. It exits 0 for a report, 3 missing, 4 blocked, 5 exited or gone, and 124 when `--timeout <duration>` passes. It follows the journal from the position the daemon took before delivering the work, so nothing that happens right after delivery is missed. It reuses watch --until-event and returns indeterminate (exit 6) if the journal epoch changes or retained history is lost; inspect the panel and inbox before retrying. Report-expected dispatches are serialized per panel and refused before writing while a result or missing-output collection is outstanding. Answer a blocked panel with panels submit --no-expect-report, then watch for the original result.\n\n`runpane inbox [--session <id|name>] [--json]` lists what a Session's orchestrator has not acknowledged: reports, missing reports, and on-screen questions (a question clears itself once its panel moves on). `runpane inbox ack <item-id>...` or `--all --session <id|name>` clears items. An orchestrator runs it when it starts or resumes, to catch anything that finished while it was down; Pane never injects the inbox into its turns.\n\n`runpane panels resume --panel <panel-id>` restarts a stopped terminal panel in place, keeping its panel id: an agent panel relaunches with its resume command (Claude `--resume <session>`, `codex resume <session>`, Agent Farm `agent-farm run <profile> --resume <id>` with the ID Agent Farm printed; the result's `agentSessionId` names it), the same restart the app runs when it shows the panel, without taking focus. A running panel is left alone (`action: \"already-running\"`); one whose agent exited back to its shell is stopped, and resume runs its saved command again in that shell (`action: \"resumed\"`). If that command exits at once, `--wait-ready` returns `ok: false` with `reason.code: \"command-exited\"` and the error line it printed. `--session <id|name>` or `--all-stopped` resumes every stopped agent panel in that scope, `--concurrency` at a time (default 3), then, with `--wait-ready`, waits on the resumed panels together, and returns `{ resumed, alreadyRunning, failed, notReady, items[] }` with each panel's own result, readiness, and blocker or error. After a Pane restart, or once an agent exits back to its shell, stopped panels report `running: false` (`initialized` stays true while the shell lives) in `panels list`, `panels screen`, and `panels wait` (which returns `stopped: true` instead of timing out), and `nextCommand` points at `panels resume`. `runpane watch` reports them as `panel.stopped` (`STOPPED ...`) when `--kinds` lists that kind; other consumers and `workspace state` see `agent.unknown` with `running: false`, never `agent.ready`.\n\n`runpane panels last-message --panel <panel-id>` reads a Claude or Codex agent's last reply from its transcript (up to `--limit` characters, default 20,000, keeping the end and reporting `truncated`). It never scrapes the screen: without a transcript it prints `{ ok: false, reason: \"transcript-unavailable\" }` and exits 1.\n\nCommands with a contract `daemonAction` (the `panes` git, script, restore, and move commands, `folders list|create`, and `links open`) call the same Pane daemon channel as the matching button in the app and print `{ ok, data, error }`. Destructive ones add a pane:// `link` to review the Pane.\n\n`runpane links create` builds `pane://open?...` links; opening one in Pane selects what it names and never changes Pane state.\n\n`runpane docs search|read` search and read Pane docs, help, and installed Pane Chat skills offline. They ship in the npm package and the Pane app only.\n\n`runpane handoff` hands a task to another agent on this or another of your machines. The sending agent writes the note from `runpane handoff --template` (goal, current state, done and verified, in progress, next steps, decisions and constraints, open questions, how to verify, git state); `runpane handoff \"claude opus on parsas-macbook-pro\" --note-file ~/handoff.md` checks every section is filled in, requires the branch to be pushed (or pushes it with `--push`, never forcing), writes the note to that machine's `~/.pane/handoffs/` and starts the agent in a new Pane branched from the sender's branch. When the sender is a Pane tab running a Claude, Codex, Cursor or OpenCode session and the receiver is the same agent, the handoff carries that session: on another machine its files are copied into that machine's agent store and the receiver resumes the same id; on this machine the receiver resumes a fork, because two agents never resume one session on one machine. The receiver starts with no prompt, and the CLI runs the continuity check and reports JSON `continuity.result` `continued`, `inconclusive` (exit 0; `next` gives a probe question) or `not-continued` (exit 1; the receiver is kept and `next` gives the `--note-only` command). `--note-only`, a different agent, or a tab with no carried session starts a fresh agent with the note (`fresh-agent`). JSON `continuity` is `{ carried, result, agent, sessionId, resumedId, fork, files, bytes, checks: { resumedById, restartResumes, historyLoaded, originalUntouched?, originalAppended? }, reason, next }`: `sessionId` is the sender's session, `resumedId` the id the receiver resumes (the fork's on this machine), `checks` are the continuity checks (`historyLoaded` is null when there was nothing to compare; `originalUntouched` is set for forks), and a dry run sets `carried`, `sessionId`, `fork`, `files` and `bytes` with no `result`. After `continued` or `inconclusive` the sending agent ends its turn and makes no more changes on the branch. An OpenCode session travels as its export: `opencode export` on this machine, then `opencode import` inside the receiving repository there, and an OpenCode receiver is accepted only to continue an OpenCode session. A Cursor session is carried only to another machine, best effort since Cursor's store is undocumented: Cursor has no fork, and a copied chat still writes to the original, so on this machine the result is `not-continued` and nothing is started. A destination's agent store is assumed at its default location (`~/.claude`, `~/.codex`, `~/.cursor`); a copy already there is replaced only when the sender's holds all of it and no agent there runs it, otherwise nothing is copied and the result is `not-continued`. A destination Pane too old to resume a carried session is refused before anything is sent (`ERR_HANDOFF_RESUME_UNSUPPORTED`). From a Session's terminal it skips git and creates a Session with the same name, goal and agent on the other machine instead; a same-agent Session resumes the sender's conversation when first opened. The receiver reports back to the sender's panel.\n\n`runpane pr watch`: Register durable GitHub PR monitoring for a Pane.\n\n`runpane pr unwatch`: Remove a Pane’s explicit PR registration.\n\n`runpane pr list`: List PR registrations and stopped watches.\n\n## Command reference\n\nEvery command and its options, from `commands` in `contracts/runpane/contract.json`.\n\n- `help`: Show help for runpane or a specific command.\n- `setup`: Open the guided setup wizard for install, remote host setup, update, and diagnostics.\n- `install`: Install Pane on this machine or configure this machine as a remote daemon host.\n- `update`: Update both Pane and the invoked supported CLI through an independent helper.\n- `version`: Print the runpane wrapper version without contacting, launching, or focusing Pane.\n- `doctor`: Run platform, release, installed Pane, daemon reachability, and remote setup diagnostics.\n- `daemon repair`: Repair and restart the managed remote-daemon launcher without changing pairing or tunnel configuration.\n- `agents doctor`: Diagnose whether a built-in agent command is available in a Pane repository environment.\n- `agent-context`: Print token-efficient Pane command context for coding agents.\n- `mcp`: Run the Pane MCP server over stdio so coding agents can call runpane commands as tools.\n- `repos list`: List repositories saved in the running Pane app.\n- `repos add`: Register an existing git repository with the running Pane app.\n- `panes list`: List Pane sessions in a saved repository.\n- `panes cost`: Report estimated token cost per Pane, with per-model breakdown and cache efficiency.\n- `workspace state`: Read one workspace snapshot of every Pane and CLI panel.\n- `workspace list`: List the machines on your tailnet, each with its owner, and whether each answers workspaces.\n- `workspace enable`: Put this machine's Pane on your tailnet for runpane workspace (the default when Tailscale is signed in).\n- `workspace disable`: Take this machine off runpane workspace and remove its tailscale serve handler on port 8443.\n- `workspace read`: Print a file from a machine on your tailnet; any path form works (C:\\..., /mnt/c/..., /home/..., ~/...).\n- `workspace write`: Write stdin to a file on a machine on your tailnet, creating parent folders.\n- `workspace exec`: Run a command in another machine's shell and return stdout, stderr, the exit code, OS, and shell.\n- `handoff`: Hand your task to another agent on this or another machine: check your handoff note, make the branch reachable, send the note, and start the agent on it. A same-agent handoff from a Pane tab carries and resumes your own session (a fork on this machine) and checks it continued; otherwise a fresh agent starts with the note. From a Session's terminal, create the Session on another machine instead.\n- `watch`: Wait for workspace agent and Pane transitions using a daemon-held cursor.\n- `panes create`: 1 feature = 1 worktree = 1 branch = 1 Pane; reviews, audits, fixes and QA for it are tabs in that Pane. Create a Pane (a new worktree) only when the work's branch has no Pane yet; for review, audit, fix or QA on existing work use `panels create --pane <id>`.\n- `panes adopt`: Adopt an existing externally managed git worktree as a Pane without changing the worktree.\n- `panes archive`: Archive a Pane (session) exactly like the UI Archive action, including safe removal of its Pane-managed git worktree, or archive every merged Pane in a Session.\n- `panes pin`: Declaratively pin a Pane; pinned is the Pane UI's favorite/pin star and repeated requests are idempotent.\n- `panes unpin`: Declaratively unpin a Pane; pinned is the Pane UI's favorite/pin star and repeated requests are idempotent.\n- `panes rename`: Rename a Pane without changing its worktree, branch, panels, or focus.\n- `panes focus`: Raise the Pane window and select a Pane (and optionally one of its panels) on explicit user request.\n- `panels create`: Open a new agent or terminal tab inside an existing Pane, sharing its worktree. The default for review, QA, fixes and any other agent on work that already has a Pane.\n- `panels open`: Open a URL or file as a browser/editor tab in a Pane, in split view beside the agent by default.\n- `panels list`: List tool panels inside a Pane session.\n- `panels output`: Read recent terminal output from a panel.\n- `panels screen`: Read a compact current-screen view from a terminal panel.\n- `panels input`: Send input bytes to a terminal panel.\n- `panels submit`: Send and submit text to a terminal panel, including idle agent composers.\n- `panels submit-composer`: Submit an agent composer using the panel-appropriate key sequence.\n- `panels wait`: Wait for a terminal panel to initialize, become ready/idle, or contain text.\n- `panels resume`: Restart a stopped terminal panel in place, resuming its agent's conversation.\n- `panels last-message`: Read an agent's last reply from its transcript, without scraping the screen.\n- `panes git-status`: Read the git status of a Pane worktree: uncommitted, unpushed, and behind-main counts.\n- `panes commit`: Stage all changes in a Pane worktree and commit them.\n- `panes push`: Push a Pane branch to its remote.\n- `panes pull`: Pull the remote branch into a Pane worktree.\n- `panes rebase-main`: Rebase a Pane branch onto the latest main branch.\n- `panes restore`: Restore an archived Pane, recreating its worktree.\n- `panes squash-rebase`: Squash a Pane branch into one commit and rebase it onto main.\n- `panes stash`: Stash uncommitted changes in a Pane worktree.\n- `panes stash-pop`: Apply and drop the latest stash in a Pane worktree.\n- `panes soft-reset`: Undo the last commit in a Pane, keeping its changes staged.\n- `panes fetch`: Fetch the remote for a Pane worktree.\n- `panes run-script`: Run the repository's run script in a Pane.\n- `panes stop-script`: Stop the run script running in a Pane.\n- `panes move`: Move a Pane into a sidebar folder.\n- `folders list`: List the sidebar folders of a repository.\n- `folders create`: Create a sidebar folder in a repository.\n- `links create`: Build a pane:// link that opens a Pane, panel, repository, or Session in the Pane app.\n- `links open`: Open a pane:// link in the running Pane app.\n- `docs search`: Search Pane's docs, runpane help, and the Pane Chat skills.\n- `docs read`: Read one Pane doc, help topic, or skill in full.\n- `agents start`: Start an agent on a task and wait until it is ready: as a new tab in an existing Pane with --pane <id> (review, audit, fix, QA), or in a new Pane with --repo and --name for new work.\n- `agents status`: Check on an agent: whether it is working, ready, blocked, or idle, plus its current screen.\n- `agents send`: Send a follow-up message to an agent and confirm it was submitted.\n- `report`: Hand back a worker's structured report: state, PR, head commit, summary, and the question when blocked.\n- `inbox`: List the reports, missing reports, and on-screen questions a Session's orchestrator has not acknowledged.\n- `inbox ack`: Clear Session inbox items once they are handled.\n- `sessions list`: List durable named orchestration Sessions.\n- `sessions create`: Create a durable named orchestration Session and its hidden terminal owner; JSON isPinned sets the pin star (default false).\n- `sessions get`: Read one durable named orchestration Session.\n- `sessions update`: Update a Session overview from structured JSON; isPinned sets the UI pin star.\n- `sessions set-agent`: Switch the durable terminal agent for a named Session.\n- `sessions associate`: Associate a user-visible Pane with a named Session.\n- `sessions detach`: Detach a Pane from a named Session.\n- `sessions overview`: Read a live status, activity, git, and pull request overview for a named Session.\n- `lock acquire`: Acquire a named lock on a resource shared between agents, such as one test account, optionally waiting for it.\n- `lock release`: Release a named lock you hold, or force-release another owner's lock.\n- `lock list`: List held named locks, optionally only one Session's.\n- `sessions unpin`: Declaratively unpin a named Session using the UI pin state; repeated requests keep the requested state.\n- `sessions pin`: Declaratively pin a named Session using the UI pin state; repeated requests keep the requested state.\n- `pr watch`: Register durable GitHub PR monitoring for a Pane.\n- `pr unwatch`: Remove a Pane’s explicit PR registration.\n- `pr list`: List PR registrations and stopped watches.\n\n```bash\nrunpane help [command]\nrunpane setup\nrunpane install [client|daemon] [options]\n runpane update [--pane-only | --cli-only] [--version <latest|vX.Y.Z>] [--pane-dir <path>] [--dry-run] [--wait] [--json]\n runpane update --status <operation-id> [--wait] [--pane-dir <path>] [--json]\nrunpane version [--pane-dir <path>]\nrunpane --version\nrunpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\nrunpane doctor --report [--title <text>] --body-file <path|-> [--yes] [--json]\nrunpane daemon repair [--pane-dir <path>] [--pane-path <path>] [--yes] [--json]\nrunpane agents doctor --agent <codex|claude|cursor> [--repo <selector>] [--json] [--pane-dir <path>]\nrunpane agent-context [--json]\nrunpane agent-context --command <command> [--json]\nrunpane mcp [--toolsets <name,...>] [--read-only]\nrunpane repos list [--json] [--pane-dir <path>]\nrunpane repos add --path <path> [--name <name>] [--json] [--yes] [--pane-dir <path>]\nrunpane panes list [--repo <selector>] [--json] [--pane-dir <path>]\nrunpane panes cost [--repo <selector>] [--pane <pane-id>] [--json] [--pane-dir <path>]\nrunpane workspace state [--repo <selector>] [--json] [--pane-dir <path>]\nrunpane workspace list [--json] [--pane-dir <path>]\nrunpane workspace enable [--json] [--pane-dir <path>]\nrunpane workspace disable [--json] [--pane-dir <path>]\nrunpane workspace [<machine>] read <path> [--json]\nrunpane workspace [<machine>] write <path> [--json]\nrunpane workspace <machine> exec [--cwd <path>] [--timeout-ms <ms>] [--json] -- <command>\nrunpane handoff --template\nrunpane handoff \"<destination>\" --note-file <path|-> [--machine <name>] [--agent <codex|claude|cursor|opencode>] [--model <model>] [--effort <level>] [--repo <selector>] [--push] [--note-only] [--dry-run] [--json]\nrunpane watch [--follow|--until-event] [--session <id|name>|--pane <id>] [--json] [overrides]\nrunpane panes create --repo <selector> --name <name> --agent <codex|claude|cursor> [--source user|agent] [--focus|--no-focus] [--base <ref>] [--branch <name>] [--prompt-file <path|->] [--expect-report|--no-expect-report] [--wait [--timeout <duration>]] [options] [--pane-dir <path>]\nrunpane panes create --repo <selector> --name <name> --tool-command <command> [--agent <codex|claude|cursor>] [--source user|agent] [--focus|--no-focus] [--base <ref>] [--branch <name>] [--prompt-file <path|->] [--expect-report|--no-expect-report] [--wait [--timeout <duration>]] [options] [--pane-dir <path>]\nrunpane panes create --from-json <path|-> [--yes] [--json] [--pane-dir <path>]\nrunpane panes adopt --repo <selector> --path <dir> --name <name> --agent <codex|claude|cursor> [--resume <id>] [--folder <name>] [--launch [--prompt <text>|--prompt-file <path|->] [--wait-ready] [--ready-timeout-ms <ms>]] [--no-pinned] [--no-associate] [--dry-run] [--yes] [--json] [--pane-dir <path>]\nrunpane panes adopt --repo <selector> --path <dir> --name <name> --tool-command <command> [--agent <codex|claude|cursor>] [--folder <name>] [--launch [--prompt <text>|--prompt-file <path|->] [--wait-ready] [--ready-timeout-ms <ms>]] [--no-pinned] [--no-associate] [--dry-run] [--yes] [--json] [--pane-dir <path>]\nrunpane panes archive --pane <pane-id> [--source user|agent] [--force] [--remove-worktree] [--dry-run] --yes [--json] [--pane-dir <path>]\nrunpane panes archive --session <id|name> --merged [--remove-worktree] [--source user|agent] [--dry-run] --yes [--json] [--pane-dir <path>]\nrunpane panes pin --pane <pane-id> --yes [--dry-run] [--json] [--pane-dir <path>]\nrunpane panes unpin --pane <pane-id> --yes [--dry-run] [--json] [--pane-dir <path>]\nrunpane panes rename --pane <pane-id> --name <new-name> --yes [--dry-run] [--json] [--pane-dir <path>]\nrunpane panes focus --pane <pane-id> [--panel <panel-id>] --source user|agent --yes [--json] [--pane-dir <path>]\nrunpane panels create --pane <pane-id> --agent <codex|claude|cursor> [--source user|agent] [--focus|--no-focus] [--wait-ready] [--expect-report|--no-expect-report] [--wait [--timeout <duration>]] --yes [--json] [--pane-dir <path>]\nrunpane panels create --pane <pane-id> --tool-command <command> [--agent <codex|claude|cursor>] [--title <title>] [--focus|--no-focus] [--wait-ready] [--expect-report|--no-expect-report] [--wait [--timeout <duration>]] --yes [--json] [--pane-dir <path>]\nrunpane panels open [--pane <pane-id>] (--url <url>|--file <path>) [--title <title>] [--split|--tab] [--focus|--no-focus] [--source user|agent] --yes [--json]\nrunpane panels list --pane <pane-id> [--json] [--pane-dir <path>]\nrunpane panels output --panel <panel-id> [--limit <count>] [--json] [--pane-dir <path>]\nrunpane panels screen --panel <panel-id> [--limit <count>] [--json] [--pane-dir <path>]\nrunpane panels input --panel <panel-id> (--text <text>|--keys <name,...>|--input-file <path|->) [--source user|agent] --yes [--json] [--pane-dir <path>]\nrunpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) [--as-file-pointer [--attach <file>]...] [--interrupt] [--source user|agent] [--expect-report|--no-expect-report] [--wait [--timeout <duration>]] --yes [--json] [--pane-dir <path>]\nrunpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter|tab] --yes [--json] [--pane-dir <path>]\nrunpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json] [--pane-dir <path>]\nrunpane panels resume --panel <panel-id> [--wait-ready] [--ready-timeout-ms <ms>] --yes [--json] [--pane-dir <path>]\nrunpane panels resume (--session <id|name>|--all-stopped) [--wait-ready] [--ready-timeout-ms <ms>] [--concurrency <count>] --yes [--json] [--pane-dir <path>]\nrunpane panels last-message --panel <panel-id> [--limit <count>] [--json] [--pane-dir <path>]\nrunpane panes git-status --pane <pane-id> [--json] [--pane-dir <path>]\nrunpane panes commit --pane <pane-id> --message <message> --yes [--json] [--pane-dir <path>]\nrunpane panes push --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes pull --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes rebase-main --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes restore --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes squash-rebase --pane <pane-id> --message <message> --yes [--json] [--pane-dir <path>]\nrunpane panes stash --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes stash-pop --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes soft-reset --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes fetch --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes run-script --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes stop-script --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes move --pane <pane-id> --folder <folder-id> --yes [--json] [--pane-dir <path>]\nrunpane folders list --repo <repo-id> [--json] [--pane-dir <path>]\nrunpane folders create --repo <repo-id> --name <name> --yes [--json] [--pane-dir <path>]\nrunpane links create --pane <pane-id> [--panel <panel-id>] [--json]\nrunpane links create --repo <repo-id> [--json]\nrunpane links create --session <session-id> [--json]\nrunpane links open --url <pane-url> --yes [--json] [--pane-dir <path>]\nrunpane docs search --query <text> [--limit <count>] [--json]\nrunpane docs read --doc <path> [--json]\nrunpane agents start (--pane <pane-id> | --repo <selector> --name <name> [--base-branch <branch>]) (--agent <codex|claude|cursor>|--tool-command <command>) --prompt <task> --yes [--json] [--pane-dir <path>]\nrunpane agents status (--pane <pane-id>|--panel <panel-id>) [--limit <count>] [--json] [--pane-dir <path>]\nrunpane agents send (--pane <pane-id>|--panel <panel-id>) --text <message> [--as-file-pointer] --yes [--json] [--pane-dir <path>]\nrunpane report --state <ready|blocked|failed|done> [--pr <number>] [--head <sha>] [--summary <text>|--summary-file <path|->] [--question <text>] [--pane <pane-id> --panel <panel-id>] [--json] [--pane-dir <path>]\nrunpane inbox [--session <id|name>] [--json] [--pane-dir <path>]\nrunpane inbox ack <item-id>... [--session <id|name>] [--json] [--pane-dir <path>]\nrunpane inbox ack --all --session <id|name> [--json] [--pane-dir <path>]\nrunpane sessions list [--json] [--pane-dir <path>]\nrunpane sessions create --from-json <path|-> [--json] [--pane-dir <path>]\nrunpane sessions get --session <id|name> [--json] [--pane-dir <path>]\nrunpane sessions update --session <id|name> --from-json <path|-> [--json] [--pane-dir <path>]\nrunpane sessions set-agent --session <id|name> --agent <codex|claude|cursor> [--json] [--pane-dir <path>]\nrunpane sessions associate --session <id|name> --pane <pane-id> [--json] [--pane-dir <path>]\nrunpane sessions detach --session <id|name> [--pane <pane-id>] [--json] [--pane-dir <path>]\nrunpane sessions overview --session <id|name> [--json] [--pane-dir <path>]\nrunpane lock acquire --name <name> --ttl <duration> [--wait <milliseconds>] [--note <text>] [--pane <pane-id>] [--panel <panel-id>] [--json] [--pane-dir <path>]\nrunpane lock release --name <name> [--force] [--session <id|name>] [--note <text>] [--pane <pane-id>] [--panel <panel-id>] [--json] [--pane-dir <path>]\nrunpane lock list [--session <id|name>] [--json] [--pane-dir <path>]\nrunpane sessions unpin --session <id|name> [--json] [--pane-dir <path>]\nrunpane sessions pin --session <id|name> [--json] [--pane-dir <path>]\nrunpane pr watch [--pane <id>] --url <PR URL> [--head <full-sha>] [--json] [--pane-dir <path>]\nrunpane pr unwatch [--pane <id>] --url <PR URL> [--json] [--pane-dir <path>]\nrunpane pr list [--pane <id>] [--json] [--pane-dir <path>]\n```\n\n## Agent Context\n\nPane lets a developer manage saved base repositories, user-visible Panes (Pane sessions) for feature/PR work, and terminal-backed panel tabs. A Pane is a visible workspace that normally maps to one Pane-managed git worktree and branch; a panel is a terminal tab inside one Pane and shares that Pane's worktree; an agent is a CLI process running inside a panel.\n\nDoctor-first environment discovery:\n\n```bash\nrunpane doctor --json\n```\n\nBrief CLI context:\n\n```bash\nrunpane agent-context\nrunpane agent-context --json\n```\n\nDetailed command discovery:\n\n```bash\nrunpane agent-context --command <command> [--json]\n```\n\nBrief tools:\n\n- `doctor`: Report wrapper, platform, installed Pane, and daemon reachability before an agent mutates Pane state.\n- `agent-context`: Print token-efficient Pane command context for coding agents.\n- `agents doctor`: Check whether Codex or Claude is available in the repo environment Pane will use.\n- `repos list`: List repositories saved in the running Pane app.\n- `repos add`: Register an existing git repository with the running Pane app.\n- `panes list`: List Pane sessions, optionally scoped to a saved repository.\n- `panes cost`: Report estimated token cost per Pane, with per-model breakdown and cache efficiency.\n- `panes create`: 1 feature = 1 worktree = 1 branch = 1 Pane; reviews, audits, fixes and QA for it are tabs in that Pane. Create a Pane (a new worktree) only when the work's branch has no Pane yet; for review, audit, fix or QA on existing work use `panels create --pane <id>`.\n- `panes archive`: Archive a Pane exactly like the UI Archive action, including safe removal of its Pane-managed git worktree, or archive every merged Pane in a Session.\n- `panes pin`: Declaratively pin a Pane (the Pane UI's favorite/pin star) without changing focus.\n- `panes unpin`: Declaratively unpin a Pane (the Pane UI's favorite/pin star) without changing focus.\n- `panes rename`: Rename a Pane without changing its worktree, branch, panels, or focus.\n- `panes focus`: Raise the Pane window and select a Pane (and optionally a panel) on explicit user request.\n- `panels create`: Open reviewer, QA, fix or helper agent tabs inside an existing Pane; they share that Pane's worktree. The default for any later agent on existing work.\n- `panels open`: Show the user an HTML page, plan, report, dev server URL, or file as a tab in split view beside the agent.\n- `panels list`: List tool panels inside a Pane session.\n- `panels output`: Read recent terminal output from a panel.\n- `panels screen`: Read a compact current-screen view from a terminal panel.\n- `panels input`: Send input bytes to a terminal panel.\n- `panels submit`: Send text plus terminal Enter to a terminal panel.\n- `panels submit-composer`: Submit an agent composer with the key for its current state.\n- `panels wait`: Wait for terminal initialized, ready, idle, or text state with compact output.\n- `panels resume`: Restart a stopped terminal panel in place (same panel id), resuming its agent's conversation.\n- `watch`: Wait for workspace events from the durable daemon journal; follow uses conservative cadence and quiet expiry by default.\n\nManaged AGENTS.md block body:\n\n```md\n## Pane\n\nThis repository is used with [Pane](https://runpane.com). Drive it with the CLI or the `pane` MCP server.\n\n1 feature = 1 worktree = 1 branch = 1 Pane; reviews, audits, fixes and QA for it are tabs in that Pane. Open later agents with `runpane panels create --pane <id>` (MCP: `panels_create` or `agents_start` with `pane`), not a new Pane.\n\nCLI: `npm i -g runpane` (or `npx --yes runpane@latest`), then `runpane doctor --json`. Full command reference: `runpane agent-context --json`.\n\nMCP: packaged Pane registers a stdio server named `pane` with Claude Code, Codex, and Cursor. Check the connection with `claude mcp list`, `codex mcp list`, or `agent mcp list`. Cursor may ask you to enable `pane` with `agent mcp enable pane`. If tools are missing, add it in the agent's MCP settings: Claude Code `claude mcp add --scope user pane -- npx --yes runpane@latest mcp`; Codex (`~/.codex/config.toml`) table `[mcp_servers.pane]` with `command = \"npx\"` and `args = [\"--yes\", \"runpane@latest\", \"mcp\"]`; Cursor (`~/.cursor/mcp.json`) uses `mcpServers.pane` with the same `npx` command and args; any other stdio client uses them too.\n```\n\n## Wrapper Flags\n\nThese flags are consumed by the wrapper:\n\n```bash\n--version <latest|vX.Y.Z>\n--download-dir <path>\n--pane-path <path>\n--format <auto|appimage|deb|dmg|zip|exe>\n--dry-run\n--yes (aliases: -y)\n--verbose\n```\n\nThe top-level `runpane --version` form prints the wrapper version. The install subcommand form `runpane install --version vX.Y.Z` selects a Pane release.\n\n## Local Control Flags\n\nThese flags are consumed by local daemon-control commands:\n\n```bash\n--pane-dir <path>\n--repo <selector>\n--pane <pane-id>\n--panel <panel-id>\n--path <path>\n--name <name>\n--worktree-name <name>\n--branch <name>\n--base-branch <ref> (aliases: --base)\n--folder <name>\n--resume <agent-session-id>\n--agent <codex|claude|cursor>\n--tool-command <command>\n--title <title>\n--url <url>\n--file <path>\n--initial-input <text> (aliases: --prompt)\n--initial-input-file <path|-> (aliases: --prompt-file)\n--attach <file>\n--from-json <path|->\n--timeout-ms <milliseconds>\n--ready-timeout-ms <milliseconds>\n--concurrency <count>\n--limit <count>\n--for <initialized|ready|idle|text>\n--contains <text>\n--interval-ms <milliseconds>\n--text <text>\n--input-file <path|->\n--source <user|agent>\n--strategy <auto|codex-ctrl-enter|enter|tab>\n--as <consumer-name>\n--since <generation>\n--from <now|earliest>\n--kinds <kind,...>\n--name-contains <text>\n--exclude-pane <pane-id>\n--heartbeat <duration>\n--idle-after <duration>\n--expire-after <duration>\n--ready-settle <duration>\n--blocked-settle <duration>\n--min-interval <duration>\n--body-file <path|->\n--session <id|name>\n--ttl <duration>\n--wait <milliseconds>\n--timeout <duration>\n--note <text>\n--message <message>\n--query <text>\n--doc <path>\n--toolsets <name,...>\n--keys <name,...>\n--state <ready|blocked|failed|done>\n--pr <number>\n--head <sha>\n--summary <text>\n--summary-file <path|->\n--question <text>\n--cwd <path>\n--note-file <path|->\n--machine <name>\n--model <model>\n--effort <level>\n--epoch <epoch>\n--json\n--wait-ready\n--no-focus\n--focus\n--split\n--tab\n--pinned\n--no-pinned\n--no-associate\n--force\n--remove-worktree\n--merged\n--launch\n--as-file-pointer\n--follow\n--until-event\n--ack-now\n--include-held-input\n--all-stopped\n--include-shells\n--no-held-input\n--self-test\n--report\n--read-only\n--template\n--push\n--note-only\n--interrupt\n--expect-report\n--no-expect-report\n--all\n```\n\n`runpane doctor --json`, `runpane repos list`, `runpane panes ...`, and `runpane panels ...` commands use or describe the local framed daemon socket/pipe for a running Pane app. `--pane-dir` points the wrapper at a non-default Pane data directory, such as `PANE_DIR=~/.pane_test` in development. `runpane agent-context` is local/offline and can be used before Pane is running. `agent-context` and `version` accept and ignore `--pane-dir`, so one `--pane-dir` can be passed to every runpane command. In a Pane repository checkout, if `runpane` is not on PATH, build the local wrapper with `pnpm --filter runpane build` and run it with Node 22 or newer, for example `node packages/runpane/dist/cli.js doctor --json`. From WSL, if the user runs Windows Pane, call the Windows wrapper through `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane ...'` so the command can reach the Windows named-pipe daemon and avoid UNC cwd issues.\n\n## Daemon Passthrough Flags\n\n`runpane install daemon` forwards these flags to `pane --remote-setup`:\n\n```bash\n--label <name>\n--prefer-tunnel <tailscale|ssh|manual|auto>\n--channel <stable|nightly>\n--base-url <url>\n--pane-dir <path>\n--listen-port <port>\n--port <port>\n--repo-ref <ref>\n--auto-listen-port\n--interactive-tailscale-setup\n--no-install-service\n--no-tailscale-serve\n--print-only\n```\n\nUnknown daemon flags should be forwarded rather than dropped so newer Pane versions can extend `--remote-setup` without requiring an immediate wrapper release. Unknown flags for non-daemon commands should fail clearly.\n\n## Machine-Readable Schemas\n\nStable `--json` response schemas live in `contracts/runpane/contract.json` under `jsonSchemas`.\n\nA failed command exits 1 and prints its message and a `Next:` line to stderr. With `--json` it also prints `jsonSchemas.error` to stdout: `{ \"ok\": false, \"error\": { \"code\": \"...\", \"message\": \"...\", \"next\": \"...\" } }`. Branch on `code`; `next` is the step to take when one is known. A command whose result has its own `ok: false` shape, such as an archive refusal or `panels last-message` without a transcript, prints that result instead, and `watch` reports failures as WATCH ERROR lines with exit 2.\n\nOpenAPI should be generated later from the stable local HTTP/API subset of the same contract, not maintained as a second source of truth.\n\n## Download Attribution\n\nThe npm package uses `source=npm` for all npm-registry consumers, including `npx`, `pnpm dlx`, `yarn dlx`, `bunx`, and global npm/pnpm installs.\nThe PyPI package uses `source=pip` for all Python consumers, including pip, pipx, uvx, and `python -m runpane`.\nWrappers should prefer `https://runpane.com/api/download?platform=<platform>&arch=<arch>&format=<format>&version=<version>&channel=<channel>&source=<npm|pip>`.\nIf the website route cannot satisfy the download, wrappers may fall back to the matching GitHub release asset and print a warning that website attribution may be incomplete for that run.\nWrappers emit best-effort telemetry to `https://runpane.com/api/runpane/telemetry` for command failures, download request/success/failure, and GitHub fallback usage. Routine command starts and successes emit nothing.\nWrapper telemetry uses a persisted anonymous `install_id` in the form `install_<uuid>` and PostHog `distinct_id = install:<install_id>` so installs reporting failures or downloads can be counted with `count(DISTINCT properties.install_id)`; this is not a count of all CLI users.\nWrapper telemetry must be disabled in CI, when `RUNPANE_TELEMETRY_DISABLED` is set, and when the local config explicitly disables analytics. It must not include raw paths, labels, prompts, raw error text, or environment values.\nDesktop local control reports useful failures as `runpane_local_control_failed`; normal polling, wait deadlines, and optional agent unavailability emit nothing. Reports contain coarse operation and failure classification only, with a five-minute deduplication interval and a maximum of 20 per hour per app process. Handled CLI failures are ordinary events, not exceptions.\n\n## Publishing Credentials\n\nLocal implementation, build, and dry-run validation do not need npm or PyPI API tokens.\nRelease publishing should prefer npm Trusted Publishing and PyPI Trusted Publishing from GitHub Actions.\nFallback `NPM_TOKEN` or `PYPI_API_TOKEN` credentials may be used for first package reservation or manual publication only. They must be supplied through local environment variables or GitHub Actions secrets, never committed, and revoked or rotated after use.\n"},{"path":"docs/RUNPANE_WORKSPACES.md","title":"Workspaces: reach all your machines with runpane","text":"# Workspaces: reach all your machines with runpane\n\n`runpane workspace` lets your Macs, Windows PCs, and Linux machines read files, write files, run commands, and drive each other's Pane over the Tailscale network you already use. There is no SSH, no keys, and no pairing code: each request is trusted because Tailscale says which login it comes from. Your own login is always let in; a teammate's machine lets you in when its owner shares it with everyone on the tailnet.\n\nThe same listener powers codeless desktop remote mode: Settings → Remote Access → **Your computers** lists the machines on your tailnet with Pane open (a teammate's machine that accepts only its owner shows as \"Owner only\" and can't be connected to), and **Connect** switches the desktop app to that machine with no `pane-remote://` code. `runpane workspace` reaches the same machines. Who may connect is set under **Access to this computer** → **Who can connect** (see [Who is trusted](#who-is-trusted)).\n\n```\nrunpane workspace list\nrunpane workspace <machine> read <path>\nrunpane workspace <machine> write <path> # content from stdin\nrunpane workspace <machine> exec -- <command> # runs in that machine's shell\nrunpane workspace <machine> <runpane command> # e.g. sessions list --json\n```\n\n`<machine>` is the Tailscale machine name (`parsa-devbox`), a unique prefix (`devbox`), the MagicDNS name, or a Tailscale IP.\n\n`runpane workspace list` shows your own machines first, then other people's machines on the tailnet labeled with their owner's login (`tylers-mac-mini (macOS, online, owner tbrownio@github)`). Each machine decides who it lets in. A machine set to \"Only me\" refuses other logins, and the error says how its owner can change that. A path shaped like another OS's (`C:\\...` on a Mac) routes only to your own machines; reach someone else's machine by name.\n\nThese commands ship in the npm CLI (`npm i -g runpane`, or `npx --yes runpane@latest workspace list`). The Python package prints the workspaces status block but does not run them.\n\n`workspace <machine> <runpane command>` runs any command that talks to Pane, such as `sessions list`, `panes list`, or `workspace state`, against that machine's Pane. Commands that only make sense on the machine you type them on (`doctor`, `agent-context`, `mcp`, `docs`, `install`) are refused with the alternative: run them there with `runpane workspace <machine> exec -- 'runpane doctor'`.\n\n## Turning it on\n\nWorkspaces are on by default. When Tailscale is installed and signed in, the desktop Pane that owns `~/.pane` joins your tailnet as it starts:\n\n- It listens on `127.0.0.1` on a free port.\n- It runs `tailscale serve --bg --https=8443 http://127.0.0.1:<port>`, next to the remote daemon's port 443 handler if you have one.\n- Nothing is opened to the public internet, and no firewall rule is added. Serve is reachable only inside your tailnet.\n\nEvery `runpane --help`, `runpane doctor`, and `runpane agent-context` prints the current state:\n\n```\nWorkspaces: on (parsas-macbook-pro)\nOther machines: parsa-devbox (Windows, online), parsas-macbook-air (macOS, offline)\nReach them: runpane workspace <machine> read|write|exec|<command>\n```\n\nWhen workspaces are off, the first line says why and gives the one step that fixes it:\n\n| Reason | Fix |\n|---|---|\n| Tailscale is not installed | Install Tailscale from https://tailscale.com/download and sign in. |\n| Tailscale is signed out | Open Tailscale and sign in. |\n| This tailnet has HTTPS certificates turned off | Turn on HTTPS Certificates at https://login.tailscale.com/admin/dns. |\n| Pane is not running | Open Pane. |\n| Turned off with `runpane workspace disable` | `runpane workspace enable` |\n\nWhile the tailnet has HTTPS Certificates turned off, the desktop app shows a notice with a link to that setting. Turning it on writes machine names (`<machine>.<tailnet>.ts.net`) to the public Certificate Transparency log. The notice clears within a minute of the change, and `runpane workspace list` names the setting as the reason other machines are not reachable.\n\n`runpane workspace disable` removes the 8443 handler and keeps this machine off until `runpane workspace enable`. This machine can still reach your other machines while it is off. A Pane started with a different data directory (`PANE_DIR` or `--pane-dir`) stays off unless you run `runpane workspace enable` against it.\n\nPane must be running on a machine for others to reach it. Pane checks Tailscale again every minute, so signing in to Tailscale after Pane starts is enough. Serve handlers belong to one tailnet, so after you switch this machine to another tailnet Pane puts the 8443 handler back on the new one and reports the new URL within a minute.\n\n## Who is trusted\n\nTwo settings decide, in Settings → Remote Access → **Access to this computer**. Every request must pass both: its login is allowed by **visibility**, and, when **password protection** is on, it carries the password.\n\n| Visibility | Who can connect | |\n|---|---|---|\n| Off | Nobody without a pairing code. Same as `runpane workspace disable`. | |\n| Only me | Devices signed into this machine's own Tailscale login. | Default |\n| Everyone on tailnet | Anyone with an untagged device in the current tailnet. Pane warns before turning it on. | |\n\nPane learns the owner from the `Self` entry in `tailscale status --json`, and Serve adds a `Tailscale-User-Login` header to every request after removing any copy the caller sent. Under \"Everyone on tailnet\", the people on the tailnet are the owners of its untagged devices, re-read every minute; people a device was shared with from another tailnet (Tailscale's sharee nodes) are not among them, even though Serve signs their requests too.\n\nPassword protection is off by default. When it is on, every client must send the password, your own machines included; the desktop app asks for it once and saves it with that machine's connection. Pane stores only a scrypt hash of it, and only someone at the machine itself can change visibility or the password: remote clients are refused. The `runpane workspace` CLI does not send a password yet, so it cannot reach a password-protected machine.\n\n- Under \"Only me\", a device signed in as another Tailscale user, including a teammate's Mac shared into your tailnet, is refused.\n- A tagged device is refused: Serve sends no identity for it.\n- Pane answers only requests that arrive through Serve: Serve's target path carries a secret that changes every launch, and requests straight to the loopback port are refused. Another OS user on the same machine who can read `tailscale serve status` could learn it, so give other accounts on a joined machine only the trust you would give the owner.\n- Browsers are refused. A web page open on one of your devices could otherwise send requests that Serve signs with your login. Pane refuses any request that carries a browser `Origin`; the phone app's WebSockets, which always carry one, send a fixed value no browser can.\n- Integration keys are shared only with the owner's login, even under \"Everyone on this tailnet\". See [Shared integration keys](SHARED_CREDENTIALS.md).\n- Any device signed in as you, your phone included, is trusted. `runpane workspace list` shows only Macs, Windows PCs, and Linux machines, but that is a listing choice, not a block.\n- Removing a device from Tailscale revokes it everywhere.\n- The same setting decides who opens the host's dev servers and HTML pages in the Remote Pane app's browser tabs, at their `https://<host>.ts.net:<n>` addresses. A page in a frame cannot send the password, so those pages are off while password protection is on. See [Browser tabs on the phone](SELF_HOSTED_REMOTE_DAEMON.md#browser-tabs-on-the-phone).\n\n## What `write` and `exec` can do\n\nEvery admitted login can also reach any port listening on the machine from desktop Pane, as [forwarded ports](SELF_HOSTED_REMOTE_DAEMON.md#forwarded-ports).\n\n`write` and `exec` give your agents SSH-level control of every joined machine, and so does anyone the machine's visibility lets in: under \"Everyone on tailnet\" that includes the other people on the tailnet. A local agent's permission prompts and sandbox see only `runpane workspace ... exec`, not what runs on the other machine. Turn workspaces off on any machine that should not accept this.\n\n## Hand a task to another machine\n\n`runpane handoff` uses workspaces to move a task to an agent on another of your machines. The sending agent writes a handoff note; the CLI checks it, makes sure the branch is pushed, and starts the agent there. When the sender is a Pane tab running Claude, Codex or Cursor and the receiver is the same agent, the agent's own session moves too:\n\n```\nrunpane handoff --template > ~/handoff.md # fill in every section\nrunpane handoff \"claude opus on parsas-macbook-pro\" --note-file ~/handoff.md --push\n```\n\nThe destination machine needs Pane running, the repository saved in Pane with a remote for the same GitHub repository, and its own `runpane` on PATH (otherwise it uses `npx runpane@latest`). The note lands in that machine's `~/.pane/handoffs/`, the agent starts in a new Pane branched from your branch, and it reports back to your panel with `runpane workspace <your machine> panels submit`. For a same-agent handoff the session's files are copied into that machine's agent store (`~/.claude`, `~/.codex` or `~/.cursor`, at their default locations), the receiver resumes that session with no prompt, and the CLI checks it continued before telling it where the session moved. The result is `continued`, `inconclusive` (ask the receiver what it was last working on), or `not-continued` (exit 1; the receiver is kept, and `--note-only` starts a fresh agent with the note if you choose). `--note-only`, a different agent, or no session to carry starts a fresh agent with the note. Carrying a Cursor chat to another machine is best effort; on the same machine it is not carried, since a copied Cursor chat still writes to the original. A copy already on the destination is replaced only when yours holds all of it and no agent there runs it; otherwise nothing is copied and the result is `not-continued`. A destination Pane too old to resume a carried session is refused before anything is sent. The note's sections are in the `handoff` skill's [note template](../main/src/services/paneChatBundle/skills/handoff/references/note-template.md).\n\nRun from a Session's terminal, whose folder is outside git, handoff moves the Session instead (the Session menu's **Move to host…** asks the Session's agent to do this). It skips the git steps and pushes nothing, writes the note to the other machine's `~/.pane/handoffs/` the same way, and creates a Session there with the same name, goal and agent, whose next action is to read the note. With the same agent, the Session's agent session is copied there too and the new Session resumes that conversation when you first open it; the CLI can't check it until then, so it reports `inconclusive` and says what to check. The destination needs only Pane running with workspaces; no repository has to be saved there. The new Session waits for your first message, and the original stays where it is. A Session can't be handed off to the machine it is on. See [Move a Session to another machine](SESSIONS.md#move-a-session-to-another-machine).\n\n## Paths across Windows, WSL, and macOS\n\n`read`, `write`, and `exec --cwd` take any path form and translate it on the machine that runs the request, so agents do not need `cat` on one machine and `Get-Content` on another:\n\n- `C:\\Users\\me\\notes.md` and `/mnt/c/Users/me/notes.md` reach the same Windows file.\n- On Windows, WSL paths such as `/home/me/repo/README.md` are read through `\\\\wsl.localhost\\<distro>\\...` using the default distribution (the one `wsl -l -v` marks with `*`). Name another distribution with `\\\\wsl.localhost\\<distro>\\...`.\n- Inside WSL, `C:\\...` becomes `/mnt/c/...`, and runpane reads the tailnet from Windows' `tailscale.exe`.\n- `~` is the home directory of the machine that runs the request.\n\n`read` prints the file to stdout (`--json` returns `content` with `encoding` `utf8` or `base64`) and lists a directory's entries. `write` reads stdin, creates missing parent folders, and replaces the file. `exec` runs in the shell Pane uses for terminals there (Git Bash or PowerShell on Windows, your default shell elsewhere), prints its stdout and stderr, ends with a line naming the machine, OS, shell, and exit code, and exits with the command's exit code. `--json` returns all of these fields. Pass the command as one quoted string (`exec -- 'grep \"a b\" notes.md'`): several words after `--` are joined with spaces. A command that starts a background job returns when the shell exits, and `--timeout-ms` stops the whole process tree. If that tree cannot be stopped, the result says so (`stillRunning` in `--json`, \"may still be running\" in text).\n\n## Without a machine name\n\nWith no `<machine>`, `read`, `write`, and `exec --cwd` route a path that cannot exist on this machine to the one joined machine it fits. `C:\\...` on a Mac goes to your Windows machine; if several fit, runpane lists them and asks for a name. Any other path is this machine's: `read` and `write` hand it to this machine's Pane, so it follows the same rules as a named machine (`~`, directories, new parent folders), and Pane must be running here.\n\nOther commands help in the same way. When a file such as a `--from-json` or `--prompt-file` path is missing and looks like another machine's path, the error names the likely machine and the exact `runpane workspace <machine> read` command. When a Session is not found here, runpane checks your other online machines and names the one that has it.\n\n## Workspaces and pairing codes\n\nWorkspaces and the [self-hosted remote daemon](SELF_HOSTED_REMOTE_DAEMON.md) both use Tailscale Serve, for different jobs:\n\n| | Workspaces | Pairing codes (remote daemon) |\n|---|---|---|\n| For | Your own machines (runpane CLI and desktop remote mode), or your tailnet when you choose | Browsers, phones, and devices on another Tailscale account |\n| Trust | Tailscale login, by visibility, plus an optional password | A `pane-remote://` code per device |\n| Serves | The desktop Pane's `~/.pane` | The remote daemon's data directory, usually `~/.pane_remote` |\n| Port | 8443 | 443 |\n| Setup | None | `runpane setup` on the host, then paste the code |\n\nMachine commands (`read`, `write`, `exec`) are served only to workspace identity, never to pairing tokens.\n\n## Troubleshooting\n\n- `runpane workspace list` shows each machine's state. \"Pane is not answering there\" means Pane is not running on that machine, its workspaces are off, or it runs a Pane version without workspaces.\n- `tailscale serve status` should list `https://<machine>.<tailnet>.ts.net:8443` proxying to `http://127.0.0.1:<port>`.\n- A Pane older than workspaces reports `Workspaces: off ... (this Pane is older than workspaces)`; update Pane.\n"},{"path":"docs/SELF_HOSTED_REMOTE_DAEMON.md","title":"Self-Hosted Remote Daemon","text":"# Self-Hosted Remote Daemon\n\nThis guide covers running Pane on your own workstation, Mac mini, Linux box, or VM and connecting to it from the local Pane desktop app or the Remote Pane browser app.\n\nThe intended flow is:\n\n1. Run one setup command on the remote machine.\n2. Copy the generated `pane-remote://...` connection code.\n3. Paste it into desktop Pane under `Settings > Remote Access`, or open `https://runpane.com/app/` and paste it there.\n\nPane saves the profile and attempts to connect immediately. Local desktop mode is unchanged until a remote profile is imported and activated.\n\n## Workspaces or pairing codes?\n\nTo let machines on your tailnet reach each other from the runpane CLI, you do not need this guide or a code. [Workspaces](RUNPANE_WORKSPACES.md) put each desktop Pane on your tailnet automatically (port 8443) and trust your Tailscale login, plus everyone on the tailnet when a machine's owner allows it, so `runpane workspace <machine> read|write|exec|<command>` works between your Macs, Windows PCs, and Linux machines, and the desktop app's remote mode connects to them from Settings → Remote Access → Your computers. Use the remote daemon and pairing codes in this guide for browsers and phones, and for devices outside your tailnet. The remote daemon keeps port 443, and the two run side by side.\n\n## Guided quick start\n\nOn the host machine, run `npx --yes runpane@latest` in an interactive terminal.\nChoose **Set up a remote host**, enter a name (or press Enter),\nand follow the Tailscale installation/login prompts. The wizard picks an available\nport and prints a connection code. Sign your other device into the same Tailscale\nnetwork, then paste the code into Pane or [runpane.com/app](https://runpane.com/app/).\n\nIf the host later switches to another tailnet, Pane notices while it runs: within\na minute it re-creates the `:443` Serve forward on the new tailnet, and new codes\nuse the new address. Codes created before the switch still point at the old\ntailnet, so create a new one. If Pane cannot confirm the forward, it shows the\nerror and the `tailscale serve` command to run instead of creating a code.\n\nSSH and manual URL setups remain available through the explicit commands below.\nThe no-argument and `setup` commands print help in non-interactive shells; they\ndo not start a login prompt.\n\n## Creating a code in desktop Settings\n\nDesktop setup uses an installed, signed-in Tailscale client. If Tailscale is\nmissing, Pane shows installation instructions; use the setup terminal to install\nit. Tailscale discovery, Serve configuration and requested daemon service setup\nrun asynchronously so the desktop stays responsive while they finish.\n\n## One-Command Setup\n\nTo skip the wizard menu, run interactive remote setup directly:\n\n```bash\nnpx --yes runpane@latest install daemon --interactive-tailscale-setup --auto-listen-port --label \"VM\"\npnpm dlx runpane@latest install daemon --interactive-tailscale-setup --auto-listen-port --label \"VM\"\npipx run runpane install daemon --interactive-tailscale-setup --auto-listen-port --label \"VM\"\nuvx runpane@latest install daemon --interactive-tailscale-setup --auto-listen-port --label \"VM\"\n```\n\nSSH tunnel mode:\n\n```bash\nnpx --yes runpane@latest install daemon --label \"VM\" --prefer-tunnel ssh\n```\n\nPersistent installs are also supported:\n\n```bash\nnpm i -g runpane\nrunpane install daemon --interactive-tailscale-setup --auto-listen-port --label \"VM\"\n\npython -m pip install runpane\nrunpane install daemon --interactive-tailscale-setup --auto-listen-port --label \"VM\"\n```\n\nFrom a source checkout:\n\n```bash\npnpm remote:setup -- --label \"VM\"\n```\n\nSSH tunnel mode:\n\n```bash\npnpm remote:setup -- --label \"VM\" --prefer-tunnel ssh\n```\n\nFor validation against a separate data directory:\n\n```bash\nPANE_DIR=/tmp/pane-remote-vm pnpm remote:setup -- --label \"VM\" --prefer-tunnel ssh --print-only\n```\n\nPackaged Pane builds can run the same setup path without opening a window:\n\n```bash\npane --remote-setup --label \"VM\"\n```\n\nOn displayless Linux hosts, prefer `runpane install daemon`; the npm and PyPI\nwrappers pass Electron's required headless flags automatically. For a direct\npackaged launch, pass them explicitly:\n\n```bash\npane --ozone-platform=headless --disable-gpu --remote-setup --label \"VM\"\n```\n\nThese must be command-line arguments: Electron picks its Ozone platform before\nthe app starts, so the environment variable `ELECTRON_OZONE_PLATFORM_HINT` no\nlonger works (removed in Electron 38, ignored from 39 on).\n\nThe wrappers are lightweight installers and configurators. They currently\ndownload the packaged Pane runtime because the daemon is not distributed as a\nseparate binary. AppImage installs therefore require FUSE; on Debian-based\nhosts, `--format deb` avoids the AppImage/FUSE requirement.\n\nIf Pane is already installed on the host, this is the most direct command:\n\n```bash\npane --remote-setup --label \"VM\" --prefer-tunnel tailscale\n```\n\nThe setup command:\n\n- detects Linux, macOS, or Windows host behavior\n- writes remote daemon config into one `PANE_DIR` (default `~/.pane_remote`)\n- enables the loopback listener on `127.0.0.1:42137`\n- creates a paired client record with a hashed token on the host\n- emits the raw token only inside the one-time `pane-remote://...` import code\n- attempts to install and start a user-level daemon service (on Linux, it also\n enables lingering so the service keeps running after you log out)\n- prints the manual daemon command if service setup is unavailable\n- detects Tailscale Serve where possible and otherwise prints an SSH local-forward command\n\nUseful options:\n\n```bash\npnpm remote:setup -- --help\npnpm remote:setup -- --channel nightly\npnpm remote:setup -- --pane-dir \"$HOME/.pane_remote\"\npnpm remote:setup -- --prefer-tunnel ssh\npnpm remote:setup -- --no-install-service\npnpm remote:setup -- --no-tailscale-serve\n```\n\n## Run Pane on a Cloud VM\n\nPane does not create or manage cloud machines. Create the VM yourself, then set\nit up as a Remote Pane host:\n\n1. Create an Ubuntu 24.04 VM with any provider. Size it for the agents and\n builds you plan to run. It only needs inbound SSH. The daemon listens on\n loopback, and you reach it through Tailscale or an SSH tunnel.\n2. SSH in as a regular user with `sudo`, not as `root`.\n3. Install what your agents need on the VM: `git`, Node.js 20 or newer (for\n `npx`), and the agent CLIs you use, such as Claude Code or Codex. Sign in\n to each CLI on the VM. Agents run there, not on your laptop.\n4. Install Tailscale and join your tailnet:\n\n ```bash\n curl -fsSL https://tailscale.com/install.sh | sh\n sudo tailscale up\n ```\n\n5. Install Pane and create a connection code. `--format deb` installs the\n Debian package, so the host does not need FUSE:\n\n ```bash\n npx --yes runpane@latest install daemon --label \"Cloud VM\" --format deb\n ```\n\n6. Check that the daemon keeps running after you log out. It runs as a systemd\n user service, and setup turns on lingering (through passwordless `sudo` if\n needed) so the service outlives your SSH session. If the setup output says\n it could not, enable lingering yourself:\n\n ```bash\n sudo loginctl enable-linger \"$USER\"\n systemctl --user status pane-remote-daemon.service\n ```\n\n7. Paste the printed `pane-remote://...` code into desktop Pane (see\n [Import Locally](#import-locally)) or into `https://runpane.com/app/`.\n\nTo start, stop, or delete the VM, use your provider's console or CLI. If\nsomething fails, run `npx --yes runpane@latest doctor --json` on the VM and\ncheck [Troubleshooting](#troubleshooting).\n\n## Import Locally\n\nOn your local desktop machine:\n\n1. Open Pane.\n2. Go to `Settings > Remote Access` and click `Add with a Code` under `Your computers`.\n3. Paste the full `pane-remote://...` code into `Connection Code`.\n4. Click `Connect`.\n\nIf the tunnel is not reachable yet, Pane still saves the host and shows the connection error. Start the printed SSH/Tailscale tunnel and click `Connect` next to the saved host.\n\n## Use the Mobile / Browser App\n\nThe same connection code works in the Remote Pane PWA:\n\n```text\nhttps://runpane.com/app/\n```\n\nUse the PWA for phone or tablet access to remote terminals, browser tabs and files:\n\n1. Set up the remote host with Tailscale or a trusted HTTPS tunnel.\n2. Copy the full `pane-remote://...` code printed by setup.\n3. Open `https://runpane.com/app/` on the client device.\n4. Paste the code and connect.\n\nFor iPhone or iPad, open the URL in Safari, tap Share, then tap `Add to Home Screen`.\nFor Android, open the URL in Chrome, open the browser menu, then tap `Add to Home screen` or `Install app`.\n\nSSH tunnel mode is mainly useful from desktop clients. For mobile browser access, prefer Tailscale or Manual HTTPS so the phone can reach the daemon URL directly.\n\n### Browser tabs on the phone\n\nAdd Tool → Browser opens a browser tab on the phone. A new tab lists the host's listening ports, and the Ports button in the address bar opens the same list. Tap a `web` port to open it; `tcp` ports read \"desktop only\". The address bar is editable and reads the host's own address, such as `localhost:5173/ on <host>`. Copy URL copies the address the phone actually loads, and Open in Safari opens it outside the app. Use Open in Safari for sites that refuse to load in a frame, such as github.com: the tab cannot tell when that happens.\n\nHow a host page reaches the phone:\n\n- When a phone first opens a `web` port, Pane gives that port its own HTTPS address on the host's tailnet name, `https://<host>.ts.net:<n>`, with a `tailscale serve` handler; the tab shows the page under a second later. Pane picks `n` from 44300 up, so it never takes the dev server's own port number. Handlers exist only for opened ports because Tailscale drops its whole Serve config, other apps' handlers included, once a machine holds a few hundred.\n- Each handler points at a loopback preview proxy that checks the visitor's Tailscale login, sends the request to `localhost:<port>` as the dev server expects, carries live-reload WebSockets, and drops the headers that stop a page from loading in a frame.\n- HTML files opened in a browser tab (for example by `runpane panels open --url file://...`) load from one more Serve address, with the files beside them. Files outside the page's folder are refused.\n- A handler goes when its port stops listening, when Pane quits, and at the next launch after a crash. Quit spends at most 5 seconds removing them; a launch removes any it left. Pane changes only handlers it created: their targets start with `/pane-<hash of the Pane data directory>/`, so a dev build and the installed app never remove each other's.\n\nWho can open these pages: the devices that **Who can connect** admits (see [Who is trusted](RUNPANE_WORKSPACES.md#who-is-trusted)). Phone pages need that setting on, Tailscale running on the host, and the phone on the same tailnet. A page in a frame cannot send the machine's password, so phone pages are off while password protection is on. A refused device sees \"This page runs on <host>. Open it from a device signed in as <login>.\" A pairing code or SSH tunnel does not grant access to these pages: they need the phone on this tailnet, signed in with a login that setting admits. Navigation inside a page stays on the phone; the tab saves only the addresses you enter or pick.\n\n### Files on the phone\n\nEach pane's Explorer tab shows its worktree as a tree. Tap a text file to read and edit it; Save writes it to the host. Text files over 1 MB, and binary files that are not images, PDFs, video or audio, stay on the desktop. Images, PDFs, video and audio of any size preview on the phone, and video seeks without downloading the whole file. Open in Safari opens a preview outside the app.\n\nBrowsing and editing go through the PWA's own connection, so they work over every connection the PWA supports. Previews load from the same Serve address as HTML files in browser tabs, so they need what [browser tabs on the phone](#browser-tabs-on-the-phone) need: Tailscale on the host, password protection off, and a phone signed in to a login that **Who can connect** admits. Without that, a preview explains why it cannot load. Files outside the worktree are refused.\n\n### Remote PWA Implementation Notes\n\nThe Remote Pane PWA is a browser runtime. It does not have `window.electronAPI`, so client-side PWA preferences must use browser-safe storage such as `localStorage` or explicit daemon adapter calls. Do not reuse desktop renderer preference stores that persist through Electron IPC unless the call path is guarded for browser mode.\n\n## Forwarded ports\n\nA desktop connected to a remote host reaches every port listening on the host as if it were local. Pane lists the host's ports on a new, empty browser tab and under the address bar's **Ports** button, and the desktop opens a matching listener on its own `127.0.0.1` and `::1` for each one. Only the desktop app sets up these listeners; the browser app and phones don't yet.\n\n- A browser tab at `localhost:5173` loads the host's dev server, live reload included. The address bar reads `localhost:5173` with **on <host name>**.\n- When something on the desktop already answers on 5173, the desktop uses the next free port, or any free port when the next 19 are taken too. The tab still reads `localhost:5173`, and the Ports list shows the local number as **here :5174**.\n- A tab never loads the desktop's own service in the host's place. While the host's Ports list is loading it waits, and when a port stops listening on the host it says the port isn't reachable, then loads once the port is back.\n- **Copy URL** copies the address that works on the desktop, for its system browser.\n- Clicking a `localhost` link in a terminal opens the host's server in your system browser, using the local port number if it moved. A link to a port that isn't forwarded right now doesn't open, so it never reaches the desktop's own service on that port.\n- When a port closes on the host, the desktop closes its listener. Disconnecting closes all of them.\n\nEach connection the desktop accepts travels over the Pane connection as a WebSocket to `/ports/<port>` on the host, which connects to `localhost:<port>` there. The host accepts it only with the same sign-in as every other request, and only for a port in its current list. Listeners stay on loopback on both machines.\n\nEvery client signed in to the host can reach these ports through that route: anyone holding a pairing code, and any Tailscale login that the Workspaces \"Who can connect\" setting allows. They reach every listening service, including databases that trust `localhost` and debugging ports such as a Node `--inspect` port, which runs code as you. Pane's own ports are forwarded too, and its remote API keeps its own sign-in. Forwarding can't be limited to some ports. If that is more access than you want, turn off the remote daemon listener in `Settings > Remote Access` and set \"Who can connect\" to Off.\n\n## Security Model\n\n- The daemon listener only supports loopback hosts: `127.0.0.1`, `::1`, or `localhost`.\n- Direct public or LAN binding is intentionally rejected.\n- Use SSH local forwarding, Tailscale Serve, or a trusted HTTPS reverse proxy that forwards to loopback.\n- Treat the generated `pane-remote://...` code like a secret. It contains the bearer token needed by the local client.\n- Any signed-in client can open a TCP connection to any port listening on the host. See [Forwarded ports](#forwarded-ports).\n\nTailscale Serve example generated by setup:\n\n```bash\ntailscale serve --bg --tls-terminated-tcp=443 42137\n```\n\nSSH fallback generated by setup:\n\n```bash\nssh -N -L 42137:127.0.0.1:42137 user@your-host\n```\n\n## Manual Advanced Flow\n\nThe old manual flow still works and is useful for debugging.\n\n### 1. Choose the Host Data Directory\n\n```bash\nexport PANE_DIR=\"$HOME/.pane_remote\"\nmkdir -p \"$PANE_DIR\"\n```\n\nPane stores config and database files under that directory. The setup command, desktop app, and headless daemon must use the same `PANE_DIR`.\n\n### 2. Configure the Remote Listener\n\nLaunch Pane on the host against that directory:\n\n```bash\nPANE_DIR=\"$HOME/.pane_remote\" pnpm dev\n```\n\nIn `Settings > Remote Access > Advanced Remote Host`:\n\n1. Enable `Enable remote daemon listener`.\n2. Keep `Listen Host` on `127.0.0.1`.\n3. Keep or change `Listen Port`, default `42137`.\n4. Leave `Require pairing / saved bearer tokens` enabled.\n5. Leave `Allow direct HTTP on loopback` enabled.\n6. Click `Apply Host Settings`.\n\n### 3. Create a Paired Connection\n\nFrom `Pairing tools` on the same page:\n\n1. Enter a label such as `Office Mac mini`.\n2. Enter the base URL the client will use after tunneling, for example `http://127.0.0.1:42137`.\n3. Click `Create Paired Profile`.\n\nPane adds a host-side allowed client record, adds a matching local profile, and shows the generated bearer token once.\n\n### 4. Start the Headless Daemon\n\n```bash\nPANE_DIR=\"$HOME/.pane_remote\" pnpm daemon:headless\n```\n\nOn success:\n\n```text\n[Pane daemon] Headless host ready on unix:<socket path>\n```\n\nOn Windows the endpoint is `pipe:<pipe name>`. This is the local control endpoint, not the remote listener port.\n\n### 5. Connect the Desktop Client\n\nUse `Add connection` for a generated code, or use `Save existing remote profile` under `Pairing tools` with:\n\n- label\n- base URL\n- bearer token\n\nClick `Save Remote Profile`, then click `Connect` on the saved profile.\n\n## Validation\n\nRecommended checks after connecting:\n\n1. Verify projects and sessions load in the client.\n2. Open a terminal-backed session and confirm output streaming works.\n3. Send terminal input and verify the remote runtime receives it.\n4. Resize a terminal and confirm the remote terminal resizes.\n5. Open a file and confirm read/write works.\n6. Check git status and commit/diff flows.\n7. Run an approve-mode command and confirm the permission dialog appears on the client.\n\n## Troubleshooting\n\nStart with machine-readable environment diagnostics:\n\n```bash\nrunpane doctor --json\n```\n\nThe `remoteSetup` section reports stable diagnostic codes and recovery commands\nfor missing AppImage/FUSE support, Electron sandbox restrictions, and missing\nuser service management. `runpane install daemon` automatically supplies the\nLinux headless launch environment. Pane never disables the Electron sandbox\nautomatically; use `--no-sandbox` only when the host requires it and you accept\nthe security tradeoff.\n\n### Remote setup exits because X11 or `$DISPLAY` is missing\n\nUse `runpane install daemon`, or add `--ozone-platform=headless --disable-gpu`\nto a direct packaged launch. This failure occurs before Tailscale setup and does\nnot mean the daemon requires a graphical session. If you previously relied on\n`ELECTRON_OZONE_PLATFORM_HINT=headless`, switch to the flags: Electron removed\nthat variable in 38 and ignores it from 39 on.\n\n### The headless daemon starts but remote connect fails\n\nCheck:\n\n- the daemon uses the same `PANE_DIR` that setup wrote\n- the tunnel/proxy forwards to the same loopback port as the host config\n- the client profile base URL matches the client-side tunnel endpoint\n- the `pane-remote://...` code was not truncated\n\n### A remote action could not be confirmed\n\nThe browser client automatically retries a reviewed set of read-only requests after\ntransient network or server failures. It sends actions such as terminal input and\npane creation once: the host may finish an action even if the response is lost.\nIf Pane says the action may have completed, inspect the current panes or terminal\nbefore submitting it again. Authentication failures stop immediately.\n\nThe browser normally uses native EventSource for output. Its fetch fallback and\nthe desktop transport share one SSE text parser, accepting LF, CRLF, and CR line\nendings across chunks. Each transport decodes UTF-8 incrementally before parsing;\nreconnecting discards the previous stream's incomplete event. Terminal output\nresponses are applied only to the terminal instance that requested them, so a\nlate response cannot clear or write to a terminal disposed during a tab switch.\n\n### I changed host settings but nothing happened\n\nThe headless daemon watches config and starts or stops the remote transport based on saved host config. If behavior looks stale, restart the daemon once and verify the correct `PANE_DIR`.\n\n### Why can’t I bind to `0.0.0.0` or a LAN IP?\n\nThat is intentionally blocked. The current security model is loopback plus a secure exposure layer.\n\n### Why are some actions disabled in remote mode?\n\nSome actions operate on the local desktop client machine rather than the remote workspace. Pane currently disables or keeps local-only behavior for:\n\n- opening a local IDE from the client\n- revealing files in the client OS file manager\n- the native clipboard-image fallback path\n\n### Where does copied terminal text go?\n\nTo the clipboard of the machine you are sitting at. In remote mode, dragging to select text in a terminal copies it right away, with no Copy popover. Programs that copy with OSC 52, such as Claude Code, tmux, and vim, also copy to your machine. Programs in the terminal cannot read your clipboard through OSC 52.\n\n## Current Limitations\n\n- No hosted relay, NAT traversal, or account-based multi-tenant auth\n- No direct non-loopback listener support\n- No full live remote end-to-end CI harness yet\n# Mobile push notifications\n\nThe native companion may register an APNs/FCM token through its existing paired bearer token. Registrations are scoped to that paired client and revalidated before every send. The daemon sends attention alerts for `blocked` transitions and for settled `working → idle` transitions of turns a person started; controls can disable either category per device. See [Native mobile](NATIVE_MOBILE.md) for the operator-only credentials and signing setup.\n"},{"path":"docs/SESSIONS.md","title":"Sessions","text":"# Sessions\n\nSessions are named, ongoing Pane Chats where work intent lives. Each Session\nopens one chat, and its sidebar row can expand to show associated Panes. The\noptional overview keeps the Session name, associated Panes, and recent\nactivity together while the chat remains the focused work surface. A tab\nshares its parent Pane's worktree.\n\nIn the sidebar, `+` opens an agent picker, an optional Session name field, and\nan optional \"What is this Session for?\" field that becomes the Session's goal.\nPane remembers the chosen agent as the default for future Sessions while\nexisting Sessions keep their own agent. A blank name starts as `New chat` or\n`New chat 2` and is renamed once from the first message you send the Session\nagent; a name you type or set later is kept. Each new Session opens as one Pane\nChat and can be renamed later from the optional read-only overview. Expand a Session to see\nits associated Panes and open any Pane in its existing sidebar view.\n\nThe Sessions section below the divider can be collapsed from its header; the\n`+` remains available while it is collapsed. Sessions are opened from this\nsection rather than from a duplicate top navigation shortcut.\n\nA Session is a persistent coordination conversation with its own scratch\nworkspace. Its default profile supports discussion, investigation, decisions,\nand delegation when authorized work benefits from it. No particular skill,\nticket system, model, or Agent Farm installation is required. Users can select\nskills and workflows without Pane imposing an implementation pipeline.\n\n## Upgrading from Pane Chat\n\nExisting Pane Chat opens under Sessions automatically, keeping its name and all\nagent conversations together. Switching agents resumes that agent's existing\npanel and history in the same stable private Session workspace. Opening the\nSession does not submit a prompt or authorize new work.\n\nEarlier development versions split old agent histories into supplemental\nSessions. Untouched imports with only generated workspace files and no competing\nconversation are reunited with the original Session. Imports with files, settings changes, or\nadditional conversations remain separate to preserve that work. Those retained\nSessions receive independent internal owners while keeping their names, folders,\npanel IDs, saved histories, and settings. Interrupted ownership moves resume on\nstartup; no transcript or workspace files are deleted by this migration.\n\n## Launch configuration and profile\n\nSessions use the same built-in launch presets and saved custom commands as\nordinary project/worktree panels, including custom arguments. The application\ndefault seeds new Sessions. Creating a Session remembers its launch command and\narguments for the next creation; cancelling leaves that default unchanged.\nSelecting the agent default clears the remembered custom command. Each Session saves its own launch configuration\nand behavior profile. Changing defaults does not overwrite existing Sessions.\nLaunch changes take effect on the next explicit restart.\nUse **Edit behavior…** during creation or in Session settings to open the\nseparate profile editor. **Back** discards the draft; **Save behavior** applies it\nto the form, which is persisted when you create the Session or save settings.\n\nThe command, behavior profile, and saved task context have separate purposes.\nA command selects how the agent runs; a profile describes how it should behave;\nsaved goals, decisions, and next actions help it respond to later user input.\nCustom commands (including Agent Farm wrappers) retain their arguments. Pane\nonly adds native flags through compatible built-in adapters, an explicit custom\ncommand resume config, or Agent Farm's resume contract (see\n[Resuming custom launchers](#resuming-custom-launchers)). Wrappers keep control of\ninstruction discovery; apart from that one Agent Farm exception, Pane does not\ninfer flags from a wrapper name. A user command that explicitly starts work\nremains the user's choice.\n\nEach Session owns a stable folder at `<PANE_DIR>/sessions/<session-id>/` for\nnotes, plans, attachments, generated instructions, and artifacts. Renaming,\nreopening, and archiving retain this folder. These folders are not Git\nworktrees or security sandboxes and do not expire with OS temporary files.\nProject implementation belongs in an appropriate Pane/worktree.\n\n## Windows and WSL runtimes\n\nOn Windows, **Create Session → Run agent in** appears directly below the agent\npicker when WSL reports installed distributions. Windows is the default on every\ncreation. Choose a named distribution to run the agent there; install the agent\ninside that distribution first. Cursor is available when WSL is selected.\nThe runtime and distribution belong to the Session and survive reopening,\napplication restart, and terminal supervisor restart. Only WSL Sessions carry a\n`runtime` (`\"wsl\"`); a Session on the native host omits it on every OS. Older\nrecords that stored `\"windows\"` for the native host lose the field when read.\nIf creation fails before publishing a terminal owner, Pane removes untouched\ngenerated files so the name can be retried; user files or edits prevent rollback.\n\nWSL Sessions retain their durable files under the Windows Pane data directory.\nTheir agent sees Linux paths such as `/mnt/c/Users/.../.pane/sessions/...`.\nThis uses Pane's existing Windows-to-WSL path conversion and requires the default\n`/mnt/<drive>` automount layout; custom `[automount] root` settings are not supported.\nGenerated `AGENTS.md` points to a Session-specific `runtime-context.md`; this\ncontext takes precedence over the shared Pane Chat routing instructions.\n\nEach WSL Session gets a managed `.pane-runtime/runpane` wrapper on PATH, also\navailable as `$PANE_RUNPANE_BIN`. It invokes this build's bundled Windows CLI via\nPowerShell interop from Windows TEMP, so the CLI can reach this Pane instance's\nnamed pipe. Arguments cross as JSON to preserve quotes and Unicode. Temporary\nfiles beside the bridge carry payloads without Windows command-line size limits\nand are removed when the call exits; stdin remains available to the CLI. File flags\naccept relative or absolute Linux paths (including `--flag=value`); `-` retains\nstdin semantics. `--repo` translates absolute Linux paths and explicit relative\npaths (`.`, `..`, `./...`, `../...`) while preserving repository names and IDs.\nPaths inside JSON request bodies must use Windows paths, and\nWindows paths returned by the CLI can be read in WSL after `wslpath -u` conversion.\nThe bridge pins `--pane-dir` to this instance and preserves the calling Session\nand panel identities. PowerShell is invoked by its mounted Windows path, so\n`appendWindowsPath=false` is supported. WSL interop must be enabled; a Linux-installed RunPane CLI\ncannot replace this bridge for a Windows daemon.\n\nFor CLI JSON creation, `runtime: \"wsl\"` and `wslDistribution: \"Ubuntu-24.04\"`\nselect an installed distribution; omitted runtime selects the native host.\n`runtime: \"windows\"` is still accepted as a legacy name for the native host and is\nnot stored.\n\n## Resuming custom launchers\n\nUse **Enable custom command resume** in Session launch settings or a saved custom\ncommand's editor. This is an explicit contract, independent of executable names.\nAgent Farm is the one exception, described in [Agent Farm](#agent-farm) below.\nChoose a session ID source and provide first-launch and resume templates:\n\n- **Claude:** Pane allocates a UUID and resumes it only when its transcript exists.\n- **Codex / Cursor:** Pane captures the native resume ID from terminal output.\n- **Pane allocates an ID:** for other CLIs that accept a caller-provided ID.\n- **CLI reports its ID:** the CLI or wrapper prints a standalone newline-terminated\n `PANE_AGENT_SESSION_ID=your-session-id` line. IDs may contain letters, digits,\n periods, underscores, colons, and hyphens (up to 256 characters).\n\n`{command}` expands to the saved command; `{sessionId}` expands to the quoted ID.\nLeave the ID placeholder unquoted. For a launcher that forwards native arguments\nthrough `--`, Claude templates could be `{command} -- --session-id {sessionId}`\nand `{command} -- --resume {sessionId}`. Codex could use `{command}` initially\nand `{command} -- resume {sessionId}` on resume. These are user configuration\nexamples, not automatic wrapper detection. Other CLIs can use other syntax.\n\nThe configuration and ID persist with the panel. Without a captured ID, Pane\nuses the first-launch template instead of guessing a latest conversation. Saved\ncustom commands copy their resume settings into new panels; changing the saved\nshortcut does not change existing panels. Session settings support changing or\ndisabling the contract for that Session's next launch. No prompt is submitted just\nto resume. Old conversations whose IDs were never saved cannot be recovered\nautomatically; use an explicit resume command if you know their ID.\n\n### Agent Farm\n\nAgent Farm is the one launcher Pane recognizes by name, because Agent Farm\npublishes this contract for Pane: when `PANE_PANEL_ID` is set it prints\n`PANE_AGENT_SESSION_ID=<launch id>` once, before the agent starts, and documents\n`{command} --resume <id>` as the way to reopen that conversation. Pane keeps the\nfirst ID line of each launch and ignores later ones, which can only be agent\noutput. Every other wrapper still needs an explicit config.\n\nA command counts when it is a single `agent-farm run <profile>` or\n`agent-farm agent <profile>`, optionally preceded by `VAR=value` assignments,\nwith no shell operators, no `--`, and no `--resume` of its own. `npx`, `pnpm dlx`\nand aliases do not count. Such a panel with no explicit config behaves as\n**CLI reports its ID** with templates `{command}` and\n`{command} --resume {sessionId}`: Pane saves the printed ID and relaunches with\nit. The editor says so under the unticked checkbox. An explicit config always\nwins: tick **Enable custom command resume** and set your own templates.\n\nA panel created before this rule usually has no saved ID. On its next relaunch Pane\nmakes one attempt to read the last `PANE_AGENT_SESSION_ID=` line from the\npanel's saved terminal buffer. Agent Farm older than 0.2.20 prints no ID, so\nthose panels relaunch fresh, as before.\n\nTo reopen an older conversation by hand, run\n`agent-farm run <profile> --resume <id>`. The ID is the launch ID (the\n`PANE_AGENT_SESSION_ID` value), shown as `resumeId` in\n`runpane panels list --json`. Agent Farm keeps launch records in\n`~/.local/state/agent-farm/launches/` for 90 days.\n\n## Session sidebar and archive\n\nPinned is the first sidebar category and can contain both Session chats and\nPanes. Right-click a Session, or use the ⋯ button that appears on hover, to pin or unpin it, or turn on **Start pinned** in\nCreate Session to pin it from the start. The dialog remembers that toggle for\nthe next Session (on the desktop in Pane's config, in the remote web app on\nthat device). The pin preference survives restarts; an archived Session stays\nout of Pinned until it is restored. RunPane can set the same preference with\n`sessions pin` or `sessions unpin --session <id|name>`. Create JSON accepts\n`isPinned` (default false), and update JSON accepts it without changing the\npreference when omitted.\n\nDrag a Session up or down in Pinned, in Sessions, or in the compact rail to\nchange its place; a line shows where it will land. With a Session row focused,\n**Alt+↑** and **Alt+↓** move it one place and a screen reader hears its new\nposition. A drag or keypress reorders only the list it starts in: Pinned\nSessions move among themselves, and a Session dragged across the Pinned line\nstays where it was. The order is saved on the host, so every desktop, the phone\nand `runpane sessions list` show it, and it survives restarts. New Sessions\nstart at the bottom. A host that has never saved an order lists Sessions oldest\nfirst. A host running an older Pane cannot save an order: the list snaps back\nand the error says to update Pane on that host. Dragging a Session onto the\nworkspace still tiles it.\n\nClick a Session row to open its chat, or use **Switch to Session: <name>** in the\ncommand palette (`⌘⇧P` / `Ctrl+Shift+P`), which also has **New Session**. A Session with associated Panes shows a\nchevron that expands or collapses them; a Session without Panes shows none.\nEvery Session starts collapsed, including a new one and one that just got its\nfirst Pane, until you press its chevron. Opening the Session's chat, an agent\nadding Panes, or `runpane` focusing one of its Panes leaves it collapsed. Each\ndevice remembers the Sessions you opened, separately for each host: the\ndesktop in its own settings, the remote web app (PWA) in browser storage\n(`pane.remotePwa.expandedSessions@<host id>`). Opening a Session under Pinned\nopens it under Sessions too. The native iOS and Android app does not follow\nthis yet: its Sessions start expanded and it forgets on restart.\nChild Pane rows are indented beyond the Session chat icon and keep the ordinary\nPane actions. Create Pane, opened while a Session is showing, has an\n**Add to <Session>** checkbox, on by default, that files the new Pane under that\nSession. A Pane that belongs to a Session names it in the window title bar as\n\"<Session> › <Pane>\"; click the Session name to go back to it. To file an\nexisting Pane under a Session, right-click the Pane, choose\n**Add to Session**, then pick the Session. **Remove from <Session>** in the same\nmenu takes it out again; the Pane and its worktree are unchanged. Pinned Pane\nrows show the Pane name with its repository muted beside it. Single-line Pane rows omit change counts and PR numbers; the\noptional two-row layout shows those details below the title. Session and Pane\ncontext menus use compact widths.\n\nThe experimental **Attention inbox** (Settings > Advanced > Experimental) turns\nthe Projects list into an Inbox of the Panes that need you: an agent waiting on\ninput or approval, an agent that completed a real turn since Pane started (it\nworked on a prompt and went idle; a freshly started agent does not count), or a\nPane that errored. Session child rows follow the same filter, and a Session row still\ncounts all of its Panes. A row leaves when its agent works again; opening it\ndoes not remove it. Hover a row and choose **Dismiss** (check mark) to hide it\nuntil its agent works again. The row under the Inbox counts the hidden Panes\n(\"3 running · 1 idle\") and **Show all** lists every Pane. Pinned rows are\nunchanged, and ⌘1–9 number the Inbox rows while it is shown.\n\nThe right sidebar has Overview, Files, and Changes tabs. Overview shows the\nSession's blockers, goal, next step, and decisions when they are set, then its\nPanes and recent activity. Agent state changes stay out of Activity; the status\ndot shows them. Files browses the Session workspace. Changes summarizes the\nSession's Panes and opens a Pane for detailed review. The upper-right sidebar\nbutton shows or hides the selected tab. A single Session uses two rows at the\ntop: the window title bar shows its name and controls, and the tab strip sits\ndirectly below it. Session file clicks open editor tabs beside the agent tab in\nthat strip. These tabs can be selected and closed directly, and reopening a file\nrestores its tab.\n\n\n\n\n\nRight-click a Session, or use its ⋯ button, to archive it. An \"Archived · Undo\"\ntoast offers to restore it for a few seconds, and archiving the open Session\nreturns the desktop view to Home. Archiving hides the chat from the active\nSessions list while retaining its identity, conversation history, and Pane\nassociations. Its Panes and worktrees remain accessible through repository\nnavigation. Restore the Session from the sidebar’s Archived section. This is\nseparate from archiving a project Pane and does not delete delegated work.\n\nThe existing Session update contract accepts `archived: true` or\n`archived: false`. List results retain archived records, in the saved order;\nactive navigation filters them. An archived Session keeps its place, so Undo or\nRestore puts it back where it was.\n\n`orchestration-sessions:reorder` takes `{ sessionIds }`. The listed Sessions\ntrade the places they already hold, so a list that shows only some Sessions\n(Pinned, or the active ones while others are archived) rearranges just those;\nunknown ids are ignored. It writes an optional `order` id list beside the\nSessions in `orchestration-sessions.json` (the store stays at version 1) and\nchanges no Session's revision, `updatedAt` or activity. An older Pane reading\nthe file ignores `order`, lists Sessions oldest first, and drops it the next\ntime it writes. Archiving the selected Session chooses an available active\nSession or clears the selection if none remain. Restoring a Session does not\nreplace an unrelated selection.\n\n### Move a Session to another machine\n\nRight-click a Session, or use its ⋯ button, and choose **Move to host…** to\ncontinue it on another machine. The dialog lists your Tailscale machines\nrunning Pane, except the one the Session is on, and your teammates' machines\nrunning Pane, shown with the owner's login after the name. A teammate's machine\nis a target only when its owner set Who can connect to Everyone on tailnet.\nMachines Pane can't reach, or that accept only their owner, are shown disabled\nwith the reason. If none can be reached, it says so:\nopen Pane on the other machine, or sign in to Tailscale. While you view a host\nconnected with a pairing code rather than over Tailscale, the item is disabled,\nbecause Pane can't tell which machine that is. Choosing a machine\nsends the Session's agent a prompt to hand the Session off with\n[`runpane handoff`](RUNPANE_WORKSPACES.md#hand-a-task-to-another-machine) and\nthe `handoff` skill.\n\nThe agent writes a handoff note, and handoff creates a Session on that machine\nwith the same name (numbered \"<name> 2\" if the name is taken), goal and agent.\nIts next action is to read the note, which handoff saved in that machine's\n`~/.pane/handoffs/`. When the new Session uses the same agent, the conversation\nmoves too: its agent session's files are copied to that machine, and the new\nSession resumes that conversation when you first open it. Check that it shows\nthe last message, and if that is unclear, ask it what it was last working on.\nWith a different agent, or `--note-only`, only the note moves and the new\nSession starts fresh from it. Like every Session, it waits for your first\nmessage: switch to that machine, open the Session, and send \"continue\". The\nSession folder and its Panes stay where they are, and the original Session is\nnot archived; archive it from the same menu when you are done with it.\n\n## Bring your existing sessions into Pane\n\nSay \"migrate to Pane\" in any Session, or \"yes\" to the first-run offer above, and\nthe `migrate-to-pane` skill brings the conversations you already have in other\nagent tools (Claude Code, Codex, OpenCode, the Cursor agent, Conductor, or\nanything similar) into Pane tabs:\n\n- It reads session metadata on this machine, including WSL distros and agents\n running in other terminals or editors, and shows one list grouped by\n repository. Running sessions and those active in the last 7 days are\n preselected; older ones can be added. Nothing is created until you confirm.\n- An idle session's checkout is adopted in place (`runpane panes adopt\n --resume`); several sessions in one checkout become tabs in one Pane. A\n session still running elsewhere is forked with its own tool into a new Pane\n on a new branch, with your uncommitted and untracked files copied, and the\n original keeps running untouched.\n- Each tab is resumed by its agent's own resume-by-id and must pass the\n continuity check in the skill's `references/continuity.md` (same native\n session, a restart comes back to it, history loaded, original untouched)\n before it is reported as continued. A session that fails is reported as not\n continued with the reason and next step, never replaced by a fresh agent.\n- It keeps a record in `<PANE_DIR>/migrate-to-pane/record.json`, so running it\n again lists what is already in Pane and creates nothing twice. Archiving an\n adopted Pane keeps your checkout.\n\n## Session startup\n\nCreating, opening, restoring, restarting, or switching the agent in a Session\ndoes not submit a bootstrap message. The terminal waits for user input with\nits role, profile, and Pane capability instructions available. There is no\nautomatic greeting, diagnostic sweep, watcher setup, or continuation of a\nsaved next action. Reopening an already-running Session reconnects without\ninterrupting work the user has already authorized.\n\nThere is one exception, and it happens at most once per Pane data directory.\nThe first time the built-in Pane Chat Session launches while Pane has no Panes\nand no repositories (the sample repository the onboarding dialog clones doesn't\ncount), Pane sends it one prompt, which says that Pane sent it and why. The\nagent then asks whether to bring your existing agent sessions into Pane, and\nstarts only when you say yes. Pane records the offer in the\n`pane_chat_migrate_offer_sent` user preference once a launch has sent it, so a\nrestart or an agent switch never sends it again. See\n[Bring your existing sessions into Pane](#bring-your-existing-sessions-into-pane).\n\nPane may prepare instruction files and terminal infrastructure at startup;\nthat preparation does not require an agent turn. Unattended work and watchers\nare separate, explicitly authorized activities.\n\n## Shared tools and authority\n\nSession orchestrators and regular agent panels share RunPane's coordination\ncapabilities. Use `runpane doctor --json` and `runpane agent-context --json`\nwhen the task requires those capabilities, and discover individual schemas\nwith `runpane agent-context --command \"<command>\" --json`.\n\nAn associated worker completes its assignment and reports results or blockers\nto its owning Session. Reporting is participation. Creating workers, assigning\nwork, redirecting agents, or changing ownership requires a user request or an\nexplicitly delegated task with that authority. Existing authorization persists\nwithin its scope. An independent Pane works locally unless coordination is\nrequested. Tool availability does not authorize unrelated work or automatic\nsharing of other conversations.\n\n## One Pane per feature\n\n**1 feature = 1 worktree = 1 branch = 1 Pane; reviews, audits, fixes and QA for it are tabs in that Pane.**\n\nA feature here means one branch and its PR. A new Pane creates a worktree that\nchecks out the repository, installs dependencies and often builds, so it is\nexpensive, and creating several at once can freeze the machine. An\norchestrator creates a Pane with `runpane panes create` only when the work's\nbranch has no Pane yet: new work, or an existing PR with no Pane.\n\nPane enforces both: `panes create --branch` refuses a branch another Pane has\nchecked out (code `ERR_RUNPANE_BRANCH_HAS_PANE`, naming that Pane), and at most\n2 Panes run setup at once while the rest wait in line.\n\nEvery later agent for that feature runs inside its Pane as a new agent tab:\nimplementer, reviewer, follow-up reviewer, auditor, fix implementer and QA. Each tab is a\nfresh agent with its own context, sharing the Pane's worktree and branch:\n\n```text\nrunpane panels create --pane <feature-pane-id> --tool-command \"<agent command>\" --source agent --no-focus --wait-ready --yes --json\n```\n\nBefore `runpane panes create`, find the work's Pane with\n`runpane panes list --repo <repo> --json`, matching the PR's head branch\n(`gh pr view <n> --json headRefName`) against\n`git -C <worktreePath> branch --show-current`, and use it when it exists.\n\nTabs share one checkout. Only the implementer changes the branch or HEAD, one\nwriter at a time, and a fix tab starts only after the previous writer has\nstopped. Review, audit and QA tabs never run `git checkout`, `reset` or\n`stash`. Never create a Pane to review, audit, fix or QA work that already has\none. Keep the\nfeature's Pane until its PR merges or closes, so later tabs still have the\nworktree; archive it then.\n\n## Pane association before delegation\n\nManagement is a Pane-level relationship; tabs inherit the relationship and\nshare the Pane's worktree. Panes the orchestrator creates or adopts with\n`runpane panes create` or `panes adopt` are associated with its Session in the\nsame call: the CLI reads `PANE_ORCHESTRATION_SESSION_ID`, each result item\nreports `association: { sessionId, ok, error? }`, and `--no-associate` opts\nout. A failed association never undoes the new Pane. Before delegating to an\nexisting Pane, or when automatic association failed, the orchestrator\nassociates it explicitly. The supported command is:\n\n```text\nrunpane sessions associate --session <id|name> --pane <pane-id> [--json] [--pane-dir <path>]\n```\n\nVerify the result with `runpane sessions overview` and reuse an existing\nassociation. A Pane already managed by another Session is a conflict: do not\ndetach, reassign, or create a duplicate Pane. When a prompt is passed to `panes\ncreate`, check the item's `association.ok` right away. Keep a Pane attached through idle and completion; do not detach on\ncompletion. Archiving preserves the association.\n\nTo close out finished work, `runpane panes archive --session <id|name> --merged\n[--remove-worktree] [--dry-run] --yes --json` archives each associated Pane\nwhen both hold:\n\n- **Its PR is merged.** Pane looks the PR up by the Pane's branch on GitHub\n (`pr`). An open PR is skipped as `pr-open`, because its review, fix and QA\n tabs still need the worktree. No PR or a PR closed without merging is\n skipped as `pr-not-merged`, so a worker that has pushed but not opened its\n PR keeps its Pane. A PR state GitHub can't confirm is skipped as\n `pr-status-unknown`.\n- **Its worktree holds no unsaved work.** It has no uncommitted or untracked\n changes, and every commit is on its upstream after a fresh `git fetch`, or,\n without an upstream, on a remote-tracking branch or the base branch. A\n failed fetch or unreadable worktree is skipped as `status-unknown`. This\n check runs after the PR lookup, right before the worktree is removed.\n\nEvery skipped Pane has a `skipped.code` and a message with the next step.\n`--remove-worktree` also removes adopted worktrees; local branches are always\nkept. Run it with `--dry-run` first.\n\nBefore mutating, use `runpane agent-context --command 'sessions associate'\n--json` to confirm the wrapper supports the command. If an older global CLI\ndoes not, select and verify the app-compatible dev wrapper from the Pane\nruntime context before proceeding. Never silently continue without the\nassociation or substitute an unverified global/`npx` wrapper.\n\nSessions do not create worktrees and do not edit project implementation files.\nAn association identifies work that a Session coordinates; it does not grant\nimplementation authority by itself. Detach a Pane before assigning it to a\ndifferent Session.\n\n## Worker file access\n\nA worker's agent may be allowed to read only its working directories (Claude\nCode's `permissions.blockReadsOutsideWorkingDirectories`). Its brief and the\nSession files the brief points to must not sit outside them, or the worker\nstops on a permission prompt nobody is watching.\n\n**Briefs and attachments.** `--as-file-pointer` (`panes create`,\n`panels create`, `panels submit`, `agents send`) writes the prompt inside the\nPane's worktree, at `<worktree>/.pane/prompts/<timestamp>.md`, and sends the\none line `Read and follow .pane/prompts/<timestamp>.md`. The path is relative\nto the worktree, where the agent starts: given an absolute path, Claude Code\noften reads it with `cd <worktree>; cat <file>`, which its read check can't\nverify, so it asks. The result's `promptFile` is always absolute. Pane adds `/.pane/prompts/` to the\nrepository's `.git/info/exclude` (never a committed `.gitignore`), so the file\ncan't be committed. `--attach <file>` (repeatable; `panes create`,\n`panels create` and `panels submit`, with `--as-file-pointer`) copies each file\ninto `<worktree>/.pane/prompts/<timestamp>/` and sends\n`Read and follow <path> (attached: <copy>, ...)`, also worktree-relative; the\nresult lists the copies, absolute, in `attachments`. Attach mockups, plans or dispatch notes the worker needs\ninstead of pointing at the Session folder. Without a local git worktree (a\nfolder that is not the top of a checkout, a WSL repo) the file goes to\n`<pane-dir>/prompts/<pane-id>/` as before, and the pointer line names it by\nabsolute path.\n\n**The Session folder.** A Pane associated with a Session gets that Session's\nfolder (`<pane-dir>/sessions/<id>`) as `PANE_SESSION_DIR` in every tab created\nafter the association. The Session's own terminals get it too.\n`PANE_ORCHESTRATION_SESSION_ID` keeps its meaning: it is set only in the\nSession's own terminals, never in a worker's. Workers read the folder and never\nwrite it; the orchestrator is its only writer. What each built-in template gets:\n\n| Agent | Read access to the Session folder |\n| --- | --- |\n| Claude Code (`claude --dangerously-skip-permissions`) | Pane adds `--settings '{\"permissions\":{\"additionalDirectories\":[\"<folder>\"],\"deny\":[\"Edit(/<folder>/**)\"]}}'`. `additionalDirectories` (like `--add-dir`) makes the folder a working directory, which grants read and write; an `allow` rule alone does not lift `blockReadsOutsideWorkingDirectories`. The `Edit` deny rule takes the file tools' write access back (it covers Write too; Claude ignores `Write(path)` rules). Under bypass permissions Bash can still write anywhere, so the read-only rule is a guard, not a sandbox. The folder's `CLAUDE.md` is not loaded. Not added on Windows. |\n| Codex (`codex --yolo`) | Nothing to add: `--yolo` runs without a sandbox, and every Codex sandbox mode reads the whole disk. Codex's `--add-dir` grants write access, so Pane does not pass it. |\n| Cursor, OpenCode | Only `PANE_SESSION_DIR`. |\n| `--tool-command` wrappers (agent-farm) and custom commands | Only `PANE_SESSION_DIR`; the wrapper maps it to its harness's flag. |\n\nThe flag is fixed when the tab is created: a tab opened before its Pane was\nassociated has no `PANE_SESSION_DIR` until a new tab is created.\n\nWrite the folder's absolute path into the brief rather than\n`$PANE_SESSION_DIR`. Under `blockReadsOutsideWorkingDirectories` Claude Code\nasks before any Bash command whose variables it cannot check, so a worker that\nruns `echo $PANE_SESSION_DIR` or `cat \"$PANE_SESSION_DIR/...\"` stops on a\nprompt. The variable is for wrappers and scripts.\n\n## Stable interfaces\n\nThe shared record and service are:\n\n- `shared/types/orchestrationSession.ts`\n- `main/src/services/orchestrationSessionManager.ts`\n- `orchestration-sessions.json` below `PANE_DIR`\n\nThe daemon and Electron IPC channels are:\n\n`orchestration-sessions:list`, `orchestration-sessions:select`,\n`orchestration-sessions:create`, `orchestration-sessions:get`,\n`orchestration-sessions:update`, `orchestration-sessions:reorder`, `orchestration-sessions:set-agent`,\n`orchestration-sessions:associate`, `orchestration-sessions:detach`, and\n`orchestration-sessions:overview`.\n\nSelectors accept a stable Session ID or an exact Session name. The existing\n`pane-chat:*` channels remain compatibility endpoints for the imported legacy\nconversation.\n\nRunPane exposes the corresponding commands:\n\n```text\nrunpane sessions list --json\nrunpane sessions get --session <session-id-or-name> --json\nrunpane sessions overview --session <session-id-or-name> --json\nrunpane sessions create --from-json <path|-> --json\nrunpane sessions update --session <session-id-or-name> --from-json <path|-> --json\nrunpane sessions pin --session <session-id-or-name> --json\nrunpane sessions unpin --session <session-id-or-name> --json\nrunpane sessions set-agent --session <session-id-or-name> --agent <agent> --json\nrunpane sessions associate --session <session-id-or-name> --pane <pane-id> --json\nrunpane sessions detach --session <session-id-or-name> --pane <pane-id> --json\n```\n\nUse `--from-json` for structured create and update input. Keep multiline\ncontext, evidence, links, and reports in the JSON file or stdin; do not put\nexternal text into shell source. Re-read mutation results and inspect the\noverview after associations or updates.\n\n### Session JSON shapes\n\n- `decisions` and `blockers` are arrays of strings.\n- `evidence` and `outputs` are arrays of links:\n `{\"label\", \"url\", \"addedAt\", \"kind\"?, \"provenance\"?}`. `url` starts with\n `https://`, `http://`, `file://` or `grain://`; `addedAt` is an ISO 8601\n date; `kind` is `evidence`, `output`, `ticket`, `pull-request` or `other`.\n- `report` (update only) is `{\"summary\", \"status\": \"reported\"|\"verified\",\n \"evidence\": [at least one link], \"reportedAt\", \"provenance\"}`; `null`\n clears it.\n- Arrays replace the stored value, so send the whole list. Each array holds at\n most 100 items and each text at most 16000 characters\n (`shared/types/orchestrationSession.ts`).\n- A bad payload is refused whole with one `ERR_SESSION_PAYLOAD_INVALID` error\n that lists every problem (`outputs[0].addedAt: missing`, ...). The CLI\n checks it before calling Pane, and the daemon checks it again for clients\n such as `runpane-py` (`main/src/services/orchestrationSessionPayload.ts`).\n\n`runpane agent-context --command \"sessions update\"` prints the fields and a\ncomplete example.\n\n## Resume and refresh persisted context\n\nWhen responding to a user task that needs Session state, read\n`PANE_ORCHESTRATION_SESSION_ID` from the current environment. Pane exports this stable identity for\nSession panels, including agent resume paths that do not receive the original\nbootstrap input. Do not infer the Session from a terminal panel ID or from\nconversation text.\n\nWhen the variable is present and the task requires it, reload the saved record\nand then reconcile live\nPane, tab, branch, and evidence state:\n\n```text\nrunpane sessions get --session \"$PANE_ORCHESTRATION_SESSION_ID\" --json\nrunpane sessions overview --session \"$PANE_ORCHESTRATION_SESSION_ID\" --json\n```\n\nRun `get` to recover persisted intent and associations, and run `overview`\nwhen continuing a task after a resume or after a mutation. For workers, this variable does not identify a parent Session. Use `runpane\nsessions list --json` and Pane associations to resolve ownership; never infer\nit from the selected UI Session. Refresh associations before coordinating work. If the\nstable ID cannot be resolved, report the error before taking Session-specific\nactions.\n\n## Identity and overview\n\nEach orchestration Session has a stable ID, a hidden detached Pane session for\nits conversation, and one deterministic terminal panel for each supported\nagent. The imported legacy Pane Chat record retains its legacy internal IDs,\nresume IDs, and terminal buffers. New Session records must not reuse those\nidentities.\n\nLegacy Pane Chat keeps its agent histories together in one Session. Previously\nsplit imports are reunited only when untouched; retained independent imports\nreceive separate hidden owners so their workspace tools cannot cross Sessions.\n\nThe overview joins persisted intent with fresh Pane, tab, branch, worktree,\nagent, and available Git or pull request evidence. Working, idle, stopped,\nexited, missing, and archived states describe activity or availability. They\ndo not prove completion. A completion report must carry inspectable evidence,\nthe report timestamp, and provenance; new activity makes an older report\nstale.\n\n## Named locks for shared resources\n\nWhen several workers share one resource that only one of them may use at a\ntime, such as a test account whose \"approve guest\" banner any logged-in worker\ncan click, name a lock in their prompts. Each worker takes the lock before it\nuses the resource and gives it back after:\n\n```text\nrunpane lock acquire --name testing-account --ttl 30m --wait 1800000 --note \"call QA\" --json\n# ... use the shared account ...\nrunpane lock release --name testing-account --json\n```\n\n- **Owner.** The calling Pane and panel, from `PANE_SESSION_ID` and\n `PANE_PANEL_ID` (or explicit `--pane`/`--panel`). Outside a Pane terminal the\n owner is external and `--note` is required: it names the owner, so pass the\n same note to release.\n- **Scope.** A lock is scoped to the owner's Session when its Pane belongs to\n one (as the Session's orchestrator or an associated Pane), so two Sessions can\n each hold `testing-account`. A Pane outside any Session uses the global scope.\n The scope is fixed when the lock is first taken.\n- **Acquire.** Succeeds when the lock is free, expired, or already held by the\n same owner, which renews the TTL (`renewed: true`). Otherwise it returns\n `ok: false` with `heldBy` and `expiresAt` and exits 1. With `--wait` the call\n blocks in the daemon, which grants the lock to the oldest waiter as soon as it\n is released or expires; the CLI chains daemon calls of up to 120 seconds for\n longer waits.\n- **Release.** Only the owner can release a lock. `--force` releases another\n owner's lock, for example a stuck worker's; add `--session <id|name>` to\n name a Session-scoped lock from outside that Session.\n- **Automatic release.** Pane releases a lock when its TTL (at most 24h) runs\n out, when the owner panel's terminal exits or is closed, and when the owner\n Pane is archived. Quitting Pane does not release locks: the resumed agents\n still expect to hold them.\n- **Storage.** Locks are saved in `locks.json` below `PANE_DIR` with\n temp-and-rename writes, so they survive a daemon restart. Expired locks and\n locks whose owner Pane or panel is gone are dropped when the file loads.\n- **Visibility.** `runpane lock list [--session <id|name>]` lists held locks,\n and `sessions overview` includes `locks`: those scoped to the Session plus\n those its Panes hold.\n\nIf a worker interrupts a waiting `lock acquire`, the daemon may still grant it\nthe lock for up to two minutes; release it, or let the TTL expire.\n\n## Orchestrator guide\n\nThe mechanics of driving workers through Pane, for any orchestrator and any\nharness (a Pane Session, a Claude Code or Codex session in a Pane terminal, or\na wrapper such as Agent Farm), live in one place: the bundled\n[`pane-orchestration`](../main/src/services/paneChatBundle/skills/pane-orchestration/SKILL.md)\nskill. Any agent can read it with\n`runpane docs read --doc skills/pane-orchestration/SKILL.md`, and the home\n`pane` skill points there. It covers one Pane per feature, launching a worker\nas a Pane or tab (including `--tool-command` wrappers, `--initial-input-file\n--as-file-pointer`, `--wait-ready` and reading the delivery result), the one\nwait primitive (`runpane watch --session <S> --as <name> --until-event`) and\nwhat each wake means, worker reports and how a status file pairs with them,\nanswering workers, resuming after a restart or sleep, showing pages beside the\nperson, shared-checkout and port etiquette, handoff, cleanup, and what changes\nafter the next release. The sections below are the reference for each piece.\n\n## Session watcher\n\nWhen monitoring is authorized, use one durable, named watcher per Session. Do not start it merely because a Session opens. `--session` takes the Session ID\nor exact name, and the daemon follows every Pane associated with it:\n\n```text\nrunpane watch --session <session-id> --follow --json\n```\n\nThe daemon resolves the Session once per request and re-reads its\nassociations on every journal read. A Pane associated after the watcher\nstarts is included; a detached or archived Pane drops out, together with any\nline still held for it by `--ready-settle` or `--min-interval`. The Session's own\nhidden owner and orchestrator panels never appear, and an association limited\nto specific panels reports only those panels. There is no re-arm after\n`sessions associate` or `sessions detach`. `--session` cannot be combined with\n`--pane`, an unknown Session fails the watch, and a daemon\nthat predates `--session` fails it with `WATCH ERROR` instead of watching every\nPane.\n\nAfter a Pane restart, an agent panel whose process did not come back is\nreported once as `panel.stopped` (`STOPPED <pane-name> pane <pane-id> panel\n<panel-id>`, `running: false`), never as `agent.ready`. Restart it in place,\nwith the same panel ID and its conversation, using `runpane panels resume\n--panel <panel-id> --wait-ready --yes --json`, or every stopped agent panel of\nthe Session at once with `runpane panels resume --session <session-id>\n--wait-ready --yes --json`. A worker started with `agent-farm run <profile>`\nresumes with `--resume` and the launch ID Agent Farm printed, the same rule a\nrestart uses (see \"Agent Farm\" above). The bulk form restarts `--concurrency` panels at a\ntime (default 3), leaves running panels alone, waits on the resumed panels\ntogether, and returns one result per panel with its readiness, blocker, or\nerror; `--all-stopped` does the same for every Pane. It relaunches finished\nworkers too, so resume single panels when only some should continue.\n\nIf a stopped panel's previous shell or agent process is still exiting, resume\nreturns a `still stopping` error instead of starting a second process. Retry\nafter the old processes exit. This also applies when bounded teardown could\nnot kill a process; bulk resume reports the error for that panel and continues\nwith the others. Archived Panes cannot be resumed, even if archive cleanup failed.\n\nMembership changes are journal entries: `pane.associated` (`JOINED <pane-name>\npane <pane-id> session <session-id>`) and `pane.detached` (`LEFT ...`). The\nSession manager emits them from `sessions associate` (only when the Pane was\nnot already a member) and `sessions detach`. A watch without `--session`\nreceives them only when `--kinds` lists them, so older clients never see an\nunknown kind. Cadence state is keyed by the Session rather than its current\nPanes, so held lines survive membership changes.\n\n### Watching a PR from any Pane\n\n```bash\nrunpane pr watch --pane <pane-id> --url https://github.com/owner/repo/pull/123 --head <full-sha> --json\nrunpane watch --pane <pane-id> --since <generation-returned-above> --epoch <epoch-returned-above> --until-event --json --kinds pr.checks,pr.conflicted,pr.base_changed,pr.base_deleted,pr.head,pr.review,pr.comment,pr.closed,pr.merged,pr.stopped,github.unavailable,github.recovered\nrunpane pr list --pane <pane-id> --json\nrunpane pr unwatch --pane <pane-id> --url https://github.com/owner/repo/pull/123 --json\n```\n\nInside a Pane, registration defaults to its `PANE_SESSION_ID`. Registration\nreturns `generation` and `epoch`; use both for the first wait, including events\nthat arrived between registration and arming. An identical active registration\nretry returns its original generation, including after daemon restart, so a\nlost response cannot skip an unread event. A new head pin or unwatch/re-register\nstarts a new registration. Process every returned NDJSON\nevent, then persist its `gen` and `epoch`. Rearm with `--since <gen> --epoch\n<epoch>`; use `--limit 1` for one event per invocation. Never reuse the initial\nregistration generation after delivering an event, and never combine `--as`\nwith `--since` or `--epoch`. A named cursor is a separate daemon-managed mode.\n\nOrdinary daemon restarts preserve the epoch and unread retained events. On\n`_reset` or `_dropped`, resynchronize current GitHub state before accepting\nsuccess, then persist the control record's `generation` and `epoch`. Ignore\n`replay`/`baseline` records as new events. Retention is bounded; a journal reset\ncannot recover lost history. A consumer crash before saving its cursor can\nreplay an event, so actions must be idempotent. Registration uses the full\nhost/repository/number identity. It pins success to `--head`, or the first\nobserved head; a new head emits `pr.head` and needs a fresh registration before\nsuccess can be reported. Read existing discussions before arming. A wake\nmeans inspect the change; it never authorizes merging.\n\nThe watch also reports when the ground under the PR moves. `pr.base_changed`\n(`BASE CHANGED`) fires when the base branch is retargeted, by a person or by\nGitHub after a stack parent merges and its branch is deleted; `base.from` and\n`base.to` name the old and new base. `pr.base_deleted` (`BASE DELETED`) fires\nwhen the base branch no longer exists; GitHub may close the PR right after.\nBoth arrive at once under `--follow`. Rebase onto the new base and recheck CI.\n\nThe daemon batches up to 25 fingerprints per host every two minutes (plus up\nto ten seconds of jitter), reads details on changes and at a 30-minute\nbackstop, and keeps 10% of GitHub quota for interactive work. It uses `gh auth\ntoken` for cached authentication and direct GraphQL for reads. Session PR\nbranch discovery still uses Pane's existing `gh pr list` lookup.\n\nFailures emit immediately, even while other checks run. Success requires a\ncomplete, nonempty set of checks on the pinned head and all required checks\npassing (all checks when none are marked required). BLOCKED/UNKNOWN merge\nstatus conservatively withholds success, including when reviews are missing.\nFeedback from every account is included: agents and people can share a login.\nChecks passing is a signal to inspect the PR, not proof of complete readiness;\noptional failures can still produce a separate failure event. Merge/close,\neight non-rate-limit failures, or ten comment-only updates stop registration;\narchiving/deleting the Pane removes it. Rate limits pause instead of consuming\nthe failure budget. Outages and recovery are journal events.\n\nFingerprints include individual check outcomes and thread resolution state, so\nequal aggregate counts do not hide swaps or edits. When the fingerprint window\nis truncated (over 100 checks/discussion items or multiple comments in a review\nthread), the daemon reads details each round. Detail reads page through up to\n2,000 checks and retain the latest 100 comments, reviews, threads, and comments\nper thread. Truncated discussions emit `pr.review` with an incomplete-feedback\nreason and withhold success; read the full discussion on GitHub.\n\nBaselines and events are replaced atomically before readers wake. This prevents\na process crash from advancing a baseline without its event. Delivery remains\nbounded by journal retention and cursor acknowledgments; replay after a consumer\ncrash before acknowledgment is possible. This is not an exactly-once or\npower-loss durability guarantee.\n\n\nThe cursor defaults to `session-<uuid>`, where `<uuid>` is the UUID inside the\nSession ID: the Session `__orchestration_session_<uuid>__` uses\n`session-<uuid>`. Passing the name instead gives `session-<name>`. Cursor names\nmay be up to 128 characters (64 before this release); when runpane derives a\nname itself, such as this default or the `PANE_PANEL_ID` fallback, it shortens\none longer than 64 characters (or one with other characters) to\n`<prefix>-<first 12 hex characters of its sha256>`.\n\nA discussion-only Session has no follow watcher. On restart, retain the\n`session-<uuid>` cursor and capture a fresh output baseline before\ninterpreting notifications. After a reset (`_reset`), the baseline restates\ncurrent state: those JSON entries carry `replay: true` (and\n`changedWhileAway: true` after a daemon restart). A replayed `agent.ready` is\nnever READY; re-read `sessions overview` instead of acting on it. Lines mode\nprints only `CHANGED` for them. Return blocked and decision findings to the\nSession conversation. Terminal idle or exit remains activity evidence only.\n\n`--follow` has one default cadence: READY settles for 3 minutes, BLOCKED\nfor 30 seconds, and non-urgent events batch every 10 minutes. IDLE starts at\n10 minutes and backs off to 30 minutes, 1 hour, 3 hours, then daily. Heartbeats\nare off. WATCH OK prints once when armed; WATCH EXPIRED and exit 0 end a watch\nafter 3 quiet hours. Meaningful events reset expiry; IDLE and replay do not.\n\nIndividual duration overrides accept `30s`, `3m`, `3h`, or `0`: `--ready-settle`, `--blocked-settle`,\n`--min-interval`, `--idle-after`, `--expire-after`, `--heartbeat`, and\nwatch `--timeout-ms` (for example, `--until-event --timeout-ms 30s`).\n`--expire-after 0` disables natural expiry. Bare values retain milliseconds,\nexcept heartbeat's legacy seconds. `--json` selects NDJSON; otherwise lines.\n\n`--present` and `--profile` were removed. Use individual timing flags such as\n`--ready-settle 1m --blocked-settle 15s --min-interval 2m` to override the defaults.\n\n```bash\nrunpane watch --session <session-id> --follow --json\nrunpane watch --session <session-id> --follow --ready-settle 1m --blocked-settle 15s --min-interval 2m --json\nrunpane watch --pane <pane-id> --until-event --json\n```\n\nSession membership changes need no re-arm. Named cursors survive daemon\nrestarts; the journal and PR baselines are saved together. RESET/DROPPED mean\nrefresh the overview; replayed baseline entries never mean a new READY.\n`--until-event` waits for REPORT, BLOCKED, EXIT or GONE by default and exits 0\nwhen one arrives, immediately without follow settling or batching. A harness\nthat consumes a named follow stream gets its cadence; a process-exit waiter\nkeeps these immediate single-event semantics. Cadence requires a named consumer\n(explicit `--as` or the name assigned by follow). `--follow --since <generation>`\nhas no consumer name and also bypasses settling and batching. Named `--as`\ncannot be combined with numeric `--since`; switching only the wait mode does\nnot enable cadence for a numeric generation/epoch cursor.\nOverride `--kinds` for PR events. Its `--timeout-ms` is a\ntotal deadline (exit 124); under follow it bounds each daemon request.\n\nREPORT remains the completion signal: workers use `runpane report --state\nready|blocked|failed|done --summary-file <path>`, adding a PR/head and a question\nwhen blocked. READY alone is activity evidence. Read the full report with\n`runpane agents status --panel <id> --json`. Resume stopped workers in the same\npanel with `runpane panels resume --panel <id> --wait-ready --yes --json`.\n\nDo not pipe follow through `head` or `grep -m1` to wait. Run `--until-event` as\na supported background task, act on its events, then re-arm with its cursor\n(the [orchestrator guide](#orchestrator-guide) has the one wait command and\nwhat to do on each wake, timeout, reset and error).\nNever sleep-poll your own background task output. WATCH ERROR/nonzero exit\nmeans failure: re-read every worker's status, then re-arm once; a second\nfailure goes to `runpane doctor --report`. A superseded cursor means keep the\nreplacement. A normal expiry means re-arm with the next dispatch.\n\n\n## Worker reports\n\nWorkers hand back a structured report when they finish or get blocked. Every\nworker prompt ends with:\n\n```text\nWhen finished or blocked, run\nrunpane report --state <ready|blocked|failed|done> --pr <number> --head <sha> --summary-file <path>\n(add --question \"<question>\" when blocked).\n```\n\nInside a Pane terminal `runpane report` finds its own panel through\n`PANE_SESSION_ID` and `PANE_PANEL_ID`; elsewhere pass `--pane` and `--panel`.\n`--question` is required with `--state blocked`, `--pr` is a positive integer,\nand `--head` is 7 to 40 hex characters.\n\nThe daemon then:\n\n- keeps the latest report in the panel's custom state, so it survives a\n restart: `{ state, pr, head, summary, summaryPath, question, reportedAt }`,\n with up to 16,000 characters of summary (a longer one ends with a truncation\n marker and sets `summaryTruncated`);\n- journals an `agent.report` entry with the same fields (summary cut to 2,000\n characters), printed as `REPORT <pane-name> pane <pane-id> panel <panel-id>\n ready pr#747 fc5dce9` or `... blocked: <question>`. It skips the\n `--min-interval` batch. Only watchers that list `agent.report` in `--kinds`,\n and Session watches, receive it, because released CLIs reject journal kinds\n they don't know. It passes `--agents-only` (which `--follow` and\n `--until-event` imply) whatever panel sent it, so a report from a shell tab\n or a wrapper launch whose agent isn't detected yet still arrives;\n- appends a `report` activity (with the worker's `paneId` and `panelId`) to\n every Session the Pane is associated with, which makes an older Session\n report stale;\n- shows the report in `agents status`, `panels list`, and `sessions overview`.\n There each panel carries its own latest report as `panes[].panels[].report`,\n and `panes[].report` is the newest report among the Pane's panels, with its\n `panelId`. When the summary is a complete JSON object (a status file sent\n with `--summary-file`, say), the panel's `report.data` holds it exactly as\n given; a plain-text, array, scalar, invalid, truncated, or more than 64\n levels deep summary has no `data`;\n- returns `liveWatchers`, the number of watches that were waiting and received\n it. The CLI prints `Delivered to 1 waiting watcher.`; with no Session and\n nobody waiting it says the report is stored on the panel and that a\n named-cursor watch receives it when it next waits.\n\nThe orchestrator treats REPORT as the completion signal. A READY without a\nreport means look, and maybe nudge: `runpane panels last-message --panel\n<panel-id> --json` returns the agent's last reply from its Claude or Codex\ntranscript (never the screen; `transcript-unavailable` otherwise), and the\nworker can be asked to run `runpane report`.\n\nAn orchestrator whose harness wakes only when a process exits (Claude Code\nbackground Bash, Codex) waits with the one wait primitive from the\n[orchestrator guide](#orchestrator-guide): `runpane watch --session <S> --as\n<name> --until-event --json --kinds\nagent.report,agent.report.missing,agent.blocked,agent.ready,panel.exited,panel.stopped,pane.gone` (`--pane <pane-id>` outside a Session), run as a background\ntask and run again after each event. `--until-event` exits 0 on the first\nevent (124 at a `--timeout-ms` deadline, if you give one); a `RESET` and the caller's own panel never end\nthe wait. When a worker keeps richer state in a status file, it writes the file\nfirst and reports with `--summary-file <status file>`; the file is the truth\nand the report is the signal.\n\n### Dispatching with `--wait`\n\nThe [orchestrator guide](#orchestrator-guide) prescribes submission without\n`--wait`, followed by its named watch as the completion consumer. The CLI also\noffers an optional per-dispatch result for callers choosing that route instead:\n\n```text\nrunpane panels submit --panel <panel-id> --input-file <prompt-file> --as-file-pointer --wait --yes\n```\n\n`panels create` and `panes create` with a prompt take `--wait` too. `--wait`\nturns the expected report on and blocks on that panel until the first\n`agent.report`, `agent.report.missing`, `agent.blocked`, `panel.exited`, or\n`pane.gone`. It then prints one line of JSON (`dispatchWaitResult` in the\ncontract) and exits:\n\n| `outcome` | Exit | Carries |\n| --- | --- | --- |\n| `report` | 0 | `report`: state, summary, `summaryPath`, PR, head, question |\n| `missing` | 3 | `lastOutput` and `lastOutputSource` |\n| `blocked` | 4 | `screenLines`, the prompt the worker waits on |\n| `exited`, `gone` | 5 | `exitCode` or `reason` |\n| `indeterminate` | 6 | history was lost; inspect the panel and inbox before retrying |\n| `timeout` | 124 | after `--timeout <duration>` (`30m`, `2h`) |\n\nThe result never includes the transcript. The daemon stamps the result of\nevery create and submit with the journal generation and epoch it read before\ndelivering the work. `--wait` adapts the existing `watch --until-event` reader,\nfiltered to the Pane and panel. It reconnects after a daemon restart; a replaced\nor truncated journal returns an explicit indeterminate result (exit 6), since\nthe task's outcome can no longer be established from that history. Like\n`--until-event`, it is a single-event wait: it delivers at once, without\n`--follow`'s settling or batching, and since it exits on the event, a plain\nbackground task is enough (no line-delivering monitor). Claude\nCode wakes the orchestrator when the background call exits, and the line\narrives as the tool result. To answer a blocked worker, send the answer with\n`panels submit --no-expect-report`, then watch for the original work's result.\nReport-expected work is serialized per panel: a new dispatch is refused before\nwriting while an expectation or missing-output collection is outstanding.\nHeld starting input is reserved but does not count startup readiness as task\ncompletion; tracking activates immediately before the prompt's submit or launch.\n\n### Expected reports\n\nWork sent to an agent through runpane expects a `runpane report` back: a\n`panels submit` to an agent's panel, and a `panels create` or `panes create`\nthat carries a prompt. A request marked `--source user` (Pane's own UI marks\nits sends that way) expects none, and neither does typing into a tab, which\nnever goes through runpane. `--no-expect-report` sends fire-and-forget input;\n`--expect-report` asks for a report anyway, also from a plain shell panel.\n`shouldExpectReport()` in `main/src/ipc/runpane.ts` holds the rule. Pane saves `{ since }` per panel in `expected-reports.json` below `PANE_DIR`\n(results echo it as `expectedReport`); only the tracker writes that file, so\npanel-state writes cannot bring back a cleared expectation. When the agent then goes ready (`agent.ready`)\nor its panel exits without a report made at or after `since`, Pane journals\none `agent.report.missing` entry (`MISSING REPORT <pane-name> pane <pane-id>\npanel <panel-id> ready|exited`) and clears the expectation. The entry's\n`lastOutput` is the agent's last reply from its transcript, or else the last\n20 lines on its screen (`lastOutputSource: transcript|screen`), cut to 2,000\ncharacters with the end kept. A report clears the expectation.\n\nAn agent can end its turn while work it started keeps running. Pane reads\nthat from the screen: Claude Code counts running background shells in its\nfooter (`⏵⏵ bypass permissions on · 1 shell`) and lists each running\nbackground subagent under it (`◯ general-purpose <task> 14s`); Codex prints\n`1 background terminal running · /ps to view` above its prompt\n(`detectAgentBackgroundWork()` in `agentScreenSignature.ts`). While one shows,\na READY does not raise the missing report. Claude takes a new turn when the\nwork finishes, and that turn's READY decides. Codex doesn't, so Pane looks at\nthe screen again every 10 seconds while the panel stays ready, and raises\nonce nothing runs. An exit raises at once.\n\nLike\n`agent.report`, the kind reaches only watchers that list it in `--kinds` (a\nSession scope alone does not deliver it), and it skips the `--min-interval`\nbatch. The tracker is `main/src/services/expectedReports.ts`.\n\n### Questions on screen\n\nPane's status detection (`main/src/services/agentStatus/manifests.ts`)\nclassifies a panel as blocked when its screen shows a live prompt waiting on\ninput: Claude's AskUserQuestion picker, permission prompts, folder trust, the\n\"Allow external CLAUDE.md file imports?\" dialog, and Codex's folder-trust,\nsign-in, approval and \"Update available\" screens. The journal records\n`agent.blocked` only once a panel has stayed blocked for 2 seconds\n(`BLOCKED_DEBOUNCE_MS` in `workspaceJournal.ts`), so a prompt-like line that\nflashes past in normal output never becomes BLOCKED. The entry carries\n`screenLines`: up to 20 non-empty lines of the screen region the prompt was\nmatched in. The agent does not need to report anything for this.\n\n### Session inbox\n\nPane keeps, per Session, what its orchestrator has not acknowledged:\nreports (`agent.report`), missing reports (`agent.report.missing`), and\nquestions on screen (`agent.blocked`, with `screenLines`). It is fed from the\njournal for every Pane associated with the Session (`main/src/services/sessionInbox.ts`)\nand saved with a journal epoch and replay watermark in `session-inbox.json`\nbelow `PANE_DIR`. Restart replays committed events after the watermark, with\nstable item IDs, including a report whose inbox write failed. Acknowledgements\nand the watermark are saved together; replay does not resurrect handled items.\nRecovery is bounded by the journal's retention. If entries expired before\nprojection, or the journal was replaced, Pane logs the history loss explicitly;\nalready projected items remain available. A newer report replaces the panel's\nolder report or missing report; a blocked item goes once its panel moves on\n(busy, ready, exited, or reported). Each Session keeps at most 200 items.\n\n```text\nrunpane inbox --session <id|name> [--json] # defaults to $PANE_ORCHESTRATION_SESSION_ID\nrunpane inbox ack <item-id>... [--session <id|name>]\nrunpane inbox ack --all --session <id|name>\n```\n\nAn orchestrator runs `runpane inbox` when it starts or resumes, to catch what\nfinished while it was down. Pane never adds the inbox to the orchestrator's\nturns: worker output reaches it only as the result of a tool call it made.\n\n## Skill contract\n\n`main/src/services/skillCacheManager.ts` installs the skills that ship in\n`main/src/services/paneChatBundle/` into\n`<PANE_DIR>/skills/pane-chat/skills/` and into the `.claude/skills/` and\n`.codex/skills/` folders of the data directory. It also generates the\n`pane-orchestrator` entry skill (plus a Cursor rule at\n`.cursor/rules/pane-orchestrator.mdc`), the runtime context, and the helper\nsubagents in `.claude/agents/` and `.codex/agents/`. A manifest\n(`skills/pane-chat/installed.json`) records what it installed and a hash of the\nbundle: startup skips the install when the hash is unchanged, and later installs\nreplace only the recorded entries.\n\nTo change Pane Chat behavior, edit the bundle or `buildPaneOrchestratorSkill()`\nin `skillCacheManager.ts`, then run `paneChatBundle.test.ts`.\n\nBundled workflow skills are available to Sessions and can be selected according\nto the user's task. The installed manifest lets Pane refresh its own skill files\nwithout replacing unrelated user skills.\n\n`shared/types/sessionProfile.ts` defines the generic default profile and shared\ncapability context. Generated guides and agent instruction files use this\ncontract. The legacy shared Cursor rule is not always applied; Session-specific\ninstructions belong to the Session's working directory. User-selected profiles\nand workflows determine behavior after the user supplies a task.\n\n## Terminal and Files\n\nEach Session has a collapsible **Terminal** dock and a **Files** button beside\nits agent tabs. The shell and file browser use the Session's workspace folder.\nSelecting a file opens an editor tab alongside the agent conversation. Panels\nare saved with the Session and reused when you reopen the tools; switching\nSessions keeps their shells and files separate.\n\nEvery Pane has the same dock: its first plain shell, titled **Terminal**, where\nthe setup progress notices appear. It is not a tool or agent tab. `runpane panels\nlist` marks it `dock: true`, and `runpane panes list` reports a Pane `running`\nonly while one of its tool or agent terminals is live, not for the dock alone.\n\n## Plans, pages, and split view\n\nAgents show pages and files with `runpane panels open --file <path>` or\n`--url <url>`. Files open in an editor tab with read-only previews for supported\nformats; HTML opens as editable source. Use `--url` with an HTTP, HTTPS or file\nURL for a live browser tab. By default the tab opens in split view:\nthe first one splits the stage to the right of the conversation, and later ones\njoin that side group as tabs. A page opened while you are looking at another\nSession or Pane is in that side group when you come back, without taking focus\nfrom the tab you left showing; tabs you have already arranged stay where you put\nthem.\n`--tab` opens it as a plain tab instead. Reopening the same file or URL reuses\nand reloads its tab; without `--tab`, a reopened tab that sits in the\nconversation's group moves to the side group. This works the same in Sessions\nand in worktree Panes.\n\nGenerated Session instructions ask the orchestrator to write plans, reports, and\nother documents as self-contained HTML files in its Session folder and open them\nwith `--url` when rendered HTML is intended, or `--file` to inspect the source.\nOpening a Session never submits a prompt or starts work just to write\na document.\n\nWhile the stage is split, each group has its own tab strip, including the agent\ntab in its group. For a single Session, the strips align directly below the\nwindow title bar. Closing the last tab in the side group returns to a single\ngroup. Layouts are saved with the Session.\n\nDrag any tab to rearrange the stage, as in a project Pane: drop it in the\nmiddle of a group to join that group, against an edge (the outer quarter) to\nsplit beside it, or between tabs on a strip to place it there. Dragging the\nlast tab out of a group closes that group. In tiled Sessions a tab moves only\nwithin its own Session.\n\n## Tiling Sessions side by side\n\n\n\nSessions tile with the same gesture as tabs, one level up. Drag a Session from\nits sidebar row — or by its title row once tiled — onto another Session and drop\nit against an edge to put the two beside each other; drop it in the middle to\nshow it in that slot instead. Edge bands are the outer quarter of the target,\nas they are for tabs, and the layout nests as deeply as you take it: a row of\nSessions with one of them split into a column is an ordinary shape, not a\nspecial case.\n\nBoth halves stay fully live. A tile that is not focused keeps its agent running\nand its output streaming; nothing is throttled or suspended, so a Session you\nare watching out of the corner of your eye is the same Session you were just\ntyping in. Each tile keeps its own tab splits, its own Terminal dock, its own\nFiles and Changes sidebar, and its own navigation.\n\nThe focused tile's Session is the selected one: its title row carries the focus\nring, and clicking a Session in the sidebar focuses its tile when it already has\none, or shows it in the focused tile when it does not. With one tile that is\nexactly the old behaviour — switching Sessions replaces the view.\n\nThe `X` on a tile's title row stops tiling that Session; the Session itself,\nits conversation and its panels are untouched, and the layout collapses back\naround the space. The last tile has no close button, since a window with no\nSession in it has nothing to show. The command palette carries the same actions\nfor the keyboard: focus the Session left, right, above or below, and stop tiling\nthe focused one.\n\nA layout belongs to the host it was built on, like the remembered location it\nsits beside: Session ids only mean anything on their own host, so switching\nhosts loads that host's layout and every tile is revalidated against the\nSessions that still exist there. Archiving or deleting a tiled Session retires\nits tile and collapses the split.\n\n## Reaching Pane and delegating work\n\nThe generated Session instructions tell the orchestrator how to reach\n`runpane` (PATH, then `$PANE_RUNPANE_BIN`, then the runtime context) and forbid\nsubstituting plain git worktrees or built-in subagents for delegated work; if\n`runpane` is unreachable the agent stops and reports it. Worktrees the\norchestrator creates or adopts with `runpane panes create` or `panes adopt`\nbecome the Session's children in the same call.\n\nEarlier development builds had an experimental progress view that rendered\n`progress.html` in a fixed side panel. It never shipped; Pane deletes its old\n`.pane-progress.json` switch and leaves any HTML files in place.\n\nNew worktrees launched from a Session default to unpinned. First association also\nclears a worktree's previous pin, including an explicit `--pinned` creation pin,\nso it appears as a Session child. Use `panes pin` manually afterward; repeated association does not undo that choice. Independent\nworktree defaults and existing historical pins are unchanged.\n\n### Generated instructions and Git\n\nSession instructions and documents live under `<PANE_DIR>/sessions/<id>/`,\noutside the project. While the Session agent shows its folder-trust prompt, a\nbanner across the top of the chat says the folder is Pane's own, holding the\nSession's notes and no project code. Session terminals set `GIT_CEILING_DIRECTORIES` to\n`<PANE_DIR>/sessions`, so git run in a Session folder never picks up a repository\nabove it, such as a home directory tracked as a dotfiles repo. Do not configure\nPANE_DIR inside a project repository.\n\n### Move an existing chat to a Session\n\nRight-click a worktree and choose **Move chat to Session…**, then choose the chat\nif it has multiple agent tabs. You can also use **Move chat to Session** in the top\nbar of a selected Claude or Codex chat. Then enter a unique Session name, and choose **Move chat**. The agent must be idle\nand have a saved conversation ID. Pane stops the old terminal before transferring\nthe existing panel and resumes the same conversation from a private Session\nfolder. No initial prompt is submitted. The original worktree becomes a child;\nits branch, files, and uncommitted changes remain in place. Other tabs stay there.\n\nCursor and custom launch wrappers are not yet supported for promotion. Missing\nhistory, conflicting ownership, and active work are rejected before transfer.\nA durable Session record allows reopening to complete an interrupted transfer\nwithout copying the conversation. Terminal, Files, and split view\ncapabilities are the same as for a newly created Session.\n\nPromotion defaults to the worktree's current name.\n"},{"path":"docs/SHARED_CREDENTIALS.md","title":"Shared integration keys","text":"# Shared integration keys\n\nSet an integration key once and every Pane host you use gets it. A key set on your Mac reaches your Windows PC, and a key you type in the iPhone app reaches every host the phone has saved.\n\n## Which keys\n\n| Key | Where you set it |\n| --- | --- |\n| Deepgram, fal, OpenRouter (voice dictation) | Settings → Integrations on a desktop, or the phone's voice setup |\n| APNs key for iPhone notifications | Settings → Integrations → iPhone notifications |\n| Anthropic, OpenAI | `anthropicApiKey`, `openaiApiKey` in the host's `config.json` |\n\nA key that exists only in a host's environment (for example `FAL_KEY` or the `PANE_APNS_*` variables) stays on that host and is never shared. On that host the environment value wins over a shared one.\n\n## How keys travel\n\nHosts never contact each other on their own. A device that has more than one host saved, whether found over Tailscale or paired with a code, carries the keys between them:\n\n- **The iPhone app** syncs whenever it opens or returns to the foreground, after you save a key from the phone, and when a host reports that its settings changed. It relays the keys and keeps no copy.\n- **A desktop with another host saved** syncs when it starts, when a key changes on it, and when a saved host reports a change. Computers on your Tailscale login are saved from Settings → Remote Access → Your computers. To pair with a code instead, create a connection code on the other host (Settings → Remote Access → Set Up This Machine) and paste it under Settings → Remote Access → Your computers → Add with a Code.\n\nSharing chains: if your Mac is paired with your Windows PC and the Windows PC with a Linux server, a key set on the Mac reaches the Linux server once both syncs have run.\n\nWhen two hosts disagree, the copy that was set most recently wins. Removing a key records when you removed it, so an older copy on another host does not bring it back. Settings → Integrations shows which host each key was set on and when.\n\nA host that is offline keeps its own copy and catches up on the next sync. Hosts running a Pane version from before key sharing are skipped.\n\n## Who can read the keys\n\nOnly your own devices. Over Tailscale, a host answers a request for its keys only from its owner's Tailscale login, even when its workspace lets other people on the tailnet in, so a teammate's computer neither receives nor replaces your keys. With a connection code, the caller must present a valid pairing token, and the host refuses the request when it does not require pairing. Either device can already open terminals on that host, so sharing keys with it exposes nothing new.\n\nKeys are never written to logs or events. They are stored in each host's `config.json` in the Pane data directory, as before.\n\n## Stop sharing\n\nRemove the host from the device, or unpair it. On a host, revoke it under Settings → Remote Access → Set Up This Machine → Host status. On a desktop, delete the saved profile under Connections. On the phone, remove the host. The keys already copied to each host stay there until you remove them, and removing a key on one host removes it from the others on the next sync.\n"},{"path":"docs/SSH_HOSTS.md","title":"SSH hosts","text":"# SSH hosts\n\nPane lists the hosts in your SSH config under **SSH hosts** in the sidebar. Click one and Pane opens a terminal tab that runs `ssh <host>` with your own `ssh`, keys and `ssh-agent`. You need no project, and no coding agent starts.\n\n## Add a host\n\nAny `Host` entry in your SSH config shows up:\n\n```\nHost mini\n HostName 192.168.1.20\n User me\n IdentityFile ~/.ssh/id_ed25519\n```\n\n## Open a host\n\nSSH tabs open in the **SSH view**, which takes the place of the main area, like Pane Chat.\n\n- **Click a host** to show its tab. If the host has no tab yet, Pane opens one.\n- **Right-click a host**, or hover it and click **+**, to open another tab on it.\n- A dot beside a host means it has an open tab.\n- With the sidebar collapsed to icons, the **SSH hosts** icon opens the SSH view. With no tab open, the view lists your hosts.\n\nEach tab is an ordinary Pane terminal. Pane starts its default shell (**Settings → Terminal → Default shell**) in your home folder and types `ssh <host>` into it. Password, passphrase and host-key prompts appear in the tab as they would in any terminal. When the connection ends or fails, you are back at a local prompt. After Pane restarts, your SSH tabs return in the SSH view, and each one runs `ssh <host>` again the first time you open it.\n\n## Which file Pane reads\n\n| System | File |\n| --- | --- |\n| macOS, Linux | `~/.ssh/config` |\n| Windows | `C:\\Users\\<you>\\.ssh\\config` |\n\nPane also reads the files that config pulls in with `Include`, the way `ssh` does: globs such as `Include conf.d/*` expand in sorted order, relative paths start from your `.ssh` folder, and `~` means your home folder. An `Include` counts only when it applies to every host: before the first `Host` or `Match` line, or under `Host *`. An `Include` inside a narrower `Host` or `Match` block applies only to that block's hosts, so `ssh` would not use its entries for a plain `ssh <host>`, and Pane does not list them. If a host from an included file is missing, move its `Include` line to the top of the config. Pane takes the home folder from your user account, as `ssh` does: on macOS and Linux changing `$HOME` does not move it, and on Windows it is the profile folder Windows records for your account.\n\nPane reads the files again on start, when its window comes back into focus, and when you expand the section. After editing the config in another app, switch back to Pane to see the change. After editing it in a Pane terminal, collapse and expand **SSH hosts**. A host you delete from the config stays in the list while it has an open tab, so you can return to that tab; it disappears once you close its tabs.\n\nPane opens only the config and its included files. It never opens, copies or uploads your keys; `ssh` reads them.\n\n## Which hosts appear\n\nEvery name on a `Host` line appears, in file order, so `Host web db` lists both `web` and `db`. Hosts from an included file appear where its `Include` line is. A repeated name appears once. Pane skips:\n\n- Patterns with `*`, `?` or `!`, such as `Host *` or `Host *.internal`.\n- Names that do not start with a letter, digit or `_`, or that contain characters other than letters, digits and `. _ @ : -`. Pane types the name into a shell, so it lists only names every shell passes to `ssh` unchanged; a leading `-` would be an `ssh` option and a leading `@` means something else in PowerShell.\n- Hosts that appear only in `Match` blocks, and the system-wide config (`/etc/ssh/ssh_config`, or `%ProgramData%\\ssh\\ssh_config` on Windows).\n\nIf a host is missing from the list, check it against these rules. A missing or unreadable config lists no hosts; an unreadable included file is skipped. With no hosts and no open SSH tabs, the section and its sidebar icon stay hidden.\n\n## Windows\n\nPane's default shell on Windows is Git Bash when it is installed, otherwise PowerShell or `cmd`. Each shell runs the first `ssh` on its PATH: usually Git's `ssh` in Git Bash, and Windows' OpenSSH client in PowerShell and `cmd` (an optional Windows feature that recent versions install by default). Both read `C:\\Users\\<you>\\.ssh\\config`.\n\nWindows' OpenSSH client refuses a private key that other accounts can read. Make the key readable only by you, replacing `id_ed25519` with your key file:\n\n```powershell\nicacls $env:USERPROFILE\\.ssh\\id_ed25519 /inheritance:r /grant:r \"$($env:USERNAME):R\"\n```\n\n## Remote Pane\n\nWhen this window controls Pane on another machine through [Remote Pane](SELF_HOSTED_REMOTE_DAEMON.md), the list shows the hosts in that machine's SSH config, and `ssh` runs there with that machine's keys and network.\n\nIf a host runs Pane itself, connect to it with Remote Pane. That gives you its projects, files, dev servers at `localhost` and running agents.\n\nThe phone app shows no SSH host list.\n"},{"path":"docs/STATE_MANAGEMENT.md","title":"State Management Guidelines","text":"# State Management Guidelines\n\n⚠️ **IMPORTANT**: Pane follows a targeted update pattern for state management to minimize unnecessary re-renders and network requests.\n\n## Overview\n\nPane uses a combination of Zustand stores, IPC events, and targeted updates to manage application state efficiently. The application prioritizes specific, targeted updates over global refreshes to improve performance and user experience.\n\nSession and project lists live in Zustand (`frontend/src/stores/sessionStore.ts` and related stores). Do not keep a parallel `projectsWithSessions` array in a tree component; `DraggableProjectTreeView` was removed.\n\n## Key Principles\n\n1. **Targeted Updates**: Always update only the specific data that changed\n2. **Event-Driven Updates**: Use IPC events to communicate changes between processes\n3. **Avoid Global Refreshes**: Never reload entire lists when only one item changes\n4. **Database as Source of Truth**: Frontend state should reflect backend state, not override it\n\n## State Update Patterns\n\n### Session Updates\n\n```typescript\n// ❌ BAD: Global refresh\nconst handleSessionCreated = () => {\n void loadSessions(); // Reloads everything\n};\n\n// ✅ GOOD: Targeted store update\nconst handleSessionCreated = (newSession: Session) => {\n useSessionStore.getState().addSession(newSession);\n};\n```\n\nIPC handlers in `useIPCEvents` already call `addSession` / `updateSession` / `deleteSession` on the store. Subscribe with selectors (`useSessionStore(s => s.sessions)`) instead of copying the list into local React state.\n\n### Project Updates\n\n```typescript\n// ❌ BAD: Reload all projects\nconst handleProjectDeleted = () => {\n fetchProjects(); // Network request for all projects\n};\n\n// ✅ GOOD: Remove from local state\nconst handleProjectDeleted = () => {\n setProjects(prev => prev.filter(p => p.id !== deletedId));\n};\n```\n\n## IPC Event Handling\n\nThe application uses IPC events to synchronize state between the main process and renderer:\n\n### Session Events\n\n- `session:created` - Add new session to appropriate project\n- `session:updated` - Update specific session properties\n- `session:deleted` - Remove session from project list\n\n### Project Events\n\n- `project:updated` - Update specific project properties\n\n## When Global Refreshes Are Acceptable\n\n- **Initial Load**: When component mounts for the first time\n- **User-Triggered Refresh**: When user explicitly requests a refresh\n- **Error Recovery**: After connection loss or critical errors\n- **Complex State Changes**: When multiple interdependent items change\n\n## Implementation Examples\n\n### ProjectSessionList.tsx\n\n- Reads sessions from Zustand `sessionStore`\n- IPC events patch the store; the tree re-renders from selectors\n- Avoids a local `projectsWithSessions` array that new-references on every status tick\n\n## Best Practices\n\n1. **Use Store Actions**: Call `useSessionStore.getState().addSession(...)` (or the matching action) from IPC handlers so every subscriber sees one update\n2. **Merge Updates**: When updating objects, spread existing properties to preserve data\n3. **Handle Edge Cases**: Always check if the item exists before updating\n4. **Log State Changes**: Add console logs for debugging state updates in development\n5. **Validate IPC Data**: Ensure IPC events contain expected data structure\n"},{"path":"docs/TIMESTAMP_HANDLING.md","title":"Timestamp Handling Guidelines","text":"# Timestamp Handling Guidelines\n\n⚠️ **IMPORTANT**: Proper timestamp handling is critical for the application to function correctly, especially for prompt duration calculations.\n\n## Overview\n\nPane uses timestamps throughout the application for tracking session activity, prompt execution times, and displaying time-based information. Due to the mix of SQLite database storage and JavaScript Date objects, special care must be taken to ensure timezone consistency.\n\n## Key Principles\n\n1. **Database Storage**: All timestamps are stored in UTC using SQLite's `CURRENT_TIMESTAMP` or `datetime()` functions\n2. **Frontend Display**: Timestamps are parsed as UTC and converted to local time only for display\n3. **Consistency**: Always use the timestamp utility functions instead of manual date parsing\n4. **Validation**: Always validate timestamps before using them in calculations\n\n## Timestamp Formats\n\n- **SQLite DATETIME**: `YYYY-MM-DD HH:MM:SS` (stored in UTC without timezone indicator)\n- **ISO 8601**: `YYYY-MM-DDTHH:MM:SS.sssZ` (with explicit UTC timezone)\n- **JavaScript Date**: Local timezone by default (be careful!)\n\n## Utility Functions\n\nPane provides timestamp utilities in both frontend and backend:\n\n### Backend (`main/src/utils/timestampUtils.ts`)\n\n```typescript\nimport { formatForDatabase, formatForDisplay, getTimeDifference, formatDuration, isValidTimestamp } from '../utils/timestampUtils';\n\nconst timestamp = formatForDatabase(); // ISO 8601 string, UTC\nconst displayTime = formatForDisplay(timestamp); // local time for display\nconst durationMs = getTimeDifference(start, end); // end defaults to now\nconst formatted = formatDuration(durationMs); // \"2m 34s\"\n```\n\n### Frontend (`frontend/src/utils/timestampUtils.ts`)\n\n```typescript\nimport { formatDistanceToNow, isValidTimestamp } from '../utils/timestampUtils';\n\nconst ago = formatDistanceToNow(timestamp); // \"5 minutes ago\"\n```\n\nThe frontend helpers call `new Date()` on their input, so pass them ISO strings\nwith a `Z` (or `Date` objects), never raw SQLite `YYYY-MM-DD HH:MM:SS` values.\n\n## Database Operations\n\nWhen working with timestamps in SQLite:\n\n```sql\n-- Use datetime() for UTC timestamps\nINSERT INTO prompt_markers (timestamp) VALUES (datetime('now'));\n\n-- When selecting, append 'Z' for proper UTC parsing\nSELECT datetime(timestamp) || 'Z' as timestamp FROM prompt_markers;\n\n-- For completion timestamps with NULL handling\nSELECT\n CASE\n WHEN completion_timestamp IS NOT NULL\n THEN datetime(completion_timestamp) || 'Z'\n ELSE NULL\n END as completion_timestamp\nFROM prompt_markers;\n```\n\n## Common Patterns\n\n### Creating a new timestamp\n\n```typescript\n// Backend - for database storage\nconst timestamp = formatForDatabase();\n\n// Frontend - for immediate use\nconst now = new Date();\n```\n\n### Tracking prompt execution time\n\n```typescript\n// When prompt starts\ndb.addPromptMarker(sessionId, promptText, outputIndex);\n\n// When prompt completes\ndb.updatePromptMarkerCompletion(sessionId);\n```\n\n### Calculating duration\n\n```typescript\n// With completion timestamp\nif (prompt.completion_timestamp) {\n const duration = getTimeDifference(prompt.timestamp, prompt.completion_timestamp);\n return formatDuration(duration);\n}\n\n// For ongoing prompts\nconst duration = getTimeDifference(prompt.timestamp); // Uses current time as end\nreturn formatDuration(duration) + ' (ongoing)';\n```\n\n## Common Pitfalls to Avoid\n\n### 1. Never parse SQLite timestamps directly with `new Date()`\n\n```typescript\n// ❌ WRONG - treats UTC as local time\nconst date = new Date(\"2024-01-01 12:00:00\");\n\n// ✅ CORRECT - select it with `datetime(col) || 'Z'` in SQL, or mark it as UTC first\nconst date = new Date(\"2024-01-01 12:00:00\".replace(' ', 'T') + 'Z');\n```\n\n### 2. Always validate timestamps before calculations\n\n```typescript\nif (!isValidTimestamp(timestamp)) {\n return 'Unknown duration';\n}\n```\n\n### 3. Be careful with timezone conversions\n\n```typescript\n// Database stores UTC, display shows local\nconst dbTime = \"2024-01-01 12:00:00\"; // UTC\nconst parsed = new Date(dbTime.replace(' ', 'T') + 'Z'); // parsed as UTC\nconst display = formatForDisplay(parsed); // Converts to local for display\n```\n\n### 4. Handle negative durations gracefully\n\n```typescript\nconst duration = endTime - startTime;\nif (duration < 0) {\n console.warn('Negative duration detected');\n return 'Invalid duration';\n}\n```\n\n## Testing Timestamp Code\n\nWhen testing timestamp-related features:\n\n1. Test with different timezones (especially negative UTC offsets)\n2. Test with daylight saving time transitions\n3. Test with very old and future timestamps\n4. Test with invalid/malformed timestamps\n5. Verify duration calculations are always positive\n"},{"path":"docs/TOOL_PANEL_SYSTEM.md","title":"Tool Panel System Implementation Guidelines","text":"# Tool Panel System Implementation Guidelines\n\n⚠️ **IMPORTANT**: The tool panel system implements a flexible, extensible architecture for managing multiple tool instances per session.\n\n## Architecture Overview\n\nThe tool panel system consists of several key components:\n\n1. **Panel Manager** (`main/src/services/panelManager.ts`): Central coordinator for panel lifecycle management\n2. **Terminal Panel Manager** (`main/src/services/terminalPanelManager.ts`): Specialized handler for terminal panel processes\n3. **Panel Event Bus** (`main/src/services/panelEventBus.ts`): Event communication system between panels\n4. **Panel Store** (`frontend/src/stores/panelStore.ts`): Frontend state management with Zustand\n5. **Panel Components**: React components for rendering different panel types\n\n## Key Implementation Principles\n\n- 1 feature = 1 worktree = 1 branch = 1 Pane; reviews, audits, fixes and QA for it are tabs in that Pane. A Pane owns one worktree and branch; panels (tabs) share it. Agents add review, QA and fix agents for existing work as new panels with `runpane panels create --pane <id>`, never as new Panes. See [Sessions](SESSIONS.md#one-pane-per-feature).\n\n1. **Lazy Initialization**: Panels are created in the database immediately but background processes (like terminal PTY) only start when the panel is first viewed\n2. **State Persistence**: All panel state including terminal scrollback, working directories, and configurations persist across application restarts\n3. **Memory Efficiency**: Inactive panels suspend rendering but maintain background processes\n4. **Event-Driven Updates**: Uses IPC events to synchronize state between main and renderer processes\n5. **Extensible Design**: Architecture supports future panel types beyond terminals\n\n## Panel Lifecycle\n\n1. **Creation**: User clicks \"Add Tool\" → Panel entry created in database → Added to UI\n2. **First View**: User clicks panel tab → Background process initializes → XTerm.js mounts\n3. **Switching**: User switches panels → Previous panel unmounts XTerm → New panel mounts\n4. **Background Operation**: Processes continue running even when panel is not visible\n5. **Deletion**: Panel closed → Process terminated → Database entry removed → UI updated\n\n## Terminal Panel Specifics\n\n- Each terminal panel spawns an independent PTY process using node-pty\n- Terminal metadata (cwd, dimensions, agent session) persists in `tool_panels.state` as JSON, ceilinged at 256 KB per panel and merged key by key inside SQLite\n- Terminal bytes (scrollback, serialized snapshot, alternate-screen frame) persist in `panel_buffers`, capped at 4 MB per panel with the oldest scrollback trimmed first\n- XTerm.js instances mount/unmount based on panel visibility to save memory\n- Working directories are maintained independently per panel\n- Command history and environment variables can be preserved across restarts\n- Cmd-click on macOS or Ctrl-click on Windows/Linux opens `pane://open?pane=<pane-id>&panel=<panel-id>` links in terminal output inside Pane. The panel parameter is optional.\n\n## Database Schema\n\n- `tool_panels` table stores panel configuration and state\n- `sessions.active_panel_id` is the last-used tab per session: the one any client picked most recently. A client with no memory of a Pane opens it there, and `runpane panels list` reports it as active\n- Foreign key constraints ensure panels are cleaned up when sessions are deleted\n\n## Event System\n\n- A client's tab click (`panels:set-active`), `runpane panes focus` and opening a Session view update `active_panel_id` and move no other client. A host- or agent-initiated activation (`runpane panels open`, `panels create`/`panes create` with focus, Pane Chat, new-Pane setup, run scripts) sends `panel:activeChanged` (`PanelActivationRequest`), which only clients already showing that Pane follow\n- The remote web and mobile app remembers its own Pane, Session and tab per host in browser storage. Each desktop remembers its own split and tabs per Pane in its local `ui_state` (`paneLayout.<pane id>[@<host profile id>]`) and its own Session in navigation memory; it opens a Pane on `active_panel_id` and the stored `panel_layout` only when it has no memory of it. Tabs missing from its memory join the primary group, except agent-opened ones (`metadata.openPlacement: 'split'`), which go to the side group, inactive\n- Terminal panels emit `terminal:command_executed`, `terminal:exit`, and `files:changed` events\n- Event bus routes events to subscribed panels (planned for future panel types)\n- Events support future inter-panel communication (e.g., diff panels reacting to file changes)\n\n## Implemented Panel Types\n\nThe tool panel system supports 8 panel types (`ToolPanelType` in `shared/types/panels.ts`):\n\n1. **Terminal** (`terminal`): PTY shells, several per pane. Agent CLIs such as\n Claude Code, Codex and Cursor Agent run in terminal panels\n (`TerminalPanelState.agentType` records which one).\n2. **Diff** (`diff`): git diff viewer. Singleton, permanent, worktrees only.\n3. **Explorer** (`explorer`): file tree.\n4. **Editor** (`editor`): Monaco editor, one tab per open file.\n5. **Logs** (`logs`): script execution output. Singleton.\n6. **Dashboard** (`dashboard`): project overview. Singleton, permanent,\n projects only.\n7. **Setup Tasks** (`setup-tasks`): project setup checklist. Singleton,\n permanent, projects only.\n8. **Browser** (`browser`): embedded browser. Worktrees only.\n\n## Panel Capabilities\n\nEach panel type has specific capabilities defined in `PANEL_CAPABILITIES`:\n\n- **requiresProcess**: Whether panel needs background process (terminal, logs)\n- **singleton**: Only one instance per session (diff, logs, dashboard, setup-tasks)\n- **permanent**: Cannot be closed by user (diff, dashboard, setup-tasks)\n- **canAppearInProjects**: Available in project view (all except diff and browser)\n- **canAppearInWorktrees**: Available in worktree sessions (all except dashboard, setup-tasks)\n- **canEmit**: Events this panel can produce\n- **canConsume**: Events this panel listens to\n\nExample: Diff panels are singleton + permanent + worktree-only, consuming `files:changed` and `terminal:command_executed` events to auto-refresh.\n\n### Named custom launch profiles\n\nIn a worktree's **Add Tool (+)** menu, choose **Add custom command…** and enter\nan optional **Name** alongside the command. **Save & launch** saves the profile\nand opens a terminal tab with that name. Leaving the name blank uses the first\nthree words of the command. Saved profiles are shared through app configuration.\nUse the pencil beside a saved profile to rename it; the menu and future tabs use\nthe new name. Existing tabs keep their titles. Hover a profile to see its full\nname and command.\n\nResume commands retain the original shell quoting and environment assignments when removing a launch prompt. Custom Claude launchers may choose a separate configuration directory; Pane trusts their recorded conversation IDs instead of checking the app configuration for their transcripts.\n"},{"path":"docs/UPDATE_RECOVERY.md","title":"Update prerequisites, status and recovery","text":"# Update prerequisites, status and recovery\n\n`runpane update` defaults to the running Pane app and the CLI that received the\ncommand, on the machine where the command runs. A remote host selected in the UI\ndoes not change that target. Use `--pane-only` or `--cli-only` for one component.\nRun `runpane update --dry-run --json` first to see the executable, data directory,\nCLI owner and supervisor capability without starting an update.\n\n## Prerequisites\n\nBoth the running app and the requested app release must support continuity\nprotocol 1. No existing 2.4.182 release has the required `continuity.json`\nmanifest. To bootstrap this feature, install a continuity-capable release with\nits normal installer before using this path; installing the CLI alone does not\nadd support to an older running app. A requested older target without that manifest is refused before shutdown.\n\nUse an account that owns the app and CLI directories, in an interactive login\nwith launchd (macOS), Task Scheduler (Windows), or user systemd (Linux). The app\nmust be a packaged macOS bundle, Windows installation, or owned Linux AppImage.\nSystem package installations on Linux require their package manager. App updates\nrequire platform signature verification on macOS and Windows. The official\nWindows 2.4.182 installer is unsigned and is refused by this verifier.\n\nInstall standalone Node 22 or newer and make it available on PATH to the process\nstarting the update. The helper copies this runtime before shutdown; Electron\ninside the app being replaced is insufficient. npm must also be available when\nupdating a JavaScript CLI. The supported CLI owners are:\n\n- Pane's managed CLI, normally under `<pane-dir>/bin`; an explicit CLI-only\n update remains selected when an older Pane next starts.\n- A writable global npm `runpane` installation in the prefix reported by its\n own npm. Local project packages and npx cache copies are refused.\n- The invoking Python environment's writable `runpane` package, with its own\n `python -m pip` available. Virtual environments, including pipx environments,\n and user-site installs are identified by their actual package paths. A pip\n dry run must accept the staged wheel. The Python wrapper also requires the\n continuity engine installed by Pane under the same `--pane-dir`; pip alone\n does not install that engine or Node.\n\nThe helper checks directory permissions, data/profile readability and free\nspace on each affected volume. Windows can exclusively lock live browser-profile\nfiles; their sizes are counted before shutdown, and the snapshot must successfully\nread them after clean shutdown before installation is allowed. It stages downloads\nbefore shutting down Pane.\nA successful dry run checks local prerequisites; it does not download or prove\nthat the requested release passes artifact verification.\n\n## Status and exit codes\n\nKeep the operation's `id` and use the same `--pane-dir`:\n\n```sh\nrunpane update --json\nrunpane update --status <id> --pane-dir <path> --wait --json\n```\n\nThe status command reads the local journal even while Pane is stopped, provided\nthe invoking CLI and its runtime can still launch. If the normal command fails\nbecause the app or CLI was damaged, read `<pane-dir>/updates/<id>/status.json`\ndirectly in a text editor. On macOS/Linux, use\n`cat \"<pane-dir>/updates/<id>/status.json\"`; in PowerShell, use\n`Get-Content -Raw '<pane-dir>\\updates\\<id>\\status.json'`. Replace the placeholders\nwith the original data directory and operation ID. This read does not start or\nresume an update. If the ID was lost, inspect the directories under `updates`\nand match the journal's ID and host with `preflight.json`; do not guess ownership\nfrom timestamps alone. A missing or unreadable journal requires maintainer help.\n\nThe command's JSON\nobject has these fields; this result is documented here and is not currently a\nnamed schema in the agent contract's `jsonSchemas` collection.\n\n| Field | Meaning |\n| --- | --- |\n| `id`, `host`, `scope`, `version` | Operation identity, local machine, `both`/`pane`/`cli`, and requested or resolved release |\n| `phase` | Current stage or terminal outcome |\n| `completed` | Stages durably recorded as finished |\n| `appUpdated`, `cliUpdated` | Selected component installation finished; neither implies host or panel readiness |\n| `hostReachable` | Relaunched host passed version, identity and endpoint/auth checks; false for CLI-only updates |\n| `restored` | Per-panel `{panelId, outcome, reason}` records; `resumed`, `preserved`, or `needs-attention` |\n| `error` | Failure explanation, when present |\n| `updatedAt`, `helperPid` | Last recorded time and acknowledged helper PID; PID presence alone does not prove liveness |\n| `statusFile`, `next` | Journal location and command to inspect it again |\n\nStages are `accepted`, `stage`, `checkpoint`, `stop`, `snapshot`, `install`,\n`launch`, `health`, and `restore`. Terminal phases are `complete`, `failed`, and\n`needs-attention`. `complete` means all selected components and applicable health\nand restoration gates finished; preserved idle/blocked panels are intentional.\n`needs-attention` can mean a partially updated installation or individual panels\nrequiring manual action. Inspect the booleans, completed stages, error and every\npanel outcome together.\n\nExit 0 means the returned status is not a terminal failure. Without `--wait`, it\ncan still be in progress. `failed` and `needs-attention` return exit 1. Command or\npreflight errors also return nonzero. `--wait` has no overall timeout. Ctrl+C stops\nthe waiting caller, not the independent operation; inspect status before retrying.\nThere is no supported operation cancellation command. A fresh update runs\npreflight before checking locks. If preflight succeeds and an existing lock is\nfound, it returns that operation's status instead of starting another install.\nDuring downtime, preflight may fail before reaching the lock. Use `--status`\n(which bypasses preflight), or read the journal directly, to inspect recovery.\n\n## What happens to agent work and drafts\n\nOnly a panel recorded as actively working, with an exact conversation ID and a\nsupported resume command, is eligible to reopen. Built-in launch resolution\ncovers Claude, Codex and Cursor; custom commands need an explicit resume template\ncontaining `{sessionId}`. Eligibility is not a promise of success: `resumed`\nrequires fresh conversation-identity and readiness evidence from the running\nharness. An incompatible command or missing evidence yields `needs-attention`.\n\nReadiness requires an explicit adapter acknowledgment after the harness has\naccepted the exact conversation and is usable. The relaunched process receives\na fresh `PANE_UPDATE_RESUME_NONCE`. An adapter may emit one complete line\n`PANE_UPDATE_RESUME_READY=<nonce>:<actual-loaded-conversation-id>` only after that\nacceptance. The nonce binds the acknowledgment to this launch; an old marker,\nwrapper ID line, generic output, startup animation or CLI-ready timeout does not\nconfirm readiness. Login/trust blockers and command exit prevent success.\nDo not print this marker merely because a wrapper started or received a resume ID.\n\nThe built-in Claude/Codex/Cursor launch commands do not currently provide this\nacknowledgment themselves. Without an instrumented harness adapter, reopening\nthese conversations reports `needs-attention`; inspect the live panel manually.\nCustom reported/generated commands can integrate the same acknowledgment. The\nnative QA adapter proves this protocol with a harmless process, not a vendor\nlogin or conversation acceptance.\n\nReopening the conversation does not submit the interrupted task again. The agent\nmay return to its prompt; review it and decide what to send. Idle, completed and\napproval-blocked work stays stopped. The updater sends no `continue`, prompt,\nqueued mutation, or draft input.\n\nBefore shutdown, the checkpoint saves each panel's prior state, activity, launch\nprofile, conversation ID and launch/worktree/Session cwd. Unsubmitted composer\ntext detected in the terminal prevents automatic reopening. Inspect that panel's\n`priorState.screenText`, `initialInput` and `initialInputFile` in `checkpoint.json`\nfor the saved evidence. Screen text can contain other terminal content and is not\na promise of a complete editable draft. Recover and verify the intended text\nmanually before submitting anything. Do not share checkpoint files publicly.\n\n## Recovering an interrupted operation\n\nAll paths below use the same Pane data directory supplied with `--pane-dir`.\nThe operation directory is `<pane-dir>/updates/<id>/`:\n\n| File or directory | Purpose |\n| --- | --- |\n| `status.json`, `preflight.json`, `staged.json` | Progress, original executable/data/profile and CLI paths, staged package/backup paths |\n| `checkpoint.json`, `restoration.json` | Prior panel state and durable per-panel launch claims/outcomes |\n| `quiesced.json` | Original host's clean-shutdown acknowledgment |\n| `snapshot/data`, `snapshot/profile`, `snapshot/complete.json` | Pre-install copy; only the complete marker establishes a finished snapshot |\n\n### Inspecting the supervisor and remaining processes\n\nUse the operation ID from the journal. These commands inspect state; they do\nnot stop jobs or clear locks:\n\n| Platform | Supervisor and logs |\n| --- | --- |\n| macOS | `launchctl print \"gui/$(id -u)/com.pane.update.<id>\"`; read `<operation-directory>/supervisor.log` and `supervisor.plist` for the executable and arguments. |\n| Windows | `schtasks /Query /TN \"com.pane.update.<id>\" /V /FO LIST`; inspect `<operation-directory>/supervisor.xml`. Task Scheduler's History tab or the `Microsoft-Windows-TaskScheduler/Operational` event log may provide execution history if enabled. This adapter does not create a stdout/stderr log file. |\n| Linux | `systemctl --user status \"com.pane.update.<id>.service\"` and `journalctl --user -u \"com.pane.update.<id>.service\"`; transient units may already have been collected. |\n\nWindows relaunch can also create `com.pane.update.<id>-app`; inspect that task\nwhen present. A completed or missing supervisor is not proof that a child\ninstaller has exited. On macOS/Linux use `ps -axo pid,ppid,command`; on Windows\nuse `Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,ExecutablePath,CommandLine`\nin PowerShell. Compare full paths and arguments with this operation's staged\nruntime/helper, `staged.json` installer path and `preflight.json` installation.\nCheck helper children and installer processes separately; a reused PID, generic\nprocess name or stale Task Scheduler result is insufficient. If processes cannot\nbe attributed confidently, access is denied, or logs contradict the journal,\nleave locks and snapshots intact and ask a maintainer to inspect them.\n\n### Recovery steps\n\n1. Read status before starting another installer. If it is progressing, let it\n finish. If it stops progressing, inspect the helper supervisor and logs. A\n missing caller or terminal is expected; a PID alone is insufficient to decide\n the helper is dead. Do not remove locks while any helper/installer is active.\n2. For `failed` before shutdown, Pane remains available and the terminal operation\n releases its locks. Correct the reported preflight/staging problem and retry.\n3. For `needs-attention` with `hostReachable: true`, open the same installed Pane\n and inspect each panel outcome. Do not replay input whose delivery is unknown.\n Use the panel's normal explicit resume/restart control after reviewing its saved\n conversation and draft evidence. Already resumed panels must not be restarted\n merely because another panel needs attention.\n4. If Pane is stopped, use `preflight.json` to identify the exact executable,\n `paneDir` and `userData`. After establishing that no update helper/installer is\n still acting, launch that installation with `PANE_DIR` set to `paneDir` and\n `--user-data-dir=<userData>`. If the executable is missing or damaged, repair\n that same installation with the same or newer compatible official installer.\n Keep the operation directory and snapshot while diagnosing the failure.\n5. Never replace a migrated database with the old snapshot while the newer app is\n running. A rollback needs a compatible application and a complete data/profile\n snapshot restored together while all relevant processes are stopped. This\n updater does not automate or validate such a rollback. Get maintainer help\n when compatibility or installer state is uncertain.\n\nA post-shutdown `needs-attention` operation deliberately keeps its locks. There\nis no automated recovery/lock-clear command. After recovery is verified and all\noperation-owned helpers/installers have exited, the installation owner or a\nmaintainer can archive the operation and move aside only locks whose `owner.json`\ncontains this exact operation ID. Lock directories are listed in\n`preflight.json` as `lockDirectory` and `additionalLockDirectories`. Preserve the\njournal/checkpoint/snapshot; do not delete another operation's locks or infer\nownership from a reused PID. Incomplete or ambiguous locks require the same\nmanual inspection. If recovery of a crashed lock cleanup is itself interrupted,\nits persistent cleanup claim also requires manual inspection; another caller\nwill not steal it or report a new update as completed. Logout/reboot continuation\nis not guaranteed.\n"},{"path":"docs/UX_PRINCIPLES.md","title":"UX principles: progressive disclosure","text":"# UX principles: progressive disclosure\n\nGuidelines for Pane's UI design, informed by Linear and Claude Code CLI patterns.\n\n## Core Principles\n\n### 1. Prompt-First, Options-Second\n\nEvery interaction should lead with the primary action. For session creation, that's the prompt. For settings, that's the most common toggle. Everything else is secondary and should be layered behind progressive disclosure.\n\n**Do:** Auto-focus the primary input. Show one clear action.\n**Don't:** Present a wall of fields, cards, and sliders on first load.\n\n### 2. Opinionated Defaults Over Choice Overload\n\nProvide one good default rather than forcing a choice. Users interpret defaults as expert recommendations (the \"default effect\"). When 80%+ of users will pick the same option, make it the default and let the rest override.\n\n**Do:** Default to 1 session. Auto-generate names. Remember the last choice where a choice is needed.\n**Don't:** Present every agent as an equal-weight card that must be picked every time.\n\n### 3. The 80/20 Rule\n\nShow the 20% of features that 80% of users need. Hide the rest behind expandable sections, overflow menus, or context menus. Every visible element competes for attention.\n\n**Do:** Show prompt + create button. Put model selection, session count, branch config behind \"Session options.\"\n**Don't:** Show session count slider, tool cards, model dropdowns, and commit mode all at once.\n\n### 4. Minimal Status Indicators\n\nMatch Claude Code CLI's simplicity. Status should be informative without being distracting. Simple text beats animated spinners. A color-coded dot beats a pulsing badge with rotating icons.\n\n**Do:** Show `Thinking...` with subtle animated dots.\n**Don't:** Show spinning rings, bouncing dots, rotating icons cycling through 8 images, and 41 wacky status messages.\n\n### 5. Progressive, Not Hidden\n\nProgressive disclosure is not about hiding features — it's about layering them. Users should always see a clear affordance that more options exist (chevron, \"More options\" link, overflow menu icon).\n\n**Do:** Use `▸ Session options` with a chevron that expands to reveal fields.\n**Don't:** Remove the session count slider entirely. Don't bury settings with no visual indicator.\n\n## Patterns to Follow\n\n### Linear's \"One Good Way\"\n\nLinear deliberately constrains flexibility in favor of purpose-built workflows:\n- Issue creation needs only a **title** — everything else is optional\n- Smart pre-population (highlighted text auto-fills issue title)\n- Opinionated at the atomic level (labels and due dates are issue properties, not configurable)\n- Purpose-built over flexible\n\n**Pane equivalent:** Creating a pane needs nothing typed: the name is auto-generated, and the agent and prompt are optional.\n\n### Claude Code CLI's Minimalism\n\n- Shows simple `thinking...` during processing — no visual noise\n- Thinking content hidden by default, revealed via `Ctrl+O`\n- Status line is optional and customizable\n- Updates debounced at 300ms to prevent visual flicker\n\n**Pane equivalent:** Keep status dots simple. No animations unless state actually changes.\n\n### Collapsible Section Pattern\n\nUse for grouping secondary options:\n- Clear heading that describes what's inside\n- Chevron indicator (▸ collapsed, ▾ expanded)\n- Remember expansion state in user preferences\n- Keep disclosure levels below 3 (avoid nesting collapsibles inside collapsibles)\n\n### Overflow Menu Pattern\n\nUse when 3+ utility actions compete for toolbar space:\n- Keep the most-used action visible (with keyboard shortcut)\n- Group remaining actions behind `⋯` (MoreHorizontal) icon\n- Menu items should have icons + labels\n- Include state indicators where relevant (e.g., \"Sort: Newest first\")\n\n## When NOT to Simplify\n\nProgressive disclosure fails when critical information is hidden:\n- Never hide destructive actions behind progressive disclosure without confirmation\n- Don't hide information users need to make decisions (e.g., hiding that a session will use Codex instead of Claude)\n- Don't over-simplify settings that have security implications (API keys, permission modes)\n- If sales/engagement drops after hiding something, bring it back — trust trumps minimalism\n\n## Visual Design Principles\n\n### Reduce Visual Noise\n- Fewer colors, fewer animations, fewer competing elements\n- Use whitespace to create hierarchy\n- Sequential content flow (top-to-bottom)\n- One primary action per view\n\n### Animations\n- Only animate to communicate state changes (not for decoration)\n- Respect `prefers-reduced-motion` OS setting\n- Debounce status updates (300ms minimum)\n- Subtle > flashy. Text > spinners. Dots > bouncing balls.\n\n### Color\n- Use color sparingly and consistently for status:\n - Green: running/success\n - Yellow: waiting/attention needed\n - Red: error\n - Blue: new/unviewed activity\n - Gray: completed/inactive\n\n## References\n\n- [The Linear Method: Opinionated Software](https://www.figma.com/blog/the-linear-method-opinionated-software/)\n- [How we redesigned the Linear UI](https://linear.app/now/how-we-redesigned-the-linear-ui)\n- [Progressive disclosure in UX: Types and use cases (LogRocket)](https://blog.logrocket.com/ux-design/progressive-disclosure-ux-types-use-cases/)\n- [Designing for Progressive Disclosure (UXmatters)](https://www.uxmatters.com/mt/archives/2020/05/designing-for-progressive-disclosure.php)\n- [The Default Effect in UX](https://www.ux-bulletin.com/default-effect-in-ux/)\n- [Claude Code CLI Status Line Docs](https://code.claude.com/docs/en/statusline)\n"},{"path":"docs/assets/visual-style.md","title":"dcouple README visual language","text":"# dcouple README visual language\n\nUse a shared pixel-art world with a distinct setting for each product. The family\nreference is the [Agent Farm banner](https://github.com/greenfield-inc/agent-farm/blob/19f1fba8ef12050eef39c1b44e725c697b57b87b/docs/assets/agent-farm-banner.png).\n\n## Shared rules\n\n- Crisp coherent pixels, readable silhouettes, and isometric miniature scenes.\n- Charcoal midnight backgrounds, warm cream lettering, and soft lantern light.\n- Cream robots with dark faceplates and green eyes; clothes and props reflect their role.\n- Restrained foliage, wood, stone, and floating islands connect the settings.\n- Wide 3:1 banners: title left, scene right. Supporting images are roughly 2:1.\n- Use each project's own setting, props, and accent colors. Avoid repeating the same farm under a new title.\n- Keep technical labels and arrows accurate. Preserve real screenshots, demos, commands, and detailed diagrams.\n- Do not depict planned features as shipped, implicit sharing as public, or required human approval as automatic.\n- Keep alt text and text equivalents for information-bearing diagrams.\n\n## This repository\n\nA multilevel terminal workshop with three separate well-lit work booths, each holding a cream robot and terminal; a human coordinator views the booths from a central desk. Tiny branch-shaped paths lead to each booth, suggesting separate workspaces. Teal and copper accents, no musical props or farmer hats.\n\nLocal reference: [pane-banner.png](pane-banner.png).\n\nSupply that banner as an image reference for future generation. Match its pixel\ntreatment, characters, lighting, and palette while adapting the scene to the\nsection. Save final assets in `docs/assets/` and add the prompt to the\ngeneration record below; inspect labels and readability at README display width\nbefore embedding. Use new filenames for experiments rather than overwriting\nchosen art.\n\n## Generation record\n\nGenerated with the built-in image-generation tool, using the Agent Farm banner as\nthe style reference.\n\n### pane-banner.png\n\nUse case: stylized-concept\nAsset type: GitHub README hero banner, 3:1 landscape.\nUse the provided Agent Farm banner as STYLE REFERENCE only. Same crisp coherent pixel-art game world, charcoal midnight opaque backdrop, warm cream chunky title type, soft lantern light, cream robots with dark faceplates and green eyes, restrained foliage on isometric floating islands. Keep readable silhouettes and generous negative space. Give this product its own props and accent palette. No blur, smooth 3D, photorealism, watermarks, fake screenshots, tiny illegible text or extra slogans.\nScene: a multilevel terminal workshop with three separate well-lit work booths, each holding a cream robot and terminal; a human coordinator views the booths from a central desk. Tiny branch-shaped paths lead to each booth, suggesting separate workspaces. Teal and copper accents, no musical props or farmer hats.\nComposition: large title on left 45%, detailed but tidy scene on right 55%; safe margins. Exact title: \"PANE\". Use two lines for long titles. Only this title as text. Make a polished sibling identity within the dcouple family.\n\n### pane-workspaces.png\n\nUse case: infographic-diagram\nAsset type: GitHub README section illustration, 2:1 landscape.\nUse the provided Agent Farm banner as STYLE REFERENCE only. Same crisp coherent pixel-art game world, charcoal midnight opaque backdrop, warm cream chunky title type, soft lantern light, cream robots with dark faceplates and green eyes, restrained foliage on isometric floating islands. Keep readable silhouettes and generous negative space. Give this product its own props and accent palette. No blur, smooth 3D, photorealism, watermarks, fake screenshots, tiny illegible text or extra slogans.\nLocal identity: a multilevel terminal workshop with three separate well-lit work booths, each holding a cream robot and terminal; a human coordinator views the booths from a central desk. Tiny branch-shaped paths lead to each booth, suggesting separate workspaces. Teal and copper accents, no musical props or farmer hats.\nDiagram: Three panels left to right with exact headings and body lines: \"Repository\" / \"Saved base repo\"; \"Pane\" / \"Worktree and branch\"; \"Panels\" / \"Terminals share the worktree\". Arrows from Repository to Pane to Panels. Illustrate a project folder, a separate workshop booth, then two small terminals within that same booth. All panels are conceptual, not an app screenshot. No implication that each panel gets its own worktree.\nLarge highly legible cream text on uncluttered dark areas; graphical precision and factual readability take priority over scenery. No large product title. Match the supplied family reference.\nInput images for the section illustration: first is the family style reference; second is this project's generated banner. Match the second image's character, props and accents.\n"},{"path":"docs/lint/anti-slop.md","title":"Anti-slop policy","text":"# Anti-slop policy\n\nPane vendors the 15 Oxlint rules from `dmmulroy/anti-slop` at commit\n`cd064fe602b5915ff35e1e1c20836ca9bcb3729a` in\n`tools/oxlint/anti-slop`. The snapshot is intentionally local so the rules run\ndeterministically and can be reviewed alongside the code they gate.\n\nThe original full-repository scan covered `frontend`, `main`,\n`packages/runpane`, `shared`, `tests`, `scripts`, all non-vendored `tools`, and\nevery Playwright config. It found 2,463 findings in 240 files. The cleanup\ncampaign reduced every rule to a zero baseline, so all 15 rules now block\nregressions in the root lint command.\n\n| Rule | Baseline findings/files | Lane | Reason |\n| --- | ---: | --- | --- |\n| `no-object-parameters` | 0/0 | blocking | Prevent new positional object bags without an explicit contract. |\n| `no-reflect-apply` | 0/0 | blocking | Prefer direct, typed calls over reflective dispatch. |\n| `no-unknown-type-aliases` | 0/0 | blocking | Prevent aliases that rename uncertainty instead of parsing it. |\n| `no-widen-then-assert` | 0/0 | blocking | Prevent discarding known types and recovering them with assertions. |\n| `no-reflect-get` | 4/1 | blocking | Reflective reads were replaced with declared or descriptor-based access. |\n| `no-chained-type-assertions` | 89/22 | blocking | Boundary contracts avoid type laundering through chained assertions. |\n| `no-conditional-empty-object-spread` | 34/16 | blocking | Optional fields are assembled explicitly. |\n| `no-known-value-widening` | 142/65 | blocking | Known values retain inferred or named domain types. |\n| `no-module-mocking` | 31/13 | blocking | Tests use explicit seams or real modules. |\n| `no-runtime-typeof` | 417/94 | blocking | Runtime boundaries use parsers, feature checks, or domain guards. |\n| `no-shape-in-symbol-names` | 18/1 | blocking | Symbols describe domain intent rather than structural shape. |\n| `no-unknown-parameters` | 250/64 | blocking | Uncertain input is parsed at its boundary. |\n| `no-unknown-returns` | 37/21 | blocking | Adapters return parsed domain values. |\n| `no-unsafe-dictionary-type` | 192/51 | blocking | Dictionary contracts use bounded keys or validated JSON objects. |\n| `require-safety-comment-for-type-assertion` | 1,249/177 | blocking | Remaining necessary assertions document their checked invariant. |\n\nRun `pnpm lint` for the blocking policy. `pnpm lint:ox:extra` and\n`pnpm lint:ox:extra:details` remain available as harness checks, but no rules\nremain in the advisory lane.\n\n## Root cause policy\n\nFor agent-written Pane code, the most important class is uncertainty that leaks\npast an I/O boundary and later becomes a widening or assertion. Ask: **is this\nchange fixing the root cause or a symptom?** The root fix parses IPC, daemon,\nfilesystem, process, or browser input once into a named domain type. A late\n`unknown` alias, representation ladder, dictionary, or assertion usually treats\nthe symptom. Blocking findings should guide that review.\n\nAny future anti-slop rule must reach a zero baseline for the full scope before\njoining the blocking configuration. Do not silence a class globally to make a\npromotion appear clean.\n\nThe root development toolchain requires Node 22.18 or newer because Oxlint's\nTypeScript plugin runs under the developer Node process. This does not change\nElectron 41's bundled Node 24 runtime, and the published `runpane` wrapper keeps\nits Node 20 runtime floor.\n"},{"path":"docs/lint/oxlint-overlap.md","title":"Oxlint and ESLint overlap","text":"# Oxlint and ESLint overlap\n\n`pnpm lint:ox` is the first blocking lane. ESLint remains blocking for rules\nwhose current Pane semantics are not reproduced by Oxlint 1.76.0, and continues\nto report the repository's existing warning-level checks. An ESLint rule is\ndisabled in `tools/eslint/oxlint-overlap.cjs` only when the explicit blocking\nOxlint rule below replaces it.\n\nThe 79 unique ESLint error-level rules were resolved from the frontend, main,\nand RunPane flat configs. Seventy-eight have blocking equivalents. `no-octal`\nhas no Oxlint 1.76.0 equivalent and therefore stays in ESLint.\n\n| ESLint rule | Blocking Oxlint rule | Scope/disposition |\n| --- | --- | --- |\n| `for-direction` | `eslint/for-direction` | common; moved |\n| `no-async-promise-executor` | `eslint/no-async-promise-executor` | common; moved |\n| `no-case-declarations` | `eslint/no-case-declarations` | common; moved |\n| `no-compare-neg-zero` | `eslint/no-compare-neg-zero` | common; moved |\n| `no-cond-assign` | `eslint/no-cond-assign` | common; moved |\n| `no-constant-binary-expression` | `eslint/no-constant-binary-expression` | common; moved |\n| `no-constant-condition` | `eslint/no-constant-condition` | common; moved |\n| `no-control-regex` | `eslint/no-control-regex` | common; moved |\n| `no-debugger` | `eslint/no-debugger` | common; moved |\n| `no-delete-var` | `eslint/no-delete-var` | common; moved |\n| `no-dupe-else-if` | `eslint/no-dupe-else-if` | common; moved |\n| `no-duplicate-case` | `eslint/no-duplicate-case` | common; moved |\n| `no-empty` | `eslint/no-empty` | frontend only; moved (main/RunPane warning retained) |\n| `no-empty-character-class` | `eslint/no-empty-character-class` | common; moved |\n| `no-empty-pattern` | `eslint/no-empty-pattern` | common; moved |\n| `no-empty-static-block` | `eslint/no-empty-static-block` | common; moved |\n| `no-ex-assign` | `eslint/no-ex-assign` | common; moved |\n| `no-extra-boolean-cast` | `eslint/no-extra-boolean-cast` | common; moved |\n| `no-fallthrough` | `eslint/no-fallthrough` | common; moved |\n| `no-global-assign` | `eslint/no-global-assign` | common; moved |\n| `no-invalid-regexp` | `eslint/no-invalid-regexp` | common; moved |\n| `no-irregular-whitespace` | `eslint/no-irregular-whitespace` | common; moved |\n| `no-loss-of-precision` | `eslint/no-loss-of-precision` | common; moved |\n| `no-misleading-character-class` | `eslint/no-misleading-character-class` | common; moved |\n| `no-nonoctal-decimal-escape` | `eslint/no-nonoctal-decimal-escape` | common; moved |\n| `no-octal` | — | common; retained in ESLint |\n| `no-prototype-builtins` | `eslint/no-prototype-builtins` | common; moved |\n| `no-regex-spaces` | `eslint/no-regex-spaces` | common; moved |\n| `no-self-assign` | `eslint/no-self-assign` | common; moved |\n| `no-shadow-restricted-names` | `eslint/no-shadow-restricted-names` | common; moved |\n| `no-sparse-arrays` | `eslint/no-sparse-arrays` | common; moved |\n| `no-unexpected-multiline` | `eslint/no-unexpected-multiline` | common; moved |\n| `no-unsafe-finally` | `eslint/no-unsafe-finally` | common; moved |\n| `no-unsafe-optional-chaining` | `eslint/no-unsafe-optional-chaining` | common; moved |\n| `no-unused-labels` | `eslint/no-unused-labels` | common; moved |\n| `no-unused-private-class-members` | `eslint/no-unused-private-class-members` | common; moved |\n| `no-useless-backreference` | `eslint/no-useless-backreference` | common; moved |\n| `no-useless-catch` | `eslint/no-useless-catch` | common; moved |\n| `no-var` | `eslint/no-var` | common; moved |\n| `prefer-const` | `eslint/prefer-const` | frontend only; moved (main/RunPane warning retained) |\n| `prefer-rest-params` | `eslint/prefer-rest-params` | common; moved |\n| `prefer-spread` | `eslint/prefer-spread` | common; moved |\n| `require-yield` | `eslint/require-yield` | common; moved |\n| `use-isnan` | `eslint/use-isnan` | common; moved |\n| `valid-typeof` | `eslint/valid-typeof` | common; moved |\n| `@typescript-eslint/ban-ts-comment` | `typescript/ban-ts-comment` | common; moved |\n| `@typescript-eslint/no-array-constructor` | `typescript/no-array-constructor` | common; moved |\n| `@typescript-eslint/no-duplicate-enum-values` | `typescript/no-duplicate-enum-values` | common; moved |\n| `@typescript-eslint/no-empty-object-type` | `typescript/no-empty-object-type` | common; moved |\n| `@typescript-eslint/no-explicit-any` | `typescript/no-explicit-any` | common; moved |\n| `@typescript-eslint/no-extra-non-null-assertion` | `typescript/no-extra-non-null-assertion` | common; moved |\n| `@typescript-eslint/no-misused-new` | `typescript/no-misused-new` | common; moved |\n| `@typescript-eslint/no-namespace` | `typescript/no-namespace` | common; moved |\n| `@typescript-eslint/no-non-null-asserted-optional-chain` | `typescript/no-non-null-asserted-optional-chain` | common; moved |\n| `@typescript-eslint/no-require-imports` | `typescript/no-require-imports` | frontend only; moved (main/RunPane warning retained) |\n| `@typescript-eslint/no-this-alias` | `typescript/no-this-alias` | common; moved |\n| `@typescript-eslint/no-unnecessary-type-constraint` | `typescript/no-unnecessary-type-constraint` | common; moved |\n| `@typescript-eslint/no-unsafe-declaration-merging` | `typescript/no-unsafe-declaration-merging` | common; moved |\n| `@typescript-eslint/no-unsafe-function-type` | `typescript/no-unsafe-function-type` | common; moved |\n| `@typescript-eslint/no-unused-expressions` | `typescript/no-unused-expressions` | common; moved |\n| `@typescript-eslint/no-wrapper-object-types` | `typescript/no-wrapper-object-types` | common; moved |\n| `@typescript-eslint/prefer-as-const` | `typescript/prefer-as-const` | common; moved |\n| `@typescript-eslint/prefer-namespace-keyword` | `typescript/prefer-namespace-keyword` | common; moved |\n| `@typescript-eslint/triple-slash-reference` | `typescript/triple-slash-reference` | common; moved |\n| `react-hooks/rules-of-hooks` | `react/rules-of-hooks` | frontend only; moved |\n| `jsx-a11y/alt-text` | `jsx-a11y/alt-text` | frontend only; moved |\n| `jsx-a11y/anchor-has-content` | `jsx-a11y/anchor-has-content` | frontend only; moved |\n| `jsx-a11y/aria-props` | `jsx-a11y/aria-props` | frontend only; moved |\n| `jsx-a11y/aria-proptypes` | `jsx-a11y/aria-proptypes` | frontend only; moved |\n| `jsx-a11y/aria-unsupported-elements` | `jsx-a11y/aria-unsupported-elements` | frontend only; moved |\n| `jsx-a11y/click-events-have-key-events` | `jsx-a11y/click-events-have-key-events` | frontend only; moved |\n| `jsx-a11y/heading-has-content` | `jsx-a11y/heading-has-content` | frontend only; moved |\n| `jsx-a11y/iframe-has-title` | `jsx-a11y/iframe-has-title` | frontend only; moved |\n| `jsx-a11y/no-access-key` | `jsx-a11y/no-access-key` | frontend only; moved |\n| `jsx-a11y/no-distracting-elements` | `jsx-a11y/no-distracting-elements` | frontend only; moved |\n| `jsx-a11y/no-static-element-interactions` | `jsx-a11y/no-static-element-interactions` | frontend only; moved with `onClick`/expression options |\n| `jsx-a11y/role-has-required-aria-props` | `jsx-a11y/role-has-required-aria-props` | frontend only; moved |\n| `jsx-a11y/role-supports-aria-props` | `jsx-a11y/role-supports-aria-props` | frontend only; moved |\n| `jsx-a11y/tabindex-no-positive` | `jsx-a11y/tabindex-no-positive` | frontend only; moved |\n\nBoth Oxlint configs disable the broad `correctness` category. This is\nintentional: the blocking contract is the explicit list above plus the 15\nzero-baseline anti-slop rules, rather than an unstable category preset.\n\nA Profiler-based render counter is deliberately deferred. Add one with the\nfirst named React subscription/render regression and a component test renderer\nthat can mount the affected Pane surface; React Scan supplies exploratory\nruntime evidence but is not a deterministic unit-test assertion.\n"},{"path":"docs/remote-daemon-lifecycle.md","title":"Remote Daemon Lifecycle","text":"# Remote Daemon Lifecycle\n\nThis is the implementation checklist for Remote Pane setup, teardown, and runtime switching. It exists to keep config writes, runtime controller actions, and renderer refreshes in sync.\n\n## Runtime Roles\n\n- Host lifecycle is owned by `PaneRemoteTransportController` and `remoteHostRuntimeStateStore`.\n- Client lifecycle is owned by `RemotePaneClientController`.\n- IPC handlers in `main/src/ipc/remoteDaemon.ts` orchestrate config writes and controller calls.\n- Renderer runtime changes are reconciled through `remote-daemon:resync-required`. Main sends `{ hostChanged: true }` when the active runtime changes; a reconnect to the same host sends it without a payload.\n- A resync refetches config, Panes, the active Pane's panels, repositories, Sessions and pinned Sessions, pending permission prompts, loaded archived Panes, archive progress, the host's terminal shells, and an open Usage view. When the host changed, it also leaves an open repository view, since repository ids are per host, loads that host's expanded repositories, opened Sessions and open Archived groups, and asks again about that host's interrupted Panes.\n- Expanded repositories are saved per host: this computer under `treeView.expandedProjects`, a remote host under `treeView.expandedProjects@<profile id>`.\n- Opened Sessions and open Archived groups are saved per host the same way, under `treeView.expandedSessions[@<profile id>]` and `treeView.expandedArchivedProjects[@<profile id>]`. A Session not in the opened list shows collapsed. Older `treeView.collapsedSessions[@<profile id>]` rows are no longer read and stay in `ui_state` unused.\n- Where the user was is saved per host too, under `navigation.lastLocation` / `navigation.lastLocation@<profile id>`: the active view, the open repository view's project id, and the open Pane. A switch clears the outgoing host's selection as above and then restores the incoming host's, validating every id against that host's freshly loaded repositories and Panes — anything archived or gone degrades to the home view, and the dead memory is overwritten rather than retried. `frontend/src/utils/hostNavigationMemory.ts` owns both halves; the renderer names the host on each read and write, because it can still be showing the outgoing host while main has already switched runtimes. Restoring selects one Pane, as a click would, and never revives every terminal once open on that host. The memory also holds this desktop's selected Session: launch and host switches both restore it, and the host's last-used Session is only the fallback when there is none. Another client's selection never moves it. The tabs and split inside each Pane are a separate per-Pane memory, `paneLayout.<pane id>` / `paneLayout.<pane id>@<profile id>` (`frontend/src/utils/paneLayoutMemory.ts`); the host's stored layout and active tab are the last used by any client and seed a Pane this desktop has not shown. Archiving or deleting a Pane drops its memory. Apart from the Session, the location memory is not applied at app startup; only a host switch reads it.\n- The terminal shell picker reads and writes the active host's shell (`terminal:get-shell-settings`, `terminal:set-preferred-shell`). It appears only when that host runs Windows.\n- `pane:focus-requested` and `pane:open-link` are host events. `runpane panes focus` and `runpane panes create --focus` move only the desktop window attached to that host as its local host; a remote-mode client ignores them. A host- or agent-initiated tab activation (`panel:activeChanged`) moves only clients already showing that Pane, and a new Pane switches only the client whose request created it (`clientRequestId`, echoed on `session:created`). A pane:// link opened on a remote-mode client is resolved by its renderer against the active host.\n- Remote terminals ack output with `terminal:ack`. This window's ptyHost port reaches only local terminals.\n- The Remote Pane PWA keeps the same rules in the browser. When its event stream returns after a drop (network loss, or the native app resuming), it refetches Panes, the selected Pane's panels, Sessions, and loaded archived Panes. Connecting to a host or disconnecting clears all host-scoped state first: Panes, panels, Sessions, the open Session, archived Panes, open dialogs, and notification settings.\n\n## Lifecycle Matrix\n\n| Action | Main-process side effect | Renderer side effect |\n| --- | --- | --- |\n| Import connection and connect succeeds | Activate profile, save/dedupe profile, set remote mode | Resync config, sessions, panels |\n| Import connection and connect fails | Save/dedupe profile, keep current active runtime | No runtime resync |\n| Connect saved profile | Activate profile before persisting remote mode | Resync config, sessions, panels |\n| Switch to local runtime | Disconnect active remote client, save local mode | Resync config, sessions, panels, clear stale active session |\n| Delete inactive profile | Remove profile | No runtime resync |\n| Delete active profile | Switch local, remove profile, save local mode | Resync config, sessions, panels |\n| Enable or update host | Save host config, transport controller syncs to live or error | Host-state event updates UI |\n| Stop host | Save disabled host config, transport controller stops server | Host-state event updates UI |\n| Disconnect host clients | Drop matching SSE clients | Host-state event updates client count |\n| Revoke host client | Remove saved client record, drop matching SSE clients | Host-state event updates client count |\n\n## Guardrails\n\n- Do not persist remote mode until the selected profile has successfully activated.\n- Failed import-connect saves the profile but must not switch runtime or emit a renderer resync.\n- Connected remote clients are runtime state, not saved client records.\n- Current Pane Data hosting is live only while that Pane app is running.\n- Isolated daemon data can install a background service; Current Pane Data should not.\n\n## Terminal Input\n\nDesktop and browser clients send at most one terminal input request per panel at a time. Keys typed while a request is pending are combined into the next request, preserving their order without a separate round trip for every buffered key. A bare Escape always ends a combined request, because terminal apps read Escape followed by another key in the same write as an Alt shortcut. Different panels and other remote commands remain independent.\n\nInput requests are not retried. On failure, disconnect, or a ten-second input timeout, queued input is discarded and outstanding callers are rejected. An interrupted request may already have reached the host, so its input must not be replayed after reconnecting. This uses the existing HTTP API and requires no host protocol upgrade.\n\n## Desktop media previews\n\nThe explorer's previews (images, PDFs, fonts, video, audio and the text formats) work from a remote desktop. The renderer still gets a `pane-media://preview/<token>` URL from its own main process; the grant records which host issued it, and a request after a host switch, or after a switch to local mode, gets 403. Each request goes to the host's `GET /media?sessionId=&filePath=` with the connection's usual authentication on that door, and its `Range` header passes through, so video and audio seek without downloading the whole file. The host checks that the path has a preview kind and resolves it through `file:getPath`, the same worktree and symlink boundary the host uses for its own previews, and answers 404 otherwise. Archive and SQLite listings use the daemon-owned `file:preview-list` command, so they run on the host too. \"Open with system app\" on a remote desktop downloads the whole file through the same route into a new temporary folder on that desktop and opens the copy, so edits in the system app do not reach the host. The copy keeps the host file name when that name is valid on every desktop OS, and is otherwise named `preview` plus the extension. A failed download or open removes the folder. While remote mode has no connected host, previews and Open fail; they never read the desktop's own files. \"Reveal in folder\" is available only on the host.\n\n## Desktop browser file previews\n\nBefore mounting a `file://` webview, the desktop renderer asks the local main process to prepare its session. In remote mode, an isolated in-memory Electron partition handles every file request through `panels:read-browser-file` on the existing authenticated remote connection. The original URL, including its filename, remains intact, so relative CSS, images, scripts and linked HTML pages resolve normally. Local-mode previews, like every other browser tab, use the shared `persist:pane-browser` store; Settings → Privacy → Clear browser data clears it on the computer showing the browser and is never forwarded to the host. This does not add browser panels to the Remote PWA.\n\nThe host reader requires an existing browser panel whose saved entry URL identifies a regular file. It serves regular files within that entry page's directory and descendants, with a 16 MiB limit per file. It checks canonical paths to reject traversal and symlinks/junctions outside that directory at validation time; it never lists directories. The host filesystem is trusted: these checks do not guarantee atomic containment against a host process maliciously replacing files or directories during a read. Thus opening a file shares its containing bundle directory with connected clients, not the entire worktree or disk. This endpoint has no caller-supplied root. File entry URLs are host-owned: HTTP command provenance is carried through async handlers, and the panel manager rejects remote attempts to create or retarget file previews (including through RunPane). Clients can update an unchanged entry but must open a new file on the host. The one exception is previewing a worktree HTML file: a remote desktop names the file and the host creates the browser tab for it, so links within that file's folder load through the reader above. Arbitrary `file://` URLs from a client stay refused.\n\nA host switch removes cached browser panels before asynchronous resync, so outgoing URLs and partitions cannot survive even when both hosts have identical panel IDs. Panel-list requests carry a host generation and refetch if their response belongs to an outgoing host. Incoming panel data recreates the guest and prepares its file session anew. While file preparation is unresolved, navigation does not mount a guest or persist URLs; starting preparation also cancels earlier pending URL writes. Remote file navigation is client-local and does not overwrite the host's saved entry URL. This keeps nested page navigation from changing the bundle boundary. If the host replaces the file entry with an HTTP(S) URL, the client returns to the shared browser store and ordinary navigation persistence, including when it was already viewing that HTTP page. Missing/deleted panels revoke reads; changing the connected profile or its base URL invalidates the previous session's requests, including responses already in flight. A failed fetch renders an error message and never falls back to the client's filesystem. For a file outside the entry page's directory, the message is the host's refusal: it names the file, that directory and the ways to fix the page. While the client is still connected to that host, the tab also shows that refusal under its address bar until its next page, so a refused stylesheet or script that the page never displays is still visible. Both desktop client and host need this version; an older host reports an unavailable preview.\n\nRun `pnpm build:main`, start the frontend with `pnpm --filter frontend dev`, then set `PANE_ELECTRON_E2E=1` and run `pnpm exec playwright test tests/remote-browser-files.spec.ts`. Set `PLAYWRIGHT_PORT` if using a different dev-server port. This opt-in Electron regression uses isolated temporary Pane data and a loopback host with the production HTTP/SSE transport. It verifies the actual BrowserPanel, CSS/image loads, linked-page navigation, the preserved filename, access denial and disconnect behavior, and saves `remote-client-rendered.png`. Linux requires a display (for example Xvfb).\n"},{"path":"docs/remote-terminal-flow-control.md","title":"Remote terminal output flow control","text":"# Remote terminal output flow control\n\nTerminal panels begin hidden until a client registers visibility with\n`terminal:setVisibility`. Hidden panels still stream to daemon subscribers and\nretain scrollback, but do not wait for renderer acknowledgements. A raw SSE\nsubscriber does not register visibility by merely opening `/events`.\n\nVisible panels pause their PTY at 100,000 unacknowledged characters and resume\nat 5,000. The five-second safety timeout abandons the stale acknowledgement\ncount before resuming; fresh output can still cross the high watermark and\npause again. This escape hatch prevents one lost acknowledgement from causing\nevery subsequent flush to wait five seconds. It does not disable backpressure\nfor a visible client that consistently fails to acknowledge output.\n\nAcknowledgements have no sequence or generation. After timeout recovery, a\ndelayed acknowledgement can temporarily reduce the fresh backlog count. Keeping\nforgiven debt would instead discard valid new acknowledgements when the old ones\nwere lost. Strict accounting across recovery would require a protocol change.\n\nDesktop clients connected to a remote host acknowledge processed output through\n`terminal:ack`, including when a PTY ID is known. Only local terminals can use\nthe local PTY host MessagePort. Clients attached to an already-running panel\ncan acknowledge by panel ID without receiving a new `terminal:ptyReady` event.\n\nAcknowledgement accounting is per terminal, not per viewer: one responsive\nviewer can release a pause even if another viewer is silent. Removing the last\nviewer clears pending flow control and resumes the PTY. Registered viewers\nsurvive a PTY host respawn, and attach/detach changes during an asynchronous spawn\nare applied to the replacement terminal.\n\nFocused regression checks:\n\n```sh\npnpm --filter main test run src/ptyHost/flowControl.test.ts src/services/terminalPanelManager.persistence.test.ts src/services/terminalPanelManager.test.ts\npnpm --filter frontend test src/utils/terminalAck.test.ts\n```\n"},{"path":"docs/troubleshooting/SETUP_TROUBLESHOOTING.md","title":"Setup Troubleshooting Guide","text":"# Setup Troubleshooting Guide\n\n## Python distutils Error\n\nIf you encounter this error during `pnpm run setup`:\n```\nModuleNotFoundError: No module named 'distutils'\n```\n\nThis happens because Python 3.12+ removed the `distutils` module that `node-gyp` depends on.\n\n### Quick Fix:\n```bash\nbrew install python-setuptools\n```\n\n### Alternative: Use Python 3.11 with pyenv\n\n```bash\nbrew install pyenv\npyenv install 3.11.9\npyenv global 3.11.9\n```\n\nThen run `pnpm run setup` again.\n\n## Other Common Issues\n\n### Modified terminal keys appear as escape-sequence text on Windows/WSL\n\nIf Alt+Up inserts `[1;3A`, Shift+Left inserts `[1;2D`, or F6 inserts `[17~`,\ncheck whether the application launched from WSL is actually a Windows executable.\nA Windows npm launcher can select `node.exe` even when invoked from a Linux shell.\n\nPane must honor ConPTY's `CSI ? 9001 h` request using xterm's\n`vtExtensions.win32InputMode`. Without it, a Windows console application reached\nthrough WSL can receive a complete VT sequence as individual character key\nrecords. A Linux raw-stdin reader can still receive the exact bytes correctly;\nthis symptom alone does not demonstrate split IPC messages or PTY writes.\nThe negotiated mode must also survive terminal snapshot restoration. The `@`\ninterceptor interprets Win32 key records locally and forwards unconsumed records\nunchanged. This handling applies to all terminal applications.\n\nThe browser regression suite is `tests/terminal-keyboard-input.spec.ts`; it checks\nwhole input messages for Alt+Up, Shift+Left, Shift+Up, and F6 with and without\nnegotiated Windows input mode. For transport diagnosis, compare a Linux raw-stdin\nreader with a Windows `ReadConsoleInputW` reader launched through WSL: compare\nbytes for the former and virtual keys/modifiers for the latter.\n\nSee Microsoft's [Win32 input protocol specification](https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md).\n\n### electron-rebuild failures\n- Ensure Xcode Command Line Tools are installed: `xcode-select --install`\n- Clear node_modules and reinstall: `rm -rf node_modules && pnpm install`\n\n### pnpm permission errors\n- Never use `sudo` with pnpm\n- Fix npm permissions: `npm config set prefix ~/.npm-global`\n\n## Windows Build Requirements\n\n### Spectre-mitigated Libraries Error\n\nIf you encounter this error during `pnpm run setup` on Windows:\n```\nLINK : fatal error LNK1181: cannot open input file 'MSVCRT.lib'\n```\n\nOr similar errors mentioning missing `.lib` files, this means you need to install the Spectre-mitigated libraries in Visual Studio.\n\n### Solution:\n\n1. **Install Visual Studio 2022** (Community Edition is free):\n - Download from [https://visualstudio.microsoft.com/](https://visualstudio.microsoft.com/)\n \n2. **Install Required Components**:\n - Open **Visual Studio Installer**\n - Click **Modify** on your Visual Studio 2022 installation\n - Go to the **Individual components** tab\n - Search for \"Spectre\" in the search box\n - Check the following components:\n - `MSVC v143 - VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)`\n - `MSVC v143 - VS 2022 C++ ARM64/ARM64EC Spectre-mitigated libs (Latest)` (if building for ARM64)\n - Click **Modify** to install\n\n3. **Restart your terminal** and run `pnpm run setup` again\n\n### Why is this needed?\n\nNode.js native modules on Windows are built with Visual Studio's C++ compiler. Recent security updates require Spectre-mitigated libraries to be installed separately. These libraries provide protection against Spectre vulnerability exploits.\n\n### Alternative: Use Pre-built Binaries\n\nIf you only need to run Pane, download the Windows installer from [GitHub Releases](https://github.com/greenfield-inc/Pane/releases/latest) instead of building from source.\n"},{"path":"docs/troubleshooting/WINDOWS_APP_HANG.md","title":"Windows freezes on startup or window focus","text":"# Windows freezes on startup or window focus\n\nA September 3, 2026 report described the latest version as \"lagging\" and \"freezing all the time.\" The reported investigation found a 250 MB `sessions.db`, approximately 200 MB of terminal history across 92 stopped sessions, an accidental Git repository at the Windows user profile root, and the isolated PTY host disabled. Windows recorded `AppHangTransient`.\n\n## Prevention\n\n- Startup loads only logs and browser panels that need restart cleanup. Terminal state is restored on demand when accessed, including history for stopped and archived sessions.\n- Workspace summaries exclude raw and serialized terminal buffers in SQLite before returning rows to JavaScript. Summary reads do not replace complete cached panel state or write reduced state back to disk.\n- Automatic Git status checks and filesystem watching skip the home directory, log a diagnostic, and report unknown Git status. Windows path comparison ignores case and normalizes separators and dot segments. Choose the actual project folder instead of `C:\\Users\\<user>`. Existing repository records and `.git` directories are not deleted.\n- New configurations no longer use the home directory as an implicit Git repository. Legacy home-directory `gitRepoPath` values are rejected by the migration accessor.\n- The isolated PTY host defaults to enabled on Windows, including existing configurations without a `usePtyHost` value. Explicit `false` values are preserved. Enable **Settings > Advanced > Use isolated PTY host** and restart Pane to change an existing opt-out. `PANE_USE_PTY_HOST=1` still forces it on for development.\n\nTerminal history remains on disk. The existing 21-day retention policy for archived sessions is unchanged, and no full `VACUUM` runs during startup. A large database file does not by itself mean all history is resident in the JavaScript heap.\n\n## Manual Windows verification\n\n1. Use an isolated `PANE_DIR` with a copy of a database containing many stopped and archived terminal panels. Launch Pane, inspect responsiveness and heap use, then reopen an old terminal and verify its history.\n2. In an isolated test profile with a home-directory Git repository, select a session whose worktree path is that home directory. Repeatedly blur/focus Pane. Confirm no recursive watcher or Git status scan starts, and check the diagnostic in the main-process log. Verify a normal project beneath the profile still refreshes.\n3. Start with a config missing `usePtyHost`: confirm the settings toggle is enabled and the supervisor forks on Windows. Repeat with explicit `false`, then enable the setting and restart.\n\nThe automated tests cover persistence, summary projections, path normalization, Git scan entry points, and platform defaults. They do not reproduce Windows `AppHangTransient` or validate a packaged Windows PTY host.\n"},{"path":"docs/troubleshooting/WINDOWS_CLAUDE_CODE_SPAWN.md","title":"Windows Claude Code Spawn Issue - RESOLVED","text":"# Windows Claude Code Spawn Issue - RESOLVED\n\n## Status: ✅ FIXED\n\nBoth issues have been resolved. Claude Code now spawns correctly on Windows.\n\n## Note: the original reproduction no longer applies\n\nThe failure described below was reproduced against a copy of Claude Code installed **inside\nPane's own `node_modules`** — Pane declared `@anthropic-ai/claude-code` as a dependency, so\n`Pane/node_modules/.bin/claude` existed and could win PATH resolution during development.\nThat dependency has since been removed (nothing ever imported it; Pane spawns the user's\nown CLI), so the specific `MODULE_NOT_FOUND` path shown below — resolving into Pane's pnpm\nstore — can no longer occur.\n\n**The fixes described here are still live and still required.** Both apply to any\nglobally installed `claude`:\n\n- npm/pnpm bin stubs are shell scripts on Windows, so `node-pty` cannot execute them\n directly (error code 193). The proactive Node.js fallback in `AbstractCliManager.ts`\n handles this.\n- Those stubs resolve their target through `$basedir`-relative paths, which break when the\n process is spawned with a worktree as `cwd`. `findCliNodeScript()` in `nodeFinder.ts`\n parses the stub and invokes the resolved `.js` file directly.\n\nRead the sections below as the rationale for that code. Only the reproduction environment\nis out of date.\n\n## Problem Summary\n\nClaude Code was failing to spawn properly on Windows when Pane runs it in a worktree directory different from where `@anthropic-ai/claude-code` is installed.\n\n## Root Cause Analysis\n\n### Issue 1: Error Code 193 (Fixed)\n\n**Error:** `Failed to spawn Claude Code: Cannot create process, error code: 193`\n\n**Cause:** Error code 193 on Windows means \"not a valid Win32 application\". This occurs when trying to execute a shell script (like npm bin stubs) directly via `node-pty` instead of through a shell or Node.js.\n\n**Fix Applied:** The PTY host classifies the spawn failure as `E193` (`classifySpawnError()` in `main/src/ptyHost/ptyHostMain.ts`). `AbstractCliManager.ts` matches the error message (`error code: 193` or `not a valid Win32 application`) and retries with the Node.js fallback.\n\n**Location:** `main/src/ptyHost/ptyHostMain.ts` and `main/src/services/panels/cli/AbstractCliManager.ts`\n\n### Issue 2: Module Not Found (Fixed)\n\n**Error:**\n```\nError: Cannot find module 'C:\\Users\\<user>\\<repo>\\worktrees\\@anthropic-ai\\claude-code\\cli.js'\n```\n\n**Cause:** The npm bin stub at `node_modules/.bin/claude` uses a relative path:\n```bash\nexec node \"$basedir/../@anthropic-ai/claude-code/cli.js\" \"$@\"\n```\n\nWhen the `cwd` is set to the worktree directory, the relative path resolves incorrectly.\n\n## Solution Applied\n\n### Fix 1: Proactive Windows Node.js Fallback\n\nOn Windows, we now proactively use the Node.js fallback instead of waiting for failures:\n\n```typescript\n// In spawnPtyProcess() at main/src/services/panels/cli/AbstractCliManager.ts\nif (os.platform() === 'win32') {\n this.logger?.verbose(`[${this.getCliToolName()}] Windows detected, using Node.js fallback proactively`);\n nodeFallbackTools.add(needsNodeFallbackKey);\n}\n```\n\nThis bypasses the problematic shell script execution entirely.\n\n### Fix 2: Enhanced pnpm Path Resolution\n\nThe `findCliNodeScript()` function in `main/src/utils/nodeFinder.ts` was enhanced to properly handle pnpm's node_modules structure:\n\n1. **Detects pnpm directory**: Checks for `node_modules/.pnpm` directory\n2. **Known package mappings**: Has specific mappings for Claude Code and Codex packages\n3. **Generic pnpm search**: Falls back to searching for any matching package\n4. **Proper path construction**: Correctly builds paths like:\n ```\n node_modules/.pnpm/@anthropic-ai+claude-code@2.0.0/node_modules/@anthropic-ai/claude-code/cli.js\n ```\n\n## Technical Details\n\n### npm Bin Stub Structure (Windows)\n\nThe file at `Pane/node_modules/.bin/claude` is a shell script that:\n1. Sets `NODE_PATH` with pnpm-specific paths\n2. Executes: `node \"$basedir/../@anthropic-ai/claude-code/cli.js\" \"$@\"`\n\nThe `$basedir` is the directory containing the script, but when node-pty spawns with a different `cwd`, the relative path resolution breaks.\n\n### Why the Proactive Approach?\n\nThe reactive fallback (catching error code 193) wasn't enough because:\n- The spawn could \"succeed\" from node-pty's perspective\n- But the process would exit immediately with code 1 (MODULE_NOT_FOUND)\n- By the time we knew it failed, it was too late to retry\n\nBy proactively using Node.js on Windows, we avoid the shell script entirely and directly invoke the CLI script with the correct absolute path.\n\n## Files Modified\n\n1. `main/src/services/panels/cli/AbstractCliManager.ts` - Added:\n - Proactive Windows Node.js fallback detection\n - Error code 193 message matching (reactive fallback)\n2. `main/src/ptyHost/ptyHostMain.ts` - `classifySpawnError()` reports error 193 as `E193`\n3. `main/src/utils/nodeFinder.ts` - Enhanced `findCliNodeScript()` with:\n - pnpm structure detection\n - Known package mappings for Claude Code and Codex\n - Generic pnpm package search\n - Standard npm path resolution\n\n## Debug Logs to Check\n\n- `backend-debug.log` - Look for:\n - `Windows detected, using Node.js fallback proactively`\n - `Detected pnpm structure`\n - `Found pnpm CLI script at:`\n - `Using Node.js:`\n\n## Test Commands\n\n```bash\n# Rebuild main process after changes\npnpm build:main\n\n# Run in dev mode\npnpm electron-dev\n\n# Check logs (PowerShell)\nSelect-String -Path backend-debug.log -Pattern \"Claude|spawn|pnpm|NodeFinder\"\n```\n"}]
|
|
1
|
+
[{"path":"docs/ADDING_NEW_CLI_TOOLS.md","title":"Adding a New CLI Agent Tool","text":"# Adding a New CLI Agent Tool\n\nAn agent tool in Pane is **not** a panel type and **not** a manager subclass. It is a\n`type: 'terminal'` ToolPanel whose `customState.initialCommand` launches the CLI in a\nPTY, plus a handful of data-driven registrations. This guide walks through every\nintegration point, using the Cursor Agent CLI (`cursor-agent`, agent id `cursor`) as\nthe worked example.\n\n> Historical note: the `AbstractCliManager`/`cliToolRegistry`/panel-type-per-tool\n> architecture was removed. Only Claude's deprecated session-level path still uses\n> it. Do not build new tools on it.\n\n## 0. Learn the CLI first\n\nBefore writing code, verify against the real binary (see the Cursor example):\n\n- Launch command and permission-skip flags (`cursor-agent --force --trust`).\n- Whether the prompt can be passed as a positional argument, including slash-prefixed\n input (Cursor: yes, unconditionally → argument delivery).\n- Session/resume mechanics (Cursor: `create-chat` pre-creates an id; `--resume <id>`;\n `--continue` for latest).\n- Screen frames for status detection: idle composer, working/spinner, approval\n prompts, first-run dialogs. Capture raw PTY bytes with a `pty` harness and distil\n them into test fixtures.\n- Whether the TUI uses the alternate screen and whether it repaints while idle.\n\n## 1. Register the agent in the RunPane contract\n\n`contracts/runpane/contract.json`:\n\n- Add the id to `enums.agents`.\n- Add `agentTemplates.<id> = { title, command, description }`.\n- Update `<codex|claude|cursor>` usage strings.\n- Run `pnpm run generate-runpane-contract` and commit all regenerated artifacts.\n- `scripts/test-runpane-contract.js` (`checkAgentTemplateParity`) loops over the agent\n enum, so the new agent is exercised automatically.\n\n## 2. Teach the classifier the new binary\n\n`main/src/services/agents/agentIdentity.ts` is the **single** command→agent\nclassifier (used by terminalPanelManager, sessionManager, and shutdown marking).\n\n- Widen `TerminalPanelState['agentType']` in `shared/types/panels.ts`.\n- Add the id to `CLI_AGENT_TYPES` and its executable name(s) to\n `AGENT_EXECUTABLES`. The classifier parses the command into tokens (it sees\n through `env`, `command`, `exec` and shell `-c` wrappers) and matches the\n executable name exactly.\n\n## 3. Launch/resume branches\n\n`main/src/services/terminalPanelManager.ts` → `resolveCliLaunchCommand` gets a branch\nper agent: fresh launch (optionally with the prompt as a quoted argument), and\ninterrupted-resume (`wasInterrupted` + `agentSessionId`). Keep agent-specific string\nbuilding in a pure module next to `agents/cursorLaunch.ts` so it is unit-testable.\n\nIf the CLI owns its session ids, scrape them from PTY output via\n`captureAgentSessionId` (add an extractor to `extractAgentSessionId`'s dispatch). If\nlaunch readiness cannot key on first PTY byte (e.g. shell traffic precedes the TUI),\nadd a ready detector like `createCursorReadyDetector` and gate `signalCliReady` on it.\n\nOpenCode validates a `ses_*` id and durably persists it before spawning the PTY,\nthen reuses that exact id with `--session`. Its idle composer gates readiness and\nonce-only stdin prompt delivery; wrapped commands remain opaque. RunPane doctor\nchecks the installed executable's session capability before reporting support.\n\n## 4. Status manifest\n\n`main/src/services/agentStatus/manifests.ts`: write a `<TOOL>_MANIFEST` from captured\nfixtures and register it in `MANIFESTS_BY_AGENT`. Unknown agents fall back to\n`GENERIC_MANIFEST` (works, less precise). Keep blocker rules narrow and live-region\ngated so answered prompts in scrollback don't stick. Record the CLI version the\nfixtures came from.\n\n## 5. Restart/auto-resume\n\n`main/src/services/agents/agentResume.ts` → `resolveResumeId` maps the agent to the\nresume id shown in the resume dialog. `sessionManager.resumeInterruptedSessions` and\nthe `index.ts` graceful-shutdown path are agent-generic once the classifier knows the\nbinary.\n\n## 6. RunPane IPC\n\n`main/src/ipc/runpane.ts`:\n\n- `shouldUseArgumentDelivery` — how the initial prompt reaches the CLI.\n- `runAgentDoctor` — add fallback binary paths (`AGENT_FALLBACK_BIN_PATHS`) when the\n install dir is typically off the GUI PATH.\n\n## 7. Frontend\n\n`shared/constants/agentLaunchPresets.ts` is the single list behind the toolbar pills,\nAdd Tool dropdowns (desktop + remote), and `mod+alt+N` hotkeys. Add one entry\n(`platforms` gates unsupported OSes); `agentLaunchPresets.test.ts` pins the list\nagainst the RunPane contract. Add the brand icon to\n`frontend/src/components/ui/BrandIcons.tsx` and register it in `CLI_BRAND_ICONS`\n(`frontend/src/components/ui/brandIconRegistry.ts`), and a search alias in\n`frontend/src/components/settings/catalog.tsx`.\n\n## 8. Worktree file sync\n\nIf the CLI reads a config directory (like `.cursor/`), add it to\n`DEFAULT_WORKTREE_FILE_SYNC_ENTRIES` in `shared/types/worktreeFileSync.ts` so it is\ncopied into new worktrees.\n\n## 9. Pane Chat (optional)\n\nTo offer the agent as a Pane Chat orchestrator: widen `PaneChatAgent` and the panel-id\nmaps in `shared/types/paneChat.ts`, extend `paneChatManager` (title, session-id\nstrategy, bootstrap prompt), and teach `skillCacheManager` to emit the orchestrator\nguide in a format the CLI actually reads (Cursor: `.cursor/rules/*.mdc`).\n\n## 10. Tests are the spec\n\nEvery step above lands test-first: `agentIdentity.test.ts`, `<tool>Launch.test.ts`,\n`terminalPanelManager.test.ts`, `agentResume.test.ts`, `manifests.test.ts` +\n`agentStatusPipeline.test.ts` (real captured bytes), `runpane.test.ts` (agent matrix +\ndoctor), `agentLaunchPresets.test.ts`, `scripts/test-runpane-contract.js`.\n\n## Agents launched through a wrapper\n\nPeople often start a supported agent through their own command, such as\n`agent-farm run free-range` or a shell script. The launch command then says\nnothing about the agent, but Pane still needs to know it, because composer\ndetection, `panels submit` staging, the agent-specific status manifest and the\nagent shown in `panels list` depend on the panel's `agentType`.\n\nManaged agent panels emit `agent.busy`, `agent.ready`, `agent.blocked`,\n`agent.unknown` and `agent.idle` events before identification. These reach the\ndefault agents-only workspace watch even without an `agentType`; ordinary shell\npanels do not emit them. For example, an Agent Farm variant picker can report\n`agent.blocked` before the agent starts. An untyped `panel.exited` is still\nfiltered by the agents-only watch.\n\nPane decides a terminal panel's agent in this order, and records the source in\n`TerminalPanelState.agentDetection`:\n\n1. **`declared`**: `--agent <id>` together with `--tool-command <command>` on\n `panes create`, `panes adopt` and `panels create`. On the wire this is\n `tool: { command, agentType }`. Here `--agent` means \"the agent this command\n runs\".\n2. **`command`**: the launch command's executable word, through\n `resolveAgentTypeFromCommand` in `main/src/services/agents/agentIdentity.ts`.\n3. **`process`**: the PTY's foreground process name, which node-pty reports as\n `pty.process`. The names `claude`, `codex`, `cursor-agent` and `opencode` map to agents.\n Claude Code's native installer runs `~/.local/share/claude/versions/<version>`,\n so it reports a bare version such as `2.1.283`. Pane resolves that name to an\n executable path with `ps` (or `/proc/<pid>/exe` on Linux) before trusting it.\n This source is skipped on Windows, WSL and ptyHost terminals, where node-pty\n cannot name the foreground program. A wrapper that stays in the foreground\n (for example a Node process that spawns the agent in its own process group)\n hides the agent from this check.\n4. **`screen`**: Claude's closed rule/`❯`/rule composer box, or Codex's\n `OpenAI Codex` header with its `›` prompt, or either agent's folder-trust\n screen: Claude's `Claude Code'll be able to read` with\n `Yes, I trust this folder`, or Codex's `Codex can read, edit, and run files here`\n with `Trust and continue`\n (`main/src/services/agents/agentScreenSignature.ts`). Pane adopts it only after\n two consecutive status polls match. It ignores the check while Pane's own\n shell is in the foreground, so an agent's last frame left above a shell\n prompt does not count.\n\nPane runs the process and screen checks on the agent-status poll\n(`TerminalPanelManager.pollAgentStatus`) until the panel's agent is known. Once\nit is known, Pane:\n\n- stores `agentType`, `agentDetection`, `launchCommand`, `isCliPanel: true` and\n `launchMode: 'wrapped'` on the panel's custom state;\n- switches the panel to that agent's status manifest;\n- restates the panel's current status with the identified agent attached.\n\nAgent-specific composer readiness and a starting message key off the identified\nagent, however it was identified. Pre-identification watch events and folder-trust\nidentification do not establish composer readiness:\n\n- `--wait-ready` (and `panels wait --for ready`) on a launched command matches\n only once the panel has an agent and that agent's own ready rule passes:\n Claude's composer, Codex's configured footer, the existing rules for the\n others. A wrapper's first output byte never counts. A command that shows no\n supported agent times out with `readiness.reason.code: no-agent-detected`;\n one that exits back to Pane's shell fails at once with `command-exited`\n (where the foreground process is known). A plain shell with no launch\n command is ready once it exists.\n- Prompts before the agent appears come back as `blocked` with their text:\n Agent Farm's `Trust this repository workspace? [y/N]`, and any `[y/N]` or\n similar last-line question a wrapper or shell asks, even when `--agent` was\n declared. Pane never answers them.\n- With `--wait-ready`, runpane owns the starting message: it claims the\n message at create, waits for readiness, stages the text and presses the\n detected agent's submit key separately (Ctrl+Enter for Codex), and verifies\n it like `panels submit`. Without `--wait-ready`, the terminal manager holds\n the message until the agent is identified and idle at its composer, then\n stages and submits it the same way. If no agent appears within 30 seconds\n (a blocking prompt restarts that wait), it records `initialInputError` and\n never types the message into the shell or the wrapper.\n- `[TerminalPanelManager] Starting message …` and `[Runpane] Starting message …`\n log lines mark each write and its outcome, with the agent, detection source,\n byte count, submit key and any `PANE_AGENT_SESSION_ID` the wrapper printed.\n They never include the message text.\n\n`launchMode: 'wrapped'` means the command is launched exactly as given.\n`resolveCliLaunchCommand` never adds `--session-id`, resume flags or a prompt\nargument to it. On restart, Pane runs the wrapper again, and the wrapper handles\nits own resume. The exceptions are a panel with an explicit custom command resume\nconfig and a recognized `agent-farm run|agent` command, which relaunch through\ntheir resume template (see\n[Resuming custom launchers](SESSIONS.md#resuming-custom-launchers)). For the same\nreason, `--resume` is rejected for wrapper commands.\n\nDirect OpenCode launches use Pane's durable session id and readiness-gated input.\nNative selector insertion accepts literal direct commands and simple assignment or\n`env NAME=value` prefixes, not recognized shell wrappers or `env --` forms. Those\nforms fail before native identity allocation; explicitly wrapped launches remain opaque.\nNative commands reject both `--continue` and `-c`, and reject OpenCode's\n`--prompt` argument because it would replay on every restore. Supply initial\ninput through Pane (or RunPane's `--prompt`) instead.\nWrapped OpenCode launches run unchanged; the wrapper owns session selection and\nresume, just as it does for the other supported agents.\n\nA new built-in agent works with wrappers once its executable name is in\n`AGENT_EXECUTABLES`. Add a screen signature only if the agent's UI has a stable,\ndistinctive frame.\n\n## Delivery state and ghost text\n\n`panels submit`, `panels submit-composer`, `agents send` and create prompts\nreport `delivery: { state, evidence }` for Claude and Codex. The state comes\nfrom the agent's own transcript when Pane can find it\n(`main/src/services/agentTranscript/`):\n\n- Claude: `~/.claude/projects/<cwd, non-alphanumerics as '-'>/<session id>.jsonl`.\n Pane picks the session id (`--session-id`), except for wrapped launches, where\n it reads the worktree's recently written transcripts. A taken turn is a\n `type: \"user\"` entry. A message sent while Claude works is first a\n `queue-operation` `enqueue` entry, then a `user` entry once taken.\n- Codex: the newest rollout under `~/.codex/sessions/YYYY/MM/DD/` whose\n `session_meta.cwd` is the worktree, or the rollout named by the thread id once\n Pane knows it. A taken turn is a `response_item` user message. Codex writes a\n queued message only once it takes it, so `queued` comes from its screen\n (`Messages to be submitted after next tool call` / `↳ <message>`).\n\nThe reader tails each file from a byte offset, so polling a long transcript\nstays cheap. Without a transcript, the screen decides: a steadily empty composer\nis `taken`, text still in it is `in-composer`.\n\nThe terminal model splits each screen into typed cells and ghost cells: dim\n(SGR 2) or placeholder-grey text, which agents use for placeholders, prompt\nsuggestions and queued-message lists (`TerminalStateEmulator.getScreenText`).\nComposer detection reads only typed cells; `panels screen` reports the ghost\ntext in the composer as `composer.ghostText`. A new agent whose placeholder is\nneither dim nor mid-grey needs its own placeholder rule in\n`agentScreenSignature.ts`.\n"},{"path":"docs/ANALYTICS_INVARIANTS.md","title":"Analytics Invariants","text":"# Analytics Invariants\n\nThese rules keep Pane's PostHog funnel person-stitchable and privacy-safe. Pane\nuses default-on product analytics for new and previously undecided installs.\nPrivacy Settings clearly discloses that default and provides the opt-out.\nExplicit choices from older versions must never be overwritten.\n\n## Bundled Executable Code\n\nImport the SDK through `posthog-js/dist/module.no-external` and keep\n`disable_external_dependency_loading` enabled. This is PostHog's\n[supported entry point for Electron](https://posthog.com/docs/libraries/js):\nanalytics requests and JSON configuration remain available, while executable\nconfiguration, session replay, surveys, and other remote extensions cannot load.\nAll executable dependencies must come from the reviewed, lockfile-pinned build.\nDo not add a second import from the default SDK entry point.\n\n## Identity Comes First\n\nResolve analytics identity in the main process before the renderer captures any\nanalytics disclosure or choice event.\n\nRequired ordering:\n\n1. Resolve GitHub CLI email, git email, or stable `install_id`.\n2. Persist the analytics identity in config.\n3. Capture `analytics_default_enabled` for an undecided install entering the\n default-on experiment.\n4. Capture `analytics_opted_in` or `analytics_opted_out` for explicit Settings\n choices.\n5. Capture `app_first_opened` and usage events only after the default or choice\n is applied.\n\nDo not flush queued usage until the initial default or stored choice has been\nresolved. Opting out must capture the choice marker directly, discard queued\nusage, and leave the PostHog SDK opted out.\n\n## Existing Choice Preservation\n\n`analytics_consent_shown=true` records an explicit choice made under the legacy\nopt-in dialog. Its stored `analytics.enabled` value is authoritative. Never\nreplace an explicit `false` with the default-on value. The\n`analytics_default_applied` distinguishes installs already processed by the\nnew experiment from previously undecided installs.\n\n## Required Event Context\n\nEvery first-run funnel event must include:\n\n- `distinct_id`\n- `install_id`\n- `app_version`\n- `platform`\n- `identity_source`\n\nThe event should also set person properties when the identity is known\n(`personProperties` in `frontend/src/services/posthog.ts`):\n\n- `install_id`\n- `identity_source`\n- `github_username`\n- `github_email`\n- `git_email`\n- `git_email_sha256`\n- `git_user_name`\n- `app_version`\n- `platform`\n\nPrivacy Settings must disclose that analytics can be associated with locally\navailable GitHub or Git account details, including a username or email. Do not\ndescribe this telemetry as anonymous while those person properties are sent.\n\n## Opt-Out Is Still Identified\n\nCapture `analytics_opted_out` with identity context before disabling analytics.\nAfter that capture, discard queued usage events and keep analytics disabled.\n\nThis records who opted out without sending their later product usage.\n\n## Config Saves Preserve Analytics Fields\n\nRenderer settings updates must deep-merge analytics config instead of replacing\nit. In particular, do not drop the identity fields in `analytics`\n(`main/src/types/config.ts`):\n\n- `installId`\n- `distinctId`\n- `identitySource`\n- `githubUsername`, `githubEmail`, `gitEmail`, `gitEmailHash`, `gitUserName`\n\n## Attribution and Versioning\n\nWhen web attribution is present, emit attribution events with the same\n`distinct_id` and `install_id` as consent and usage events.\n\nApp version should come from the running app context and be attached to disclosure,\nfirst-open, usage, attribution, and close events. This lets PostHog distinguish\ncurrent users from older installs.\n\n## Remote Pane Privacy\n\nRemote Pane analytics must use explicit sanitized events. Do not rely on\nautocapture for Remote Pane connection-code, token, host, or command surfaces.\nAny UI that renders or accepts `pane-remote://` codes, remote access tokens, or\nremote setup commands must include `ph-no-capture`.\n\nSSH host aliases from the user's SSH config never go into events or logs sent\noff the machine. The SSH hosts sidebar section and the SSH view carry\n`ph-no-capture`.\n\nRemote Pane events must never include:\n\n- connection codes\n- bearer tokens\n- token hashes\n- base URLs\n- hostnames or IP addresses\n- profile labels\n- client labels\n- device labels\n- raw error messages\n- local paths or Pane data directories\n- remote runtime ids\n\nAllowed properties should stay enum-like: `surface`, `role`, `flow`, `result`,\n`tunnel_preference`, `tunnel_kind`, `data_mode`, `client_kind`,\n`connection_mode`, `failure_stage`, `failure_category`, and connected-client\ncount buckets.\n\n## Remote Pane Event Catalog\n\n| Event | Primary source | Funnel role |\n|---|---|---|\n| `remote_pane_host_setup_started` | `main/src/ipc/remoteDaemon.ts` | Host setup started |\n| `remote_pane_host_setup_succeeded` | `main/src/ipc/remoteDaemon.ts` | Host setup completed |\n| `remote_pane_host_setup_failed` | `main/src/ipc/remoteDaemon.ts`, `main/src/daemon/remoteTransportController.ts` | Host setup or transport failed |\n| `remote_pane_setup_terminal_opened` | `main/src/ipc/remoteDaemon.ts` | Setup command opened in terminal |\n| `remote_pane_connection_code_created` | `main/src/ipc/remoteDaemon.ts` | Host code created |\n| `remote_pane_connection_pair_created` | `main/src/ipc/remoteDaemon.ts` | Advanced paired profile created |\n| `remote_pane_connection_code_imported` | `main/src/ipc/remoteDaemon.ts` | Client imported code |\n| `remote_pane_connection_code_import_failed` | `main/src/ipc/remoteDaemon.ts` | Client import failed |\n| `remote_pane_client_connect_started` | `main/src/daemon/client/remotePaneClient.ts` | Desktop client connection started |\n| `remote_pane_client_connected` | `main/src/daemon/client/remotePaneClient.ts`, `main/src/daemon/httpApiServer.ts` | Desktop client connected |\n| `remote_pane_client_connection_failed` | `main/src/daemon/client/remotePaneClient.ts`, `main/src/ipc/remoteDaemon.ts` | Desktop client connection failed |\n| `remote_pane_client_disconnected` | `main/src/daemon/client/remotePaneClient.ts`, `main/src/daemon/httpApiServer.ts` | Desktop client disconnected |\n| `remote_pane_profile_deleted` | `main/src/ipc/remoteDaemon.ts` | Client profile removed |\n| `remote_pane_host_access_cleared` | `main/src/ipc/remoteDaemon.ts` | Host access revoked |\n| `remote_pane_host_clients_disconnected` | `main/src/ipc/remoteDaemon.ts` | Host disconnected clients |\n| `remote_pane_host_transport_started` | `main/src/daemon/remoteTransportController.ts` | Host transport live |\n| `remote_pane_host_transport_stopped` | `main/src/daemon/remoteTransportController.ts` | Host transport stopped |\n| `remote_pane_pwa_client_connected` | `main/src/daemon/httpApiServer.ts` | Browser/PWA client connected |\n| `remote_pane_pwa_client_disconnected` | `main/src/daemon/httpApiServer.ts` | Browser/PWA client disconnected |\n| `remote_pane_remote_runtime_used` | `main/src/daemon/client/remotePaneClient.ts` | Remote runtime actually used |\n\n## Failure Volume and Error Privacy\n\nKeep core desktop activity (`app_opened`, `app_closed`, `session_created`,\n`panel_switched`, `terminal_panel_created`). Neither wrapper emits routine\ncommand starts or successes. Preserve sanitized wrapper failures and download\nlifecycle events; honor explicit local analytics opt-out as well as CI and\n`RUNPANE_TELEMETRY_DISABLED`.\n\nLocal control emits only `runpane_local_control_failed` for thrown failures or\nhandled `ok: false` results. Normal wait deadlines and optional agent\nunavailability are excluded. Its properties are `action`, `status`, `command_ok`,\n`failure_kind`, `error_type`, `error_code`, and `failure_category`, plus app version,\nplatform and Electron version. Each coarse operation/category/code combination\nreports at most once per five minutes, with an overall cap of 20 per hour per\napp process. Limits are in memory and reset on restart. Local logging remains\nindependent of analytics suppression.\n\nProduction ingestion drops legacy `runpane_wrapper_command_started`,\n`runpane_wrapper_command_succeeded`, and `runpane_local_control`. Keep the new\nfailure event name separate so useful failures survive these exclusions.\n\nPostHog `$exception` events are reserved for app errors: renderer global errors\nand unhandled rejections, React error boundaries, unexpected renderer crashes/OOM,\nmain uncaught exceptions, and shutdown errors. Console errors and handled CLI\nfailures never become exceptions. Exceptions have their own five-minute\ndeduplication and 20-per-hour cap. Both error event paths send directly from main\nthrough PostHog's capture API, with at most four concurrent requests and a 1.5s\ntimeout; neither waits for delivery. Errors obey analytics opt-out and use only\nthe existing install UUID identity, without person-profile updates, account\nproperties, SDK URL enrichment or persistent error storage.\n\nException payloads contain coarse source, built-in error type, allowlisted error\ncode/category, version/platform, a fixed message, and up to ten packaged app JS\nframes. Frames contain relative app filenames and numeric line/column offsets,\nnever absolute paths, function names or source context. Raw messages, causes,\nCLI argv, terminal contents, prompts, source code, repository paths, usernames,\nhome paths and environment values are excluded. Development/server URLs and\nexternal dependency frames are omitted. Restricted interactive-element\nautocapture and pageviews stay enabled, with text and attributes masked;\nautomatic exception capture is disabled. Session replay policy is unchanged.\n\nMain fatal capture uses `uncaughtExceptionMonitor`, preserving exit behavior.\nDelivery during process exit is best effort. Native main-process aborts and\nsegfaults cannot be captured by JavaScript; there is no minidump uploader or\nsource-map upload pipeline. Renderer process loss reports a coarse crash reason\nwithout a native stack. Native crash reporting remains a separate follow-up.\nPostHog's [Electron support request](https://github.com/PostHog/posthog/issues/43993)\ndoes not provide native Electron crash support. The payload follows the supported\n[manual exception schema](https://posthog.com/docs/error-tracking/installation/manual)\nfor JavaScript errors; this is not a replacement for native Electron crash reporting.\nSource-map symbolication is not wired or verified: current reports contain\npackaged JavaScript offsets only, and a future source-map upload integration must\nbe verified against a packaged build before claiming original TypeScript locations.\n\n## Test Coverage\n\nChanges to disclosure, analytics config, or first-run event ordering should update\nor add coverage in:\n\n- `main/src/services/analyticsIdentity.test.ts`\n- `tests/analytics-consent.spec.ts`\n\nThe Playwright test should verify both event order and payload shape, including\nSDK identity and interaction events, and reject external script requests even\nwhen remote configuration enables extensions.\n"},{"path":"docs/APPEARANCE.md","title":"Appearance","text":"# Appearance\n\nPane can follow the operating system or stay on a fixed palette. **System** remembers one light palette and one dark palette and selects between them as the OS appearance changes. **Fixed** uses a third, independent palette. Switching modes never replaces any remembered selection.\n\nNew installations default to System with Light (rounded) for the light slot and Dark (sharp) for the dark slot; the remembered Fixed palette is Light (rounded). Existing configurations that only contain `theme` migrate to Fixed so their current palette remains exact. The legacy palette also seeds the matching System slot, while the opposite slot keeps its default.\n\nPalette slots are validated at load, renderer, IPC, and persistence boundaries. Only palettes whose canonical base class is `light` can occupy the light slot, and only palettes whose base is `dark` can occupy the dark slot. Invalid stored fields are repaired independently and reported by the main process.\n\n## First paint\n\nAt startup the persisted config selects Electron's `nativeTheme` source. Main passes the four-field appearance snapshot through an argv argument and selects a cached, theme-specific BrowserWindow background colour. The preload decodes the snapshot synchronously; the HTML bootstrap resolves System against `prefers-color-scheme` and stamps the full class chain before React mounts. The provider then owns live OS updates, persistence, rollback, window controls, terminal, and editor updates. A versioned local cache and legacy `localStorage.theme` are fallback-only.\n\nOn Linux, System depends on the desktop environment and Chromium exposing `prefers-color-scheme`. Desktops that do not publish an appearance preference may remain on their reported default slot.\n\n## App icon\n\n`pnpm generate-icons` renders the general app assets from\n`main/assets/icon-source.svg` and the macOS Dock assets from\n`main/assets/icon-macos-source.svg`. The macOS Dock PNG and bundle ICNS use the\nblack-sky landscape artwork inside a transparent 1024px canvas so its visible size\nmatches neighboring Dock icons. The simpler PNG, ICO, and favicons continue to\nserve Linux, Windows, and small web sizes. `pnpm dev` copies an updated Dock PNG\ninto the main-process build before launching Electron.\n"},{"path":"docs/ARCHITECTURE.md","title":"Pane architecture","text":"# Pane architecture\n\nThis is an index of how Pane fits together. It names the owning files; the code\nis the reference for details.\n\n## Words\n\n- **Repository (project):** a saved base git repo.\n- **Pane (session):** one feature or PR workspace. It normally owns one\n Pane-managed git worktree and branch.\n- **Panel:** a tab inside a pane. Types are `terminal`, `diff`, `explorer`,\n `editor`, `logs`, `dashboard`, `setup-tasks`, `browser` and `notes`\n (`shared/types/panels.ts`). Agent CLIs run in `terminal` panels.\n\n## Processes\n\n```\nrenderer (frontend/, React) ──IPC via preload──▶ main process (main/src)\n │ services, SQLite, git\nremote PWA / mobile ──HTTP + SSE──▶ daemon ◀──socket── runpane CLI\n │\n ptyHost (optional utility process)\n```\n\n- **Main process** (`main/src/index.ts`) owns native integration, git\n worktrees, CLI processes and persistence. Business logic lives in\n `main/src/services/`; IPC handlers live in `main/src/ipc/`.\n- **Preload** (`main/src/preload.ts`) is bundled with esbuild because the\n renderer is sandboxed (`scripts/verify-sandboxed-preload.js` checks it). It\n sends each invoke either to the main process or to the daemon, decided by\n `isDaemonOwnedChannel` in `shared/types/daemon.ts`. Keep ownership lists in\n that module, and run `pnpm build:main` to check the bundle still loads.\n- **Renderer** (`frontend/src`) is React with Zustand stores\n (`frontend/src/stores`). `frontend/index.html` is the desktop app;\n `frontend/remote.html` is the Remote PWA that `mobile/` also bundles.\n- **Daemon** (`main/src/daemon`) serves the command registry over a local\n socket (a `pane-daemon-*` directory under the system temp directory, or a\n named pipe on Windows). The `runpane` CLI talks to it. For Remote Pane it also\n serves HTTP and SSE (`httpApiServer.ts`), and the same server, behind\n `tailscale serve` on port 8443, gives your other machines `runpane workspace`\n and codeless desktop remote mode (`workspaceHost.ts`,\n `services/tailnetMachines.ts`, [RUNPANE_WORKSPACES.md](RUNPANE_WORKSPACES.md)).\n Who it admits is the visibility and optional password in `auth.ts`.\n `pnpm daemon:headless` runs it\n without a window. Remote setup and lifecycle:\n [SELF_HOSTED_REMOTE_DAEMON.md](SELF_HOSTED_REMOTE_DAEMON.md) and\n [remote-daemon-lifecycle.md](remote-daemon-lifecycle.md).\n- **ptyHost** (`main/src/ptyHost`) runs PTYs in an Electron utility process.\n It is on by default on Windows (`usePtyHost` in `configManager.ts`), and\n `PANE_USE_PTY_HOST=1` turns it on elsewhere. Otherwise PTYs run in the main\n process. See [troubleshooting/WINDOWS_APP_HANG.md](troubleshooting/WINDOWS_APP_HANG.md).\n\n## Terminal output\n\nPTY output goes through `terminalPanelManager.ts`, which batches it and applies\nflow control. It feeds a headless xterm emulator (`terminalStateEmulator.ts`),\nwhose serialization restores a terminal when its panel is shown again, and\nstores capped scrollback in the `panel_buffers` table. The renderer draws with\nxterm.js in `frontend/src/components/panels/TerminalPanel.tsx`. The comment\nabove that component describes the activation lifecycle; read it before you\nchange how panels show, hide or refresh.\n\nAgent status (working, idle, blocked) is derived in\n`main/src/services/agentStatus/`.\n\n## File previews\n\n`FileEditorView.tsx` routes editor tabs from the Files inspector, terminal links,\nand `runpane panels open --file` using `shared/utils/filePreview.ts`. Supported\nformats open read-only; ordinary code and text, HTML included, retain the editor\nand auto-save.\nImages, PDFs, fonts, video and audio load through `pane-media`, registered in\n`main/src/services/mediaPreview.ts`. A revocable URL grants access to one file;\neach request rechecks the existing worktree/symlink boundary through\n`file:getPath`. `mediaStream.ts` streams byte ranges from disk. File bodies do\nnot pass through text/base64 IPC. On a remote desktop, the URL is bound to the\nconnected host, and each request streams from the host's authenticated `/media`\nroute with the same boundary check and byte ranges. Archive and SQLite listings\n(`file:preview-list`) run on the host. \"Open with system app\" on a remote desktop\ncopies the file to a temporary folder on that desktop and opens the copy;\n\"Reveal in folder\" appears only on the host.\n\nHTML is not a preview format. Clicking an `.html`/`.htm` file in the Explorer,\nor its Eye button, opens it rendered in a browser tab: the tab already showing\nthat file, or else a new one (a tab showing anything else is never retargeted).\nThe \"Open HTML files\" setting (Settings → General) switches Explorer clicks to\neditable source, and an HTML file's right-click menu offers both \"Open preview\nin browser\" and \"Open as file\". A browser tab showing a worktree file offers\n\"View source\", and an HTML editor tab offers \"Preview\", which works like a click. `panels open --file` always\nopens the source; `--url file://...` renders.\n\nMarkdown, CSV/TSV and structured text fetch at most 1 MiB. Markdown renders\nwith a source toggle.\nTables display at most 500 data rows and 100 columns. JSON/JSONL are formatted;\nYAML/TOML retain source formatting in read-only Monaco with folding. Fonts use\na temporary FontFace specimen. Decode failures offer system open/reveal actions.\n\n`filePreviewListing.ts` lists ZIP central directories (up to 4 MiB) and TAR\nheaders without extraction, with at most 1,000 entries. ZIP64/split ZIP and\ncompressed TAR are unsupported; extended TAR names are not expanded. SQLite\nlists tables/views from a bounded temporary copy (32 MiB), opened read-only and\nremoved afterward. Nonempty WAL and rollback journals are refused before and\nafter copying beside the canonical target; hard-linked databases are refused\nbecause their recovery-file owner path is ambiguous. No source sidecars are\ncreated. Native schema inspection runs in\na child process with a 3-second deadline, a 1 MiB output cap and at most 1,000\ndisplayed entries. No table data queries are executed.\n\n`file:read` samples at most 8 KiB before decoding UTF-8. Binary files show a\nread-only notice, and `file:write` refuses to overwrite binary content as text.\nPreview components never enter the editable auto-save path, even on errors.\n\n## Data\n\n- The data directory is `~/.pane` for an installed app. `--pane-dir` or\n `PANE_DIR` overrides it (`main/src/utils/appDirectory.ts`).\n- `sessions.db` is SQLite in WAL mode. The base schema is\n `main/src/database/schema.sql`; later changes are inline migrations in\n `main/src/database/database.ts` that run at startup.\n- `config.json` holds app settings (`main/src/services/configManager.ts`,\n types in `main/src/types/config.ts`).\n- Worktrees go in `<repo>/worktrees/` unless the project sets another folder\n (`worktreeManager.ts`). A worktree on a remote-tracking base fetches that\n branch first (`worktreeBaseRefresh.ts`: capped at about 10 s, shared by\n concurrent creates, never fails the create). `worktreePoolManager.ts` keeps\n empty reserve worktrees on `_reserve/<hex>` branches so new panes open fast;\n a reserve is only claimed while its base ref still points at its commit.\n- Archive cleanup intents live in SQLite's `archive_cleanup_jobs`, atomically\n committed with the session archive flag. `ArchiveCleanupManager` resumes only\n those intents, validates physical directory/Git identity, and serializes work\n per repository with two global slots. Awaited, cooperative purge batches use\n Electron's `original-fs` and job-level busy retries. There are no detached\n deletion helpers or timed-out deletions left running. Failed jobs remain\n visible through ArchiveProgress; interrupted scripts require explicit skip\n on retry. Create/remove and pool background mutations share FIFO repository\n admission with no waiter timeout; archive scripts and purge run outside it.\n Known process trees are captured before committing intent. Teardown preserves\n PTY retirement cleanup, escalates freshly matched process identities, and\n verifies exit before touching files. Survivors remain in the job across\n restart and Retry; missing captures and snapshot errors fail closed.\n Process-table reads are confined to archive preparation/teardown/retry, each\n subprocess bounded to five seconds. Windows uses kernel start times and\n pinned process handles; POSIX revalidates start times before individual signals.\n Create/remove also check the cleanup path guard;\n Restore is blocked until cleanup completes. Quarantine lives under\n `<git-common-dir>/pane-archive-cleanup`, separate from legacy trash sweeps.\n- Per-repo setup, run and archive scripts come from `pane.json` and friends:\n [CONFIG_FILES.md](CONFIG_FILES.md).\n\n## Notes\n\nLocal Notes panels use `main/src/services/notes.ts` through `main/src/ipc/notes.ts`.\nCanonical documents live in `<PANE_DIR>/notes/notes.json`, outside disposable\nworktrees. A revision check rejects stale edits; writes atomically replace the\nstore. Promotion changes the canonical scope and retains references to earlier\nscopes. The Session view derives associated project notebooks from existing Pane\nassociations. Renderer recovery drafts retain edits when an autosave fails. The borderless\neditor inserts text/drawing blocks through inline plus or slash menus; the\nsettings cog holds promotion and deletion actions. Agent visibility is automatic\nwithin scope, with no sharing controls in the editor. Restoring an archived\nPane refreshes its derived context from current project notes.\n\n`noteExports.ts` derives marked instruction sections and drawing assets from saved\ndocuments. Excalidraw scenes remain editable JSON, with PNG previews and readable\nlabels in agent-facing Markdown. Global destinations are Claude's user\n`CLAUDE.md`, Codex's user `AGENTS.md` (or existing override), and Cursor's local\n`rules/pane-memories.mdc`. Cursor CLI also receives the generated memory through\na user `sessionStart` hook (`cursorNoteHook.ts`), preserving existing hooks.\nScoped content is exported outside repositories to\n`<PANE_DIR>/notes/contexts/<pane-id>.md`. Each new native Pane terminal receives\nits path through `PANE_NOTES_FILE`; the user-level integration tells agents to\nread that file before each turn. Project contexts contain only that project's\nnotes plus the current feature; Session contexts contain only Session notes.\nOrdinary saves leave repositories clean. The existing `managedAgentsMd` opt-in\nadditionally exports scoped notes into repository `AGENTS.md` and `CLAUDE.md`;\nturning it off removes those memory sections.\n\nExports preserve symlinks and permissions and replace files atomically. A store\nownership marker prevents a second Pane data directory from overwriting the\nfirst directory's user memory. Malformed marker pairs produce a visible error.\nChanging notebooks only reads data. Terminals predating this update need reopening\nto gain the context environment variable. After the first scoped note, start a\nnew agent conversation to load the scoped-reading instruction. Global-file changes refresh on Codex's next\nturn, Claude resume, or a new Cursor conversation in the tested CLI versions;\nupdating a file alone cannot erase context already read. Notes support native\nmacOS, Windows, and Linux installations; WSL agent exports report an unsupported\ndestination instead of claiming delivery. There is no cross-machine sync.\n\n## Subsystems\n\n| Area | Owner | Doc |\n| --- | --- | --- |\n| Adding an agent CLI | `main/src/services/agents/` | [ADDING_NEW_CLI_TOOLS.md](ADDING_NEW_CLI_TOOLS.md) |\n| Panels | `panelManager.ts`, `shared/types/panels.ts` | [TOOL_PANEL_SYSTEM.md](TOOL_PANEL_SYSTEM.md) |\n| Renderer updates | IPC events in `main/src/events.ts` | [STATE_MANAGEMENT.md](STATE_MANAGEMENT.md) |\n| Timestamps | `main/src/utils/timestampUtils.ts` | [TIMESTAMP_HANDLING.md](TIMESTAMP_HANDLING.md) |\n| Pane Chat and orchestration sessions | `paneChatManager.ts`, `skillCacheManager.ts`, `paneChatBundle/` | [SESSIONS.md](SESSIONS.md) |\n| SSH hosts view | `sshConfigHosts.ts`, `ipc/sshHosts.ts`, `SshView.tsx` | [SSH_HOSTS.md](SSH_HOSTS.md) |\n| `runpane` CLI contract | `contracts/runpane/` | [RUNPANE_CLI_CONTRACT.md](RUNPANE_CLI_CONTRACT.md) |\n| Analytics | `analyticsManager.ts`, `frontend/src/services/posthog.ts` | [ANALYTICS_INVARIANTS.md](ANALYTICS_INVARIANTS.md) |\n| Themes | `frontend/src/styles/tokens/colors.css` | [APPEARANCE.md](APPEARANCE.md), [scripts/README.md](../scripts/README.md) |\n| Usage and cost | `main/src/services/usage/` | [usage/README.md](../main/src/services/usage/README.md) |\n| Mobile companion | `mobile/` | [NATIVE_MOBILE.md](NATIVE_MOBILE.md) |\n\n## Rules that follow from this design\n\n- Run project commands through the asynchronous `CommandRunner` and\n `CommandExecutor` (`main/src/utils/`). Await results through IPC and the\n lifecycle callers, drop results from stopped or replaced watchers, and\n serialize Spotlight checkouts with restoration.\n- For git status in WSL, watch from inside the distro: prefer `inotifywait`.\n Without it, Pane polls `git status` in the distro every five seconds while\n focused. Don't add Windows-side recursive watchers over `\\\\wsl.localhost` or\n `\\\\wsl$`.\n- Production builds must never include React Scan (`pnpm perf:scan` is dev\n only).\n"},{"path":"docs/BUILDING_ON_WINDOWS.md","title":"Building Pane on Windows","text":"# Building Pane on Windows\n\nThis document explains how to build Pane for Windows.\n\n> **Note:** The primary native modules (`@lydell/node-pty` and `better-sqlite3-multiple-ciphers`) have prebuilt binaries for Windows.\n\n## Quick Start\n\n```bash\n# Build for x64 (recommended for most users)\npnpm run build:win:x64\n\n# Build for ARM64\npnpm run build:win:arm64\n```\n\nOutput files will be in `dist-electron/`:\n- `Pane-{version}-Windows-x64.exe` - x64 installer\n- `Pane-{version}-Windows-arm64.exe` - ARM64 installer\n\n## Prerequisites\n\n1. **Node.js** - v22.18 or later\n2. **pnpm** - v10\n3. **Python** - v3.x (for native module compilation)\n4. **Visual Studio Build Tools** - With C++ workload\n\n### Installing Visual Studio Build Tools\n\n```bash\n# Using winget\nwinget install Microsoft.VisualStudio.2022.BuildTools\n\n# Or download from:\n# https://visualstudio.microsoft.com/visual-cpp-build-tools/\n```\n\nMake sure to install the \"Desktop development with C++\" workload.\n\n## How the Build Works\n\nThe Windows build uses a custom script (`scripts/build-win.js`). It:\n\n1. Downloads the Electron prebuilt for `better-sqlite3-multiple-ciphers` for the target architecture.\n2. When building for a different architecture than the host, installs the matching `@lydell/node-pty-win32-<arch>` prebuilt package.\n3. Builds the frontend and main process, injects build info, and generates notices.\n4. Runs electron-builder with native module rebuild disabled.\n5. Checks the icons in the packaged app.\n\n### Why These Steps\n\n#### better-sqlite3-multiple-ciphers Electron Prebuilt\n\n**Problem:** When using `pnpm install --ignore-scripts`, the `prebuild-install` postinstall script doesn't run, so the package gets the wrong binary (Node.js ABI instead of Electron ABI). This causes \"is not a valid Win32 application\" errors at runtime.\n\n**Solution:** The build script runs `prebuild-install` manually with the correct Electron runtime and version to download the Electron-compatible prebuilt binary.\n\n#### Cross-Architecture node-pty\n\n**Problem:** pnpm only installs the `@lydell/node-pty` platform package that matches the host architecture.\n\n**Solution:** For cross-architecture builds, the build script downloads the target platform package and links it into `node_modules`.\n\n#### Native Module Rebuild\n\n**Problem:** Rebuilding native modules for Electron can fail on Windows due to pnpm path issues.\n\n**Solution:** The build script disables npm rebuild (`--config.npmRebuild=false`) and relies on:\n- Manually downloaded Electron prebuilts for `better-sqlite3-multiple-ciphers`\n- Prebuilt platform packages for `@lydell/node-pty` (no rebuild needed)\n\n## Manual Build Process\n\nIf the build script fails, you can try these manual steps:\n\n### Step 1: Install dependencies without running scripts\n\n```bash\npnpm install --ignore-scripts\n```\n\n### Step 2: Download Electron prebuilt for better-sqlite3\n\n```bash\n# Navigate to the better-sqlite3-multiple-ciphers package directory\ncd node_modules/.pnpm/better-sqlite3-multiple-ciphers@*/node_modules/better-sqlite3-multiple-ciphers\n\n# Download the Electron-compatible prebuilt (replace 41.10.3 with your Electron version)\nnpx prebuild-install --runtime electron --target 41.10.3 --arch x64 --verbose\n\n# Return to project root\ncd -\n```\n\n### Step 3: Build\n\n```bash\npnpm run build:frontend\npnpm run build:main\npnpm run inject-build-info\npnpm run generate-notices\npnpm exec electron-builder --win --x64 --publish never --config.npmRebuild=false\n```\n\n## Troubleshooting\n\n### Error: `node-gyp failed to rebuild`\n\nTry building with `--config.npmRebuild=false` to skip native module rebuild:\n```bash\npnpm exec electron-builder --win --x64 --publish never --config.npmRebuild=false\n```\n\n### Error: `better_sqlite3.node is not a valid Win32 application`\n\nThe better-sqlite3 native module was built for Node.js instead of Electron. Run the prebuild-install step:\n```bash\ncd node_modules/.pnpm/better-sqlite3-multiple-ciphers@*/node_modules/better-sqlite3-multiple-ciphers\nnpx prebuild-install --runtime electron --target 41.10.3 --arch x64 --verbose\n```\n\n### Native modules not working at runtime\n\nIf the app crashes due to native module issues:\n1. Ensure you're running on the same architecture you built for\n2. Verify the better-sqlite3 prebuilt was downloaded for Electron (see above)\n3. Check that Python and Visual Studio Build Tools are properly installed\n\n## Architecture Notes\n\n### x64 vs ARM64\n\n- **x64 build**: Works on both x64 and ARM64 Windows (ARM64 uses emulation)\n- **ARM64 build**: Native performance on ARM64, but requires ARM64 native modules\n\nFor most users, the x64 build is recommended as it works on all Windows machines.\n\n### Native Module Compatibility\n\nThe following native modules are used:\n- `better-sqlite3-multiple-ciphers` - SQLite database (has Windows Electron prebuilts)\n- `@lydell/node-pty` - Terminal emulation (has Windows x64 prebuilts, validate after each Electron major upgrade)\n\nBoth modules have prebuilt binaries that work on Windows without compilation.\n\n## CI/CD\n\nFor CI/CD pipelines, use:\n```bash\nnode scripts/build-win.js x64\n```\n\nThis runs all of the steps above.\n"},{"path":"docs/CONFIG_FILES.md","title":"Config File Detection","text":"# Config File Detection\n\nPane automatically detects workspace configuration files at your repository root and uses them to provide default scripts for session setup, the Play button, and cleanup on archive. This lets teams share workspace configuration via git — commit the file, push, and every teammate gets the same setup.\n\n## Supported Files\n\nPane checks for these files in priority order. The first file found with at least one script defined wins:\n\n| Priority | File | Format | Origin |\n|----------|------|--------|--------|\n| 1 | `pane.json` | JSON | Pane's native format |\n| 2 | `conductor.json` | JSON | [Conductor.build](https://conductor.build) |\n| 3 | `.gitpod.yml` | YAML | [Gitpod](https://gitpod.io) |\n| 4 | `.devcontainer/devcontainer.json` | JSON | [Dev Containers](https://containers.dev) |\n\nIf none of these exist, Pane falls back to checking for `scripts/pane-run-script.js` in the worktree (the legacy auto-generated script).\n\n## pane.json\n\nThis is Pane's native config format. It follows the same schema as [conductor.json](https://docs.conductor.build/core/conductor-json), so teams using Conductor can switch to Pane without changing their config.\n\n### Schema\n\n```json\n{\n \"scripts\": {\n \"setup\": \"pnpm install\",\n \"run\": \"pnpm dev\",\n \"archive\": \"pnpm cleanup\"\n },\n \"runScriptMode\": \"concurrent\"\n}\n```\n\nAll fields are optional.\n\n### Fields\n\n#### `scripts.setup`\n\nRuns automatically when a new worktree/session is created. Use for dependency installation, database setup, environment configuration, etc.\n\n**Examples:**\n```json\n\"setup\": \"npm install\"\n\"setup\": \"pnpm install && pnpm db:migrate\"\n\"setup\": \"bundle install && rails db:setup\"\n```\n\n#### `scripts.run`\n\nRuns when you click the **Play** button in the panel tab bar (or press `Ctrl+Shift+D`). Use for starting dev servers, test watchers, or any long-running process.\n\nPane sets `PANE_PORT` to the first of a block of 10 ports picked from the pane's id (range 3000–8999), so dev servers in parallel panes rarely collide.\n\n**Examples:**\n```json\n\"run\": \"pnpm dev\"\n\"run\": \"npm run dev -- --port $PANE_PORT\"\n\"run\": \"concurrently \\\"npm run server\\\" \\\"npm run worker\\\"\"\n```\n\n#### `scripts.archive`\n\nRuns before a worktree is deleted when you archive a session. Use for cleaning up external resources — dropping test databases, revoking tokens, stopping background services.\n\n**Examples:**\n```json\n\"archive\": \"npm run db:teardown\"\n\"archive\": \"docker compose down\"\n```\n\n#### `runScriptMode`\n\nControls behavior when a new run script starts while one is already running.\n\n| Value | Behavior |\n|-------|----------|\n| `\"concurrent\"` | (Default) New script starts alongside existing ones |\n| `\"nonconcurrent\"` | Existing script is stopped before starting the new one |\n\n> **Note:** `runScriptMode` is detected but not yet enforced. This will be implemented in a future release.\n\n## How Scripts Map to Pane\n\nEach config file format maps to Pane's three lifecycle hooks differently:\n\n| Lifecycle | pane.json / conductor.json | .gitpod.yml | devcontainer.json |\n|-----------|---------------------------|-------------|-------------------|\n| **Setup** (worktree creation) | `scripts.setup` | First task's `init` | `postCreateCommand` |\n| **Run** (Play button) | `scripts.run` | First task's `command` | `postStartCommand` |\n| **Archive** (session deletion) | `scripts.archive` | — | — |\n\n## Override Model\n\nPane follows the same override model as Conductor:\n\n```\nProject Settings (UI) > Config File > Nothing\n```\n\n1. **If you set a value in Project Settings**, it always wins — the config file is ignored for that field\n2. **If Project Settings is empty**, Pane uses the config file value\n3. **If neither exists**, no script runs (except for the Play button, which falls back to Claude-generated setup)\n\nThis means config files provide **team-shared defaults** that individual users can override locally through the UI.\n\n### Per-Field Override\n\nEach script field is independent. You can override `run` in Project Settings while still using `setup` from `pane.json`:\n\n```\nBuild Script: empty → uses pane.json scripts.setup\nRun Commands: \"npm run dev:custom\" → overrides pane.json\nArchive Script: empty → uses pane.json scripts.archive\n```\n\n## Branch-Local Config\n\nConfig files are read from the **session's worktree**, not the project root. This means:\n\n- If a branch modifies `pane.json`, that branch's sessions use the modified config\n- You can test config changes in a worktree before merging to main\n- Different branches can have different setup/run/archive scripts\n\n## Quick Start\n\n### 1. Create a `pane.json` in your repo root\n\n```json\n{\n \"scripts\": {\n \"setup\": \"pnpm install\",\n \"run\": \"pnpm dev\"\n }\n}\n```\n\n### 2. Commit and push\n\n```bash\ngit add pane.json\ngit commit -m \"Add pane workspace config\"\ngit push\n```\n\n### 3. That's it\n\nWhen your teammates pull, Pane will automatically:\n- Run `pnpm install` when they create a new session\n- Run `pnpm dev` when they click Play\n- Show \"From pane.json\" in Project Settings so they know where the scripts come from\n\n### Already using another tool?\n\nIf your repo already has a `conductor.json`, `.gitpod.yml`, or `.devcontainer/devcontainer.json`, Pane will detect and use it automatically. No changes needed.\n\n## Visibility\n\n### Project Settings\n\nWhen Pane detects a config file, Project Settings shows:\n- An info banner identifying which file was detected\n- \"From pane.json\" badges beneath empty script fields, previewing the command that will run\n- Updated tooltips explaining the fallback behavior\n\n### Play Button\n\nThe Play button tooltip shows exactly what will run and where it came from:\n- **With config:** \"Run: pnpm dev\" with \"from pane.json\" subtitle\n- **Without config:** \"Set up run script (via Claude)\" — launches AI-assisted setup\n\n## Examples\n\n### Node.js / TypeScript project\n\n```json\n{\n \"scripts\": {\n \"setup\": \"pnpm install\",\n \"run\": \"pnpm dev\",\n \"archive\": \"rm -rf node_modules/.cache\"\n }\n}\n```\n\n### Python / Django project\n\n```json\n{\n \"scripts\": {\n \"setup\": \"pip install -r requirements.txt && python manage.py migrate\",\n \"run\": \"python manage.py runserver 0.0.0.0:8000\",\n \"archive\": \"python manage.py flush --noinput\"\n }\n}\n```\n\n### Monorepo with multiple services\n\n```json\n{\n \"scripts\": {\n \"setup\": \"pnpm install --frozen-lockfile\",\n \"run\": \"concurrently \\\"pnpm --filter api dev\\\" \\\"pnpm --filter web dev\\\"\",\n \"archive\": \"docker compose -f docker-compose.test.yml down -v\"\n },\n \"runScriptMode\": \"nonconcurrent\"\n}\n```\n\n### Migrating from Conductor\n\nIf you already have a `conductor.json`, it works as-is — the schema is identical. You can optionally rename it to `pane.json`. If both files exist, `pane.json` wins.\n"},{"path":"docs/IMPORTING_FROM_ANOTHER_TOOL.md","title":"Importing from another tool","text":"# Importing from another tool\n\nPane can adopt a git worktree created by Conductor, another orchestrator, or by hand. An adopted Pane points at the existing directory while ownership stays with the tool that created it.\n\n## Adopt a worktree\n\nFirst save the base repository in Pane. Then adopt one of that repository's registered worktrees:\n\n```bash\nrunpane panes adopt \\\n --repo active \\\n --path /absolute/path/to/existing-worktree \\\n --name existing-work \\\n --agent codex \\\n --resume 00000000-0000-0000-0000-000000000000 \\\n --yes \\\n --json\n```\n\nUse `--dry-run` first to validate the path without creating a Pane. `--folder <name>` places the Pane in a top-level sidebar folder, and `--no-pinned` opts out of the default pin. The command resolves symlinks before storing the path and refuses directories that are not registered worktrees of the selected repository or are already registered in Pane.\n\nWhen `--resume` is present, Pane persists the agent session id and stages that agent's resume command in the terminal without pressing Enter. Review the command and press Enter when ready. Pass `--launch` to run it immediately. Do not drive the same transcript from two applications at the same time because both may append to the same transcript.\n\nThe JSON form supports the same fields:\n\n```json\n{\n \"repo\": \"active\",\n \"panes\": [\n {\n \"path\": \"/absolute/path/to/existing-worktree\",\n \"name\": \"existing-work\",\n \"folder\": \"in-review\",\n \"pinned\": true,\n \"tool\": { \"agent\": \"codex\" },\n \"resume\": \"00000000-0000-0000-0000-000000000000\",\n \"launch\": false\n }\n ]\n}\n```\n\nRun it with `runpane panes adopt --from-json panes.json --yes --json`.\n\n## Ownership and lifecycle\n\nPane stores `worktree_ownership: \"external\"` for adopted Panes. The sidebar labels them **External**, and `runpane panes list --json` returns `ownership: \"external\"` (ordinary Pane worktrees return `\"pane\"`). This marker is separate from main-repository mode and from the worktree name.\n\nPane never creates, file-syncs, builds, or recreates an external directory, and removes one only when asked. Archiving or permanently deleting an adopted Pane only removes Pane's record, unless you archive it with `runpane panes archive --pane <id> --remove-worktree`: that runs the same safety check as a Pane-managed worktree (uncommitted, untracked, and unpushed work block the archive without `--force`; untracked files under the root `.codex/`, `.claude/`, and `.agent-farm/` directories are ignored) and then removes the worktree. The local branch is kept. Deleting its saved project also leaves the directory and git worktree registration untouched. Restoring an adopted Pane whose directory has disappeared succeeds and displays `External worktree directory is missing`; Pane does not recreate it. Automatic checkpoint commits default to disabled for adopted Panes.\n\nOtherwise the tool that owns the worktree remains responsible for its archive and deletion lifecycle, and `runpane panes archive --dry-run --pane <id> --json` reports worktree cleanup as not applicable. Restoring an adopted Pane whose worktree was removed does not recreate it; `git worktree add <path> <branch>` does.\n\n## Conductor imports\n\nThe bulk `runpane import conductor` workflow is planned as a follow-up to the core adoption support. Until then, enumerate active Conductor workspaces and call `panes adopt` for each one. Pane already reads `conductor.json` for project scripts, but adoption deliberately does not run setup/build scripts.\n"},{"path":"docs/NATIVE_MOBILE.md","title":"Native Pane mobile companion","text":"# Native Pane mobile companion\n\n`mobile/` is a Capacitor companion that bundles `frontend/remote.html`; it never navigates a WebView to a remote host. Remote hosts are ordinary authenticated HTTP/SSE endpoints, so an arbitrary host never receives a privileged native bridge.\n\n## Development\n\nUse Node 22.18 or newer and pnpm:\n\n```bash\npnpm mobile:sync\npnpm mobile:build:ios\npnpm mobile:build:android\n```\n\nThe first command builds the Remote Pane entrypoint and copies it to `mobile/www/` before syncing checked-in iOS and Android projects. iOS Simulator builds need Xcode. Android builds need `ANDROID_HOME` plus a JDK (Android Studio's bundled JBR is suitable).\n\nFor a first local Android build on macOS:\n\n```bash\nexport JAVA_HOME=\"/Applications/Android Studio.app/Contents/jbr/Contents/Home\"\nexport ANDROID_HOME=\"$HOME/Library/Android/sdk\"\n# Install the complete API 36 platform (including android.jar), build tools,\n# and accept its licenses in Android Studio's SDK Manager before this command.\n# Equivalent command-line setup when cmdline-tools is installed:\n# \"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager\" \"platforms;android-36\" \"build-tools;36.0.0\"\npnpm mobile:build:android\n```\n\n`mobile:build:ios` is a non-signing Simulator build. To run it, select a booted simulator in Xcode or use `xcrun simctl boot <device-udid>` first. It is not a substitute for a signed-device push test.\n\nProfiles and bearer tokens are in Keychain on iOS and EncryptedSharedPreferences on Android. Native profile removal makes a best-effort authenticated per-installation revoke before deleting the local bearer token; if the host is unreachable, rotate that host's pairing credential to revoke it server-side. Browser PWA storage remains browser `localStorage`.\n\nVoice dictation uses the transcription provider configured on the host. In the Expo app (`native/`), tapping the mic on a host without voice keys opens a sheet that asks for the one key the mode needs (live: Deepgram; recorded: fal), with OpenRouter as an optional key that cleans up the transcript. It saves them to the host through `remote:settings:update`, then starts recording. Saved keys show only as \"Set\", and Settings › Voice replaces them; see `native/README.md` (The composer). iOS declares its microphone purpose in `Info.plist`; Android declares `RECORD_AUDIO` and `MODIFY_AUDIO_SETTINGS`, which Capacitor requests when recording starts. Test both allowing and denying microphone access on a device before distributing a mobile build. The launcher icons reuse the existing opaque 1024px Pane artwork in `main/assets/icon.png`.\n\n## Push delivery operator setup\n\nPush is host-originated: killed-app delivery is an APNs alert (iOS) or FCM notification payload (Android), not a local notification or background JavaScript claim. The host sends the Pane or Session name and routing metadata; see the payload details below.\n\nFor iOS, register `com.dcouple.pane.mobile` (or your signed bundle identifier), enable Push Notifications, and add the capability in Xcode. Then give one host the APNs key in Settings → Integrations → iPhone notifications: choose the `.p8` file and enter the team ID. Pane shares it with your other hosts through your paired devices ([Shared integration keys](SHARED_CREDENTIALS.md)). A host can instead read it from its service environment, which wins over the shared key and is never shared:\n\n```bash\nPANE_APNS_TEAM_ID=... PANE_APNS_KEY_ID=... PANE_APNS_KEY_PATH=/secure/path/AuthKey.p8 PANE_APNS_TOPIC=com.dcouple.pane.mobile PANE_APNS_ENVIRONMENT=sandbox\n```\n\nUse `PANE_APNS_ENVIRONMENT=production` only for a production/TestFlight-signed build. The checked-in Debug entitlement uses the APNs sandbox and the Release entitlement uses production. Never place the `.p8` key in this repository, the app bundle, or a mobile build setting.\n\n### Dcouple's credentials\n\nThese are the provider credentials Dcouple uses for `com.dcouple.pane.mobile`. Only paths and IDs are listed here; the files stay outside git.\n\n- **APNs:** team `FBM5YSF467`, key ID `DGM25BV23X` (team scoped, sandbox and production). The key file is `AuthKey_DGM25BV23X.p8`, kept at `~/.config/pane/apns/` (mode 600) on the machine that created it. Load it once in Settings → Integrations on one host, or copy it to a host outside the repo and set:\n\n ```bash\n PANE_APNS_TEAM_ID=FBM5YSF467\n PANE_APNS_KEY_ID=DGM25BV23X\n PANE_APNS_KEY_PATH=/secure/path/AuthKey_DGM25BV23X.p8\n PANE_APNS_TOPIC=com.dcouple.pane.mobile\n PANE_APNS_ENVIRONMENT=production # TestFlight and App Store builds; use sandbox for Expo dev-client builds\n ```\n\n A host launched as a service reads these from its service environment (systemd unit, launchd plist, or the shell that runs `pnpm daemon:headless`). `mobile:push-status` reports `APNs delivery is configured.` once they're valid.\nFor Android, create a Firebase Android app with the same application ID and download its real `google-services.json` (to `native/google-services.json` for the Expo app, or `mobile/android/app/google-services.json` for Capacitor; the checked-in `.example` is only a shape guide). The host then needs a way to call FCM as a sender service account. It supports two ways to do that:\n\n**Keyless (recommended).** The host impersonates a sender service account with the operator's own gcloud login, so no key file exists anywhere. This is the only option where the organization policy `iam.disableServiceAccountKeyCreation` is on. For the Pane project (`pane-pwa-preview`) the sender account already exists and has the Firebase Cloud Messaging API Admin role. Each operator needs two things:\n\n```bash\nSENDER=pane-push-sender@pane-pwa-preview.iam.gserviceaccount.com\n# Once, by a project owner: let this operator mint tokens for the sender, and nothing else.\ngcloud iam service-accounts add-iam-policy-binding \"$SENDER\" --project pane-pwa-preview \\\n --member=user:you@example.com --role=roles/iam.serviceAccountTokenCreator\n# On the host machine, as that operator: writes ~/.config/gcloud/application_default_credentials.json\ngcloud auth application-default login\n```\n\nThen set these in the host service environment:\n\n```bash\nPANE_FCM_IMPERSONATE_SERVICE_ACCOUNT=pane-push-sender@pane-pwa-preview.iam.gserviceaccount.com PANE_FCM_PROJECT_ID=pane-pwa-preview\n```\n\nFor each send, the host exchanges the refresh token from that login for a user access token, then calls the IAM Credentials API `generateAccessToken` for the sender with the `firebase.messaging` scope. It sends the message with that one-hour token. The login file is read from `GOOGLE_APPLICATION_CREDENTIALS` if that is set, otherwise from gcloud's default location. It must be an `authorized_user` login. Revoking the Token Creator binding, or running `gcloud auth application-default revoke`, stops delivery. Pane reports FCM as not configured when the login file is missing or unreadable.\n\nFor your own Firebase project, create a sender service account with `roles/firebasecloudmessaging.admin` on that project. Then grant Token Creator on that service account only, never at the project level.\n\n**Service-account key.** Where key creation is allowed, `PANE_FCM_SERVICE_ACCOUNT_PATH=/secure/path/service-account.json` still works and takes precedence over impersonation.\n\nDo not commit any provider credential. CI gets the Firebase app config from the `PANE_ANDROID_GOOGLE_SERVICES_JSON` secret (see `native/README.md`); that file identifies the app and is not a sending credential.\n\nThe app asks notification permission after a successful paired connection. Notification setup runs independently of the terminal connection, so permission or provider errors leave remote terminal use available. Registration renewals preserve the installation's alert preferences and deduplication history. OS token registration and each provider network request time out after 15 seconds.\n\nPush-state writes merge with the latest host configuration inside the serialized config-write queue, so they cannot restore a concurrently revoked pairing. Hosts without registered mobile clients do not persist push state on every agent transition.\n\nOnly agent panels send alerts, never plain shells. The name is the Session's for a Session's orchestrator, \"<Session> › <Pane>\" for a Pane associated with a Session, and the Pane's otherwise. A newly blocked agent sends \"<name> is blocked\" at once. A finished turn sends \"<name> needs your attention\" only for a turn a person started and only once: input ending in Enter through `terminal:input` (desktop, phone, remote web) or `runpane panels submit|input --source user` arms the panel, and the next finished turn disarms it. Turns an agent starts by itself (watchers, timers, `runpane` input without `--source user`) finish silently. A turn counts as finished when the agent showed its own working signal (title spinner or working chrome) and then stayed idle for 10 s; terminal output alone, such as typing or a redraw, never counts. Each Pane has one APNs collapse id and one FCM `tag`, so a newer alert replaces the older one. iOS stacks alerts by `thread-id`, the Session's workspace pane id (`sessionPaneId` in the payload) or the Pane's own id. Opening a Pane in the app removes its delivered alerts, and opening a Session also removes its workers'. Every send logs `[Pane mobile push]` with the platform, the kind and, on failure, the provider's status and reason. Tap metadata contains the client-local profile ID (including its host label, URL, and token suffix) and pane/panel IDs; no terminal output or full bearer token is sent. A tap is held in encrypted native storage until saved profiles load, then reconnects only to the matching saved profile, including when switching panels inside the selected pane. An unknown/deleted profile or pane produces an error without connecting to an unrecognized host. Push commands derive client identity from the authenticated request, ignoring caller-supplied identity arguments.\n\nThe Expo app in `native/` uses the same host commands and payload. It registers the raw APNs/FCM device token, not an Expo push token, and routes taps and `pane-remote://` / `pane://pane/…` links as described in `native/README.md` (Notifications and links).\n\nProvider credentials, Apple signing, Firebase setup, and physical-device delivery cannot be validated by this repository alone. In Apple Developer, create an App ID matching the final bundle ID, enable Push Notifications, create an APNs auth key, and configure an App Store Connect record/signing team before TestFlight upload. In Google Play/Firebase, create the Android app and give the host a sender identity as described above. iOS Simulator builds do not prove APNs delivery; A Google Play system image in the Android emulator does receive real FCM messages, so the Android path can be checked end to end without a device. Missing host delivery configuration produces an actionable registration error while ordinary remote terminal use continues.\n"},{"path":"docs/PANE_MCP.md","title":"Pane MCP Server","text":"# Pane MCP Server\n\n`runpane mcp` is an MCP server that gives coding agents Pane's `runpane` commands as tools. With it, an agent in any repository can list saved repositories, create Panes, open panels, read terminal screens, and send input. Pane installs a `pane` skill in the user's home skill folders so agents can find the CLI and how to connect.\n\n## Automatic registration\n\nThe Pane desktop app registers the server for you. On launch, Pane adds a `pane` MCP server to your user-level config:\n\n- **Claude Code**: via `claude mcp add pane --scope user …`, which writes `~/.claude.json` (or `$CLAUDE_CONFIG_DIR/.claude.json`).\n- **Codex**: a `[mcp_servers.pane]` table in `~/.codex/config.toml` (or `$CODEX_HOME/config.toml`).\n- **Cursor**: a `mcpServers.pane` entry in `~/.cursor/mcp.json`. Pane detects Cursor from `~/.cursor`, `cursor`, `agent`, or `cursor-agent` on PATH.\n\nPackaged Pane also installs the on-demand `pane-manage-and-message-agents` user skill for detected Claude Code, Codex, and Cursor clients. Cursor uses `~/.cursor/skills/`; Claude Code and Codex use their user skill folders (including `$CLAUDE_CONFIG_DIR` and `$CODEX_HOME`). Pane leaves hand-added skills alone and removes only its own copy when registration is off. The same applies inside detected WSL distros. [Cursor documents `~/.cursor/skills/` as a user-level Agent Skills directory](https://cursor.com/docs/skills).\n\nPane registers only when it detects the corresponding client, and it manages only the entry it wrote:\n\n- It leaves other MCP servers and settings alone and never writes a second `pane` entry.\n- It rewrites its own entry when the app moves or updates.\n- A `pane` entry you added yourself (for example with `npx runpane mcp`) is left untouched, including when you turn the setting off. For Claude Code, Pane's entry is the one that runs `<Pane data dir>/mcp/runpane/dist/cli.js`. For Codex, it's the `[mcp_servers.pane]` table carrying the `# Managed by Pane` comment. For Cursor, it is the entry whose first argument is Pane's bundled runpane copy.\n- Pane edits `config.toml` as text, so your comments and formatting survive. It then parses the result and writes nothing unless the only change is Pane's own entry. Cursor JSON is parsed before editing; invalid JSON is left untouched. Writes are atomic and follow a symlinked config.\n\nOn Windows, Pane also registers inside each WSL distro that has a saved WSL repository. Agents there run the Windows Pane binary through WSL interop.\n\nThe registered command is the Pane executable in Node mode (`ELECTRON_RUN_AS_NODE=1`) running a copy of the runpane CLI at `<Pane data dir>/mcp/runpane/dist/cli.js`. No Node.js or npm install is needed. Only packaged builds register; a development build never touches your agent config.\n\nTo turn this off, open **Settings → AI & Agents** and switch off **Register Pane tools with Claude Code, Codex, and Cursor**. Pane then removes its entry from each config.\n\nRestart a running `claude` or `codex` session to pick up a new registration. In Cursor, start a new chat or use **Developer: Reload Window**. Check with `claude mcp list`, `codex mcp list`, or `agent mcp list`. Cursor CLI may require `agent mcp enable pane` to add the server to its local approved list; Pane leaves that trust decision to you.\n\n## Toolsets\n\nA server that offers dozens of tools makes models, especially smaller ones, worse at choosing the right one. So by default the server serves a lean `core` toolset, and everything else is opt-in:\n\n| Toolset | Tools |\n|---|---|\n| `core` (default) | `agents_start`, `agents_status`, `agents_send`, `panels_create` (a new agent tab in an existing Pane), `panels_input` (exact keys, for menus and prompts), `repos_list`, `repos_add`, `panes_list`, `workspace_state`, `panes_git_status`, `panes_archive`, `panes_restore`, `links_create`, `docs_search`, `docs_read`, `doctor` |\n| `agents` | the three agent tasks, `workspace_state`, `watch` |\n| `panes` | create, adopt, list, archive, restore, pin, unpin, rename, focus, cost, run and stop the run script, move to a folder, `folders_list`, `folders_create` |\n| `panels` | create, list, output, screen, input, submit, submit-composer, wait |\n| `git` | status, commit, push, pull, fetch, rebase onto main, squash-rebase onto main, stash, stash-pop, soft reset (`panes_*`) |\n| `sessions` | the eight `sessions_*` tools and the three named-lock tools (`lock_acquire`, `lock_release`, `lock_list`) |\n| `repos`, `docs`, `links`, `admin` | repository, documentation, deep-link, and diagnostic tools |\n| `all` / `read` | every tool / every read-only tool |\n\nChoose with `runpane mcp --toolsets core,git` (comma-separated). `--read-only` keeps only read-only tools from whatever is selected. The Pane app registers `core` by default; **Settings → AI & Agents → Pane tools to register** switches it to `all`. Each command's toolsets live in the contract's `toolsets` field.\n\n## The three agent jobs\n\nThe core set is built around the three jobs agents most often need, each finished in one call:\n\n- `agents_start`: starts the agent with the task, waits until it is ready, and returns the pane and panel ids and a `pane://` link. Pass `pane` to open it as a new tab in an existing Pane (review, audit, fix, QA); pass `repo` and `name` only for new work, which creates a Pane. 1 feature = 1 worktree = 1 branch = 1 Pane; reviews, audits, fixes and QA for it are tabs in that Pane.\n- `agents_status`: whether the agent is working, ready, blocked (waiting on a person), idle, or exited, plus its current screen.\n- `agents_send`: submits a follow-up and reports whether the agent took it or queued it behind its current turn (`delivery`), read from the agent's transcript where Pane can find it.\n\n## Docs and links\n\n`docs_search` searches Pane's docs, `runpane` help, per-command context, and the Pane Chat skills installed in the Pane data directory. It returns short excerpts with paths. `docs_read` returns one path in full. Both work offline, and the same docs are listed as `runpane-docs:` MCP resources.\n\n`links_create` builds `pane://open?pane=<id>[&panel=<id>]`, `pane://open?repo=<id>`, or `pane://open?session=<id>`. Pane registers the `pane://` scheme on macOS, Windows, and Linux. Opening a link (from a browser, a terminal, or `links_open`) raises Pane and selects what it names. It never changes Pane state, and a link with any unexpected part is rejected.\n\n## Registering by hand\n\nWithout the desktop app, or for another MCP client, run the server from npm: `npx --yes runpane@latest mcp`. Add `--toolsets <names>` to choose tools.\n\n```bash\nclaude mcp add --scope user pane -- npx --yes runpane@latest mcp\ncodex mcp add pane -- npx --yes runpane@latest mcp\n```\n\nCursor (`~/.cursor/mcp.json`):\n\n```json\n{ \"mcpServers\": { \"pane\": { \"type\": \"stdio\", \"command\": \"npx\", \"args\": [\"--yes\", \"runpane@latest\", \"mcp\"] } } }\n```\n\nVS Code (`.vscode/mcp.json`, or run **MCP: Add Server**):\n\n```json\n{ \"servers\": { \"pane\": { \"type\": \"stdio\", \"command\": \"npx\", \"args\": [\"--yes\", \"runpane@latest\", \"mcp\"] } } }\n```\n\nAny other client that can launch a stdio server uses the same command and arguments.\n\n`agents_send` uses the target agent's composer state. For Codex, it queues with Tab while the agent is working and submits with Enter when ready. A send is confirmed only after the text leaves the composer; if it remains, the result includes a command to retry the appropriate key. `panels_submit_composer` with `strategy: auto` uses the same keys.\n\nThe server is stdio only: it runs next to the Pane app on the same machine, so there is no HTTP transport and no OAuth. To drive a remote Pane, run the server on the remote host.\n\nThe Python package (`pipx run runpane`) does not include the MCP server, the docs search, or the agent tasks, because they need Node. From Python, those commands print the npm command and exit with status 2.\n\n## Manual test steps\n\nAutomated tests cover the registration and link logic on every OS. Check these by hand on a packaged build of each platform.\n\n**macOS, Windows, and Linux**\n\n1. Install and launch Pane, with Claude Code, Codex, and Cursor installed.\n2. Run `claude mcp list`, `codex mcp list`, and `agent mcp list`. Each lists `pane`. If Cursor asks for approval, run `agent mcp enable pane`; it should report ready with 15 core tools.\n3. In a fresh `claude` session, ask \"Use Pane to list my repos\". It calls `repos_list`.\n4. Quit and relaunch Pane. The Claude, Codex, and Cursor configs are unchanged.\n5. Turn off **Settings → AI & Agents → Register Pane tools with Claude Code, Codex, and Cursor**. `pane` is gone from all three configs, and the other entries are unchanged.\n\n**pane:// links**\n\n1. Get a link: `runpane links create --pane <pane-id>`.\n2. With Pane running, open it from a browser or a terminal:\n - macOS: `open \"pane://open?pane=<pane-id>\"`\n - Windows: `start \"\" \"pane://open?pane=<pane-id>\"`\n - Linux: `xdg-open \"pane://open?pane=<pane-id>\"`\n3. Pane comes to the front on that Pane, and no second Pane window opens. On Windows and Linux, the extra process hands the link to the running Pane and exits.\n4. Quit Pane and open the link again. Pane starts and opens that Pane.\n5. Open `pane://open?pane=<pane-id>&archive=1`. Pane logs `Unknown pane link parameter: archive` and changes nothing.\n6. On Linux, check the handler: `xdg-mime query default x-scheme-handler/pane` names Pane's desktop file (AppImage and .deb).\n\n**Windows with WSL**\n\n1. Save a repository that lives in a WSL distro, with Claude Code, Codex, and Cursor installed inside that distro.\n2. Relaunch Pane. Inside the distro, `claude mcp list`, `codex mcp list`, and `agent mcp list` show `pane`, whose command is `/mnt/c/.../Pane.exe`.\n3. From a `claude` session inside the distro, ask \"Use Pane to list my repos\". The call reaches the Windows Pane.\n4. Turn the setting off. `pane` is gone from the distro's configs too.\n\n## Example prompts\n\n- \"Use Pane to start Claude on fixing the login redirect in the web repo, and give me a link to watch it.\"\n- \"How is the agent in my fix-login Pane doing? Does it need anything from me?\"\n- \"Tell the fix-login agent to also add a test for the redirect.\"\n- \"Which of my Panes have uncommitted or unpushed work?\"\n- \"How do I archive a Pane and get it back later? Check Pane's docs.\"\n\n## Not supported\n\n- Streaming. `watch --follow` and `watch --until-event` aren't offered; use `agents_status`, or `watch` with `timeoutMs`.\n- App settings. Agents can't read or change Pane's settings.\n- Permanently deleting Panes or removing repositories. Those stay in the app, where the user sees what goes.\n- Progress notifications and pagination (the full list is small).\n- Code or scripts run inside the MCP server itself. Agents run code in Pane terminals through `agents_start` and `agents_send`.\n\n## Protocol\n\nThe server uses the MCP TypeScript SDK v2 over stdio. It speaks MCP revision 2026-07-28, which has no handshake: `server/discover`, with the version and client capabilities carried on each request. It also accepts the 2025 `initialize` handshake that current Claude Code and Codex releases send, negotiating down to 2025-11-25.\n\nOnly JSON-RPC goes to stdout, and the server exits when stdin closes. Cancelling a call (`notifications/cancelled`) stops the `runpane` process running it.\n\n## Tools\n\nTools are generated from [`contracts/runpane/contract.json`](../contracts/runpane/contract.json) when the server starts:\n\n- Every command that has result `jsonSchemas` becomes a tool. Spaces and hyphens in the name become underscores: `repos list` → `repos_list`, `panels submit-composer` → `panels_submit_composer`.\n- The tool's inputs are the flags from the command's `usage` lines and its `agentContext` arguments, in camelCase (`--timeout-ms` → `timeoutMs`). Flags that take a value are strings; flags without one are booleans. Daemon commands also accept `paneDir`.\n- The description combines the command's summary, details, and notes from `agentContext`. The title is `runpane <command>`.\n- The output schema is the command's `*Result` JSON schema. Every call runs `runpane <command> … --json` and returns its JSON output unchanged, both as `structuredContent` and as a text block for older clients.\n- A non-zero exit returns a tool error (`isError: true`). A CLI failure reads as its message, a `Next:` line that names tools instead of CLI commands, and a `Code:` line; a command's own `ok: false` result comes back as its JSON. Bad arguments are tool errors too, so the model can correct them. An unknown tool name is a JSON-RPC Invalid Params error (`-32602`).\n- Annotations come from the contract. `readOnlyHint` is the inverse of `mutates`. A mutating tool is `destructiveHint: true` unless the command is marked `additive`. `idempotentHint` and `openWorldHint` follow the `idempotent` and `openWorld` fields.\n- Values are passed as `--flag=value`, so a value may start with `-` (for example `- [ ] item`).\n\nAdding or changing a command in the contract changes its tool. There is no per-tool code.\n\nA few flags are left out. `--json` is always passed. `--follow` and `--until-event` are omitted because a streaming or open-ended watch cannot return a single result; call `watch` with `timeoutMs` instead. Tools have no stdin, so pass exact terminal bytes in `text` (for example `\\u0003` for Ctrl-C) rather than `inputFile: \"-\"`.\n\n## Confirmation\n\nMutating tools keep the CLI's confirmation rule. Every command whose usage includes `--yes` gets a `yes` input. Without `yes: true`, the call fails with the CLI's own refusal and changes nothing. Tools are also annotated with `readOnlyHint`, so clients can auto-approve read-only tools and prompt for the rest.\n\n## Agent guidance\n\nPane installs a managed `pane` skill under `~/.claude/skills/pane` (or `$CLAUDE_CONFIG_DIR/skills/pane`) and `~/.agents/skills/pane`, including saved WSL distros on Windows. It leaves any existing skill without Pane's marker alone. **Install Pane skill for agents** in Settings → AI & Agents controls the installation; turning it off removes Pane's copy.\n\n**Publish Pane instructions to AGENTS.md** is off by default. If enabled, Pane writes a marked block into the active repository's `AGENTS.md` with CLI and MCP setup instructions. Turning it off removes only Pane's marked block from saved repositories. On upgrade, Pane turns off the old default once and removes those blocks; a later opt-in is remembered. The full command catalog lives in MCP tools and `runpane agent-context`.\n"},{"path":"docs/RUNPANE_BUNDLED_CLI.md","title":"Bundled runpane on terminal PATH","text":"# Bundled runpane on terminal PATH\n\nEvery terminal Pane launches (Session orchestrators, agent panels, plain\nterminals, and run scripts) can call `runpane` without a global install. The\ncommand always matches the running Pane build.\n\n## How it works\n\n- `pnpm build:main` bundles `packages/runpane/src/cli.ts` into\n `main/dist/runpane/runpane.cjs` (`main/build-runpane-cli.js`). It ships\n inside the app with the rest of `main/dist`, and `pnpm dev` builds it\n through `pnpm build:main`. The bundle embeds its version and docs search\n index, so both still work after the single file is copied out of the app.\n- At startup (`createPaneDaemonHost`, which both the desktop app and the\n headless daemon use), Pane copies that file to `<PANE_DIR>/bin/runpane.cjs`\n and writes a `<PANE_DIR>/bin/runpane` shim (plus `runpane.cmd` on Windows).\n The shim runs the CLI with Pane's own Electron binary as Node\n (`ELECTRON_RUN_AS_NODE=1`) and defaults `PANE_DIR` to that Pane data\n directory. Files are rewritten only when their content changes. Pane never\n follows a symbolic link there or overwrites a file it did not write.\n- Terminal launches put `<PANE_DIR>/bin` first on `PATH` and export\n `PANE_RUNPANE_BIN` with the shim's absolute path. User startup files can\n rebuild `PATH`, so supported shells re-apply it afterwards:\n - **zsh:** `ZDOTDIR` points at `<PANE_DIR>/shell/zsh`. Each wrapper file\n sources the user's matching file from their own `ZDOTDIR` (or `$HOME`),\n then the last one moves `<PANE_DIR>/bin` to the front and restores the\n user's `ZDOTDIR`. A `precmd` hook keeps it first when tools such as direnv\n or nvm edit `PATH` before a prompt. History stays in the user's\n `.zsh_history`.\n - **bash:** `--rcfile <PANE_DIR>/shell/bash/rcfile` sources\n `/etc/bash.bashrc` and `~/.bashrc` (what an interactive non-login bash\n normally reads), then moves the directory to the front and adds a\n `PROMPT_COMMAND` hook.\n - **fish:** `--init-command` runs after the user's config and adds a\n `fish_prompt` handler.\n - **Other shells, Windows, and run scripts:** `PATH` is prepended only.\n - **WSL terminals** are unchanged. The Windows Electron binary cannot run\n inside the distro. Install `runpane` in the distro, or use\n `npx --yes runpane@latest`.\n\n## Existing global installs\n\nA globally installed `runpane` (`npm i -g runpane`) still works in other\nterminals. Inside Pane terminals, Pane's shim resolves first because it\nmatches the running build's local-control contract. Use `$PANE_RUNPANE_BIN`\nto call it by absolute path.\n\nSeveral Pane builds that share one `PANE_DIR` (for example a dev build and\nthe installed app, both on `~/.pane`) rewrite the same shim at startup; the\nmost recently started build wins. Use `PANE_DIR=~/.pane_test pnpm dev` to keep\nthem apart.\n"},{"path":"docs/RUNPANE_CLI_CONTRACT.md","title":"Runpane CLI Contract","text":"# Runpane CLI Contract\n\n<!-- Generated by scripts/generate-runpane-contract.js. Do not edit generated sections by hand. -->\n\n`runpane` is a thin installer and configurator for Pane. The npm and PyPI\npackages expose the same command contract and download the real Pane release\nartifact at command runtime.\n\nThe packages must not download, install, or configure Pane during package installation.\nWork starts only when a user runs `runpane ...`.\n\n## Maintainer Rules\n\n- Treat `contracts/runpane/contract.json` as the source of truth for both wrapper packages and generated docs.\n- Every command, flag, platform default, artifact-selection rule, and attribution rule change must be reflected in the contract.\n- The npm and PyPI wrappers must expose the same command behavior unless the contract explicitly documents a package-manager-specific difference.\n- Root `README.md` and package READMEs should lead with one guided quick-start command. Explicit commands, package-manager variants, and flags belong in an Advanced section.\n- Release version bumps must keep root `package.json`, `packages/runpane`, and `packages/runpane-py` versions in sync. Run `pnpm run check:runpane-package-versions` before release.\n- `pnpm run test:runpane-contract` must pass before changing wrapper command parsing, help output, platform defaults, release asset selection, or generated contract artifacts.\n- Token-based npm or PyPI publishing is a temporary fallback. Prefer trusted publishing once the package names are reserved and trusted publishers are configured.\n\n## Compatibility Floors\n\nThe npm wrapper should run on Node.js `>=20.0.0` and newer. The root Electron app\nmay require a newer Node.js version for development and packaging.\n\nThe PyPI wrapper should run on Python `>=3.8` and newer. Keep runtime dependencies\nout of the wrapper unless a compatibility test covers the new dependency.\n\n## Public Terminology\n\n- `repo`: Saved Pane repository/project record\n- `pane`: User-visible Pane session\n- `tool`: Terminal-backed tab\n- `agent`: Built-in agent command template\n\n## Package Manager Entrypoints\n\nRecommended guided quick starts:\n\n```bash\nnpx --yes runpane@latest\nnpm i -g runpane && runpane setup\npipx run runpane\npython -m pip install runpane && python -m runpane setup\n```\n\nCanonical npm and Node commands:\n\n```bash\nnpx --yes runpane@latest\nnpx --yes runpane@latest setup\nnpx --yes runpane@latest install client\nnpx --yes runpane@latest install daemon --label \"My Server\"\npnpm dlx runpane@latest\npnpm dlx runpane@latest install daemon --label \"My Server\"\nnpm i -g runpane && runpane\nnpm i -g runpane && runpane setup\nnpm i -g runpane && runpane install daemon --label \"My Server\"\npnpm add -g runpane && runpane\npnpm add -g runpane && runpane setup\npnpm add -g runpane && runpane install daemon --label \"My Server\"\nyarn dlx runpane@latest install daemon --label \"My Server\"\nbunx runpane@latest install daemon --label \"My Server\"\n```\n\nCanonical Python commands:\n\n```bash\npipx run runpane\npipx run runpane setup\npython -m pip install runpane\nrunpane\nrunpane setup\npython -m runpane setup\nrunpane install daemon --label \"My Server\"\npipx install runpane\nrunpane\nrunpane setup\npipx run runpane install daemon --label \"My Server\"\nuvx runpane@latest\nuvx runpane@latest setup\nuvx runpane@latest install daemon --label \"My Server\"\npython -m runpane install daemon --label \"My Server\"\n```\n\nUse `pnpm dlx` for one-shot pnpm execution and `pnpm add -g` for persistent CLI installation.\nDo not document `pnpm install runpane` as the public CLI install path.\n\n## Commands\n\n```bash\nrunpane\nrunpane setup\nrunpane install\nrunpane install client\nrunpane install daemon\nrunpane update\nrunpane version\nrunpane doctor\nrunpane doctor --json\nrunpane daemon repair --pane-dir ~/.pane_remote --yes --json\nrunpane agent-context\nrunpane agent-context --command \"panes create\" --json\nrunpane mcp\nrunpane repos list --json\nrunpane repos add --path /path/to/repo --name Pane --yes --json\nrunpane panes list --repo active --json\nrunpane panes cost --pane <pane-id> --json\nrunpane panes create --repo active --name issue-252 --agent <agent> --prompt \"Kick off the discussion skill for issue 252\" --source agent --no-focus --wait-ready --yes --json\nrunpane panes create --from-json panes.json --yes --json\nrunpane panes archive --pane <pane-id> --source agent --yes --json\nrunpane panes rename --pane <pane-id> --name issue-393 --yes --json\nrunpane panels list --pane <pane-id> --json\nrunpane panels output --panel <panel-id> --limit 200 --json\nprintf 'Continue\\n' | runpane panels input --panel <panel-id> --input-file - --yes --json\nrunpane sessions list [--json] [--pane-dir <path>]\nrunpane sessions create --from-json <path|-> [--json] [--pane-dir <path>]\nrunpane sessions get --session <id|name> [--json] [--pane-dir <path>]\nrunpane sessions update --session <id|name> --from-json <path|-> [--json] [--pane-dir <path>]\nrunpane sessions pin --session <id|name> [--json] [--pane-dir <path>]\nrunpane sessions unpin --session <id|name> [--json] [--pane-dir <path>]\nrunpane sessions set-agent --session <id|name> --agent <codex|claude|cursor> [--json] [--pane-dir <path>]\nrunpane sessions associate --session <id|name> --pane <pane-id> [--json] [--pane-dir <path>]\nrunpane sessions detach --session <id|name> [--pane <pane-id>] [--json] [--pane-dir <path>]\nrunpane sessions overview --session <id|name> [--json] [--pane-dir <path>]\nrunpane lock acquire --name testing-account --ttl 30m --wait 1800000 --note \"call QA\" --json\nrunpane lock release --name testing-account --json\nrunpane lock list --json\nrunpane agents start --repo active --name fix-login --agent claude --prompt \"Fix the login redirect\" --yes --json\nrunpane agents start --pane <pane-id> --agent codex --prompt \"Review the PR from this branch and report findings\" --yes --json\nrunpane agents status --pane <pane-id> --json\nrunpane agents send --pane <pane-id> --text \"Also add a test\" --yes --json\nrunpane report --state ready --pr 747 --head fc5dce9 --summary-file /tmp/report.md --json\nrunpane panels last-message --panel <panel-id> --json\nrunpane panels resume --panel <panel-id> --wait-ready --yes --json\nrunpane panels resume --session <id|name> --wait-ready --yes --json\nrunpane docs search --query \"archive a pane\" --json\nrunpane links create --pane <pane-id> --json\nrunpane panes git-status --pane <pane-id> --json\nrunpane help\nrunpane <command> --help\nrunpane watch --until-event\nrunpane watch --follow\nrunpane watch --session <id|name> --follow\nrunpane watch --session <id|name> --follow --ready-settle 1m --blocked-settle 15s --min-interval 2m\nrunpane pr watch [--pane <id>] --url <PR URL> [--head <full-sha>] [--json] [--pane-dir <path>]\nrunpane pr unwatch [--pane <id>] --url <PR URL> [--json] [--pane-dir <path>]\nrunpane pr list [--pane <id>] [--json] [--pane-dir <path>]\n```\n\n`runpane` with no arguments and `runpane setup` open an interactive wizard when stdin and stdout are TTYs. The remote-host wizard asks only for a name, then runs interactive Tailscale setup with automatic port selection; explicit install daemon flags remain available for SSH and manual URLs. In non-interactive shells or CI, both forms must print help, common commands, and agent discovery hints, then exit successfully instead of waiting for input.\n\n`runpane install` is an alias for `runpane install client`.\n\n`runpane install client` downloads the selected Pane desktop artifact and installs, opens, or launches it for the current platform.\n\n`runpane install daemon` downloads or installs Pane, resolves a stable Pane executable path, and spawns `<pane executable> --remote-setup <forwarded remote setup args>`.\n\nThe wrapper must stream Pane stdout/stderr without reformatting because `pane --remote-setup` prints the one-time `pane-remote://...` connection code.\n\n`runpane update` defaults to both app and active supported CLI, with independent supervision, staging, durable status, and explicit restoration outcomes. Prerequisites, status fields/exit codes, draft handling and manual recovery are documented in [Update recovery](UPDATE_RECOVERY.md).\n\n`runpane version` prints only wrapper package metadata and does not contact, launch, or focus the Pane app or daemon.\n\n`runpane doctor` checks platform support, release metadata reachability, download URL selection, installed Pane detection, daemon reachability, and remote-daemon hints. Add `--json` for a machine-readable report that agents should run before mutating Pane state. Installed app version detection must be best-effort and must not launch, focus, or configure Pane; macOS wrappers read app bundle metadata instead of executing `Pane --version`.\n\n`runpane daemon repair` rewrites and restarts only the managed remote-daemon launcher/service. It never creates pairing credentials, changes tunnels, or downloads Pane; doctor only recommends it and never runs it automatically.\n\n`runpane agent-context` prints a brief, token-efficient command schema for coding agents without connecting to the Pane daemon.\n\n`runpane agent-context --command \"panes create\"` prints the detailed definition for one command. Add `--json` for machine-readable output. An unknown command exits 2 and lists the closest command names; with `--json` it prints `{ ok: false, code: \"unknown_command\", message, candidates }` on stdout.\n\n`runpane mcp` runs a stdio MCP server whose tools are generated from this contract: every command with result `jsonSchemas` becomes a tool that runs `runpane <command> --json` and returns its output. Only the npm package and the Pane app include it; the Python wrapper prints how to run it with Node and exits non-zero.\n\n`runpane repos list` connects to the running local Pane daemon and prints saved repository records.\n\n`runpane repos add` registers an existing git repository with the running local Pane daemon. It does not create directories or initialize git repositories by default.\n\n`runpane panes list` lists Pane sessions, optionally scoped to one saved repository.\n\n`runpane panes cost` reports estimated token costs per Pane for the last 30 days, including per-model breakdowns and cache efficiency; unscoped output includes an Unattributed bucket that reconciles against workspace totals.\n\n`runpane panes create` connects to the running local Pane daemon, resolves the requested saved base repository, creates user-visible Pane sessions backed by Pane-managed worktrees/branches, opens terminal-backed tool tabs, and optionally sends initial input to the started tool. Built-in agent panes and `--source agent` default to background/no-focus unless `--focus` is passed. New Panes are pinned into the UI's favorite/pin set by default, except when the CLI runs inside a Session orchestrator (`PANE_ORCHESTRATION_SESSION_ID`), where child worktrees default to unpinned. Explicit `--pinned` / `--no-pinned` override creation defaults. First Session association clears an existing pin, including an explicit creation pin; use `panes pin` after association to pin a child. This avoids duplicating Session children in the global pinned list. Manual pins applied afterward are preserved. Panes created interactively in the Pane UI are unaffected. Inside a Session orchestrator (`PANE_ORCHESTRATION_SESSION_ID` set), new Panes are associated with that Session automatically; `--no-associate` opts out. A failed association is reported on the item and never undoes the Pane.\n\nFor `panes create --wait-ready` (and `panels create`, `panes adopt --launch`), ready means an identified agent at its own input box: declared with `--agent`, or detected from the command, its foreground process or its screen. A `--tool-command` that never shows a supported agent is not ready: `readiness.ok` is false with `readiness.reason.code` `no-agent-detected` after the timeout, or `command-exited` as soon as the command exits back to the shell, and nothing is typed into it. `agent-not-ready` means the agent was detected but its input box was not ready in time. CLI readiness is confirmed on consecutive polls. Codex also requires a recognized configured model or context footer below its live composer; its provisional startup composer is not ready. Hidden or unrecognized Codex status lines conservatively time out rather than prove readiness. Startup prompts return blocked with the prompt's own text in blocked.message and a suggestedCommand, and Pane never answers them: first-run trust and daemon dialogs and Agent Farm's `Trust this repository workspace? [y/N]` as blocked.kind=first-run-dialog; Codex's security setup banner and any other prompt, including a wrapper's or shell's `[y/N]` before the agent appears, as agent-prompt. A starting message blocked this way is held and sent once the prompt is answered and the agent's input box is ready. Every create with a starting message returns `initialInput`. Pane stages the text in the composer, presses the agent's submit key separately (Ctrl+Enter for Codex, declared or detected), and checks where it went: `initialInput.delivery` is `taken` or `queued` (from the agent's transcript or its screen), `in-composer`, or `unknown` (`argv` means a prompt was routed at launch; Pane gives the transcript up to 10 seconds before settling on unknown). `initialInput.verifiedSubmitted` is true exactly when it is `taken` or `queued`, and the create's `ok` is false unless a Claude or Codex starting message is verified. Cursor and OpenCode have no acceptance probe: their delivery stays `unknown` with a delivery.message saying so, `verification: unverifiable`, and `ok` follows readiness. Without `--wait-ready` the create returns at once with `initialInput.verifiedSubmitted: false`, a delivery.message saying the call did not wait, and a `panels wait` nextCommand; Pane still sends the message once the agent is identified and at its input box, and gives up without typing it if no supported agent appears within 30 seconds.\n\n`runpane panes archive` refreshes the configured upstream, reports exact unpushed commit evidence, and refuses unsafe archive operations unless `--force` is used. A branch whose upstream is gone counts as pushed when a merged GitHub pull request has HEAD as its head (`safetyCheck.mergedViaPr`). Add `--dry-run` to inspect the same evidence without archiving. `--remove-worktree` applies the same check and removal to an adopted worktree; local branches are always kept. Successful archives report `worktreeCleanup: completed` once the worktree is gone from its path and from git; `trashDeletion: pending` means its files are still being deleted in the background. `runpane panes archive --session <id|name> --merged` archives every Session Pane that is clean and pushed or merged, and reports a reason for each skipped Pane.\n\n`runpane panes rename` trims and updates a Pane's display name without changing its worktree, branch, panels, or focus, and returns the updated pane summary.\n\n`runpane panes focus` raises the Pane window and selects a Pane (and optionally one of its panels) exactly like clicking it in the UI. Because it steals the user's window focus, run it only on an explicit user request to open, focus, show, or switch to a Pane; never focus a Pane proactively, the same doctrine that keeps `panes create` background/no-focus for `--source agent`.\n\n`runpane panels open` opens a URL or a file from the Pane worktree as a tab in an existing Pane (default: the calling panel's Pane from `PANE_SESSION_ID`), in split view beside the agent unless `--tab` is passed. Files open in an editor tab with read-only previews for supported formats, and HTML opens as editable source; an existing tab showing the same target is reused. It brings the tab forward for clients already showing that Pane, and never raises or focuses the Pane window or switches anyone's Pane.\n\n`runpane panels list` lists tool panels inside one Pane session. Each panel with a saved agent session ID reports it as `resumeId`, with `resumeSource` (`agent-farm`, `configured`, or `agent`) naming the rule that reopens the conversation.\n\n`runpane panels output` reads bounded recent terminal output from one panel and strips common terminal control noise for agent use.\n\n`runpane panels input` sends exact input bytes to one terminal panel. Prefer `--input-file` for newlines, Ctrl-C, quotes, or shell-sensitive text.\n\n`runpane panes create --prompt` is an alias for `--initial-input`; request JSON and daemon payloads should use the canonical `initialInput` field.\n\n`runpane panes create --branch <name>` creates the Pane's worktree on exactly that new branch, slashes included (for example `agents/w5a`). The name is checked with `git check-ref-format --branch`, creation fails if the branch already exists, and Pane never renames it to make it unique. `--worktree-name` still names the directory and defaults to `--name`. `--base` is an alias for `--base-branch`, and `--prompt-file` for `--initial-input-file`. `panes create --base <ref> --branch <name> --prompt-file <file>` replaces `git worktree add` plus `panes adopt`. When the base is a remote-tracking branch (the default `origin/main`, or one named with `--base`), Pane fetches it first, waiting at most about 10 seconds, so the Pane starts at the remote's current tip. Each created item reports `base` (`ref`, `commit`, `refresh`); if the fetch fails the Pane still starts, from the local ref, and `base.notice` says so and how to catch up.\n\n`runpane panes adopt --launch` accepts `--prompt`, `--prompt-file`, `--wait-ready`, and `--ready-timeout-ms`, and reports `readiness`, `initialInput`, and `nextCommand` like `panes create`. A prompt without `--launch` is an error rather than being dropped.\n\nIf composer submission cannot be verified without risking a duplicate, the create item is unsuccessful with `initialInput.staged`, `initialInput.attempts`, `initialInput.blocked.kind: submission_unverified`, and an actionable `nextCommand`. The CLI-facing `--prompt` alias maps to this canonical `initialInput` result.\n\nWhen running from WSL while Pane is installed on Windows, the Linux wrapper may look for a missing `/tmp/pane-daemon.../daemon.sock` or resolve to a Windows shim such as Volta. In that case invoke the Windows wrapper through PowerShell from a Windows cwd, for example `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane repos list --json'`.\n\n`runpane watch` waits for agent, Pane, panel, and PR events from the daemon journal without polling. `--follow` keeps watching with defaults tuned for an agent watching unattended (quiet, batched, and it ends itself after 3 hours with nothing happening); individual duration flags override these defaults. Cadence requires a named follow consumer; `--follow --since <generation>` has no consumer name and bypasses settling and batching. `--until-event` waits for one event worth waking for, prints it immediately without follow settling or batching, and exits 0. The defaults are listed in the contract under defaults.watch.\n\nA watch releases its pending slot when its client socket or named pipe disconnects. Re-arming the same named cursor takes over the previous request; the superseded watcher exits with a WATCH ERROR instead of competing for the cursor. Anonymous concurrent watchers retain the eight-wait limit. Each request also has a 125-second lease (slightly longer than the maximum 120-second long poll), so an abandoned request is reclaimed even if no disconnect arrives.\n\nThe desktop app treats one consumer name specially: while a `runpane watch --as dictate` request is in flight, and for 30 seconds after the last one ends, Pane skips its own \"needs your input\" and \"finished\" desktop notifications, because [Dictate](https://github.com/parsakhaz/open-dictate) shows those events on its pill. When that watcher stops, Pane's notifications come back. The CLI's flags and output are unchanged.\n\n`sessions list` list durable named orchestration Sessions.\n\n`sessions create` create a durable named orchestration Session and its hidden terminal owner.\n\n`sessions get` read one durable named orchestration Session.\n\n`sessions update` update a Session overview from structured JSON.\n\n`sessions pin` and `sessions unpin` set the Session pin star through the existing UI update path. Create JSON accepts `isPinned` (default false); update JSON preserves the pin when the field is omitted.\n\n`sessions set-agent` switch the durable terminal agent for a named Session.\n\n`sessions associate` associate a user-visible Pane with a named Session.\n\n`sessions detach` detach a Pane from a named Session.\n\n`sessions overview` read a live status, activity, git, and pull request overview for a named Session. Each panel in `panes[].panels[]` carries its own latest `runpane report` as `report`, so one call shows what every worker last reported and when (`reportedAt`, next to the panel's `state` and `lastActivityAt`). When a report's summary is a complete JSON object, such as a status file sent with `--summary-file`, `report.data` holds it exactly as the worker gave it; Pane never reads, renames or validates its fields. A summary that is not a JSON object (plain text, an array, a scalar or invalid JSON), is nested more than 64 levels deep, or was truncated (`summaryTruncated`), has no `data`. A panel that never reported has no `report`. `panes[].report` is still the newest report among the Pane's panels.\n\n`runpane lock acquire|release|list` coordinate a resource shared between agents, such as one test account. The caller's Pane and panel own the lock; it is scoped to the owner's Session (or global outside one), renews for the same owner, and is released on TTL expiry, owner panel exit, or owner Pane archive. `--wait` blocks in the daemon until the lock comes free.\n\n`runpane agents start|status|send` finish the three common agent jobs in one call each: start an agent on a task (a new tab in the work's Pane with `--pane <id>`, or a new Pane for new work), check on it, and send it a follow-up.\n\n`runpane report --state ready|blocked|failed|done` is how a worker hands back its result. It stores the latest report on the worker's panel (state, `--pr`, `--head`, up to 16,000 characters of `--summary` or `--summary-file`, and the `--question` a blocked worker needs answered), journals an opt-in `agent.report` watch event (`REPORT <pane-name> pane <pane-id> panel <panel-id> ready pr#747 fc5dce9`; it skips the `--min-interval` batch), records it as Session activity, and shows it in `agents status`, `panels list`, and `sessions overview` (each panel's own report as `panes[].panels[].report`, with a JSON object summary as `data`, and the Pane's newest as `panes[].report`). Inside a Pane terminal the panel comes from `PANE_SESSION_ID` and `PANE_PANEL_ID`; elsewhere pass `--pane` and `--panel`. It prints how many watches were waiting for the report and received it (`Delivered to 1 waiting watcher.`, `liveWatchers` in JSON). Zero means nobody was waiting right now, not that the report is lost: it stays on the panel, and a named-cursor watch receives it when it next waits.\n\nWork sent to an agent through `runpane panels create`, `panes create` (with a prompt), or `panels submit` records that the sender waits for a `runpane report`, unless the request says `--source user` (Pane's own UI does) or `--no-expect-report`; `--expect-report` asks for one anyway. Typing into a tab never does. If the agent then goes ready or its panel exits without a report, Pane journals one opt-in `agent.report.missing` entry (`MISSING REPORT <pane-name> pane <pane-id> panel <panel-id> ready|exited`) whose `lastOutput` is the agent's last transcript reply or last screen lines; it skips the `--min-interval` batch and reaches only watchers that list it in `--kinds`. While the agent's screen shows background work (Claude's `· 1 shell` footer or a running background subagent row, Codex's `background terminal running` line), Pane waits and looks again every 10 seconds instead. `agent.blocked` entries carry `screenLines`, the prompt the agent waits on, and are journalled once a panel has stayed blocked for 2 seconds.\n\nThe orchestrator guide prescribes submit without `--wait`, followed by its named watch; use one completion consumer. As an optional CLI route, `--wait` on `runpane panels submit`, `panels create`, and `panes create` turns the expected report on and blocks on that panel until the first `agent.report`, `agent.report.missing`, `agent.blocked`, `panel.exited`, or `pane.gone`, then prints one line of JSON (`dispatchWaitResult`): the outcome, ids, the report's state, summary, `summaryPath`, PR and head, `screenLines` for a prompt, or `lastOutput` for a missing report, never the transcript. It exits 0 for a report, 3 missing, 4 blocked, 5 exited or gone, and 124 when `--timeout <duration>` passes. It follows the journal from the position the daemon took before delivering the work, so nothing that happens right after delivery is missed. It reuses watch --until-event and returns indeterminate (exit 6) if the journal epoch changes or retained history is lost; inspect the panel and inbox before retrying. Report-expected dispatches are serialized per panel and refused before writing while a result or missing-output collection is outstanding. Answer a blocked panel with panels submit --no-expect-report, then watch for the original result.\n\n`runpane inbox [--session <id|name>] [--json]` lists what a Session's orchestrator has not acknowledged: reports, missing reports, and on-screen questions (a question clears itself once its panel moves on). `runpane inbox ack <item-id>...` or `--all --session <id|name>` clears items. An orchestrator runs it when it starts or resumes, to catch anything that finished while it was down; Pane never injects the inbox into its turns.\n\n`runpane panels resume --panel <panel-id>` restarts a stopped terminal panel in place, keeping its panel id: an agent panel relaunches with its resume command (Claude `--resume <session>`, `codex resume <session>`, Agent Farm `agent-farm run <profile> --resume <id>` with the ID Agent Farm printed; the result's `agentSessionId` names it), the same restart the app runs when it shows the panel, without taking focus. A running panel is left alone (`action: \"already-running\"`); one whose agent exited back to its shell is stopped, and resume runs its saved command again in that shell (`action: \"resumed\"`). If that command exits at once, `--wait-ready` returns `ok: false` with `reason.code: \"command-exited\"` and the error line it printed. `--session <id|name>` or `--all-stopped` resumes every stopped agent panel in that scope, `--concurrency` at a time (default 3), then, with `--wait-ready`, waits on the resumed panels together, and returns `{ resumed, alreadyRunning, failed, notReady, items[] }` with each panel's own result, readiness, and blocker or error. After a Pane restart, or once an agent exits back to its shell, stopped panels report `running: false` (`initialized` stays true while the shell lives) in `panels list`, `panels screen`, and `panels wait` (which returns `stopped: true` instead of timing out), and `nextCommand` points at `panels resume`. `runpane watch` reports them as `panel.stopped` (`STOPPED ...`) when `--kinds` lists that kind; other consumers and `workspace state` see `agent.unknown` with `running: false`, never `agent.ready`.\n\n`runpane panels last-message --panel <panel-id>` reads a Claude or Codex agent's last reply from its transcript (up to `--limit` characters, default 20,000, keeping the end and reporting `truncated`). It never scrapes the screen: without a transcript it prints `{ ok: false, reason: \"transcript-unavailable\" }` and exits 1.\n\nCommands with a contract `daemonAction` (the `panes` git, script, restore, and move commands, `folders list|create`, and `links open`) call the same Pane daemon channel as the matching button in the app and print `{ ok, data, error }`. Destructive ones add a pane:// `link` to review the Pane.\n\n`runpane links create` builds `pane://open?...` links; opening one in Pane selects what it names and never changes Pane state.\n\n`runpane docs search|read` search and read Pane docs, help, and installed Pane Chat skills offline. They ship in the npm package and the Pane app only.\n\n`runpane handoff` hands a task to another agent on this or another of your machines. The sending agent writes the note from `runpane handoff --template` (goal, current state, done and verified, in progress, next steps, decisions and constraints, open questions, how to verify, git state); `runpane handoff \"claude opus on parsas-macbook-pro\" --note-file ~/handoff.md` checks every section is filled in, requires the branch to be pushed (or pushes it with `--push`, never forcing), writes the note to that machine's `~/.pane/handoffs/` and starts the agent in a new Pane branched from the sender's branch. When the sender is a Pane tab running a Claude, Codex, Cursor or OpenCode session and the receiver is the same agent, the handoff carries that session: on another machine its files are copied into that machine's agent store and the receiver resumes the same id; on this machine the receiver resumes a fork, because two agents never resume one session on one machine. The receiver starts with no prompt, and the CLI runs the continuity check and reports JSON `continuity.result` `continued`, `inconclusive` (exit 0; `next` gives a probe question) or `not-continued` (exit 1; the receiver is kept and `next` gives the `--note-only` command). `--note-only`, a different agent, or a tab with no carried session starts a fresh agent with the note (`fresh-agent`). JSON `continuity` is `{ carried, result, agent, sessionId, resumedId, fork, files, bytes, checks: { resumedById, restartResumes, historyLoaded, originalUntouched?, originalAppended? }, reason, next }`: `sessionId` is the sender's session, `resumedId` the id the receiver resumes (the fork's on this machine), `checks` are the continuity checks (`historyLoaded` is null when there was nothing to compare; `originalUntouched` is set for forks), and a dry run sets `carried`, `sessionId`, `fork`, `files` and `bytes` with no `result`. After `continued` or `inconclusive` the sending agent ends its turn and makes no more changes on the branch. An OpenCode session travels as its export: `opencode export` on this machine, then `opencode import` inside the receiving repository there, and an OpenCode receiver is accepted only to continue an OpenCode session. A Cursor session is carried only to another machine, best effort since Cursor's store is undocumented: Cursor has no fork, and a copied chat still writes to the original, so on this machine the result is `not-continued` and nothing is started. A destination's agent store is assumed at its default location (`~/.claude`, `~/.codex`, `~/.cursor`); a copy already there is replaced only when the sender's holds all of it and no agent there runs it, otherwise nothing is copied and the result is `not-continued`. A destination Pane too old to resume a carried session is refused before anything is sent (`ERR_HANDOFF_RESUME_UNSUPPORTED`). From a Session's terminal it skips git and creates a Session with the same name, goal and agent on the other machine instead; a same-agent Session resumes the sender's conversation when first opened. The receiver reports back to the sender's panel.\n\n`runpane pr watch`: Register durable GitHub PR monitoring for a Pane.\n\n`runpane pr unwatch`: Remove a Pane’s explicit PR registration.\n\n`runpane pr list`: List PR registrations and stopped watches.\n\n## Command reference\n\nEvery command and its options, from `commands` in `contracts/runpane/contract.json`.\n\n- `help`: Show help for runpane or a specific command.\n- `setup`: Open the guided setup wizard for install, remote host setup, update, and diagnostics.\n- `install`: Install Pane on this machine or configure this machine as a remote daemon host.\n- `update`: Update both Pane and the invoked supported CLI through an independent helper.\n- `version`: Print the runpane wrapper version without contacting, launching, or focusing Pane.\n- `doctor`: Run platform, release, installed Pane, daemon reachability, and remote setup diagnostics.\n- `daemon repair`: Repair and restart the managed remote-daemon launcher without changing pairing or tunnel configuration.\n- `agents doctor`: Diagnose whether a built-in agent command is available in a Pane repository environment.\n- `agent-context`: Print token-efficient Pane command context for coding agents.\n- `mcp`: Run the Pane MCP server over stdio so coding agents can call runpane commands as tools.\n- `repos list`: List repositories saved in the running Pane app.\n- `repos add`: Register an existing git repository with the running Pane app.\n- `panes list`: List Pane sessions in a saved repository.\n- `panes cost`: Report estimated token cost per Pane, with per-model breakdown and cache efficiency.\n- `workspace state`: Read one workspace snapshot of every Pane and CLI panel.\n- `workspace list`: List the machines on your tailnet, each with its owner, and whether each answers workspaces.\n- `workspace enable`: Put this machine's Pane on your tailnet for runpane workspace (the default when Tailscale is signed in).\n- `workspace disable`: Take this machine off runpane workspace and remove its tailscale serve handler on port 8443.\n- `workspace read`: Print a file from a machine on your tailnet; any path form works (C:\\..., /mnt/c/..., /home/..., ~/...).\n- `workspace write`: Write stdin to a file on a machine on your tailnet, creating parent folders.\n- `workspace exec`: Run a command in another machine's shell and return stdout, stderr, the exit code, OS, and shell.\n- `handoff`: Hand your task to another agent on this or another machine: check your handoff note, make the branch reachable, send the note, and start the agent on it. A same-agent handoff from a Pane tab carries and resumes your own session (a fork on this machine) and checks it continued; otherwise a fresh agent starts with the note. From a Session's terminal, create the Session on another machine instead.\n- `watch`: Wait for workspace agent and Pane transitions using a daemon-held cursor.\n- `panes create`: 1 feature = 1 worktree = 1 branch = 1 Pane; reviews, audits, fixes and QA for it are tabs in that Pane. Create a Pane (a new worktree) only when the work's branch has no Pane yet; for review, audit, fix or QA on existing work use `panels create --pane <id>`.\n- `panes adopt`: Adopt an existing externally managed git worktree as a Pane without changing the worktree.\n- `panes archive`: Archive a Pane (session) exactly like the UI Archive action, including safe removal of its Pane-managed git worktree, or archive every merged Pane in a Session.\n- `panes pin`: Declaratively pin a Pane; pinned is the Pane UI's favorite/pin star and repeated requests are idempotent.\n- `panes unpin`: Declaratively unpin a Pane; pinned is the Pane UI's favorite/pin star and repeated requests are idempotent.\n- `panes rename`: Rename a Pane without changing its worktree, branch, panels, or focus.\n- `panes focus`: Raise the Pane window and select a Pane (and optionally one of its panels) on explicit user request.\n- `panels create`: Open a new agent or terminal tab inside an existing Pane, sharing its worktree. The default for review, QA, fixes and any other agent on work that already has a Pane.\n- `panels open`: Open a URL or file as a browser/editor tab in a Pane, in split view beside the agent by default.\n- `panels list`: List tool panels inside a Pane session.\n- `panels output`: Read recent terminal output from a panel.\n- `panels screen`: Read a compact current-screen view from a terminal panel.\n- `panels input`: Send input bytes to a terminal panel.\n- `panels submit`: Send and submit text to a terminal panel, including idle agent composers.\n- `panels submit-composer`: Submit an agent composer using the panel-appropriate key sequence.\n- `panels wait`: Wait for a terminal panel to initialize, become ready/idle, or contain text.\n- `panels resume`: Restart a stopped terminal panel in place, resuming its agent's conversation.\n- `panels last-message`: Read an agent's last reply from its transcript, without scraping the screen.\n- `panes git-status`: Read the git status of a Pane worktree: uncommitted, unpushed, and behind-main counts.\n- `panes commit`: Stage all changes in a Pane worktree and commit them.\n- `panes push`: Push a Pane branch to its remote.\n- `panes pull`: Pull the remote branch into a Pane worktree.\n- `panes rebase-main`: Rebase a Pane branch onto the latest main branch.\n- `panes restore`: Restore an archived Pane, recreating its worktree.\n- `panes squash-rebase`: Squash a Pane branch into one commit and rebase it onto main.\n- `panes stash`: Stash uncommitted changes in a Pane worktree.\n- `panes stash-pop`: Apply and drop the latest stash in a Pane worktree.\n- `panes soft-reset`: Undo the last commit in a Pane, keeping its changes staged.\n- `panes fetch`: Fetch the remote for a Pane worktree.\n- `panes run-script`: Run the repository's run script in a Pane.\n- `panes stop-script`: Stop the run script running in a Pane.\n- `panes move`: Move a Pane into a sidebar folder.\n- `folders list`: List the sidebar folders of a repository.\n- `folders create`: Create a sidebar folder in a repository.\n- `links create`: Build a pane:// link that opens a Pane, panel, repository, or Session in the Pane app.\n- `links open`: Open a pane:// link in the running Pane app.\n- `docs search`: Search Pane's docs, runpane help, and the Pane Chat skills.\n- `docs read`: Read one Pane doc, help topic, or skill in full.\n- `agents start`: Start an agent on a task and wait until it is ready: as a new tab in an existing Pane with --pane <id> (review, audit, fix, QA), or in a new Pane with --repo and --name for new work.\n- `agents status`: Check on an agent: whether it is working, ready, blocked, or idle, plus its current screen.\n- `agents send`: Send a follow-up message to an agent and confirm it was submitted.\n- `report`: Hand back a worker's structured report: state, PR, head commit, summary, and the question when blocked.\n- `inbox`: List the reports, missing reports, and on-screen questions a Session's orchestrator has not acknowledged.\n- `inbox ack`: Clear Session inbox items once they are handled.\n- `sessions list`: List durable named orchestration Sessions.\n- `sessions create`: Create a durable named orchestration Session and its hidden terminal owner; JSON isPinned sets the pin star (default false).\n- `sessions get`: Read one durable named orchestration Session.\n- `sessions update`: Update a Session overview from structured JSON; isPinned sets the UI pin star.\n- `sessions set-agent`: Switch the durable terminal agent for a named Session.\n- `sessions associate`: Associate a user-visible Pane with a named Session.\n- `sessions detach`: Detach a Pane from a named Session.\n- `sessions overview`: Read a live status, activity, git, and pull request overview for a named Session.\n- `lock acquire`: Acquire a named lock on a resource shared between agents, such as one test account, optionally waiting for it.\n- `lock release`: Release a named lock you hold, or force-release another owner's lock.\n- `lock list`: List held named locks, optionally only one Session's.\n- `sessions unpin`: Declaratively unpin a named Session using the UI pin state; repeated requests keep the requested state.\n- `sessions pin`: Declaratively pin a named Session using the UI pin state; repeated requests keep the requested state.\n- `pr watch`: Register durable GitHub PR monitoring for a Pane.\n- `pr unwatch`: Remove a Pane’s explicit PR registration.\n- `pr list`: List PR registrations and stopped watches.\n\n```bash\nrunpane help [command]\nrunpane setup\nrunpane install [client|daemon] [options]\n runpane update [--pane-only | --cli-only] [--version <latest|vX.Y.Z>] [--pane-dir <path>] [--dry-run] [--wait] [--json]\n runpane update --status <operation-id> [--wait] [--pane-dir <path>] [--json]\nrunpane version [--pane-dir <path>]\nrunpane --version\nrunpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\nrunpane doctor --report [--title <text>] --body-file <path|-> [--yes] [--json]\nrunpane daemon repair [--pane-dir <path>] [--pane-path <path>] [--yes] [--json]\nrunpane agents doctor --agent <codex|claude|cursor> [--repo <selector>] [--json] [--pane-dir <path>]\nrunpane agent-context [--json]\nrunpane agent-context --command <command> [--json]\nrunpane mcp [--toolsets <name,...>] [--read-only]\nrunpane repos list [--json] [--pane-dir <path>]\nrunpane repos add --path <path> [--name <name>] [--json] [--yes] [--pane-dir <path>]\nrunpane panes list [--repo <selector>] [--json] [--pane-dir <path>]\nrunpane panes cost [--repo <selector>] [--pane <pane-id>] [--json] [--pane-dir <path>]\nrunpane workspace state [--repo <selector>] [--json] [--pane-dir <path>]\nrunpane workspace list [--json] [--pane-dir <path>]\nrunpane workspace enable [--json] [--pane-dir <path>]\nrunpane workspace disable [--json] [--pane-dir <path>]\nrunpane workspace [<machine>] read <path> [--json]\nrunpane workspace [<machine>] write <path> [--json]\nrunpane workspace <machine> exec [--cwd <path>] [--timeout-ms <ms>] [--json] -- <command>\nrunpane handoff --template\nrunpane handoff \"<destination>\" --note-file <path|-> [--machine <name>] [--agent <codex|claude|cursor|opencode>] [--model <model>] [--effort <level>] [--repo <selector>] [--push] [--note-only] [--dry-run] [--json]\nrunpane watch [--follow|--until-event] [--session <id|name>|--pane <id>] [--json] [overrides]\nrunpane panes create --repo <selector> --name <name> --agent <codex|claude|cursor> [--source user|agent] [--focus|--no-focus] [--base <ref>] [--branch <name>] [--prompt-file <path|->] [--expect-report|--no-expect-report] [--wait [--timeout <duration>]] [options] [--pane-dir <path>]\nrunpane panes create --repo <selector> --name <name> --tool-command <command> [--agent <codex|claude|cursor>] [--source user|agent] [--focus|--no-focus] [--base <ref>] [--branch <name>] [--prompt-file <path|->] [--expect-report|--no-expect-report] [--wait [--timeout <duration>]] [options] [--pane-dir <path>]\nrunpane panes create --from-json <path|-> [--yes] [--json] [--pane-dir <path>]\nrunpane panes adopt --repo <selector> --path <dir> --name <name> --agent <codex|claude|cursor> [--resume <id>] [--folder <name>] [--launch [--prompt <text>|--prompt-file <path|->] [--wait-ready] [--ready-timeout-ms <ms>]] [--no-pinned] [--no-associate] [--dry-run] [--yes] [--json] [--pane-dir <path>]\nrunpane panes adopt --repo <selector> --path <dir> --name <name> --tool-command <command> [--agent <codex|claude|cursor>] [--folder <name>] [--launch [--prompt <text>|--prompt-file <path|->] [--wait-ready] [--ready-timeout-ms <ms>]] [--no-pinned] [--no-associate] [--dry-run] [--yes] [--json] [--pane-dir <path>]\nrunpane panes archive --pane <pane-id> [--source user|agent] [--force] [--remove-worktree] [--dry-run] --yes [--json] [--pane-dir <path>]\nrunpane panes archive --session <id|name> --merged [--remove-worktree] [--source user|agent] [--dry-run] --yes [--json] [--pane-dir <path>]\nrunpane panes pin --pane <pane-id> --yes [--dry-run] [--json] [--pane-dir <path>]\nrunpane panes unpin --pane <pane-id> --yes [--dry-run] [--json] [--pane-dir <path>]\nrunpane panes rename --pane <pane-id> --name <new-name> --yes [--dry-run] [--json] [--pane-dir <path>]\nrunpane panes focus --pane <pane-id> [--panel <panel-id>] --source user|agent --yes [--json] [--pane-dir <path>]\nrunpane panels create --pane <pane-id> --agent <codex|claude|cursor> [--source user|agent] [--focus|--no-focus] [--wait-ready] [--expect-report|--no-expect-report] [--wait [--timeout <duration>]] --yes [--json] [--pane-dir <path>]\nrunpane panels create --pane <pane-id> --tool-command <command> [--agent <codex|claude|cursor>] [--title <title>] [--focus|--no-focus] [--wait-ready] [--expect-report|--no-expect-report] [--wait [--timeout <duration>]] --yes [--json] [--pane-dir <path>]\nrunpane panels open [--pane <pane-id>] (--url <url>|--file <path>) [--title <title>] [--split|--tab] [--focus|--no-focus] [--source user|agent] --yes [--json]\nrunpane panels list --pane <pane-id> [--json] [--pane-dir <path>]\nrunpane panels output --panel <panel-id> [--limit <count>] [--json] [--pane-dir <path>]\nrunpane panels screen --panel <panel-id> [--limit <count>] [--json] [--pane-dir <path>]\nrunpane panels input --panel <panel-id> (--text <text>|--keys <name,...>|--input-file <path|->) [--source user|agent] --yes [--json] [--pane-dir <path>]\nrunpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) [--as-file-pointer [--attach <file>]...] [--interrupt] [--source user|agent] [--expect-report|--no-expect-report] [--wait [--timeout <duration>]] --yes [--json] [--pane-dir <path>]\nrunpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter|tab] --yes [--json] [--pane-dir <path>]\nrunpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json] [--pane-dir <path>]\nrunpane panels resume --panel <panel-id> [--wait-ready] [--ready-timeout-ms <ms>] --yes [--json] [--pane-dir <path>]\nrunpane panels resume (--session <id|name>|--all-stopped) [--wait-ready] [--ready-timeout-ms <ms>] [--concurrency <count>] --yes [--json] [--pane-dir <path>]\nrunpane panels last-message --panel <panel-id> [--limit <count>] [--json] [--pane-dir <path>]\nrunpane panes git-status --pane <pane-id> [--json] [--pane-dir <path>]\nrunpane panes commit --pane <pane-id> --message <message> --yes [--json] [--pane-dir <path>]\nrunpane panes push --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes pull --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes rebase-main --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes restore --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes squash-rebase --pane <pane-id> --message <message> --yes [--json] [--pane-dir <path>]\nrunpane panes stash --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes stash-pop --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes soft-reset --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes fetch --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes run-script --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes stop-script --pane <pane-id> --yes [--json] [--pane-dir <path>]\nrunpane panes move --pane <pane-id> --folder <folder-id> --yes [--json] [--pane-dir <path>]\nrunpane folders list --repo <repo-id> [--json] [--pane-dir <path>]\nrunpane folders create --repo <repo-id> --name <name> --yes [--json] [--pane-dir <path>]\nrunpane links create --pane <pane-id> [--panel <panel-id>] [--json]\nrunpane links create --repo <repo-id> [--json]\nrunpane links create --session <session-id> [--json]\nrunpane links open --url <pane-url> --yes [--json] [--pane-dir <path>]\nrunpane docs search --query <text> [--limit <count>] [--json]\nrunpane docs read --doc <path> [--json]\nrunpane agents start (--pane <pane-id> | --repo <selector> --name <name> [--base-branch <branch>]) (--agent <codex|claude|cursor>|--tool-command <command>) --prompt <task> --yes [--json] [--pane-dir <path>]\nrunpane agents status (--pane <pane-id>|--panel <panel-id>) [--limit <count>] [--json] [--pane-dir <path>]\nrunpane agents send (--pane <pane-id>|--panel <panel-id>) --text <message> [--as-file-pointer] --yes [--json] [--pane-dir <path>]\nrunpane report --state <ready|blocked|failed|done> [--pr <number>] [--head <sha>] [--summary <text>|--summary-file <path|->] [--question <text>] [--pane <pane-id> --panel <panel-id>] [--json] [--pane-dir <path>]\nrunpane inbox [--session <id|name>] [--json] [--pane-dir <path>]\nrunpane inbox ack <item-id>... [--session <id|name>] [--json] [--pane-dir <path>]\nrunpane inbox ack --all --session <id|name> [--json] [--pane-dir <path>]\nrunpane sessions list [--json] [--pane-dir <path>]\nrunpane sessions create --from-json <path|-> [--json] [--pane-dir <path>]\nrunpane sessions get --session <id|name> [--json] [--pane-dir <path>]\nrunpane sessions update --session <id|name> --from-json <path|-> [--json] [--pane-dir <path>]\nrunpane sessions set-agent --session <id|name> --agent <codex|claude|cursor> [--json] [--pane-dir <path>]\nrunpane sessions associate --session <id|name> --pane <pane-id> [--json] [--pane-dir <path>]\nrunpane sessions detach --session <id|name> [--pane <pane-id>] [--json] [--pane-dir <path>]\nrunpane sessions overview --session <id|name> [--json] [--pane-dir <path>]\nrunpane lock acquire --name <name> --ttl <duration> [--wait <milliseconds>] [--note <text>] [--pane <pane-id>] [--panel <panel-id>] [--json] [--pane-dir <path>]\nrunpane lock release --name <name> [--force] [--session <id|name>] [--note <text>] [--pane <pane-id>] [--panel <panel-id>] [--json] [--pane-dir <path>]\nrunpane lock list [--session <id|name>] [--json] [--pane-dir <path>]\nrunpane sessions unpin --session <id|name> [--json] [--pane-dir <path>]\nrunpane sessions pin --session <id|name> [--json] [--pane-dir <path>]\nrunpane pr watch [--pane <id>] --url <PR URL> [--head <full-sha>] [--json] [--pane-dir <path>]\nrunpane pr unwatch [--pane <id>] --url <PR URL> [--json] [--pane-dir <path>]\nrunpane pr list [--pane <id>] [--json] [--pane-dir <path>]\n```\n\n## Agent Context\n\nPane lets a developer manage saved base repositories, user-visible Panes (Pane sessions) for feature/PR work, and terminal-backed panel tabs. A Pane is a visible workspace that normally maps to one Pane-managed git worktree and branch; a panel is a terminal tab inside one Pane and shares that Pane's worktree; an agent is a CLI process running inside a panel.\n\nDoctor-first environment discovery:\n\n```bash\nrunpane doctor --json\n```\n\nBrief CLI context:\n\n```bash\nrunpane agent-context\nrunpane agent-context --json\n```\n\nDetailed command discovery:\n\n```bash\nrunpane agent-context --command <command> [--json]\n```\n\nBrief tools:\n\n- `doctor`: Report wrapper, platform, installed Pane, and daemon reachability before an agent mutates Pane state.\n- `agent-context`: Print token-efficient Pane command context for coding agents.\n- `agents doctor`: Check whether Codex or Claude is available in the repo environment Pane will use.\n- `repos list`: List repositories saved in the running Pane app.\n- `repos add`: Register an existing git repository with the running Pane app.\n- `panes list`: List Pane sessions, optionally scoped to a saved repository.\n- `panes cost`: Report estimated token cost per Pane, with per-model breakdown and cache efficiency.\n- `panes create`: 1 feature = 1 worktree = 1 branch = 1 Pane; reviews, audits, fixes and QA for it are tabs in that Pane. Create a Pane (a new worktree) only when the work's branch has no Pane yet; for review, audit, fix or QA on existing work use `panels create --pane <id>`.\n- `panes archive`: Archive a Pane exactly like the UI Archive action, including safe removal of its Pane-managed git worktree, or archive every merged Pane in a Session.\n- `panes pin`: Declaratively pin a Pane (the Pane UI's favorite/pin star) without changing focus.\n- `panes unpin`: Declaratively unpin a Pane (the Pane UI's favorite/pin star) without changing focus.\n- `panes rename`: Rename a Pane without changing its worktree, branch, panels, or focus.\n- `panes focus`: Raise the Pane window and select a Pane (and optionally a panel) on explicit user request.\n- `panels create`: Open reviewer, QA, fix or helper agent tabs inside an existing Pane; they share that Pane's worktree. The default for any later agent on existing work.\n- `panels open`: Show the user an HTML page, plan, report, dev server URL, or file as a tab in split view beside the agent.\n- `panels list`: List tool panels inside a Pane session.\n- `panels output`: Read recent terminal output from a panel.\n- `panels screen`: Read a compact current-screen view from a terminal panel.\n- `panels input`: Send input bytes to a terminal panel.\n- `panels submit`: Send text plus terminal Enter to a terminal panel.\n- `panels submit-composer`: Submit an agent composer with the key for its current state.\n- `panels wait`: Wait for terminal initialized, ready, idle, or text state with compact output.\n- `panels resume`: Restart a stopped terminal panel in place (same panel id), resuming its agent's conversation.\n- `watch`: Wait for workspace events from the durable daemon journal; follow uses conservative cadence and quiet expiry by default.\n\nManaged AGENTS.md block body:\n\n```md\n## Pane\n\nThis repository is used with [Pane](https://runpane.com). Drive it with the CLI or the `pane` MCP server.\n\n1 feature = 1 worktree = 1 branch = 1 Pane; reviews, audits, fixes and QA for it are tabs in that Pane. Open later agents with `runpane panels create --pane <id>` (MCP: `panels_create` or `agents_start` with `pane`), not a new Pane.\n\nCLI: `npm i -g runpane` (or `npx --yes runpane@latest`), then `runpane doctor --json`. Full command reference: `runpane agent-context --json`.\n\nMCP: packaged Pane registers a stdio server named `pane` with Claude Code, Codex, and Cursor. Check the connection with `claude mcp list`, `codex mcp list`, or `agent mcp list`. Cursor may ask you to enable `pane` with `agent mcp enable pane`. If tools are missing, add it in the agent's MCP settings: Claude Code `claude mcp add --scope user pane -- npx --yes runpane@latest mcp`; Codex (`~/.codex/config.toml`) table `[mcp_servers.pane]` with `command = \"npx\"` and `args = [\"--yes\", \"runpane@latest\", \"mcp\"]`; Cursor (`~/.cursor/mcp.json`) uses `mcpServers.pane` with the same `npx` command and args; any other stdio client uses them too.\n```\n\n## Wrapper Flags\n\nThese flags are consumed by the wrapper:\n\n```bash\n--version <latest|vX.Y.Z>\n--download-dir <path>\n--pane-path <path>\n--format <auto|appimage|deb|dmg|zip|exe>\n--dry-run\n--yes (aliases: -y)\n--verbose\n```\n\nThe top-level `runpane --version` form prints the wrapper version. The install subcommand form `runpane install --version vX.Y.Z` selects a Pane release.\n\n## Local Control Flags\n\nThese flags are consumed by local daemon-control commands:\n\n```bash\n--pane-dir <path>\n--repo <selector>\n--pane <pane-id>\n--panel <panel-id>\n--path <path>\n--name <name>\n--worktree-name <name>\n--branch <name>\n--base-branch <ref> (aliases: --base)\n--folder <name>\n--resume <agent-session-id>\n--agent <codex|claude|cursor>\n--tool-command <command>\n--title <title>\n--url <url>\n--file <path>\n--initial-input <text> (aliases: --prompt)\n--initial-input-file <path|-> (aliases: --prompt-file)\n--attach <file>\n--from-json <path|->\n--timeout-ms <milliseconds>\n--ready-timeout-ms <milliseconds>\n--concurrency <count>\n--limit <count>\n--for <initialized|ready|idle|text>\n--contains <text>\n--interval-ms <milliseconds>\n--text <text>\n--input-file <path|->\n--source <user|agent>\n--strategy <auto|codex-ctrl-enter|enter|tab>\n--as <consumer-name>\n--since <generation>\n--from <now|earliest>\n--kinds <kind,...>\n--name-contains <text>\n--exclude-pane <pane-id>\n--heartbeat <duration>\n--idle-after <duration>\n--expire-after <duration>\n--ready-settle <duration>\n--blocked-settle <duration>\n--min-interval <duration>\n--body-file <path|->\n--session <id|name>\n--ttl <duration>\n--wait <milliseconds>\n--timeout <duration>\n--note <text>\n--message <message>\n--query <text>\n--doc <path>\n--toolsets <name,...>\n--keys <name,...>\n--state <ready|blocked|failed|done>\n--pr <number>\n--head <sha>\n--summary <text>\n--summary-file <path|->\n--question <text>\n--cwd <path>\n--note-file <path|->\n--machine <name>\n--model <model>\n--effort <level>\n--epoch <epoch>\n--json\n--wait-ready\n--no-focus\n--focus\n--split\n--tab\n--pinned\n--no-pinned\n--no-associate\n--force\n--remove-worktree\n--merged\n--launch\n--as-file-pointer\n--follow\n--until-event\n--ack-now\n--include-held-input\n--all-stopped\n--include-shells\n--no-held-input\n--self-test\n--report\n--read-only\n--template\n--push\n--note-only\n--interrupt\n--expect-report\n--no-expect-report\n--all\n```\n\n`runpane doctor --json`, `runpane repos list`, `runpane panes ...`, and `runpane panels ...` commands use or describe the local framed daemon socket/pipe for a running Pane app. `--pane-dir` points the wrapper at a non-default Pane data directory, such as `PANE_DIR=~/.pane_test` in development. `runpane agent-context` is local/offline and can be used before Pane is running. `agent-context` and `version` accept and ignore `--pane-dir`, so one `--pane-dir` can be passed to every runpane command. In a Pane repository checkout, if `runpane` is not on PATH, build the local wrapper with `pnpm --filter runpane build` and run it with Node 22 or newer, for example `node packages/runpane/dist/cli.js doctor --json`. From WSL, if the user runs Windows Pane, call the Windows wrapper through `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane ...'` so the command can reach the Windows named-pipe daemon and avoid UNC cwd issues.\n\n## Daemon Passthrough Flags\n\n`runpane install daemon` forwards these flags to `pane --remote-setup`:\n\n```bash\n--label <name>\n--prefer-tunnel <tailscale|ssh|manual|auto>\n--channel <stable|nightly>\n--base-url <url>\n--pane-dir <path>\n--listen-port <port>\n--port <port>\n--repo-ref <ref>\n--auto-listen-port\n--interactive-tailscale-setup\n--no-install-service\n--no-tailscale-serve\n--print-only\n```\n\nUnknown daemon flags should be forwarded rather than dropped so newer Pane versions can extend `--remote-setup` without requiring an immediate wrapper release. Unknown flags for non-daemon commands should fail clearly.\n\n## Machine-Readable Schemas\n\nStable `--json` response schemas live in `contracts/runpane/contract.json` under `jsonSchemas`.\n\nA failed command exits 1 and prints its message and a `Next:` line to stderr. With `--json` it also prints `jsonSchemas.error` to stdout: `{ \"ok\": false, \"error\": { \"code\": \"...\", \"message\": \"...\", \"next\": \"...\" } }`. Branch on `code`; `next` is the step to take when one is known. A command whose result has its own `ok: false` shape, such as an archive refusal or `panels last-message` without a transcript, prints that result instead, and `watch` reports failures as WATCH ERROR lines with exit 2.\n\nOpenAPI should be generated later from the stable local HTTP/API subset of the same contract, not maintained as a second source of truth.\n\n## Download Attribution\n\nThe npm package uses `source=npm` for all npm-registry consumers, including `npx`, `pnpm dlx`, `yarn dlx`, `bunx`, and global npm/pnpm installs.\nThe PyPI package uses `source=pip` for all Python consumers, including pip, pipx, uvx, and `python -m runpane`.\nWrappers should prefer `https://runpane.com/api/download?platform=<platform>&arch=<arch>&format=<format>&version=<version>&channel=<channel>&source=<npm|pip>`.\nIf the website route cannot satisfy the download, wrappers may fall back to the matching GitHub release asset and print a warning that website attribution may be incomplete for that run.\nWrappers emit best-effort telemetry to `https://runpane.com/api/runpane/telemetry` for command failures, download request/success/failure, and GitHub fallback usage. Routine command starts and successes emit nothing.\nWrapper telemetry uses a persisted anonymous `install_id` in the form `install_<uuid>` and PostHog `distinct_id = install:<install_id>` so installs reporting failures or downloads can be counted with `count(DISTINCT properties.install_id)`; this is not a count of all CLI users.\nWrapper telemetry must be disabled in CI, when `RUNPANE_TELEMETRY_DISABLED` is set, and when the local config explicitly disables analytics. It must not include raw paths, labels, prompts, raw error text, or environment values.\nDesktop local control reports useful failures as `runpane_local_control_failed`; normal polling, wait deadlines, and optional agent unavailability emit nothing. Reports contain coarse operation and failure classification only, with a five-minute deduplication interval and a maximum of 20 per hour per app process. Handled CLI failures are ordinary events, not exceptions.\n\n## Publishing Credentials\n\nLocal implementation, build, and dry-run validation do not need npm or PyPI API tokens.\nRelease publishing should prefer npm Trusted Publishing and PyPI Trusted Publishing from GitHub Actions.\nFallback `NPM_TOKEN` or `PYPI_API_TOKEN` credentials may be used for first package reservation or manual publication only. They must be supplied through local environment variables or GitHub Actions secrets, never committed, and revoked or rotated after use.\n"},{"path":"docs/RUNPANE_WORKSPACES.md","title":"Workspaces: reach all your machines with runpane","text":"# Workspaces: reach all your machines with runpane\n\n`runpane workspace` lets your Macs, Windows PCs, and Linux machines read files, write files, run commands, and drive each other's Pane over the Tailscale network you already use. There is no SSH, no keys, and no pairing code: each request is trusted because Tailscale says which login it comes from. Your own login is always let in; a teammate's machine lets you in when its owner shares it with everyone on the tailnet.\n\nThe same listener powers codeless desktop remote mode: Settings → Remote Access → **Your computers** lists the machines on your tailnet with Pane open (a teammate's machine that accepts only its owner shows as \"Owner only\" and can't be connected to), and **Connect** switches the desktop app to that machine with no `pane-remote://` code. `runpane workspace` reaches the same machines. Who may connect is set under **Access to this computer** → **Who can connect** (see [Who is trusted](#who-is-trusted)).\n\n```\nrunpane workspace list\nrunpane workspace <machine> read <path>\nrunpane workspace <machine> write <path> # content from stdin\nrunpane workspace <machine> exec -- <command> # runs in that machine's shell\nrunpane workspace <machine> <runpane command> # e.g. sessions list --json\n```\n\n`<machine>` is the Tailscale machine name (`parsa-devbox`), a unique prefix (`devbox`), the MagicDNS name, or a Tailscale IP.\n\n`runpane workspace list` shows your own machines first, then other people's machines on the tailnet labeled with their owner's login (`tylers-mac-mini (macOS, online, owner tbrownio@github)`). Each machine decides who it lets in. A machine set to \"Only me\" refuses other logins, and the error says how its owner can change that. A path shaped like another OS's (`C:\\...` on a Mac) routes only to your own machines; reach someone else's machine by name.\n\nThese commands ship in the npm CLI (`npm i -g runpane`, or `npx --yes runpane@latest workspace list`). The Python package prints the workspaces status block but does not run them.\n\n`workspace <machine> <runpane command>` runs any command that talks to Pane, such as `sessions list`, `panes list`, or `workspace state`, against that machine's Pane. Commands that only make sense on the machine you type them on (`doctor`, `agent-context`, `mcp`, `docs`, `install`) are refused with the alternative: run them there with `runpane workspace <machine> exec -- 'runpane doctor'`.\n\n## Turning it on\n\nWorkspaces are on by default. When Tailscale is installed and signed in, the desktop Pane that owns `~/.pane` joins your tailnet as it starts:\n\n- It listens on `127.0.0.1` on a free port.\n- It runs `tailscale serve --bg --https=8443 http://127.0.0.1:<port>`, next to the remote daemon's port 443 handler if you have one.\n- Nothing is opened to the public internet, and no firewall rule is added. Serve is reachable only inside your tailnet.\n\nEvery `runpane --help`, `runpane doctor`, and `runpane agent-context` prints the current state:\n\n```\nWorkspaces: on (parsas-macbook-pro)\nOther machines: parsa-devbox (Windows, online), parsas-macbook-air (macOS, offline)\nReach them: runpane workspace <machine> read|write|exec|<command>\n```\n\nWhen workspaces are off, the first line says why and gives the one step that fixes it:\n\n| Reason | Fix |\n|---|---|\n| Tailscale is not installed | Install Tailscale from https://tailscale.com/download and sign in. |\n| Tailscale is signed out | Open Tailscale and sign in. |\n| This tailnet has HTTPS certificates turned off | Turn on HTTPS Certificates at https://login.tailscale.com/admin/dns. |\n| Pane is not running | Open Pane. |\n| Turned off with `runpane workspace disable` | `runpane workspace enable` |\n\nWhile the tailnet has HTTPS Certificates turned off, the desktop app shows a notice with a link to that setting. Turning it on writes machine names (`<machine>.<tailnet>.ts.net`) to the public Certificate Transparency log. The notice clears within a minute of the change, and `runpane workspace list` names the setting as the reason other machines are not reachable.\n\n`runpane workspace disable` removes the 8443 handler and keeps this machine off until `runpane workspace enable`. This machine can still reach your other machines while it is off. A Pane started with a different data directory (`PANE_DIR` or `--pane-dir`) stays off unless you run `runpane workspace enable` against it.\n\nPane must be running on a machine for others to reach it. Pane checks Tailscale again every minute, so signing in to Tailscale after Pane starts is enough. Serve handlers belong to one tailnet, so after you switch this machine to another tailnet Pane puts the 8443 handler back on the new one and reports the new URL within a minute.\n\n## Who is trusted\n\nTwo settings decide, in Settings → Remote Access → **Access to this computer**. Every request must pass both: its login is allowed by **visibility**, and, when **password protection** is on, it carries the password.\n\n| Visibility | Who can connect | |\n|---|---|---|\n| Off | Nobody without a pairing code. Same as `runpane workspace disable`. | |\n| Only me | Devices signed into this machine's own Tailscale login. | Default |\n| Everyone on tailnet | Anyone with an untagged device in the current tailnet. Pane warns before turning it on. | |\n\nPane learns the owner from the `Self` entry in `tailscale status --json`, and Serve adds a `Tailscale-User-Login` header to every request after removing any copy the caller sent. Under \"Everyone on tailnet\", the people on the tailnet are the owners of its untagged devices, re-read every minute; people a device was shared with from another tailnet (Tailscale's sharee nodes) are not among them, even though Serve signs their requests too.\n\nPassword protection is off by default. When it is on, every client must send the password, your own machines included; the desktop app asks for it once and saves it with that machine's connection. Pane stores only a scrypt hash of it, and only someone at the machine itself can change visibility or the password: remote clients are refused. The `runpane workspace` CLI does not send a password yet, so it cannot reach a password-protected machine.\n\n- Under \"Only me\", a device signed in as another Tailscale user, including a teammate's Mac shared into your tailnet, is refused.\n- A tagged device is refused: Serve sends no identity for it.\n- Pane answers only requests that arrive through Serve: Serve's target path carries a secret that changes every launch, and requests straight to the loopback port are refused. Another OS user on the same machine who can read `tailscale serve status` could learn it, so give other accounts on a joined machine only the trust you would give the owner.\n- Browsers are refused. A web page open on one of your devices could otherwise send requests that Serve signs with your login. Pane refuses any request that carries a browser `Origin`; the phone app's WebSockets, which always carry one, send a fixed value no browser can.\n- Integration keys are shared only with the owner's login, even under \"Everyone on this tailnet\". See [Shared integration keys](SHARED_CREDENTIALS.md).\n- Any device signed in as you, your phone included, is trusted. `runpane workspace list` shows only Macs, Windows PCs, and Linux machines, but that is a listing choice, not a block.\n- Removing a device from Tailscale revokes it everywhere.\n- The same setting decides who opens the host's dev servers and HTML pages in the Remote Pane app's browser tabs, at their `https://<host>.ts.net:<n>` addresses. A page in a frame cannot send the password, so those pages are off while password protection is on. See [Browser tabs on the phone](SELF_HOSTED_REMOTE_DAEMON.md#browser-tabs-on-the-phone).\n\n## What `write` and `exec` can do\n\nEvery admitted login can also reach any port listening on the machine from desktop Pane, as [forwarded ports](SELF_HOSTED_REMOTE_DAEMON.md#forwarded-ports).\n\n`write` and `exec` give your agents SSH-level control of every joined machine, and so does anyone the machine's visibility lets in: under \"Everyone on tailnet\" that includes the other people on the tailnet. A local agent's permission prompts and sandbox see only `runpane workspace ... exec`, not what runs on the other machine. Turn workspaces off on any machine that should not accept this.\n\n## Hand a task to another machine\n\n`runpane handoff` uses workspaces to move a task to an agent on another of your machines. The sending agent writes a handoff note; the CLI checks it, makes sure the branch is pushed, and starts the agent there. When the sender is a Pane tab running Claude, Codex or Cursor and the receiver is the same agent, the agent's own session moves too:\n\n```\nrunpane handoff --template > ~/handoff.md # fill in every section\nrunpane handoff \"claude opus on parsas-macbook-pro\" --note-file ~/handoff.md --push\n```\n\nThe destination machine needs Pane running, the repository saved in Pane with a remote for the same GitHub repository, and its own `runpane` on PATH (otherwise it uses `npx runpane@latest`). The note lands in that machine's `~/.pane/handoffs/`, the agent starts in a new Pane branched from your branch, and it reports back to your panel with `runpane workspace <your machine> panels submit`. For a same-agent handoff the session's files are copied into that machine's agent store (`~/.claude`, `~/.codex` or `~/.cursor`, at their default locations), the receiver resumes that session with no prompt, and the CLI checks it continued before telling it where the session moved. The result is `continued`, `inconclusive` (ask the receiver what it was last working on), or `not-continued` (exit 1; the receiver is kept, and `--note-only` starts a fresh agent with the note if you choose). `--note-only`, a different agent, or no session to carry starts a fresh agent with the note. Carrying a Cursor chat to another machine is best effort; on the same machine it is not carried, since a copied Cursor chat still writes to the original. A copy already on the destination is replaced only when yours holds all of it and no agent there runs it; otherwise nothing is copied and the result is `not-continued`. A destination Pane too old to resume a carried session is refused before anything is sent. The note's sections are in the `handoff` skill's [note template](../main/src/services/paneChatBundle/skills/handoff/references/note-template.md).\n\nRun from a Session's terminal, whose folder is outside git, handoff moves the Session instead (the Session menu's **Move to host…** asks the Session's agent to do this). It skips the git steps and pushes nothing, writes the note to the other machine's `~/.pane/handoffs/` the same way, and creates a Session there with the same name, goal and agent, whose next action is to read the note. With the same agent, the Session's agent session is copied there too and the new Session resumes that conversation when you first open it; the CLI can't check it until then, so it reports `inconclusive` and says what to check. The destination needs only Pane running with workspaces; no repository has to be saved there. The new Session waits for your first message, and the original stays where it is. A Session can't be handed off to the machine it is on. See [Move a Session to another machine](SESSIONS.md#move-a-session-to-another-machine).\n\n## Paths across Windows, WSL, and macOS\n\n`read`, `write`, and `exec --cwd` take any path form and translate it on the machine that runs the request, so agents do not need `cat` on one machine and `Get-Content` on another:\n\n- `C:\\Users\\me\\notes.md` and `/mnt/c/Users/me/notes.md` reach the same Windows file.\n- On Windows, WSL paths such as `/home/me/repo/README.md` are read through `\\\\wsl.localhost\\<distro>\\...` using the default distribution (the one `wsl -l -v` marks with `*`). Name another distribution with `\\\\wsl.localhost\\<distro>\\...`.\n- Inside WSL, `C:\\...` becomes `/mnt/c/...`, and runpane reads the tailnet from Windows' `tailscale.exe`.\n- `~` is the home directory of the machine that runs the request.\n\n`read` prints the file to stdout (`--json` returns `content` with `encoding` `utf8` or `base64`) and lists a directory's entries. `write` reads stdin, creates missing parent folders, and replaces the file. `exec` runs in the shell Pane uses for terminals there (Git Bash or PowerShell on Windows, your default shell elsewhere), prints its stdout and stderr, ends with a line naming the machine, OS, shell, and exit code, and exits with the command's exit code. `--json` returns all of these fields. Pass the command as one quoted string (`exec -- 'grep \"a b\" notes.md'`): several words after `--` are joined with spaces. A command that starts a background job returns when the shell exits, and `--timeout-ms` stops the whole process tree. If that tree cannot be stopped, the result says so (`stillRunning` in `--json`, \"may still be running\" in text).\n\n## Without a machine name\n\nWith no `<machine>`, `read`, `write`, and `exec --cwd` route a path that cannot exist on this machine to the one joined machine it fits. `C:\\...` on a Mac goes to your Windows machine; if several fit, runpane lists them and asks for a name. Any other path is this machine's: `read` and `write` hand it to this machine's Pane, so it follows the same rules as a named machine (`~`, directories, new parent folders), and Pane must be running here.\n\nOther commands help in the same way. When a file such as a `--from-json` or `--prompt-file` path is missing and looks like another machine's path, the error names the likely machine and the exact `runpane workspace <machine> read` command. When a Session is not found here, runpane checks your other online machines and names the one that has it.\n\n## Workspaces and pairing codes\n\nWorkspaces and the [self-hosted remote daemon](SELF_HOSTED_REMOTE_DAEMON.md) both use Tailscale Serve, for different jobs:\n\n| | Workspaces | Pairing codes (remote daemon) |\n|---|---|---|\n| For | Your own machines (runpane CLI and desktop remote mode), or your tailnet when you choose | Browsers, phones, and devices on another Tailscale account |\n| Trust | Tailscale login, by visibility, plus an optional password | A `pane-remote://` code per device |\n| Serves | The desktop Pane's `~/.pane` | The remote daemon's data directory, usually `~/.pane_remote` |\n| Port | 8443 | 443 |\n| Setup | None | `runpane setup` on the host, then paste the code |\n\nMachine commands (`read`, `write`, `exec`) are served only to workspace identity, never to pairing tokens.\n\n## Troubleshooting\n\n- `runpane workspace list` shows each machine's state. \"Pane is not answering there\" means Pane is not running on that machine, its workspaces are off, or it runs a Pane version without workspaces.\n- `tailscale serve status` should list `https://<machine>.<tailnet>.ts.net:8443` proxying to `http://127.0.0.1:<port>`.\n- A Pane older than workspaces reports `Workspaces: off ... (this Pane is older than workspaces)`; update Pane.\n"},{"path":"docs/SELF_HOSTED_REMOTE_DAEMON.md","title":"Self-Hosted Remote Daemon","text":"# Self-Hosted Remote Daemon\n\nThis guide covers running Pane on your own workstation, Mac mini, Linux box, or VM and connecting to it from the local Pane desktop app or the Remote Pane browser app.\n\nThe intended flow is:\n\n1. Run one setup command on the remote machine.\n2. Copy the generated `pane-remote://...` connection code.\n3. Paste it into desktop Pane under `Settings > Remote Access`, or open `https://runpane.com/app/` and paste it there.\n\nPane saves the profile and attempts to connect immediately. Local desktop mode is unchanged until a remote profile is imported and activated.\n\n## Workspaces or pairing codes?\n\nTo let machines on your tailnet reach each other from the runpane CLI, you do not need this guide or a code. [Workspaces](RUNPANE_WORKSPACES.md) put each desktop Pane on your tailnet automatically (port 8443) and trust your Tailscale login, plus everyone on the tailnet when a machine's owner allows it, so `runpane workspace <machine> read|write|exec|<command>` works between your Macs, Windows PCs, and Linux machines, and the desktop app's remote mode connects to them from Settings → Remote Access → Your computers. Use the remote daemon and pairing codes in this guide for browsers and phones, and for devices outside your tailnet. The remote daemon keeps port 443, and the two run side by side.\n\n## Guided quick start\n\nOn the host machine, run `npx --yes runpane@latest` in an interactive terminal.\nChoose **Set up a remote host**, enter a name (or press Enter),\nand follow the Tailscale installation/login prompts. The wizard picks an available\nport and prints a connection code. Sign your other device into the same Tailscale\nnetwork, then paste the code into Pane or [runpane.com/app](https://runpane.com/app/).\n\nIf the host later switches to another tailnet, Pane notices while it runs: within\na minute it re-creates the `:443` Serve forward on the new tailnet, and new codes\nuse the new address. Codes created before the switch still point at the old\ntailnet, so create a new one. If Pane cannot confirm the forward, it shows the\nerror and the `tailscale serve` command to run instead of creating a code.\n\nSSH and manual URL setups remain available through the explicit commands below.\nThe no-argument and `setup` commands print help in non-interactive shells; they\ndo not start a login prompt.\n\n## Creating a code in desktop Settings\n\nDesktop setup uses an installed, signed-in Tailscale client. If Tailscale is\nmissing, Pane shows installation instructions; use the setup terminal to install\nit. Tailscale discovery, Serve configuration and requested daemon service setup\nrun asynchronously so the desktop stays responsive while they finish.\n\n## One-Command Setup\n\nTo skip the wizard menu, run interactive remote setup directly:\n\n```bash\nnpx --yes runpane@latest install daemon --interactive-tailscale-setup --auto-listen-port --label \"VM\"\npnpm dlx runpane@latest install daemon --interactive-tailscale-setup --auto-listen-port --label \"VM\"\npipx run runpane install daemon --interactive-tailscale-setup --auto-listen-port --label \"VM\"\nuvx runpane@latest install daemon --interactive-tailscale-setup --auto-listen-port --label \"VM\"\n```\n\nSSH tunnel mode:\n\n```bash\nnpx --yes runpane@latest install daemon --label \"VM\" --prefer-tunnel ssh\n```\n\nPersistent installs are also supported:\n\n```bash\nnpm i -g runpane\nrunpane install daemon --interactive-tailscale-setup --auto-listen-port --label \"VM\"\n\npython -m pip install runpane\nrunpane install daemon --interactive-tailscale-setup --auto-listen-port --label \"VM\"\n```\n\nFrom a source checkout:\n\n```bash\npnpm remote:setup -- --label \"VM\"\n```\n\nSSH tunnel mode:\n\n```bash\npnpm remote:setup -- --label \"VM\" --prefer-tunnel ssh\n```\n\nFor validation against a separate data directory:\n\n```bash\nPANE_DIR=/tmp/pane-remote-vm pnpm remote:setup -- --label \"VM\" --prefer-tunnel ssh --print-only\n```\n\nPackaged Pane builds can run the same setup path without opening a window:\n\n```bash\npane --remote-setup --label \"VM\"\n```\n\nOn displayless Linux hosts, prefer `runpane install daemon`; the npm and PyPI\nwrappers pass Electron's required headless flags automatically. For a direct\npackaged launch, pass them explicitly:\n\n```bash\npane --ozone-platform=headless --disable-gpu --remote-setup --label \"VM\"\n```\n\nThese must be command-line arguments: Electron picks its Ozone platform before\nthe app starts, so the environment variable `ELECTRON_OZONE_PLATFORM_HINT` no\nlonger works (removed in Electron 38, ignored from 39 on).\n\nThe wrappers are lightweight installers and configurators. They currently\ndownload the packaged Pane runtime because the daemon is not distributed as a\nseparate binary. AppImage installs therefore require FUSE; on Debian-based\nhosts, `--format deb` avoids the AppImage/FUSE requirement.\n\nIf Pane is already installed on the host, this is the most direct command:\n\n```bash\npane --remote-setup --label \"VM\" --prefer-tunnel tailscale\n```\n\nThe setup command:\n\n- detects Linux, macOS, or Windows host behavior\n- writes remote daemon config into one `PANE_DIR` (default `~/.pane_remote`)\n- enables the loopback listener on `127.0.0.1:42137`\n- creates a paired client record with a hashed token on the host\n- emits the raw token only inside the one-time `pane-remote://...` import code\n- attempts to install and start a user-level daemon service (on Linux, it also\n enables lingering so the service keeps running after you log out)\n- prints the manual daemon command if service setup is unavailable\n- detects Tailscale Serve where possible and otherwise prints an SSH local-forward command\n\nUseful options:\n\n```bash\npnpm remote:setup -- --help\npnpm remote:setup -- --channel nightly\npnpm remote:setup -- --pane-dir \"$HOME/.pane_remote\"\npnpm remote:setup -- --prefer-tunnel ssh\npnpm remote:setup -- --no-install-service\npnpm remote:setup -- --no-tailscale-serve\n```\n\n## Run Pane on a Cloud VM\n\nPane does not create or manage cloud machines. Create the VM yourself, then set\nit up as a Remote Pane host:\n\n1. Create an Ubuntu 24.04 VM with any provider. Size it for the agents and\n builds you plan to run. It only needs inbound SSH. The daemon listens on\n loopback, and you reach it through Tailscale or an SSH tunnel.\n2. SSH in as a regular user with `sudo`, not as `root`.\n3. Install what your agents need on the VM: `git`, Node.js 20 or newer (for\n `npx`), and the agent CLIs you use, such as Claude Code or Codex. Sign in\n to each CLI on the VM. Agents run there, not on your laptop.\n4. Install Tailscale and join your tailnet:\n\n ```bash\n curl -fsSL https://tailscale.com/install.sh | sh\n sudo tailscale up\n ```\n\n5. Install Pane and create a connection code. `--format deb` installs the\n Debian package, so the host does not need FUSE:\n\n ```bash\n npx --yes runpane@latest install daemon --label \"Cloud VM\" --format deb\n ```\n\n6. Check that the daemon keeps running after you log out. It runs as a systemd\n user service, and setup turns on lingering (through passwordless `sudo` if\n needed) so the service outlives your SSH session. If the setup output says\n it could not, enable lingering yourself:\n\n ```bash\n sudo loginctl enable-linger \"$USER\"\n systemctl --user status pane-remote-daemon.service\n ```\n\n7. Paste the printed `pane-remote://...` code into desktop Pane (see\n [Import Locally](#import-locally)) or into `https://runpane.com/app/`.\n\nTo start, stop, or delete the VM, use your provider's console or CLI. If\nsomething fails, run `npx --yes runpane@latest doctor --json` on the VM and\ncheck [Troubleshooting](#troubleshooting).\n\n## Import Locally\n\nOn your local desktop machine:\n\n1. Open Pane.\n2. Go to `Settings > Remote Access` and click `Add with a Code` under `Your computers`.\n3. Paste the full `pane-remote://...` code into `Connection Code`.\n4. Click `Connect`.\n\nIf the tunnel is not reachable yet, Pane still saves the host and shows the connection error. Start the printed SSH/Tailscale tunnel and click `Connect` next to the saved host.\n\n## Use the Mobile / Browser App\n\nThe same connection code works in the Remote Pane PWA:\n\n```text\nhttps://runpane.com/app/\n```\n\nUse the PWA for phone or tablet access to remote terminals, browser tabs and files:\n\n1. Set up the remote host with Tailscale or a trusted HTTPS tunnel.\n2. Copy the full `pane-remote://...` code printed by setup.\n3. Open `https://runpane.com/app/` on the client device.\n4. Paste the code and connect.\n\nFor iPhone or iPad, open the URL in Safari, tap Share, then tap `Add to Home Screen`.\nFor Android, open the URL in Chrome, open the browser menu, then tap `Add to Home screen` or `Install app`.\n\nSSH tunnel mode is mainly useful from desktop clients. For mobile browser access, prefer Tailscale or Manual HTTPS so the phone can reach the daemon URL directly.\n\n### Browser tabs on the phone\n\nAdd Tool → Browser opens a browser tab on the phone. A new tab lists the host's listening ports, and the Ports button in the address bar opens the same list. Tap a `web` port to open it; `tcp` ports read \"desktop only\". The address bar is editable and reads the host's own address, such as `localhost:5173/ on <host>`. Copy URL copies the address the phone actually loads, and Open in Safari opens it outside the app. Use Open in Safari for sites that refuse to load in a frame, such as github.com: the tab cannot tell when that happens.\n\nHow a host page reaches the phone:\n\n- When a phone first opens a `web` port, Pane gives that port its own HTTPS address on the host's tailnet name, `https://<host>.ts.net:<n>`, with a `tailscale serve` handler; the tab shows the page under a second later. Pane picks `n` from 44300 up, so it never takes the dev server's own port number. Handlers exist only for opened ports because Tailscale drops its whole Serve config, other apps' handlers included, once a machine holds a few hundred.\n- Each handler points at a loopback preview proxy that checks the visitor's Tailscale login, sends the request to `localhost:<port>` as the dev server expects, carries live-reload WebSockets, and drops the headers that stop a page from loading in a frame.\n- HTML files opened in a browser tab (for example by `runpane panels open --url file://...`) load from one more Serve address, with the files beside them. Files outside the page's folder are refused.\n- A handler goes when its port stops listening, when Pane quits, and at the next launch after a crash. Quit spends at most 5 seconds removing them; a launch removes any it left. Pane changes only handlers it created: their targets start with `/pane-<hash of the Pane data directory>/`, so a dev build and the installed app never remove each other's.\n\nWho can open these pages: the devices that **Who can connect** admits (see [Who is trusted](RUNPANE_WORKSPACES.md#who-is-trusted)). Phone pages need that setting on, Tailscale running on the host, and the phone on the same tailnet. A page in a frame cannot send the machine's password, so phone pages are off while password protection is on. A refused device sees \"This page runs on <host>. Open it from a device signed in as <login>.\" A pairing code or SSH tunnel does not grant access to these pages: they need the phone on this tailnet, signed in with a login that setting admits. Navigation inside a page stays on the phone; the tab saves only the addresses you enter or pick.\n\n### Files on the phone\n\nEach pane's Explorer tab shows its worktree as a tree. Tap a text file to read and edit it; Save writes it to the host. Text files over 1 MB, and binary files that are not images, PDFs, video or audio, stay on the desktop. Images, PDFs, video and audio of any size preview on the phone, and video seeks without downloading the whole file. Open in Safari opens a preview outside the app.\n\nBrowsing and editing go through the PWA's own connection, so they work over every connection the PWA supports. Previews load from the same Serve address as HTML files in browser tabs, so they need what [browser tabs on the phone](#browser-tabs-on-the-phone) need: Tailscale on the host, password protection off, and a phone signed in to a login that **Who can connect** admits. Without that, a preview explains why it cannot load. Files outside the worktree are refused.\n\n### Remote PWA Implementation Notes\n\nThe Remote Pane PWA is a browser runtime. It does not have `window.electronAPI`, so client-side PWA preferences must use browser-safe storage such as `localStorage` or explicit daemon adapter calls. Do not reuse desktop renderer preference stores that persist through Electron IPC unless the call path is guarded for browser mode.\n\n## Forwarded ports\n\nA desktop connected to a remote host reaches every port listening on the host as if it were local. Pane lists the host's ports on a new, empty browser tab and under the address bar's **Ports** button, and the desktop opens a matching listener on its own `127.0.0.1` and `::1` for each one. Only the desktop app sets up these listeners; the browser app and phones don't yet.\n\n- A browser tab at `localhost:5173` loads the host's dev server, live reload included. The address bar reads `localhost:5173` with **on <host name>**.\n- When something on the desktop already answers on 5173, the desktop uses the next free port, or any free port when the next 19 are taken too. The tab still reads `localhost:5173`, and the Ports list shows the local number as **here :5174**.\n- A tab never loads the desktop's own service in the host's place. While the host's Ports list is loading it waits, and when a port stops listening on the host it says the port isn't reachable, then loads once the port is back.\n- **Copy URL** copies the address that works on the desktop, for its system browser.\n- Clicking a `localhost` link in a terminal opens the host's server in your system browser, using the local port number if it moved. A link to a port that isn't forwarded right now doesn't open, so it never reaches the desktop's own service on that port.\n- When a port closes on the host, the desktop closes its listener. Disconnecting closes all of them.\n\nEach connection the desktop accepts travels over the Pane connection as a WebSocket to `/ports/<port>` on the host, which connects to `localhost:<port>` there. The host accepts it only with the same sign-in as every other request, and only for a port in its current list. Listeners stay on loopback on both machines.\n\nEvery client signed in to the host can reach these ports through that route: anyone holding a pairing code, and any Tailscale login that the Workspaces \"Who can connect\" setting allows. They reach every listening service, including databases that trust `localhost` and debugging ports such as a Node `--inspect` port, which runs code as you. Pane's own ports are forwarded too, and its remote API keeps its own sign-in. Forwarding can't be limited to some ports. If that is more access than you want, turn off the remote daemon listener in `Settings > Remote Access` and set \"Who can connect\" to Off.\n\n## Security Model\n\n- The daemon listener only supports loopback hosts: `127.0.0.1`, `::1`, or `localhost`.\n- Direct public or LAN binding is intentionally rejected.\n- Use SSH local forwarding, Tailscale Serve, or a trusted HTTPS reverse proxy that forwards to loopback.\n- Treat the generated `pane-remote://...` code like a secret. It contains the bearer token needed by the local client.\n- Any signed-in client can open a TCP connection to any port listening on the host. See [Forwarded ports](#forwarded-ports).\n\nTailscale Serve example generated by setup:\n\n```bash\ntailscale serve --bg --tls-terminated-tcp=443 42137\n```\n\nSSH fallback generated by setup:\n\n```bash\nssh -N -L 42137:127.0.0.1:42137 user@your-host\n```\n\n## Manual Advanced Flow\n\nThe old manual flow still works and is useful for debugging.\n\n### 1. Choose the Host Data Directory\n\n```bash\nexport PANE_DIR=\"$HOME/.pane_remote\"\nmkdir -p \"$PANE_DIR\"\n```\n\nPane stores config and database files under that directory. The setup command, desktop app, and headless daemon must use the same `PANE_DIR`.\n\n### 2. Configure the Remote Listener\n\nLaunch Pane on the host against that directory:\n\n```bash\nPANE_DIR=\"$HOME/.pane_remote\" pnpm dev\n```\n\nIn `Settings > Remote Access > Advanced Remote Host`:\n\n1. Enable `Enable remote daemon listener`.\n2. Keep `Listen Host` on `127.0.0.1`.\n3. Keep or change `Listen Port`, default `42137`.\n4. Leave `Require pairing / saved bearer tokens` enabled.\n5. Leave `Allow direct HTTP on loopback` enabled.\n6. Click `Apply Host Settings`.\n\n### 3. Create a Paired Connection\n\nFrom `Pairing tools` on the same page:\n\n1. Enter a label such as `Office Mac mini`.\n2. Enter the base URL the client will use after tunneling, for example `http://127.0.0.1:42137`.\n3. Click `Create Paired Profile`.\n\nPane adds a host-side allowed client record, adds a matching local profile, and shows the generated bearer token once.\n\n### 4. Start the Headless Daemon\n\n```bash\nPANE_DIR=\"$HOME/.pane_remote\" pnpm daemon:headless\n```\n\nOn success:\n\n```text\n[Pane daemon] Headless host ready on unix:<socket path>\n```\n\nOn Windows the endpoint is `pipe:<pipe name>`. This is the local control endpoint, not the remote listener port.\n\n### 5. Connect the Desktop Client\n\nUse `Add connection` for a generated code, or use `Save existing remote profile` under `Pairing tools` with:\n\n- label\n- base URL\n- bearer token\n\nClick `Save Remote Profile`, then click `Connect` on the saved profile.\n\n## Validation\n\nRecommended checks after connecting:\n\n1. Verify projects and sessions load in the client.\n2. Open a terminal-backed session and confirm output streaming works.\n3. Send terminal input and verify the remote runtime receives it.\n4. Resize a terminal and confirm the remote terminal resizes.\n5. Open a file and confirm read/write works.\n6. Check git status and commit/diff flows.\n7. Run an approve-mode command and confirm the permission dialog appears on the client.\n\n## Troubleshooting\n\nStart with machine-readable environment diagnostics:\n\n```bash\nrunpane doctor --json\n```\n\nThe `remoteSetup` section reports stable diagnostic codes and recovery commands\nfor missing AppImage/FUSE support, Electron sandbox restrictions, and missing\nuser service management. `runpane install daemon` automatically supplies the\nLinux headless launch environment. Pane never disables the Electron sandbox\nautomatically; use `--no-sandbox` only when the host requires it and you accept\nthe security tradeoff.\n\n### Remote setup exits because X11 or `$DISPLAY` is missing\n\nUse `runpane install daemon`, or add `--ozone-platform=headless --disable-gpu`\nto a direct packaged launch. This failure occurs before Tailscale setup and does\nnot mean the daemon requires a graphical session. If you previously relied on\n`ELECTRON_OZONE_PLATFORM_HINT=headless`, switch to the flags: Electron removed\nthat variable in 38 and ignores it from 39 on.\n\n### The headless daemon starts but remote connect fails\n\nCheck:\n\n- the daemon uses the same `PANE_DIR` that setup wrote\n- the tunnel/proxy forwards to the same loopback port as the host config\n- the client profile base URL matches the client-side tunnel endpoint\n- the `pane-remote://...` code was not truncated\n\n### A remote action could not be confirmed\n\nThe browser client automatically retries a reviewed set of read-only requests after\ntransient network or server failures. It sends actions such as terminal input and\npane creation once: the host may finish an action even if the response is lost.\nIf Pane says the action may have completed, inspect the current panes or terminal\nbefore submitting it again. Authentication failures stop immediately.\n\nThe browser normally uses native EventSource for output. Its fetch fallback and\nthe desktop transport share one SSE text parser, accepting LF, CRLF, and CR line\nendings across chunks. Each transport decodes UTF-8 incrementally before parsing;\nreconnecting discards the previous stream's incomplete event. Terminal output\nresponses are applied only to the terminal instance that requested them, so a\nlate response cannot clear or write to a terminal disposed during a tab switch.\n\n### I changed host settings but nothing happened\n\nThe headless daemon watches config and starts or stops the remote transport based on saved host config. If behavior looks stale, restart the daemon once and verify the correct `PANE_DIR`.\n\n### Why can’t I bind to `0.0.0.0` or a LAN IP?\n\nThat is intentionally blocked. The current security model is loopback plus a secure exposure layer.\n\n### Why are some actions disabled in remote mode?\n\nSome actions operate on the local desktop client machine rather than the remote workspace. Pane currently disables or keeps local-only behavior for:\n\n- opening a local IDE from the client\n- revealing files in the client OS file manager\n- the native clipboard-image fallback path\n\n### Where does copied terminal text go?\n\nTo the clipboard of the machine you are sitting at. In remote mode, dragging to select text in a terminal copies it right away, with no Copy popover. Programs that copy with OSC 52, such as Claude Code, tmux, and vim, also copy to your machine. Programs in the terminal cannot read your clipboard through OSC 52.\n\n## Current Limitations\n\n- No hosted relay, NAT traversal, or account-based multi-tenant auth\n- No direct non-loopback listener support\n- No full live remote end-to-end CI harness yet\n# Mobile push notifications\n\nThe native companion may register an APNs/FCM token through its existing paired bearer token. Registrations are scoped to that paired client and revalidated before every send. The daemon sends attention alerts for `blocked` transitions and for settled `working → idle` transitions of turns a person started; controls can disable either category per device. See [Native mobile](NATIVE_MOBILE.md) for the operator-only credentials and signing setup.\n"},{"path":"docs/SESSIONS.md","title":"Sessions","text":"# Sessions\n\nSessions are named, ongoing Pane Chats where work intent lives. Each Session\nopens one chat, and its sidebar row can expand to show associated Panes. The\noptional overview keeps the Session name, associated Panes, and recent\nactivity together while the chat remains the focused work surface. A tab\nshares its parent Pane's worktree.\n\nIn the sidebar, `+` opens an agent picker, an optional Session name field, and\nan optional \"What is this Session for?\" field that becomes the Session's goal.\nPane remembers the chosen agent as the default for future Sessions while\nexisting Sessions keep their own agent. A blank name starts as `New chat` or\n`New chat 2` and is renamed once from the first message you send the Session\nagent; a name you type or set later is kept. Each new Session opens as one Pane\nChat and can be renamed later from the optional read-only overview. Expand a Session to see\nits associated Panes and open any Pane in its existing sidebar view.\n\nThe Sessions section below the divider can be collapsed from its header; the\n`+` remains available while it is collapsed. Sessions are opened from this\nsection rather than from a duplicate top navigation shortcut.\n\nA Session is a persistent coordination conversation with its own scratch\nworkspace. Its default profile supports discussion, investigation, decisions,\nand delegation when authorized work benefits from it. No particular skill,\nticket system, model, or Agent Farm installation is required. Users can select\nskills and workflows without Pane imposing an implementation pipeline.\n\n## Upgrading from Pane Chat\n\nExisting Pane Chat opens under Sessions automatically, keeping its name and all\nagent conversations together. Switching agents resumes that agent's existing\npanel and history in the same stable private Session workspace. Opening the\nSession does not submit a prompt or authorize new work.\n\nEarlier development versions split old agent histories into supplemental\nSessions. Untouched imports with only generated workspace files and no competing\nconversation are reunited with the original Session. Imports with files, settings changes, or\nadditional conversations remain separate to preserve that work. Those retained\nSessions receive independent internal owners while keeping their names, folders,\npanel IDs, saved histories, and settings. Interrupted ownership moves resume on\nstartup; no transcript or workspace files are deleted by this migration.\n\n## Launch configuration and profile\n\nSessions use the same built-in launch presets and saved custom commands as\nordinary project/worktree panels, including custom arguments. The application\ndefault seeds new Sessions. Creating a Session remembers its launch command and\narguments for the next creation; cancelling leaves that default unchanged.\nSelecting the agent default clears the remembered custom command. Each Session saves its own launch configuration\nand behavior profile. Changing defaults does not overwrite existing Sessions.\nLaunch changes take effect on the next explicit restart.\nUse **Edit behavior…** during creation or in Session settings to open the\nseparate profile editor. **Back** discards the draft; **Save behavior** applies it\nto the form, which is persisted when you create the Session or save settings.\n\nThe command, behavior profile, and saved task context have separate purposes.\nA command selects how the agent runs; a profile describes how it should behave;\nsaved goals, decisions, and next actions help it respond to later user input.\nCustom commands (including Agent Farm wrappers) retain their arguments. Pane\nonly adds native flags through compatible built-in adapters, an explicit custom\ncommand resume config, or Agent Farm's resume contract (see\n[Resuming custom launchers](#resuming-custom-launchers)). Wrappers keep control of\ninstruction discovery; apart from that one Agent Farm exception, Pane does not\ninfer flags from a wrapper name. A user command that explicitly starts work\nremains the user's choice.\n\nEach Session owns a stable folder at `<PANE_DIR>/sessions/<session-id>/` for\nnotes, plans, attachments, generated instructions, and artifacts. Renaming,\nreopening, and archiving retain this folder. These folders are not Git\nworktrees or security sandboxes and do not expire with OS temporary files.\nProject implementation belongs in an appropriate Pane/worktree.\n\n## Windows and WSL runtimes\n\nOn Windows, **Create Session → Run agent in** appears directly below the agent\npicker when WSL reports installed distributions. Windows is the default on every\ncreation. Choose a named distribution to run the agent there; install the agent\ninside that distribution first. Cursor is available when WSL is selected.\nThe runtime and distribution belong to the Session and survive reopening,\napplication restart, and terminal supervisor restart. Only WSL Sessions carry a\n`runtime` (`\"wsl\"`); a Session on the native host omits it on every OS. Older\nrecords that stored `\"windows\"` for the native host lose the field when read.\nIf creation fails before publishing a terminal owner, Pane removes untouched\ngenerated files so the name can be retried; user files or edits prevent rollback.\n\nWSL Sessions retain their durable files under the Windows Pane data directory.\nTheir agent sees Linux paths such as `/mnt/c/Users/.../.pane/sessions/...`.\nThis uses Pane's existing Windows-to-WSL path conversion and requires the default\n`/mnt/<drive>` automount layout; custom `[automount] root` settings are not supported.\nGenerated `AGENTS.md` points to a Session-specific `runtime-context.md`; this\ncontext takes precedence over the shared Pane Chat routing instructions.\n\nEach WSL Session gets a managed `.pane-runtime/runpane` wrapper on PATH, also\navailable as `$PANE_RUNPANE_BIN`. It invokes this build's bundled Windows CLI via\nPowerShell interop from Windows TEMP, so the CLI can reach this Pane instance's\nnamed pipe. Arguments cross as JSON to preserve quotes and Unicode. Temporary\nfiles beside the bridge carry payloads without Windows command-line size limits\nand are removed when the call exits; stdin remains available to the CLI. File flags\naccept relative or absolute Linux paths (including `--flag=value`); `-` retains\nstdin semantics. `--repo` translates absolute Linux paths and explicit relative\npaths (`.`, `..`, `./...`, `../...`) while preserving repository names and IDs.\nPaths inside JSON request bodies must use Windows paths, and\nWindows paths returned by the CLI can be read in WSL after `wslpath -u` conversion.\nThe bridge pins `--pane-dir` to this instance and preserves the calling Session\nand panel identities. PowerShell is invoked by its mounted Windows path, so\n`appendWindowsPath=false` is supported. WSL interop must be enabled; a Linux-installed RunPane CLI\ncannot replace this bridge for a Windows daemon.\n\nFor CLI JSON creation, `runtime: \"wsl\"` and `wslDistribution: \"Ubuntu-24.04\"`\nselect an installed distribution; omitted runtime selects the native host.\n`runtime: \"windows\"` is still accepted as a legacy name for the native host and is\nnot stored.\n\n## Resuming custom launchers\n\nUse **Enable custom command resume** in Session launch settings or a saved custom\ncommand's editor. This is an explicit contract, independent of executable names.\nAgent Farm is the one exception, described in [Agent Farm](#agent-farm) below.\nChoose a session ID source and provide first-launch and resume templates:\n\n- **Claude:** Pane allocates a UUID and resumes it only when its transcript exists.\n- **Codex / Cursor:** Pane captures the native resume ID from terminal output.\n- **Pane allocates an ID:** for other CLIs that accept a caller-provided ID.\n- **CLI reports its ID:** the CLI or wrapper prints a standalone newline-terminated\n `PANE_AGENT_SESSION_ID=your-session-id` line. IDs may contain letters, digits,\n periods, underscores, colons, and hyphens (up to 256 characters).\n\n`{command}` expands to the saved command; `{sessionId}` expands to the quoted ID.\nLeave the ID placeholder unquoted. For a launcher that forwards native arguments\nthrough `--`, Claude templates could be `{command} -- --session-id {sessionId}`\nand `{command} -- --resume {sessionId}`. Codex could use `{command}` initially\nand `{command} -- resume {sessionId}` on resume. These are user configuration\nexamples, not automatic wrapper detection. Other CLIs can use other syntax.\n\nThe configuration and ID persist with the panel. Without a captured ID, Pane\nuses the first-launch template instead of guessing a latest conversation. Saved\ncustom commands copy their resume settings into new panels; changing the saved\nshortcut does not change existing panels. Session settings support changing or\ndisabling the contract for that Session's next launch. No prompt is submitted just\nto resume. Old conversations whose IDs were never saved cannot be recovered\nautomatically; use an explicit resume command if you know their ID.\n\n### Agent Farm\n\nAgent Farm is the one launcher Pane recognizes by name, because Agent Farm\npublishes this contract for Pane: when `PANE_PANEL_ID` is set it prints\n`PANE_AGENT_SESSION_ID=<launch id>` once, before the agent starts, and documents\n`{command} --resume <id>` as the way to reopen that conversation. Pane keeps the\nfirst ID line of each launch and ignores later ones, which can only be agent\noutput. Every other wrapper still needs an explicit config.\n\nA command counts when it is a single `agent-farm run <profile>` or\n`agent-farm agent <profile>`, optionally preceded by `VAR=value` assignments,\nwith no shell operators, no `--`, and no `--resume` of its own. `npx`, `pnpm dlx`\nand aliases do not count. Such a panel with no explicit config behaves as\n**CLI reports its ID** with templates `{command}` and\n`{command} --resume {sessionId}`: Pane saves the printed ID and relaunches with\nit. The editor says so under the unticked checkbox. An explicit config always\nwins: tick **Enable custom command resume** and set your own templates.\n\nA panel created before this rule usually has no saved ID. On its next relaunch Pane\nmakes one attempt to read the last `PANE_AGENT_SESSION_ID=` line from the\npanel's saved terminal buffer. Agent Farm older than 0.2.20 prints no ID, so\nthose panels relaunch fresh, as before.\n\nTo reopen an older conversation by hand, run\n`agent-farm run <profile> --resume <id>`. The ID is the launch ID (the\n`PANE_AGENT_SESSION_ID` value), shown as `resumeId` in\n`runpane panels list --json`. Agent Farm keeps launch records in\n`~/.local/state/agent-farm/launches/` for 90 days.\n\n## Session sidebar and archive\n\nPinned is the first sidebar category and can contain both Session chats and\nPanes. Right-click a Session, or use the ⋯ button that appears on hover, to pin or unpin it, or turn on **Start pinned** in\nCreate Session to pin it from the start. The dialog remembers that toggle for\nthe next Session (on the desktop in Pane's config, in the remote web app on\nthat device). The pin preference survives restarts; an archived Session stays\nout of Pinned until it is restored. RunPane can set the same preference with\n`sessions pin` or `sessions unpin --session <id|name>`. Create JSON accepts\n`isPinned` (default false), and update JSON accepts it without changing the\npreference when omitted.\n\nDrag a Session up or down in Pinned, in Sessions, or in the compact rail to\nchange its place; a line shows where it will land. With a Session row focused,\n**Alt+↑** and **Alt+↓** move it one place and a screen reader hears its new\nposition. A drag or keypress reorders only the list it starts in: Pinned\nSessions move among themselves, and a Session dragged across the Pinned line\nstays where it was. The order is saved on the host, so every desktop, the phone\nand `runpane sessions list` show it, and it survives restarts. New Sessions\nstart at the bottom. A host that has never saved an order lists Sessions oldest\nfirst. A host running an older Pane cannot save an order: the list snaps back\nand the error says to update Pane on that host. Dragging a Session onto the\nworkspace still tiles it.\n\nClick a Session row to open its chat, or use **Switch to Session: <name>** in the\ncommand palette (`⌘⇧P` / `Ctrl+Shift+P`), which also has **New Session**. A Session with associated Panes shows a\nchevron that expands or collapses them; a Session without Panes shows none.\nEvery Session starts collapsed, including a new one and one that just got its\nfirst Pane, until you press its chevron. Opening the Session's chat, an agent\nadding Panes, or `runpane` focusing one of its Panes leaves it collapsed. Each\ndevice remembers the Sessions you opened, separately for each host: the\ndesktop in its own settings, the remote web app (PWA) in browser storage\n(`pane.remotePwa.expandedSessions@<host id>`). Opening a Session under Pinned\nopens it under Sessions too. The native iOS and Android app does not follow\nthis yet: its Sessions start expanded and it forgets on restart.\nChild Pane rows are indented beyond the Session chat icon and keep the ordinary\nPane actions. Create Pane, opened while a Session is showing, has an\n**Add to <Session>** checkbox, on by default, that files the new Pane under that\nSession. A Pane that belongs to a Session names it in the window title bar as\n\"<Session> › <Pane>\"; click the Session name to go back to it. To file an\nexisting Pane under a Session, right-click the Pane, choose\n**Add to Session**, then pick the Session. **Remove from <Session>** in the same\nmenu takes it out again; the Pane and its worktree are unchanged. Pinned Pane\nrows show the Pane name with its repository muted beside it. Single-line Pane rows omit change counts and PR numbers; the\noptional two-row layout shows those details below the title. Session and Pane\ncontext menus use compact widths.\n\nThe experimental **Attention inbox** (Settings > Advanced > Experimental) turns\nthe Projects list into an Inbox of the Panes that need you: an agent waiting on\ninput or approval, an agent that completed a real turn since Pane started (it\nworked on a prompt and went idle; a freshly started agent does not count), or a\nPane that errored. Session child rows follow the same filter, and a Session row still\ncounts all of its Panes. A row leaves when its agent works again; opening it\ndoes not remove it. Hover a row and choose **Dismiss** (check mark) to hide it\nuntil its agent works again. The row under the Inbox counts the hidden Panes\n(\"3 running · 1 idle\") and **Show all** lists every Pane. Pinned rows are\nunchanged, and ⌘1–9 number the Inbox rows while it is shown.\n\nThe right sidebar has Overview, Files, and Changes tabs. Overview shows the\nSession's blockers, goal, next step, and decisions when they are set, then its\nPanes and recent activity. Agent state changes stay out of Activity; the status\ndot shows them. Files browses the Session workspace. Changes summarizes the\nSession's Panes and opens a Pane for detailed review. The upper-right sidebar\nbutton shows or hides the selected tab. A single Session uses two rows at the\ntop: the window title bar shows its name and controls, and the tab strip sits\ndirectly below it. Session file clicks open editor tabs beside the agent tab in\nthat strip. These tabs can be selected and closed directly, and reopening a file\nrestores its tab.\n\n\n\n\n\nRight-click a Session, or use its ⋯ button, to archive it. An \"Archived · Undo\"\ntoast offers to restore it for a few seconds, and archiving the open Session\nreturns the desktop view to Home. Archiving hides the chat from the active\nSessions list while retaining its identity, conversation history, and Pane\nassociations. Its Panes and worktrees remain accessible through repository\nnavigation. Restore the Session from the sidebar’s Archived section. This is\nseparate from archiving a project Pane and does not delete delegated work.\n\nThe existing Session update contract accepts `archived: true` or\n`archived: false`. List results retain archived records, in the saved order;\nactive navigation filters them. An archived Session keeps its place, so Undo or\nRestore puts it back where it was.\n\n`orchestration-sessions:reorder` takes `{ sessionIds }`. The listed Sessions\ntrade the places they already hold, so a list that shows only some Sessions\n(Pinned, or the active ones while others are archived) rearranges just those;\nunknown ids are ignored. It writes an optional `order` id list beside the\nSessions in `orchestration-sessions.json` (the store stays at version 1) and\nchanges no Session's revision, `updatedAt` or activity. An older Pane reading\nthe file ignores `order`, lists Sessions oldest first, and drops it the next\ntime it writes. Archiving the selected Session chooses an available active\nSession or clears the selection if none remain. Restoring a Session does not\nreplace an unrelated selection.\n\n### Move a Session to another machine\n\nRight-click a Session, or use its ⋯ button, and choose **Move to host…** to\ncontinue it on another machine. The dialog lists your Tailscale machines\nrunning Pane, except the one the Session is on, and your teammates' machines\nrunning Pane, shown with the owner's login after the name. A teammate's machine\nis a target only when its owner set Who can connect to Everyone on tailnet.\nMachines Pane can't reach, or that accept only their owner, are shown disabled\nwith the reason. If none can be reached, it says so:\nopen Pane on the other machine, or sign in to Tailscale. While you view a host\nconnected with a pairing code rather than over Tailscale, the item is disabled,\nbecause Pane can't tell which machine that is. Choosing a machine\nsends the Session's agent a prompt to hand the Session off with\n[`runpane handoff`](RUNPANE_WORKSPACES.md#hand-a-task-to-another-machine) and\nthe `handoff` skill.\n\nThe agent writes a handoff note, and handoff creates a Session on that machine\nwith the same name (numbered \"<name> 2\" if the name is taken), goal and agent.\nIts next action is to read the note, which handoff saved in that machine's\n`~/.pane/handoffs/`. When the new Session uses the same agent, the conversation\nmoves too: its agent session's files are copied to that machine, and the new\nSession resumes that conversation when you first open it. Check that it shows\nthe last message, and if that is unclear, ask it what it was last working on.\nWith a different agent, or `--note-only`, only the note moves and the new\nSession starts fresh from it. Like every Session, it waits for your first\nmessage: switch to that machine, open the Session, and send \"continue\". The\nSession folder and its Panes stay where they are, and the original Session is\nnot archived; archive it from the same menu when you are done with it.\n\n## Bring your existing sessions into Pane\n\nSay \"migrate to Pane\" in any Session, or \"yes\" to the first-run offer above, and\nthe `migrate-to-pane` skill brings the conversations you already have in other\nagent tools (Claude Code, Codex, OpenCode, the Cursor agent, Conductor, or\nanything similar) into Pane tabs:\n\n- It reads session metadata on this machine, including WSL distros and agents\n running in other terminals or editors, and shows one list grouped by\n repository. Running sessions and those active in the last 7 days are\n preselected; older ones can be added. Nothing is created until you confirm.\n- An idle session's checkout is adopted in place (`runpane panes adopt\n --resume`); several sessions in one checkout become tabs in one Pane. A\n session still running elsewhere is forked with its own tool into a new Pane\n on a new branch, with your uncommitted and untracked files copied, and the\n original keeps running untouched.\n- Each tab is resumed by its agent's own resume-by-id and must pass the\n continuity check in the skill's `references/continuity.md` (same native\n session, a restart comes back to it, history loaded, original untouched)\n before it is reported as continued. A session that fails is reported as not\n continued with the reason and next step, never replaced by a fresh agent.\n- It keeps a record in `<PANE_DIR>/migrate-to-pane/record.json`, so running it\n again lists what is already in Pane and creates nothing twice. Archiving an\n adopted Pane keeps your checkout.\n\n## Session startup\n\nCreating, opening, restoring, restarting, or switching the agent in a Session\ndoes not submit a bootstrap message. The terminal waits for user input with\nits role, profile, and Pane capability instructions available. There is no\nautomatic greeting, diagnostic sweep, watcher setup, or continuation of a\nsaved next action. Reopening an already-running Session reconnects without\ninterrupting work the user has already authorized.\n\nThere is one exception, and it happens at most once per Pane data directory.\nThe first time the built-in Pane Chat Session launches while Pane has no Panes\nand no repositories (the sample repository the onboarding dialog clones doesn't\ncount), Pane sends it one prompt, which says that Pane sent it and why. The\nagent then asks whether to bring your existing agent sessions into Pane, and\nstarts only when you say yes. Pane records the offer in the\n`pane_chat_migrate_offer_sent` user preference once a launch has sent it, so a\nrestart or an agent switch never sends it again. See\n[Bring your existing sessions into Pane](#bring-your-existing-sessions-into-pane).\n\nPane may prepare instruction files and terminal infrastructure at startup;\nthat preparation does not require an agent turn. Unattended work and watchers\nare separate, explicitly authorized activities.\n\n## Shared tools and authority\n\nSession orchestrators and regular agent panels share RunPane's coordination\ncapabilities. Use `runpane doctor --json` and `runpane agent-context --json`\nwhen the task requires those capabilities, and discover individual schemas\nwith `runpane agent-context --command \"<command>\" --json`.\n\nAn associated worker completes its assignment and reports results or blockers\nto its owning Session. Reporting is participation. Creating workers, assigning\nwork, redirecting agents, or changing ownership requires a user request or an\nexplicitly delegated task with that authority. Existing authorization persists\nwithin its scope. An independent Pane works locally unless coordination is\nrequested. Tool availability does not authorize unrelated work or automatic\nsharing of other conversations.\n\n## One Pane per feature\n\n**1 feature = 1 worktree = 1 branch = 1 Pane; reviews, audits, fixes and QA for it are tabs in that Pane.**\n\nA feature here means one branch and its PR. A new Pane creates a worktree that\nchecks out the repository, installs dependencies and often builds, so it is\nexpensive, and creating several at once can freeze the machine. An\norchestrator creates a Pane with `runpane panes create` only when the work's\nbranch has no Pane yet: new work, or an existing PR with no Pane.\n\nPane enforces both: `panes create --branch` refuses a branch another Pane has\nchecked out (code `ERR_RUNPANE_BRANCH_HAS_PANE`, naming that Pane), and at most\n2 Panes run setup at once while the rest wait in line.\n\nEvery later agent for that feature runs inside its Pane as a new agent tab:\nimplementer, reviewer, follow-up reviewer, auditor, fix implementer and QA. Each tab is a\nfresh agent with its own context, sharing the Pane's worktree and branch:\n\n```text\nrunpane panels create --pane <feature-pane-id> --tool-command \"<agent command>\" --source agent --no-focus --wait-ready --yes --json\n```\n\nBefore `runpane panes create`, find the work's Pane with\n`runpane panes list --repo <repo> --json`, matching the PR's head branch\n(`gh pr view <n> --json headRefName`) against\n`git -C <worktreePath> branch --show-current`, and use it when it exists.\n\nTabs share one checkout. Only the implementer changes the branch or HEAD, one\nwriter at a time, and a fix tab starts only after the previous writer has\nstopped. Review, audit and QA tabs never run `git checkout`, `reset` or\n`stash`. Never create a Pane to review, audit, fix or QA work that already has\none. Keep the\nfeature's Pane until its PR merges or closes, so later tabs still have the\nworktree; archive it then.\n\n## Pane association before delegation\n\nManagement is a Pane-level relationship; tabs inherit the relationship and\nshare the Pane's worktree. Panes the orchestrator creates or adopts with\n`runpane panes create` or `panes adopt` are associated with its Session in the\nsame call: the CLI reads `PANE_ORCHESTRATION_SESSION_ID`, each result item\nreports `association: { sessionId, ok, error? }`, and `--no-associate` opts\nout. A failed association never undoes the new Pane. Before delegating to an\nexisting Pane, or when automatic association failed, the orchestrator\nassociates it explicitly. The supported command is:\n\n```text\nrunpane sessions associate --session <id|name> --pane <pane-id> [--json] [--pane-dir <path>]\n```\n\nVerify the result with `runpane sessions overview` and reuse an existing\nassociation. A Pane already managed by another Session is a conflict: do not\ndetach, reassign, or create a duplicate Pane. When a prompt is passed to `panes\ncreate`, check the item's `association.ok` right away. Keep a Pane attached through idle and completion; do not detach on\ncompletion. Archiving preserves the association.\n\nTo close out finished work, `runpane panes archive --session <id|name> --merged\n[--remove-worktree] [--dry-run] --yes --json` archives each associated Pane\nwhen both hold:\n\n- **Its PR is merged.** Pane looks the PR up by the Pane's branch on GitHub\n (`pr`). An open PR is skipped as `pr-open`, because its review, fix and QA\n tabs still need the worktree. No PR or a PR closed without merging is\n skipped as `pr-not-merged`, so a worker that has pushed but not opened its\n PR keeps its Pane. A PR state GitHub can't confirm is skipped as\n `pr-status-unknown`.\n- **Its worktree holds no unsaved work.** It has no uncommitted or untracked\n changes, and every commit is on its upstream after a fresh `git fetch`, or,\n without an upstream, on a remote-tracking branch or the base branch. A\n failed fetch or unreadable worktree is skipped as `status-unknown`. This\n check runs after the PR lookup, right before the worktree is removed.\n\nEvery skipped Pane has a `skipped.code` and a message with the next step.\n`--remove-worktree` also removes adopted worktrees; local branches are always\nkept. Run it with `--dry-run` first.\n\nBefore mutating, use `runpane agent-context --command 'sessions associate'\n--json` to confirm the wrapper supports the command. If an older global CLI\ndoes not, select and verify the app-compatible dev wrapper from the Pane\nruntime context before proceeding. Never silently continue without the\nassociation or substitute an unverified global/`npx` wrapper.\n\nSessions do not create worktrees and do not edit project implementation files.\nAn association identifies work that a Session coordinates; it does not grant\nimplementation authority by itself. Detach a Pane before assigning it to a\ndifferent Session.\n\n## Worker file access\n\nA worker's agent may be allowed to read only its working directories (Claude\nCode's `permissions.blockReadsOutsideWorkingDirectories`). Its brief and the\nSession files the brief points to must not sit outside them, or the worker\nstops on a permission prompt nobody is watching.\n\n**Briefs and attachments.** `--as-file-pointer` (`panes create`,\n`panels create`, `panels submit`, `agents send`) writes the prompt inside the\nPane's worktree, at `<worktree>/.pane/prompts/<timestamp>.md`, and sends the\none line `Read and follow .pane/prompts/<timestamp>.md`. The path is relative\nto the worktree, where the agent starts: given an absolute path, Claude Code\noften reads it with `cd <worktree>; cat <file>`, which its read check can't\nverify, so it asks. The result's `promptFile` is always absolute. Pane adds `/.pane/prompts/` to the\nrepository's `.git/info/exclude` (never a committed `.gitignore`), so the file\ncan't be committed. `--attach <file>` (repeatable; `panes create`,\n`panels create` and `panels submit`, with `--as-file-pointer`) copies each file\ninto `<worktree>/.pane/prompts/<timestamp>/` and sends\n`Read and follow <path> (attached: <copy>, ...)`, also worktree-relative; the\nresult lists the copies, absolute, in `attachments`. Attach mockups, plans or dispatch notes the worker needs\ninstead of pointing at the Session folder. Without a local git worktree (a\nfolder that is not the top of a checkout, a WSL repo) the file goes to\n`<pane-dir>/prompts/<pane-id>/` as before, and the pointer line names it by\nabsolute path.\n\n**The Session folder.** A Pane associated with a Session gets that Session's\nfolder (`<pane-dir>/sessions/<id>`) as `PANE_SESSION_DIR` in every tab created\nafter the association. The Session's own terminals get it too.\n`PANE_ORCHESTRATION_SESSION_ID` keeps its meaning: it is set only in the\nSession's own terminals, never in a worker's. Workers read the folder and never\nwrite it; the orchestrator is its only writer. What each built-in template gets:\n\n| Agent | Read access to the Session folder |\n| --- | --- |\n| Claude Code (`claude --dangerously-skip-permissions`) | Pane adds `--settings '{\"permissions\":{\"additionalDirectories\":[\"<folder>\"],\"deny\":[\"Edit(/<folder>/**)\"]}}'`. `additionalDirectories` (like `--add-dir`) makes the folder a working directory, which grants read and write; an `allow` rule alone does not lift `blockReadsOutsideWorkingDirectories`. The `Edit` deny rule takes the file tools' write access back (it covers Write too; Claude ignores `Write(path)` rules). Under bypass permissions Bash can still write anywhere, so the read-only rule is a guard, not a sandbox. The folder's `CLAUDE.md` is not loaded. Not added on Windows. |\n| Codex (`codex --yolo`) | Nothing to add: `--yolo` runs without a sandbox, and every Codex sandbox mode reads the whole disk. Codex's `--add-dir` grants write access, so Pane does not pass it. |\n| Cursor, OpenCode | Only `PANE_SESSION_DIR`. |\n| `--tool-command` wrappers (agent-farm) and custom commands | Only `PANE_SESSION_DIR`; the wrapper maps it to its harness's flag. |\n\nThe flag is fixed when the tab is created: a tab opened before its Pane was\nassociated has no `PANE_SESSION_DIR` until a new tab is created.\n\nWrite the folder's absolute path into the brief rather than\n`$PANE_SESSION_DIR`. Under `blockReadsOutsideWorkingDirectories` Claude Code\nasks before any Bash command whose variables it cannot check, so a worker that\nruns `echo $PANE_SESSION_DIR` or `cat \"$PANE_SESSION_DIR/...\"` stops on a\nprompt. The variable is for wrappers and scripts.\n\n## Stable interfaces\n\nThe shared record and service are:\n\n- `shared/types/orchestrationSession.ts`\n- `main/src/services/orchestrationSessionManager.ts`\n- `orchestration-sessions.json` below `PANE_DIR`\n\nThe daemon and Electron IPC channels are:\n\n`orchestration-sessions:list`, `orchestration-sessions:select`,\n`orchestration-sessions:create`, `orchestration-sessions:get`,\n`orchestration-sessions:update`, `orchestration-sessions:reorder`, `orchestration-sessions:set-agent`,\n`orchestration-sessions:associate`, `orchestration-sessions:detach`, and\n`orchestration-sessions:overview`.\n\nSelectors accept a stable Session ID or an exact Session name. The existing\n`pane-chat:*` channels remain compatibility endpoints for the imported legacy\nconversation.\n\nRunPane exposes the corresponding commands:\n\n```text\nrunpane sessions list --json\nrunpane sessions get --session <session-id-or-name> --json\nrunpane sessions overview --session <session-id-or-name> --json\nrunpane sessions create --from-json <path|-> --json\nrunpane sessions update --session <session-id-or-name> --from-json <path|-> --json\nrunpane sessions pin --session <session-id-or-name> --json\nrunpane sessions unpin --session <session-id-or-name> --json\nrunpane sessions set-agent --session <session-id-or-name> --agent <agent> --json\nrunpane sessions associate --session <session-id-or-name> --pane <pane-id> --json\nrunpane sessions detach --session <session-id-or-name> --pane <pane-id> --json\n```\n\nUse `--from-json` for structured create and update input. Keep multiline\ncontext, evidence, links, and reports in the JSON file or stdin; do not put\nexternal text into shell source. Re-read mutation results and inspect the\noverview after associations or updates.\n\n### Session JSON shapes\n\n- `decisions` and `blockers` are arrays of strings.\n- `evidence` and `outputs` are arrays of links:\n `{\"label\", \"url\", \"addedAt\", \"kind\"?, \"provenance\"?}`. `url` starts with\n `https://`, `http://`, `file://` or `grain://`; `addedAt` is an ISO 8601\n date; `kind` is `evidence`, `output`, `ticket`, `pull-request` or `other`.\n- `report` (update only) is `{\"summary\", \"status\": \"reported\"|\"verified\",\n \"evidence\": [at least one link], \"reportedAt\", \"provenance\"}`; `null`\n clears it.\n- Arrays replace the stored value, so send the whole list. Each array holds at\n most 100 items and each text at most 16000 characters\n (`shared/types/orchestrationSession.ts`).\n- A bad payload is refused whole with one `ERR_SESSION_PAYLOAD_INVALID` error\n that lists every problem (`outputs[0].addedAt: missing`, ...). The CLI\n checks it before calling Pane, and the daemon checks it again for clients\n such as `runpane-py` (`main/src/services/orchestrationSessionPayload.ts`).\n\n`runpane agent-context --command \"sessions update\"` prints the fields and a\ncomplete example.\n\n## Resume and refresh persisted context\n\nWhen responding to a user task that needs Session state, read\n`PANE_ORCHESTRATION_SESSION_ID` from the current environment. Pane exports this stable identity for\nSession panels, including agent resume paths that do not receive the original\nbootstrap input. Do not infer the Session from a terminal panel ID or from\nconversation text.\n\nWhen the variable is present and the task requires it, reload the saved record\nand then reconcile live\nPane, tab, branch, and evidence state:\n\n```text\nrunpane sessions get --session \"$PANE_ORCHESTRATION_SESSION_ID\" --json\nrunpane sessions overview --session \"$PANE_ORCHESTRATION_SESSION_ID\" --json\n```\n\nRun `get` to recover persisted intent and associations, and run `overview`\nwhen continuing a task after a resume or after a mutation. For workers, this variable does not identify a parent Session. Use `runpane\nsessions list --json` and Pane associations to resolve ownership; never infer\nit from the selected UI Session. Refresh associations before coordinating work. If the\nstable ID cannot be resolved, report the error before taking Session-specific\nactions.\n\n## Identity and overview\n\nEach orchestration Session has a stable ID, a hidden detached Pane session for\nits conversation, and one deterministic terminal panel for each supported\nagent. The imported legacy Pane Chat record retains its legacy internal IDs,\nresume IDs, and terminal buffers. New Session records must not reuse those\nidentities.\n\nLegacy Pane Chat keeps its agent histories together in one Session. Previously\nsplit imports are reunited only when untouched; retained independent imports\nreceive separate hidden owners so their workspace tools cannot cross Sessions.\n\nThe overview joins persisted intent with fresh Pane, tab, branch, worktree,\nagent, and available Git or pull request evidence. Working, idle, stopped,\nexited, missing, and archived states describe activity or availability. They\ndo not prove completion. A completion report must carry inspectable evidence,\nthe report timestamp, and provenance; new activity makes an older report\nstale.\n\n## Named locks for shared resources\n\nWhen several workers share one resource that only one of them may use at a\ntime, such as a test account whose \"approve guest\" banner any logged-in worker\ncan click, name a lock in their prompts. Each worker takes the lock before it\nuses the resource and gives it back after:\n\n```text\nrunpane lock acquire --name testing-account --ttl 30m --wait 1800000 --note \"call QA\" --json\n# ... use the shared account ...\nrunpane lock release --name testing-account --json\n```\n\n- **Owner.** The calling Pane and panel, from `PANE_SESSION_ID` and\n `PANE_PANEL_ID` (or explicit `--pane`/`--panel`). Outside a Pane terminal the\n owner is external and `--note` is required: it names the owner, so pass the\n same note to release.\n- **Scope.** A lock is scoped to the owner's Session when its Pane belongs to\n one (as the Session's orchestrator or an associated Pane), so two Sessions can\n each hold `testing-account`. A Pane outside any Session uses the global scope.\n The scope is fixed when the lock is first taken.\n- **Acquire.** Succeeds when the lock is free, expired, or already held by the\n same owner, which renews the TTL (`renewed: true`). Otherwise it returns\n `ok: false` with `heldBy` and `expiresAt` and exits 1. With `--wait` the call\n blocks in the daemon, which grants the lock to the oldest waiter as soon as it\n is released or expires; the CLI chains daemon calls of up to 120 seconds for\n longer waits.\n- **Release.** Only the owner can release a lock. `--force` releases another\n owner's lock, for example a stuck worker's; add `--session <id|name>` to\n name a Session-scoped lock from outside that Session.\n- **Automatic release.** Pane releases a lock when its TTL (at most 24h) runs\n out, when the owner panel's terminal exits or is closed, and when the owner\n Pane is archived. Quitting Pane does not release locks: the resumed agents\n still expect to hold them.\n- **Storage.** Locks are saved in `locks.json` below `PANE_DIR` with\n temp-and-rename writes, so they survive a daemon restart. Expired locks and\n locks whose owner Pane or panel is gone are dropped when the file loads.\n- **Visibility.** `runpane lock list [--session <id|name>]` lists held locks,\n and `sessions overview` includes `locks`: those scoped to the Session plus\n those its Panes hold.\n\nIf a worker interrupts a waiting `lock acquire`, the daemon may still grant it\nthe lock for up to two minutes; release it, or let the TTL expire.\n\n## Orchestrator guide\n\nThe mechanics of driving workers through Pane, for any orchestrator and any\nharness (a Pane Session, a Claude Code or Codex session in a Pane terminal, or\na wrapper such as Agent Farm), live in one place: the bundled\n[`pane-orchestration`](../main/src/services/paneChatBundle/skills/pane-orchestration/SKILL.md)\nskill. Any agent can read it with\n`runpane docs read --doc skills/pane-orchestration/SKILL.md`, and the home\n`pane` skill points there. It covers one Pane per feature, launching a worker\nas a Pane or tab (including `--tool-command` wrappers, `--initial-input-file\n--as-file-pointer`, `--wait-ready` and reading the delivery result), the one\nwait primitive (`runpane watch --session <S> --as <name> --until-event`) and\nwhat each wake means, worker reports and how a status file pairs with them,\nanswering workers, resuming after a restart or sleep, showing pages beside the\nperson, shared-checkout and port etiquette, handoff, cleanup, and what changes\nafter the next release. The sections below are the reference for each piece.\n\n## Session watcher\n\nWhen monitoring is authorized, use one durable, named watcher per Session. Do not start it merely because a Session opens. `--session` takes the Session ID\nor exact name, and the daemon follows every Pane associated with it:\n\n```text\nrunpane watch --session <session-id> --follow --json\n```\n\nThe daemon resolves the Session once per request and re-reads its\nassociations on every journal read. A Pane associated after the watcher\nstarts is included; a detached or archived Pane drops out, together with any\nline still held for it by `--ready-settle` or `--min-interval`. The Session's own\nhidden owner and orchestrator panels never appear, and an association limited\nto specific panels reports only those panels. There is no re-arm after\n`sessions associate` or `sessions detach`. `--session` cannot be combined with\n`--pane`, an unknown Session fails the watch, and a daemon\nthat predates `--session` fails it with `WATCH ERROR` instead of watching every\nPane.\n\nAfter a Pane restart, an agent panel whose process did not come back is\nreported once as `panel.stopped` (`STOPPED <pane-name> pane <pane-id> panel\n<panel-id>`, `running: false`), never as `agent.ready`. Restart it in place,\nwith the same panel ID and its conversation, using `runpane panels resume\n--panel <panel-id> --wait-ready --yes --json`, or every stopped agent panel of\nthe Session at once with `runpane panels resume --session <session-id>\n--wait-ready --yes --json`. A worker started with `agent-farm run <profile>`\nresumes with `--resume` and the launch ID Agent Farm printed, the same rule a\nrestart uses (see \"Agent Farm\" above). The bulk form restarts `--concurrency` panels at a\ntime (default 3), leaves running panels alone, waits on the resumed panels\ntogether, and returns one result per panel with its readiness, blocker, or\nerror; `--all-stopped` does the same for every Pane. It relaunches finished\nworkers too, so resume single panels when only some should continue.\n\nIf a stopped panel's previous shell or agent process is still exiting, resume\nreturns a `still stopping` error instead of starting a second process. Retry\nafter the old processes exit. This also applies when bounded teardown could\nnot kill a process; bulk resume reports the error for that panel and continues\nwith the others. Archived Panes cannot be resumed, even if archive cleanup failed.\n\nMembership changes are journal entries: `pane.associated` (`JOINED <pane-name>\npane <pane-id> session <session-id>`) and `pane.detached` (`LEFT ...`). The\nSession manager emits them from `sessions associate` (only when the Pane was\nnot already a member) and `sessions detach`. A watch without `--session`\nreceives them only when `--kinds` lists them, so older clients never see an\nunknown kind. Cadence state is keyed by the Session rather than its current\nPanes, so held lines survive membership changes.\n\n### Watching a PR from any Pane\n\n```bash\nrunpane pr watch --pane <pane-id> --url https://github.com/owner/repo/pull/123 --head <full-sha> --json\nrunpane watch --pane <pane-id> --since <generation-returned-above> --epoch <epoch-returned-above> --until-event --json --kinds pr.checks,pr.conflicted,pr.base_changed,pr.base_deleted,pr.head,pr.review,pr.comment,pr.closed,pr.merged,pr.stopped,github.unavailable,github.recovered\nrunpane pr list --pane <pane-id> --json\nrunpane pr unwatch --pane <pane-id> --url https://github.com/owner/repo/pull/123 --json\n```\n\nInside a Pane, registration defaults to its `PANE_SESSION_ID`. Registration\nreturns `generation` and `epoch`; use both for the first wait, including events\nthat arrived between registration and arming. An identical active registration\nretry returns its original generation, including after daemon restart, so a\nlost response cannot skip an unread event. A new head pin or unwatch/re-register\nstarts a new registration. Process every returned NDJSON\nevent, then persist its `gen` and `epoch`. Rearm with `--since <gen> --epoch\n<epoch>`; use `--limit 1` for one event per invocation. Never reuse the initial\nregistration generation after delivering an event, and never combine `--as`\nwith `--since` or `--epoch`. A named cursor is a separate daemon-managed mode.\n\nOrdinary daemon restarts preserve the epoch and unread retained events. On\n`_reset` or `_dropped`, resynchronize current GitHub state before accepting\nsuccess, then persist the control record's `generation` and `epoch`. Ignore\n`replay`/`baseline` records as new events. Retention is bounded; a journal reset\ncannot recover lost history. A consumer crash before saving its cursor can\nreplay an event, so actions must be idempotent. Registration uses the full\nhost/repository/number identity. It pins success to `--head`, or the first\nobserved head; a new head emits `pr.head` and needs a fresh registration before\nsuccess can be reported. Read existing discussions before arming. A wake\nmeans inspect the change; it never authorizes merging.\n\nThe watch also reports when the ground under the PR moves. `pr.base_changed`\n(`BASE CHANGED`) fires when the base branch is retargeted, by a person or by\nGitHub after a stack parent merges and its branch is deleted; `base.from` and\n`base.to` name the old and new base. `pr.base_deleted` (`BASE DELETED`) fires\nwhen the base branch no longer exists; GitHub may close the PR right after.\nBoth arrive at once under `--follow`. Rebase onto the new base and recheck CI.\n\nThe daemon batches up to 25 fingerprints per host every two minutes (plus up\nto ten seconds of jitter), reads details on changes and at a 30-minute\nbackstop, and keeps 10% of GitHub quota for interactive work. It uses `gh auth\ntoken` for cached authentication and direct GraphQL for reads. Session PR\nbranch discovery still uses Pane's existing `gh pr list` lookup.\n\nFailures emit immediately, even while other checks run. Success requires a\ncomplete, nonempty set of checks on the pinned head and all required checks\npassing (all checks when none are marked required). BLOCKED/UNKNOWN merge\nstatus conservatively withholds success, including when reviews are missing.\nFeedback from every account is included: agents and people can share a login.\nChecks passing is a signal to inspect the PR, not proof of complete readiness;\noptional failures can still produce a separate failure event. Merge/close,\neight non-rate-limit failures, or ten comment-only updates stop registration;\narchiving/deleting the Pane removes it. Rate limits pause instead of consuming\nthe failure budget. Outages and recovery are journal events.\n\nFingerprints include individual check outcomes and thread resolution state, so\nequal aggregate counts do not hide swaps or edits. When the fingerprint window\nis truncated (over 100 checks/discussion items or multiple comments in a review\nthread), the daemon reads details each round. Detail reads page through up to\n2,000 checks and retain the latest 100 comments, reviews, threads, and comments\nper thread. Truncated discussions emit `pr.review` with an incomplete-feedback\nreason and withhold success; read the full discussion on GitHub.\n\nBaselines and events are replaced atomically before readers wake. This prevents\na process crash from advancing a baseline without its event. Delivery remains\nbounded by journal retention and cursor acknowledgments; replay after a consumer\ncrash before acknowledgment is possible. This is not an exactly-once or\npower-loss durability guarantee.\n\n\nThe cursor defaults to `session-<uuid>`, where `<uuid>` is the UUID inside the\nSession ID: the Session `__orchestration_session_<uuid>__` uses\n`session-<uuid>`. Passing the name instead gives `session-<name>`. Cursor names\nmay be up to 128 characters (64 before this release); when runpane derives a\nname itself, such as this default or the `PANE_PANEL_ID` fallback, it shortens\none longer than 64 characters (or one with other characters) to\n`<prefix>-<first 12 hex characters of its sha256>`.\n\nA discussion-only Session has no follow watcher. On restart, retain the\n`session-<uuid>` cursor and capture a fresh output baseline before\ninterpreting notifications. After a reset (`_reset`), the baseline restates\ncurrent state: those JSON entries carry `replay: true` (and\n`changedWhileAway: true` after a daemon restart). A replayed `agent.ready` is\nnever READY; re-read `sessions overview` instead of acting on it. Lines mode\nprints only `CHANGED` for them. Return blocked and decision findings to the\nSession conversation. Terminal idle or exit remains activity evidence only.\n\n`--follow` has one default cadence: READY settles for 3 minutes, BLOCKED\nfor 30 seconds, and non-urgent events batch every 10 minutes. IDLE starts at\n10 minutes and backs off to 30 minutes, 1 hour, 3 hours, then daily. Heartbeats\nare off. WATCH OK prints once when armed; WATCH EXPIRED and exit 0 end a watch\nafter 3 quiet hours. Meaningful events reset expiry; IDLE and replay do not.\n\nIndividual duration overrides accept `30s`, `3m`, `3h`, or `0`: `--ready-settle`, `--blocked-settle`,\n`--min-interval`, `--idle-after`, `--expire-after`, `--heartbeat`, and\nwatch `--timeout-ms` (for example, `--until-event --timeout-ms 30s`).\n`--expire-after 0` disables natural expiry. Bare values retain milliseconds,\nexcept heartbeat's legacy seconds. `--json` selects NDJSON; otherwise lines.\n\n`--present` and `--profile` were removed. Use individual timing flags such as\n`--ready-settle 1m --blocked-settle 15s --min-interval 2m` to override the defaults.\n\n```bash\nrunpane watch --session <session-id> --follow --json\nrunpane watch --session <session-id> --follow --ready-settle 1m --blocked-settle 15s --min-interval 2m --json\nrunpane watch --pane <pane-id> --until-event --json\n```\n\nSession membership changes need no re-arm. Named cursors survive daemon\nrestarts; the journal and PR baselines are saved together. RESET/DROPPED mean\nrefresh the overview; replayed baseline entries never mean a new READY.\n`--until-event` waits for REPORT, BLOCKED, EXIT or GONE by default and exits 0\nwhen one arrives, immediately without follow settling or batching. A harness\nthat consumes a named follow stream gets its cadence; a process-exit waiter\nkeeps these immediate single-event semantics. Cadence requires a named consumer\n(explicit `--as` or the name assigned by follow). `--follow --since <generation>`\nhas no consumer name and also bypasses settling and batching. Named `--as`\ncannot be combined with numeric `--since`; switching only the wait mode does\nnot enable cadence for a numeric generation/epoch cursor.\nOverride `--kinds` for PR events. Its `--timeout-ms` is a\ntotal deadline (exit 124); under follow it bounds each daemon request.\n\nREPORT remains the completion signal: workers use `runpane report --state\nready|blocked|failed|done --summary-file <path>`, adding a PR/head and a question\nwhen blocked. READY alone is activity evidence. Read the full report with\n`runpane agents status --panel <id> --json`. Resume stopped workers in the same\npanel with `runpane panels resume --panel <id> --wait-ready --yes --json`.\n\nDo not pipe follow through `head` or `grep -m1` to wait. Run `--until-event` as\na supported background task, act on its events, then re-arm with its cursor\n(the [orchestrator guide](#orchestrator-guide) has the one wait command and\nwhat to do on each wake, timeout, reset and error).\nNever sleep-poll your own background task output. WATCH ERROR/nonzero exit\nmeans failure: re-read every worker's status, then re-arm once; a second\nfailure goes to `runpane doctor --report`. A superseded cursor means keep the\nreplacement. A normal expiry means re-arm with the next dispatch.\n\n\n## Worker reports\n\nWorkers hand back a structured report when they finish or get blocked. Every\nworker prompt ends with:\n\n```text\nWhen finished or blocked, run\nrunpane report --state <ready|blocked|failed|done> --pr <number> --head <sha> --summary-file <path>\n(add --question \"<question>\" when blocked).\n```\n\nInside a Pane terminal `runpane report` finds its own panel through\n`PANE_SESSION_ID` and `PANE_PANEL_ID`; elsewhere pass `--pane` and `--panel`.\n`--question` is required with `--state blocked`, `--pr` is a positive integer,\nand `--head` is 7 to 40 hex characters.\n\nThe daemon then:\n\n- keeps the latest report in the panel's custom state, so it survives a\n restart: `{ state, pr, head, summary, summaryPath, question, reportedAt }`,\n with up to 16,000 characters of summary (a longer one ends with a truncation\n marker and sets `summaryTruncated`);\n- journals an `agent.report` entry with the same fields (summary cut to 2,000\n characters), printed as `REPORT <pane-name> pane <pane-id> panel <panel-id>\n ready pr#747 fc5dce9` or `... blocked: <question>`. It skips the\n `--min-interval` batch. Only watchers that list `agent.report` in `--kinds`,\n and Session watches, receive it, because released CLIs reject journal kinds\n they don't know. It passes `--agents-only` (which `--follow` and\n `--until-event` imply) whatever panel sent it, so a report from a shell tab\n or a wrapper launch whose agent isn't detected yet still arrives;\n- appends a `report` activity (with the worker's `paneId` and `panelId`) to\n every Session the Pane is associated with, which makes an older Session\n report stale;\n- shows the report in `agents status`, `panels list`, and `sessions overview`.\n There each panel carries its own latest report as `panes[].panels[].report`,\n and `panes[].report` is the newest report among the Pane's panels, with its\n `panelId`. When the summary is a complete JSON object (a status file sent\n with `--summary-file`, say), the panel's `report.data` holds it exactly as\n given; a plain-text, array, scalar, invalid, truncated, or more than 64\n levels deep summary has no `data`;\n- returns `liveWatchers`, the number of watches that were waiting and received\n it. The CLI prints `Delivered to 1 waiting watcher.`; with no Session and\n nobody waiting it says the report is stored on the panel and that a\n named-cursor watch receives it when it next waits.\n\nThe orchestrator treats REPORT as the completion signal. A READY without a\nreport means look, and maybe nudge: `runpane panels last-message --panel\n<panel-id> --json` returns the agent's last reply from its Claude or Codex\ntranscript (never the screen; `transcript-unavailable` otherwise), and the\nworker can be asked to run `runpane report`.\n\nAn orchestrator whose harness wakes only when a process exits (Claude Code\nbackground Bash, Codex) waits with the one wait primitive from the\n[orchestrator guide](#orchestrator-guide): `runpane watch --session <S> --as\n<name> --until-event --json --kinds\nagent.report,agent.report.missing,agent.blocked,agent.ready,panel.exited,panel.stopped,pane.gone` (`--pane <pane-id>` outside a Session), run as a background\ntask and run again after each event. `--until-event` exits 0 on the first\nevent (124 at a `--timeout-ms` deadline, if you give one); a `RESET` and the caller's own panel never end\nthe wait. When a worker keeps richer state in a status file, it writes the file\nfirst and reports with `--summary-file <status file>`; the file is the truth\nand the report is the signal.\n\n### Dispatching with `--wait`\n\nThe [orchestrator guide](#orchestrator-guide) prescribes submission without\n`--wait`, followed by its named watch as the completion consumer. The CLI also\noffers an optional per-dispatch result for callers choosing that route instead:\n\n```text\nrunpane panels submit --panel <panel-id> --input-file <prompt-file> --as-file-pointer --wait --yes\n```\n\n`panels create` and `panes create` with a prompt take `--wait` too. `--wait`\nturns the expected report on and blocks on that panel until the first\n`agent.report`, `agent.report.missing`, `agent.blocked`, `panel.exited`, or\n`pane.gone`. It then prints one line of JSON (`dispatchWaitResult` in the\ncontract) and exits:\n\n| `outcome` | Exit | Carries |\n| --- | --- | --- |\n| `report` | 0 | `report`: state, summary, `summaryPath`, PR, head, question |\n| `missing` | 3 | `lastOutput` and `lastOutputSource` |\n| `blocked` | 4 | `screenLines`, the prompt the worker waits on |\n| `exited`, `gone` | 5 | `exitCode` or `reason` |\n| `indeterminate` | 6 | history was lost; inspect the panel and inbox before retrying |\n| `timeout` | 124 | after `--timeout <duration>` (`30m`, `2h`) |\n\nThe result never includes the transcript. The daemon stamps the result of\nevery create and submit with the journal generation and epoch it read before\ndelivering the work. `--wait` adapts the existing `watch --until-event` reader,\nfiltered to the Pane and panel. It reconnects after a daemon restart; a replaced\nor truncated journal returns an explicit indeterminate result (exit 6), since\nthe task's outcome can no longer be established from that history. Like\n`--until-event`, it is a single-event wait: it delivers at once, without\n`--follow`'s settling or batching, and since it exits on the event, a plain\nbackground task is enough (no line-delivering monitor). Claude\nCode wakes the orchestrator when the background call exits, and the line\narrives as the tool result. To answer a blocked worker, send the answer with\n`panels submit --no-expect-report`, then watch for the original work's result.\nReport-expected work is serialized per panel: a new dispatch is refused before\nwriting while an expectation or missing-output collection is outstanding.\nHeld starting input is reserved but does not count startup readiness as task\ncompletion; tracking activates immediately before the prompt's submit or launch.\n\n### Expected reports\n\nWork sent to an agent through runpane expects a `runpane report` back: a\n`panels submit` to an agent's panel, and a `panels create` or `panes create`\nthat carries a prompt. A request marked `--source user` (Pane's own UI marks\nits sends that way) expects none, and neither does typing into a tab, which\nnever goes through runpane. `--no-expect-report` sends fire-and-forget input;\n`--expect-report` asks for a report anyway, also from a plain shell panel.\n`shouldExpectReport()` in `main/src/ipc/runpane.ts` holds the rule. Pane saves `{ since }` per panel in `expected-reports.json` below `PANE_DIR`\n(results echo it as `expectedReport`); only the tracker writes that file, so\npanel-state writes cannot bring back a cleared expectation. When the agent then goes ready (`agent.ready`)\nor its panel exits without a report made at or after `since`, Pane journals\none `agent.report.missing` entry (`MISSING REPORT <pane-name> pane <pane-id>\npanel <panel-id> ready|exited`) and clears the expectation. The entry's\n`lastOutput` is the agent's last reply from its transcript, or else the last\n20 lines on its screen (`lastOutputSource: transcript|screen`), cut to 2,000\ncharacters with the end kept. A report clears the expectation.\n\nAn agent can end its turn while work it started keeps running. Pane reads\nthat from the screen: Claude Code counts running background shells in its\nfooter (`⏵⏵ bypass permissions on · 1 shell`) and lists each running\nbackground subagent under it (`◯ general-purpose <task> 14s`); Codex prints\n`1 background terminal running · /ps to view` above its prompt\n(`detectAgentBackgroundWork()` in `agentScreenSignature.ts`). While one shows,\na READY does not raise the missing report. Claude takes a new turn when the\nwork finishes, and that turn's READY decides. Codex doesn't, so Pane looks at\nthe screen again every 10 seconds while the panel stays ready, and raises\nonce nothing runs. An exit raises at once.\n\nLike\n`agent.report`, the kind reaches only watchers that list it in `--kinds` (a\nSession scope alone does not deliver it), and it skips the `--min-interval`\nbatch. The tracker is `main/src/services/expectedReports.ts`.\n\n### Questions on screen\n\nPane's status detection (`main/src/services/agentStatus/manifests.ts`)\nclassifies a panel as blocked when its screen shows a live prompt waiting on\ninput: Claude's AskUserQuestion picker, permission prompts, folder trust, the\n\"Allow external CLAUDE.md file imports?\" dialog, and Codex's folder-trust,\nsign-in, approval and \"Update available\" screens. The journal records\n`agent.blocked` only once a panel has stayed blocked for 2 seconds\n(`BLOCKED_DEBOUNCE_MS` in `workspaceJournal.ts`), so a prompt-like line that\nflashes past in normal output never becomes BLOCKED. The entry carries\n`screenLines`: up to 20 non-empty lines of the screen region the prompt was\nmatched in. The agent does not need to report anything for this.\n\n### Session inbox\n\nPane keeps, per Session, what its orchestrator has not acknowledged:\nreports (`agent.report`), missing reports (`agent.report.missing`), and\nquestions on screen (`agent.blocked`, with `screenLines`). It is fed from the\njournal for every Pane associated with the Session (`main/src/services/sessionInbox.ts`)\nand saved with a journal epoch and replay watermark in `session-inbox.json`\nbelow `PANE_DIR`. Restart replays committed events after the watermark, with\nstable item IDs, including a report whose inbox write failed. Acknowledgements\nand the watermark are saved together; replay does not resurrect handled items.\nRecovery is bounded by the journal's retention. If entries expired before\nprojection, or the journal was replaced, Pane logs the history loss explicitly;\nalready projected items remain available. A newer report replaces the panel's\nolder report or missing report; a blocked item goes once its panel moves on\n(busy, ready, exited, or reported). Each Session keeps at most 200 items.\n\n```text\nrunpane inbox --session <id|name> [--json] # defaults to $PANE_ORCHESTRATION_SESSION_ID\nrunpane inbox ack <item-id>... [--session <id|name>]\nrunpane inbox ack --all --session <id|name>\n```\n\nAn orchestrator runs `runpane inbox` when it starts or resumes, to catch what\nfinished while it was down. Pane never adds the inbox to the orchestrator's\nturns: worker output reaches it only as the result of a tool call it made.\n\n## Skill contract\n\n`main/src/services/skillCacheManager.ts` installs the skills that ship in\n`main/src/services/paneChatBundle/` into\n`<PANE_DIR>/skills/pane-chat/skills/` and into the `.claude/skills/` and\n`.codex/skills/` folders of the data directory. It also generates the\n`pane-orchestrator` entry skill (plus a Cursor rule at\n`.cursor/rules/pane-orchestrator.mdc`), the runtime context, and the helper\nsubagents in `.claude/agents/` and `.codex/agents/`. A manifest\n(`skills/pane-chat/installed.json`) records what it installed and a hash of the\nbundle: startup skips the install when the hash is unchanged, and later installs\nreplace only the recorded entries.\n\nTo change Pane Chat behavior, edit the bundle or `buildPaneOrchestratorSkill()`\nin `skillCacheManager.ts`, then run `paneChatBundle.test.ts`.\n\nBundled workflow skills are available to Sessions and can be selected according\nto the user's task. The installed manifest lets Pane refresh its own skill files\nwithout replacing unrelated user skills.\n\n`shared/types/sessionProfile.ts` defines the generic default profile and shared\ncapability context. Generated guides and agent instruction files use this\ncontract. The legacy shared Cursor rule is not always applied; Session-specific\ninstructions belong to the Session's working directory. User-selected profiles\nand workflows determine behavior after the user supplies a task.\n\n## Terminal and Files\n\nEach Session has a collapsible **Terminal** dock and a **Files** button beside\nits agent tabs. The shell and file browser use the Session's workspace folder.\nSelecting a file opens an editor tab alongside the agent conversation. Panels\nare saved with the Session and reused when you reopen the tools; switching\nSessions keeps their shells and files separate.\n\nEvery Pane has the same dock: its first plain shell, titled **Terminal**, where\nthe setup progress notices appear. It is not a tool or agent tab. `runpane panels\nlist` marks it `dock: true`, and `runpane panes list` reports a Pane `running`\nonly while one of its tool or agent terminals is live, not for the dock alone.\n\n## Plans, pages, and split view\n\nAgents show pages and files with `runpane panels open --file <path>` or\n`--url <url>`. Files open in an editor tab with read-only previews for supported\nformats; HTML opens as editable source. Use `--url` with an HTTP, HTTPS or file\nURL for a live browser tab. By default the tab opens in split view:\nthe first one splits the stage to the right of the conversation, and later ones\njoin that side group as tabs. A page opened while you are looking at another\nSession or Pane is in that side group when you come back, without taking focus\nfrom the tab you left showing; tabs you have already arranged stay where you put\nthem.\n`--tab` opens it as a plain tab instead. Reopening the same file or URL reuses\nand reloads its tab; without `--tab`, a reopened tab that sits in the\nconversation's group moves to the side group. This works the same in Sessions\nand in worktree Panes.\n\nGenerated Session instructions ask the orchestrator to write plans, reports, and\nother documents as self-contained HTML files in its Session folder and open them\nwith `--url` when rendered HTML is intended, or `--file` to inspect the source.\nOpening a Session never submits a prompt or starts work just to write\na document.\n\nWhile the stage is split, each group has its own tab strip, including the agent\ntab in its group. For a single Session, the strips align directly below the\nwindow title bar. Closing the last tab in the side group returns to a single\ngroup. Layouts are saved with the Session.\n\nDrag any tab to rearrange the stage, as in a project Pane: drop it in the\nmiddle of a group to join that group, against an edge (the outer quarter) to\nsplit beside it, or between tabs on a strip to place it there. Dragging the\nlast tab out of a group closes that group. In tiled Sessions a tab moves only\nwithin its own Session.\n\n## Tiling Sessions side by side\n\n\n\nSessions tile with the same gesture as tabs, one level up. Drag a Session from\nits sidebar row — or by its title row once tiled — onto another Session and drop\nit against an edge to put the two beside each other; drop it in the middle to\nshow it in that slot instead. Edge bands are the outer quarter of the target,\nas they are for tabs, and the layout nests as deeply as you take it: a row of\nSessions with one of them split into a column is an ordinary shape, not a\nspecial case.\n\nBoth halves stay fully live. A tile that is not focused keeps its agent running\nand its output streaming; nothing is throttled or suspended, so a Session you\nare watching out of the corner of your eye is the same Session you were just\ntyping in. Each tile keeps its own tab splits, its own Terminal dock, its own\nFiles and Changes sidebar, and its own navigation.\n\nThe focused tile's Session is the selected one: its title row carries the focus\nring, and clicking a Session in the sidebar focuses its tile when it already has\none, or shows it in the focused tile when it does not. With one tile that is\nexactly the old behaviour — switching Sessions replaces the view.\n\nThe `X` on a tile's title row stops tiling that Session; the Session itself,\nits conversation and its panels are untouched, and the layout collapses back\naround the space. The last tile has no close button, since a window with no\nSession in it has nothing to show. The command palette carries the same actions\nfor the keyboard: focus the Session left, right, above or below, and stop tiling\nthe focused one.\n\nA layout belongs to the host it was built on, like the remembered location it\nsits beside: Session ids only mean anything on their own host, so switching\nhosts loads that host's layout and every tile is revalidated against the\nSessions that still exist there. Archiving or deleting a tiled Session retires\nits tile and collapses the split.\n\n## Reaching Pane and delegating work\n\nThe generated Session instructions tell the orchestrator how to reach\n`runpane` (PATH, then `$PANE_RUNPANE_BIN`, then the runtime context) and forbid\nsubstituting plain git worktrees or built-in subagents for delegated work; if\n`runpane` is unreachable the agent stops and reports it. Worktrees the\norchestrator creates or adopts with `runpane panes create` or `panes adopt`\nbecome the Session's children in the same call.\n\nEarlier development builds had an experimental progress view that rendered\n`progress.html` in a fixed side panel. It never shipped; Pane deletes its old\n`.pane-progress.json` switch and leaves any HTML files in place.\n\nNew worktrees launched from a Session default to unpinned. First association also\nclears a worktree's previous pin, including an explicit `--pinned` creation pin,\nso it appears as a Session child. Use `panes pin` manually afterward; repeated association does not undo that choice. Independent\nworktree defaults and existing historical pins are unchanged.\n\n### Generated instructions and Git\n\nSession instructions and documents live under `<PANE_DIR>/sessions/<id>/`,\noutside the project. While the Session agent shows its folder-trust prompt, a\nbanner across the top of the chat says the folder is Pane's own, holding the\nSession's notes and no project code. Session terminals set `GIT_CEILING_DIRECTORIES` to\n`<PANE_DIR>/sessions`, so git run in a Session folder never picks up a repository\nabove it, such as a home directory tracked as a dotfiles repo. Do not configure\nPANE_DIR inside a project repository.\n\n### Move an existing chat to a Session\n\nRight-click a worktree and choose **Move chat to Session…**, then choose the chat\nif it has multiple agent tabs. You can also use **Move chat to Session** in the top\nbar of a selected Claude or Codex chat. Then enter a unique Session name, and choose **Move chat**. The agent must be idle\nand have a saved conversation ID. Pane stops the old terminal before transferring\nthe existing panel and resumes the same conversation from a private Session\nfolder. No initial prompt is submitted. The original worktree becomes a child;\nits branch, files, and uncommitted changes remain in place. Other tabs stay there.\n\nCursor and custom launch wrappers are not yet supported for promotion. Missing\nhistory, conflicting ownership, and active work are rejected before transfer.\nA durable Session record allows reopening to complete an interrupted transfer\nwithout copying the conversation. Terminal, Files, and split view\ncapabilities are the same as for a newly created Session.\n\nPromotion defaults to the worktree's current name.\n"},{"path":"docs/SHARED_CREDENTIALS.md","title":"Shared integration keys","text":"# Shared integration keys\n\nSet an integration key once and every Pane host you use gets it. A key set on your Mac reaches your Windows PC, and a key you type in the iPhone app reaches every host the phone has saved.\n\n## Which keys\n\n| Key | Where you set it |\n| --- | --- |\n| Deepgram, fal, OpenRouter (voice dictation) | Settings → Integrations on a desktop, or the phone's voice setup |\n| APNs key for iPhone notifications | Settings → Integrations → iPhone notifications |\n| Anthropic, OpenAI | `anthropicApiKey`, `openaiApiKey` in the host's `config.json` |\n\nA key that exists only in a host's environment (for example `FAL_KEY` or the `PANE_APNS_*` variables) stays on that host and is never shared. On that host the environment value wins over a shared one.\n\n## How keys travel\n\nHosts never contact each other on their own. A device that has more than one host saved, whether found over Tailscale or paired with a code, carries the keys between them:\n\n- **The iPhone app** syncs whenever it opens or returns to the foreground, after you save a key from the phone, and when a host reports that its settings changed. It relays the keys and keeps no copy.\n- **A desktop with another host saved** syncs when it starts, when a key changes on it, and when a saved host reports a change. Computers on your Tailscale login are saved from Settings → Remote Access → Your computers. To pair with a code instead, create a connection code on the other host (Settings → Remote Access → Set Up This Machine) and paste it under Settings → Remote Access → Your computers → Add with a Code.\n\nSharing chains: if your Mac is paired with your Windows PC and the Windows PC with a Linux server, a key set on the Mac reaches the Linux server once both syncs have run.\n\nWhen two hosts disagree, the copy that was set most recently wins. Removing a key records when you removed it, so an older copy on another host does not bring it back. Settings → Integrations shows which host each key was set on and when.\n\nA host that is offline keeps its own copy and catches up on the next sync. Hosts running a Pane version from before key sharing are skipped.\n\n## Who can read the keys\n\nOnly your own devices. Over Tailscale, a host answers a request for its keys only from its owner's Tailscale login, even when its workspace lets other people on the tailnet in, so a teammate's computer neither receives nor replaces your keys. With a connection code, the caller must present a valid pairing token, and the host refuses the request when it does not require pairing. Either device can already open terminals on that host, so sharing keys with it exposes nothing new.\n\nKeys are never written to logs or events. They are stored in each host's `config.json` in the Pane data directory, as before.\n\n## Stop sharing\n\nRemove the host from the device, or unpair it. On a host, revoke it under Settings → Remote Access → Set Up This Machine → Host status. On a desktop, delete the saved profile under Connections. On the phone, remove the host. The keys already copied to each host stay there until you remove them, and removing a key on one host removes it from the others on the next sync.\n"},{"path":"docs/SSH_HOSTS.md","title":"SSH hosts","text":"# SSH hosts\n\nPane lists the hosts in your SSH config under **SSH hosts** in the sidebar. Click one and Pane opens a terminal tab that runs `ssh <host>` with your own `ssh`, keys and `ssh-agent`. You need no project, and no coding agent starts.\n\n## Add a host\n\nAny `Host` entry in your SSH config shows up:\n\n```\nHost mini\n HostName 192.168.1.20\n User me\n IdentityFile ~/.ssh/id_ed25519\n```\n\n## Open a host\n\nSSH tabs open in the **SSH view**, which takes the place of the main area, like Pane Chat.\n\n- **Click a host** to show its tab. If the host has no tab yet, Pane opens one.\n- **Right-click a host**, or hover it and click **+**, to open another tab on it.\n- A dot beside a host means it has an open tab.\n- With the sidebar collapsed to icons, the **SSH hosts** icon opens the SSH view. With no tab open, the view lists your hosts.\n\nEach tab is an ordinary Pane terminal. Pane starts its default shell (**Settings → Terminal → Default shell**) in your home folder and types `ssh <host>` into it. Password, passphrase and host-key prompts appear in the tab as they would in any terminal. When the connection ends or fails, you are back at a local prompt. After Pane restarts, your SSH tabs return in the SSH view, and each one runs `ssh <host>` again the first time you open it.\n\n## Which file Pane reads\n\n| System | File |\n| --- | --- |\n| macOS, Linux | `~/.ssh/config` |\n| Windows | `C:\\Users\\<you>\\.ssh\\config` |\n\nPane also reads the files that config pulls in with `Include`, the way `ssh` does: globs such as `Include conf.d/*` expand in sorted order, relative paths start from your `.ssh` folder, and `~` means your home folder. An `Include` counts only when it applies to every host: before the first `Host` or `Match` line, or under `Host *`. An `Include` inside a narrower `Host` or `Match` block applies only to that block's hosts, so `ssh` would not use its entries for a plain `ssh <host>`, and Pane does not list them. If a host from an included file is missing, move its `Include` line to the top of the config. Pane takes the home folder from your user account, as `ssh` does: on macOS and Linux changing `$HOME` does not move it, and on Windows it is the profile folder Windows records for your account.\n\nPane reads the files again on start, when its window comes back into focus, and when you expand the section. After editing the config in another app, switch back to Pane to see the change. After editing it in a Pane terminal, collapse and expand **SSH hosts**. A host you delete from the config stays in the list while it has an open tab, so you can return to that tab; it disappears once you close its tabs.\n\nPane opens only the config and its included files. It never opens, copies or uploads your keys; `ssh` reads them.\n\n## Which hosts appear\n\nEvery name on a `Host` line appears, in file order, so `Host web db` lists both `web` and `db`. Hosts from an included file appear where its `Include` line is. A repeated name appears once. Pane skips:\n\n- Patterns with `*`, `?` or `!`, such as `Host *` or `Host *.internal`.\n- Names that do not start with a letter, digit or `_`, or that contain characters other than letters, digits and `. _ @ : -`. Pane types the name into a shell, so it lists only names every shell passes to `ssh` unchanged; a leading `-` would be an `ssh` option and a leading `@` means something else in PowerShell.\n- Hosts that appear only in `Match` blocks, and the system-wide config (`/etc/ssh/ssh_config`, or `%ProgramData%\\ssh\\ssh_config` on Windows).\n\nIf a host is missing from the list, check it against these rules. A missing or unreadable config lists no hosts; an unreadable included file is skipped. With no hosts and no open SSH tabs, the section and its sidebar icon stay hidden.\n\n## Windows\n\nPane's default shell on Windows is Git Bash when it is installed, otherwise PowerShell or `cmd`. Each shell runs the first `ssh` on its PATH: usually Git's `ssh` in Git Bash, and Windows' OpenSSH client in PowerShell and `cmd` (an optional Windows feature that recent versions install by default). Both read `C:\\Users\\<you>\\.ssh\\config`.\n\nWindows' OpenSSH client refuses a private key that other accounts can read. Make the key readable only by you, replacing `id_ed25519` with your key file:\n\n```powershell\nicacls $env:USERPROFILE\\.ssh\\id_ed25519 /inheritance:r /grant:r \"$($env:USERNAME):R\"\n```\n\n## Remote Pane\n\nWhen this window controls Pane on another machine through [Remote Pane](SELF_HOSTED_REMOTE_DAEMON.md), the list shows the hosts in that machine's SSH config, and `ssh` runs there with that machine's keys and network.\n\nIf a host runs Pane itself, connect to it with Remote Pane. That gives you its projects, files, dev servers at `localhost` and running agents.\n\nThe phone app shows no SSH host list.\n"},{"path":"docs/STATE_MANAGEMENT.md","title":"State Management Guidelines","text":"# State Management Guidelines\n\n⚠️ **IMPORTANT**: Pane follows a targeted update pattern for state management to minimize unnecessary re-renders and network requests.\n\n## Overview\n\nPane uses a combination of Zustand stores, IPC events, and targeted updates to manage application state efficiently. The application prioritizes specific, targeted updates over global refreshes to improve performance and user experience.\n\nSession and project lists live in Zustand (`frontend/src/stores/sessionStore.ts` and related stores). Do not keep a parallel `projectsWithSessions` array in a tree component; `DraggableProjectTreeView` was removed.\n\n## Key Principles\n\n1. **Targeted Updates**: Always update only the specific data that changed\n2. **Event-Driven Updates**: Use IPC events to communicate changes between processes\n3. **Avoid Global Refreshes**: Never reload entire lists when only one item changes\n4. **Database as Source of Truth**: Frontend state should reflect backend state, not override it\n\n## State Update Patterns\n\n### Session Updates\n\n```typescript\n// ❌ BAD: Global refresh\nconst handleSessionCreated = () => {\n void loadSessions(); // Reloads everything\n};\n\n// ✅ GOOD: Targeted store update\nconst handleSessionCreated = (newSession: Session) => {\n useSessionStore.getState().addSession(newSession);\n};\n```\n\nIPC handlers in `useIPCEvents` already call `addSession` / `updateSession` / `deleteSession` on the store. Subscribe with selectors (`useSessionStore(s => s.sessions)`) instead of copying the list into local React state.\n\n### Project Updates\n\n```typescript\n// ❌ BAD: Reload all projects\nconst handleProjectDeleted = () => {\n fetchProjects(); // Network request for all projects\n};\n\n// ✅ GOOD: Remove from local state\nconst handleProjectDeleted = () => {\n setProjects(prev => prev.filter(p => p.id !== deletedId));\n};\n```\n\n## IPC Event Handling\n\nThe application uses IPC events to synchronize state between the main process and renderer:\n\n### Session Events\n\n- `session:created` - Add new session to appropriate project\n- `session:updated` - Update specific session properties\n- `session:deleted` - Remove session from project list\n\n### Project Events\n\n- `project:updated` - Update specific project properties\n\n## When Global Refreshes Are Acceptable\n\n- **Initial Load**: When component mounts for the first time\n- **User-Triggered Refresh**: When user explicitly requests a refresh\n- **Error Recovery**: After connection loss or critical errors\n- **Complex State Changes**: When multiple interdependent items change\n\n## Implementation Examples\n\n### ProjectSessionList.tsx\n\n- Reads sessions from Zustand `sessionStore`\n- IPC events patch the store; the tree re-renders from selectors\n- Avoids a local `projectsWithSessions` array that new-references on every status tick\n\n## Best Practices\n\n1. **Use Store Actions**: Call `useSessionStore.getState().addSession(...)` (or the matching action) from IPC handlers so every subscriber sees one update\n2. **Merge Updates**: When updating objects, spread existing properties to preserve data\n3. **Handle Edge Cases**: Always check if the item exists before updating\n4. **Log State Changes**: Add console logs for debugging state updates in development\n5. **Validate IPC Data**: Ensure IPC events contain expected data structure\n"},{"path":"docs/TIMESTAMP_HANDLING.md","title":"Timestamp Handling Guidelines","text":"# Timestamp Handling Guidelines\n\n⚠️ **IMPORTANT**: Proper timestamp handling is critical for the application to function correctly, especially for prompt duration calculations.\n\n## Overview\n\nPane uses timestamps throughout the application for tracking session activity, prompt execution times, and displaying time-based information. Due to the mix of SQLite database storage and JavaScript Date objects, special care must be taken to ensure timezone consistency.\n\n## Key Principles\n\n1. **Database Storage**: All timestamps are stored in UTC using SQLite's `CURRENT_TIMESTAMP` or `datetime()` functions\n2. **Frontend Display**: Timestamps are parsed as UTC and converted to local time only for display\n3. **Consistency**: Always use the timestamp utility functions instead of manual date parsing\n4. **Validation**: Always validate timestamps before using them in calculations\n\n## Timestamp Formats\n\n- **SQLite DATETIME**: `YYYY-MM-DD HH:MM:SS` (stored in UTC without timezone indicator)\n- **ISO 8601**: `YYYY-MM-DDTHH:MM:SS.sssZ` (with explicit UTC timezone)\n- **JavaScript Date**: Local timezone by default (be careful!)\n\n## Utility Functions\n\nPane provides timestamp utilities in both frontend and backend:\n\n### Backend (`main/src/utils/timestampUtils.ts`)\n\n```typescript\nimport { formatForDatabase, formatForDisplay, getTimeDifference, formatDuration, isValidTimestamp } from '../utils/timestampUtils';\n\nconst timestamp = formatForDatabase(); // ISO 8601 string, UTC\nconst displayTime = formatForDisplay(timestamp); // local time for display\nconst durationMs = getTimeDifference(start, end); // end defaults to now\nconst formatted = formatDuration(durationMs); // \"2m 34s\"\n```\n\n### Frontend (`frontend/src/utils/timestampUtils.ts`)\n\n```typescript\nimport { formatDistanceToNow, isValidTimestamp } from '../utils/timestampUtils';\n\nconst ago = formatDistanceToNow(timestamp); // \"5 minutes ago\"\n```\n\nThe frontend helpers call `new Date()` on their input, so pass them ISO strings\nwith a `Z` (or `Date` objects), never raw SQLite `YYYY-MM-DD HH:MM:SS` values.\n\n## Database Operations\n\nWhen working with timestamps in SQLite:\n\n```sql\n-- Use datetime() for UTC timestamps\nINSERT INTO prompt_markers (timestamp) VALUES (datetime('now'));\n\n-- When selecting, append 'Z' for proper UTC parsing\nSELECT datetime(timestamp) || 'Z' as timestamp FROM prompt_markers;\n\n-- For completion timestamps with NULL handling\nSELECT\n CASE\n WHEN completion_timestamp IS NOT NULL\n THEN datetime(completion_timestamp) || 'Z'\n ELSE NULL\n END as completion_timestamp\nFROM prompt_markers;\n```\n\n## Common Patterns\n\n### Creating a new timestamp\n\n```typescript\n// Backend - for database storage\nconst timestamp = formatForDatabase();\n\n// Frontend - for immediate use\nconst now = new Date();\n```\n\n### Tracking prompt execution time\n\n```typescript\n// When prompt starts\ndb.addPromptMarker(sessionId, promptText, outputIndex);\n\n// When prompt completes\ndb.updatePromptMarkerCompletion(sessionId);\n```\n\n### Calculating duration\n\n```typescript\n// With completion timestamp\nif (prompt.completion_timestamp) {\n const duration = getTimeDifference(prompt.timestamp, prompt.completion_timestamp);\n return formatDuration(duration);\n}\n\n// For ongoing prompts\nconst duration = getTimeDifference(prompt.timestamp); // Uses current time as end\nreturn formatDuration(duration) + ' (ongoing)';\n```\n\n## Common Pitfalls to Avoid\n\n### 1. Never parse SQLite timestamps directly with `new Date()`\n\n```typescript\n// ❌ WRONG - treats UTC as local time\nconst date = new Date(\"2024-01-01 12:00:00\");\n\n// ✅ CORRECT - select it with `datetime(col) || 'Z'` in SQL, or mark it as UTC first\nconst date = new Date(\"2024-01-01 12:00:00\".replace(' ', 'T') + 'Z');\n```\n\n### 2. Always validate timestamps before calculations\n\n```typescript\nif (!isValidTimestamp(timestamp)) {\n return 'Unknown duration';\n}\n```\n\n### 3. Be careful with timezone conversions\n\n```typescript\n// Database stores UTC, display shows local\nconst dbTime = \"2024-01-01 12:00:00\"; // UTC\nconst parsed = new Date(dbTime.replace(' ', 'T') + 'Z'); // parsed as UTC\nconst display = formatForDisplay(parsed); // Converts to local for display\n```\n\n### 4. Handle negative durations gracefully\n\n```typescript\nconst duration = endTime - startTime;\nif (duration < 0) {\n console.warn('Negative duration detected');\n return 'Invalid duration';\n}\n```\n\n## Testing Timestamp Code\n\nWhen testing timestamp-related features:\n\n1. Test with different timezones (especially negative UTC offsets)\n2. Test with daylight saving time transitions\n3. Test with very old and future timestamps\n4. Test with invalid/malformed timestamps\n5. Verify duration calculations are always positive\n"},{"path":"docs/TOOL_PANEL_SYSTEM.md","title":"Tool Panel System Implementation Guidelines","text":"# Tool Panel System Implementation Guidelines\n\n⚠️ **IMPORTANT**: The tool panel system implements a flexible, extensible architecture for managing multiple tool instances per session.\n\n## Architecture Overview\n\nThe tool panel system consists of several key components:\n\n1. **Panel Manager** (`main/src/services/panelManager.ts`): Central coordinator for panel lifecycle management\n2. **Terminal Panel Manager** (`main/src/services/terminalPanelManager.ts`): Specialized handler for terminal panel processes\n3. **Panel Event Bus** (`main/src/services/panelEventBus.ts`): Event communication system between panels\n4. **Panel Store** (`frontend/src/stores/panelStore.ts`): Frontend state management with Zustand\n5. **Panel Components**: React components for rendering different panel types\n\n## Key Implementation Principles\n\n- 1 feature = 1 worktree = 1 branch = 1 Pane; reviews, audits, fixes and QA for it are tabs in that Pane. A Pane owns one worktree and branch; panels (tabs) share it. Agents add review, QA and fix agents for existing work as new panels with `runpane panels create --pane <id>`, never as new Panes. See [Sessions](SESSIONS.md#one-pane-per-feature).\n\n1. **Lazy Initialization**: Panels are created in the database immediately but background processes (like terminal PTY) only start when the panel is first viewed\n2. **State Persistence**: All panel state including terminal scrollback, working directories, and configurations persist across application restarts\n3. **Memory Efficiency**: Inactive panels suspend rendering but maintain background processes\n4. **Event-Driven Updates**: Uses IPC events to synchronize state between main and renderer processes\n5. **Extensible Design**: Architecture supports future panel types beyond terminals\n\n## Panel Lifecycle\n\n1. **Creation**: User clicks \"Add Tool\" → Panel entry created in database → Added to UI\n2. **First View**: User clicks panel tab → Background process initializes → XTerm.js mounts\n3. **Switching**: User switches panels → Previous panel unmounts XTerm → New panel mounts\n4. **Background Operation**: Processes continue running even when panel is not visible\n5. **Deletion**: Panel closed → Process terminated → Database entry removed → UI updated\n\n## Terminal Panel Specifics\n\n- Each terminal panel spawns an independent PTY process using node-pty\n- Terminal metadata (cwd, dimensions, agent session) persists in `tool_panels.state` as JSON, ceilinged at 256 KB per panel and merged key by key inside SQLite\n- Terminal bytes (scrollback, serialized snapshot, alternate-screen frame) persist in `panel_buffers`, capped at 4 MB per panel with the oldest scrollback trimmed first\n- XTerm.js instances mount/unmount based on panel visibility to save memory\n- Working directories are maintained independently per panel\n- Command history and environment variables can be preserved across restarts\n- Cmd-click on macOS or Ctrl-click on Windows/Linux opens `pane://open?pane=<pane-id>&panel=<panel-id>` links in terminal output inside Pane. The panel parameter is optional.\n\n## Database Schema\n\n- `tool_panels` table stores panel configuration and state\n- `sessions.active_panel_id` is the last-used tab per session: the one any client picked most recently. A client with no memory of a Pane opens it there, and `runpane panels list` reports it as active\n- Foreign key constraints ensure panels are cleaned up when sessions are deleted\n\n## Event System\n\n- A client's tab click (`panels:set-active`), `runpane panes focus` and opening a Session view update `active_panel_id` and move no other client. A host- or agent-initiated activation (`runpane panels open`, `panels create`/`panes create` with focus, Pane Chat, new-Pane setup, run scripts) sends `panel:activeChanged` (`PanelActivationRequest`), which only clients already showing that Pane follow\n- The remote web and mobile app remembers its own Pane, Session and tab per host in browser storage. Each desktop remembers its own split and tabs per Pane in its local `ui_state` (`paneLayout.<pane id>[@<host profile id>]`) and its own Session in navigation memory; it opens a Pane on `active_panel_id` and the stored `panel_layout` only when it has no memory of it. Tabs missing from its memory join the primary group, except agent-opened ones (`metadata.openPlacement: 'split'`), which go to the side group, inactive\n- Terminal panels emit `terminal:command_executed`, `terminal:exit`, and `files:changed` events\n- Event bus routes events to subscribed panels (planned for future panel types)\n- Events support future inter-panel communication (e.g., diff panels reacting to file changes)\n\n## Implemented Panel Types\n\nThe tool panel system supports 8 panel types (`ToolPanelType` in `shared/types/panels.ts`):\n\n1. **Terminal** (`terminal`): PTY shells, several per pane. Agent CLIs such as\n Claude Code, Codex and Cursor Agent run in terminal panels\n (`TerminalPanelState.agentType` records which one).\n2. **Diff** (`diff`): git diff viewer. Singleton, permanent, worktrees only.\n3. **Explorer** (`explorer`): file tree.\n4. **Editor** (`editor`): Monaco editor, one tab per open file.\n5. **Logs** (`logs`): script execution output. Singleton.\n6. **Dashboard** (`dashboard`): project overview. Singleton, permanent,\n projects only.\n7. **Setup Tasks** (`setup-tasks`): project setup checklist. Singleton,\n permanent, projects only.\n8. **Browser** (`browser`): embedded browser. Worktrees only.\n\n## Panel Capabilities\n\nEach panel type has specific capabilities defined in `PANEL_CAPABILITIES`:\n\n- **requiresProcess**: Whether panel needs background process (terminal, logs)\n- **singleton**: Only one instance per session (diff, logs, dashboard, setup-tasks)\n- **permanent**: Cannot be closed by user (diff, dashboard, setup-tasks)\n- **canAppearInProjects**: Available in project view (all except diff and browser)\n- **canAppearInWorktrees**: Available in worktree sessions (all except dashboard, setup-tasks)\n- **canEmit**: Events this panel can produce\n- **canConsume**: Events this panel listens to\n\nExample: Diff panels are singleton + permanent + worktree-only, consuming `files:changed` and `terminal:command_executed` events to auto-refresh.\n\n### Named custom launch profiles\n\nIn a worktree's **Add Tool (+)** menu, choose **Add custom command…** and enter\nan optional **Name** alongside the command. **Save & launch** saves the profile\nand opens a terminal tab with that name. Leaving the name blank uses the first\nthree words of the command. Saved profiles are shared through app configuration.\nUse the pencil beside a saved profile to rename it; the menu and future tabs use\nthe new name. Existing tabs keep their titles. Hover a profile to see its full\nname and command.\n\nResume commands retain the original shell quoting and environment assignments when removing a launch prompt. Custom Claude launchers may choose a separate configuration directory; Pane trusts their recorded conversation IDs instead of checking the app configuration for their transcripts.\n"},{"path":"docs/UPDATE_RECOVERY.md","title":"Update prerequisites, status and recovery","text":"# Update prerequisites, status and recovery\n\n`runpane update` defaults to the running Pane app and the CLI that received the\ncommand, on the machine where the command runs. A remote host selected in the UI\ndoes not change that target. Use `--pane-only` or `--cli-only` for one component.\nRun `runpane update --dry-run --json` first to see the executable, data directory,\nCLI owner and supervisor capability without starting an update.\n\n## Prerequisites\n\nBoth the running app and the requested app release must support continuity\nprotocol 1. No existing 2.4.182 release has the required `continuity.json`\nmanifest. To bootstrap this feature, install a continuity-capable release with\nits normal installer before using this path; installing the CLI alone does not\nadd support to an older running app. A requested older target without that manifest is refused before shutdown.\n\nUse an account that owns the app and CLI directories, in an interactive login\nwith launchd (macOS), Task Scheduler (Windows), or user systemd (Linux). The app\nmust be a packaged macOS bundle, Windows installation, or owned Linux AppImage.\nSystem package installations on Linux require their package manager. App updates\nrequire platform signature verification on macOS and Windows. The official\nWindows 2.4.182 installer is unsigned and is refused by this verifier.\n\nInstall standalone Node 22 or newer and make it available on PATH to the process\nstarting the update. The helper copies this runtime before shutdown; Electron\ninside the app being replaced is insufficient. npm must also be available when\nupdating a JavaScript CLI. The supported CLI owners are:\n\n- Pane's managed CLI, normally under `<pane-dir>/bin`; an explicit CLI-only\n update remains selected when an older Pane next starts.\n- A writable global npm `runpane` installation in the prefix reported by its\n own npm. Local project packages and npx cache copies are refused.\n- The invoking Python environment's writable `runpane` package, with its own\n `python -m pip` available. Virtual environments, including pipx environments,\n and user-site installs are identified by their actual package paths. A pip\n dry run must accept the staged wheel. The Python wrapper also requires the\n continuity engine installed by Pane under the same `--pane-dir`; pip alone\n does not install that engine or Node.\n\nThe helper checks directory permissions, data/profile readability and free\nspace on each affected volume. Windows can exclusively lock live browser-profile\nfiles; their sizes are counted before shutdown, and the snapshot must successfully\nread them after clean shutdown before installation is allowed. It stages downloads\nbefore shutting down Pane.\nA successful dry run checks local prerequisites; it does not download or prove\nthat the requested release passes artifact verification.\n\n## Status and exit codes\n\nKeep the operation's `id` and use the same `--pane-dir`:\n\n```sh\nrunpane update --json\nrunpane update --status <id> --pane-dir <path> --wait --json\n```\n\nThe status command reads the local journal even while Pane is stopped, provided\nthe invoking CLI and its runtime can still launch. If the normal command fails\nbecause the app or CLI was damaged, read `<pane-dir>/updates/<id>/status.json`\ndirectly in a text editor. On macOS/Linux, use\n`cat \"<pane-dir>/updates/<id>/status.json\"`; in PowerShell, use\n`Get-Content -Raw '<pane-dir>\\updates\\<id>\\status.json'`. Replace the placeholders\nwith the original data directory and operation ID. This read does not start or\nresume an update. If the ID was lost, inspect the directories under `updates`\nand match the journal's ID and host with `preflight.json`; do not guess ownership\nfrom timestamps alone. A missing or unreadable journal requires maintainer help.\n\nThe command's JSON\nobject has these fields; this result is documented here and is not currently a\nnamed schema in the agent contract's `jsonSchemas` collection.\n\n| Field | Meaning |\n| --- | --- |\n| `id`, `host`, `scope`, `version` | Operation identity, local machine, `both`/`pane`/`cli`, and requested or resolved release |\n| `phase` | Current stage or terminal outcome |\n| `completed` | Stages durably recorded as finished |\n| `appUpdated`, `cliUpdated` | Selected component installation finished; neither implies host or panel readiness |\n| `hostReachable` | Relaunched host passed version, identity and endpoint/auth checks; false for CLI-only updates |\n| `restored` | Per-panel `{panelId, outcome, reason}` records; `resumed`, `preserved`, or `needs-attention` |\n| `error` | Failure explanation, when present |\n| `updatedAt`, `helperPid` | Last recorded time and acknowledged helper PID; PID presence alone does not prove liveness |\n| `statusFile`, `next` | Journal location and command to inspect it again |\n\nStages are `accepted`, `stage`, `checkpoint`, `stop`, `snapshot`, `install`,\n`launch`, `health`, and `restore`. Terminal phases are `complete`, `failed`, and\n`needs-attention`. `complete` means all selected components and applicable health\nand restoration gates finished; preserved idle/blocked panels are intentional.\n`needs-attention` can mean a partially updated installation or individual panels\nrequiring manual action. Inspect the booleans, completed stages, error and every\npanel outcome together.\n\nExit 0 means the returned status is not a terminal failure. Without `--wait`, it\ncan still be in progress. `failed` and `needs-attention` return exit 1. Command or\npreflight errors also return nonzero. `--wait` has no overall timeout. Ctrl+C stops\nthe waiting caller, not the independent operation; inspect status before retrying.\nThere is no supported operation cancellation command. A fresh update runs\npreflight before checking locks. If preflight succeeds and an existing lock is\nfound, it returns that operation's status instead of starting another install.\nDuring downtime, preflight may fail before reaching the lock. Use `--status`\n(which bypasses preflight), or read the journal directly, to inspect recovery.\n\n## What happens to agent work and drafts\n\nOnly a panel recorded as actively working, with an exact conversation ID and a\nsupported resume command, is eligible to reopen. Built-in launch resolution\ncovers Claude, Codex and Cursor; custom commands need an explicit resume template\ncontaining `{sessionId}`. Eligibility is not a promise of success: `resumed`\nrequires fresh conversation-identity and readiness evidence from the running\nharness. An incompatible command or missing evidence yields `needs-attention`.\n\nReadiness requires an explicit adapter acknowledgment after the harness has\naccepted the exact conversation and is usable. The relaunched process receives\na fresh `PANE_UPDATE_RESUME_NONCE`. An adapter may emit one complete line\n`PANE_UPDATE_RESUME_READY=<nonce>:<actual-loaded-conversation-id>` only after that\nacceptance. The nonce binds the acknowledgment to this launch; an old marker,\nwrapper ID line, generic output, startup animation or CLI-ready timeout does not\nconfirm readiness. Login/trust blockers and command exit prevent success.\nDo not print this marker merely because a wrapper started or received a resume ID.\n\nThe built-in Claude/Codex/Cursor launch commands do not currently provide this\nacknowledgment themselves. Without an instrumented harness adapter, reopening\nthese conversations reports `needs-attention`; inspect the live panel manually.\nCustom reported/generated commands can integrate the same acknowledgment. The\nnative QA adapter proves this protocol with a harmless process, not a vendor\nlogin or conversation acceptance.\n\nReopening the conversation does not submit the interrupted task again. The agent\nmay return to its prompt; review it and decide what to send. Idle, completed and\napproval-blocked work stays stopped. The updater sends no `continue`, prompt,\nqueued mutation, or draft input.\n\nBefore shutdown, the checkpoint saves each panel's prior state, activity, launch\nprofile, conversation ID and launch/worktree/Session cwd. Unsubmitted composer\ntext detected in the terminal prevents automatic reopening. Inspect that panel's\n`priorState.screenText`, `initialInput` and `initialInputFile` in `checkpoint.json`\nfor the saved evidence. Screen text can contain other terminal content and is not\na promise of a complete editable draft. Recover and verify the intended text\nmanually before submitting anything. Do not share checkpoint files publicly.\n\n## Recovering an interrupted operation\n\nAll paths below use the same Pane data directory supplied with `--pane-dir`.\nThe operation directory is `<pane-dir>/updates/<id>/`:\n\n| File or directory | Purpose |\n| --- | --- |\n| `status.json`, `preflight.json`, `staged.json` | Progress, original executable/data/profile and CLI paths, staged package/backup paths |\n| `checkpoint.json`, `restoration.json` | Prior panel state and durable per-panel launch claims/outcomes |\n| `quiesced.json` | Original host's clean-shutdown acknowledgment |\n| `snapshot/data`, `snapshot/profile`, `snapshot/complete.json` | Pre-install copy; only the complete marker establishes a finished snapshot |\n\n### Inspecting the supervisor and remaining processes\n\nUse the operation ID from the journal. These commands inspect state; they do\nnot stop jobs or clear locks:\n\n| Platform | Supervisor and logs |\n| --- | --- |\n| macOS | `launchctl print \"gui/$(id -u)/com.pane.update.<id>\"`; read `<operation-directory>/supervisor.log` and `supervisor.plist` for the executable and arguments. |\n| Windows | `schtasks /Query /TN \"com.pane.update.<id>\" /V /FO LIST`; inspect `<operation-directory>/supervisor.xml`. Task Scheduler's History tab or the `Microsoft-Windows-TaskScheduler/Operational` event log may provide execution history if enabled. This adapter does not create a stdout/stderr log file. |\n| Linux | `systemctl --user status \"com.pane.update.<id>.service\"` and `journalctl --user -u \"com.pane.update.<id>.service\"`; transient units may already have been collected. |\n\nWindows relaunch can also create `com.pane.update.<id>-app`; inspect that task\nwhen present. A completed or missing supervisor is not proof that a child\ninstaller has exited. On macOS/Linux use `ps -axo pid,ppid,command`; on Windows\nuse `Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,ExecutablePath,CommandLine`\nin PowerShell. Compare full paths and arguments with this operation's staged\nruntime/helper, `staged.json` installer path and `preflight.json` installation.\nCheck helper children and installer processes separately; a reused PID, generic\nprocess name or stale Task Scheduler result is insufficient. If processes cannot\nbe attributed confidently, access is denied, or logs contradict the journal,\nleave locks and snapshots intact and ask a maintainer to inspect them.\n\n### Recovery steps\n\n1. Read status before starting another installer. If it is progressing, let it\n finish. If it stops progressing, inspect the helper supervisor and logs. A\n missing caller or terminal is expected; a PID alone is insufficient to decide\n the helper is dead. Do not remove locks while any helper/installer is active.\n2. For `failed` before shutdown, Pane remains available and the terminal operation\n releases its locks. Correct the reported preflight/staging problem and retry.\n3. For `needs-attention` with `hostReachable: true`, open the same installed Pane\n and inspect each panel outcome. Do not replay input whose delivery is unknown.\n Use the panel's normal explicit resume/restart control after reviewing its saved\n conversation and draft evidence. Already resumed panels must not be restarted\n merely because another panel needs attention.\n4. If Pane is stopped, use `preflight.json` to identify the exact executable,\n `paneDir` and `userData`. After establishing that no update helper/installer is\n still acting, launch that installation with `PANE_DIR` set to `paneDir` and\n `--user-data-dir=<userData>`. If the executable is missing or damaged, repair\n that same installation with the same or newer compatible official installer.\n Keep the operation directory and snapshot while diagnosing the failure.\n5. Never replace a migrated database with the old snapshot while the newer app is\n running. A rollback needs a compatible application and a complete data/profile\n snapshot restored together while all relevant processes are stopped. This\n updater does not automate or validate such a rollback. Get maintainer help\n when compatibility or installer state is uncertain.\n\nA post-shutdown `needs-attention` operation deliberately keeps its locks. There\nis no automated recovery/lock-clear command. After recovery is verified and all\noperation-owned helpers/installers have exited, the installation owner or a\nmaintainer can archive the operation and move aside only locks whose `owner.json`\ncontains this exact operation ID. Lock directories are listed in\n`preflight.json` as `lockDirectory` and `additionalLockDirectories`. Preserve the\njournal/checkpoint/snapshot; do not delete another operation's locks or infer\nownership from a reused PID. Incomplete or ambiguous locks require the same\nmanual inspection. If recovery of a crashed lock cleanup is itself interrupted,\nits persistent cleanup claim also requires manual inspection; another caller\nwill not steal it or report a new update as completed. Logout/reboot continuation\nis not guaranteed.\n"},{"path":"docs/UX_PRINCIPLES.md","title":"UX principles: progressive disclosure","text":"# UX principles: progressive disclosure\n\nGuidelines for Pane's UI design, informed by Linear and Claude Code CLI patterns.\n\n## Core Principles\n\n### 1. Prompt-First, Options-Second\n\nEvery interaction should lead with the primary action. For session creation, that's the prompt. For settings, that's the most common toggle. Everything else is secondary and should be layered behind progressive disclosure.\n\n**Do:** Auto-focus the primary input. Show one clear action.\n**Don't:** Present a wall of fields, cards, and sliders on first load.\n\n### 2. Opinionated Defaults Over Choice Overload\n\nProvide one good default rather than forcing a choice. Users interpret defaults as expert recommendations (the \"default effect\"). When 80%+ of users will pick the same option, make it the default and let the rest override.\n\n**Do:** Default to 1 session. Auto-generate names. Remember the last choice where a choice is needed.\n**Don't:** Present every agent as an equal-weight card that must be picked every time.\n\n### 3. The 80/20 Rule\n\nShow the 20% of features that 80% of users need. Hide the rest behind expandable sections, overflow menus, or context menus. Every visible element competes for attention.\n\n**Do:** Show prompt + create button. Put model selection, session count, branch config behind \"Session options.\"\n**Don't:** Show session count slider, tool cards, model dropdowns, and commit mode all at once.\n\n### 4. Minimal Status Indicators\n\nMatch Claude Code CLI's simplicity. Status should be informative without being distracting. Simple text beats animated spinners. A color-coded dot beats a pulsing badge with rotating icons.\n\n**Do:** Show `Thinking...` with subtle animated dots.\n**Don't:** Show spinning rings, bouncing dots, rotating icons cycling through 8 images, and 41 wacky status messages.\n\n### 5. Progressive, Not Hidden\n\nProgressive disclosure is not about hiding features — it's about layering them. Users should always see a clear affordance that more options exist (chevron, \"More options\" link, overflow menu icon).\n\n**Do:** Use `▸ Session options` with a chevron that expands to reveal fields.\n**Don't:** Remove the session count slider entirely. Don't bury settings with no visual indicator.\n\n## Patterns to Follow\n\n### Linear's \"One Good Way\"\n\nLinear deliberately constrains flexibility in favor of purpose-built workflows:\n- Issue creation needs only a **title** — everything else is optional\n- Smart pre-population (highlighted text auto-fills issue title)\n- Opinionated at the atomic level (labels and due dates are issue properties, not configurable)\n- Purpose-built over flexible\n\n**Pane equivalent:** Creating a pane needs nothing typed: the name is auto-generated, and the agent and prompt are optional.\n\n### Claude Code CLI's Minimalism\n\n- Shows simple `thinking...` during processing — no visual noise\n- Thinking content hidden by default, revealed via `Ctrl+O`\n- Status line is optional and customizable\n- Updates debounced at 300ms to prevent visual flicker\n\n**Pane equivalent:** Keep status dots simple. No animations unless state actually changes.\n\n### Collapsible Section Pattern\n\nUse for grouping secondary options:\n- Clear heading that describes what's inside\n- Chevron indicator (▸ collapsed, ▾ expanded)\n- Remember expansion state in user preferences\n- Keep disclosure levels below 3 (avoid nesting collapsibles inside collapsibles)\n\n### Overflow Menu Pattern\n\nUse when 3+ utility actions compete for toolbar space:\n- Keep the most-used action visible (with keyboard shortcut)\n- Group remaining actions behind `⋯` (MoreHorizontal) icon\n- Menu items should have icons + labels\n- Include state indicators where relevant (e.g., \"Sort: Newest first\")\n\n## When NOT to Simplify\n\nProgressive disclosure fails when critical information is hidden:\n- Never hide destructive actions behind progressive disclosure without confirmation\n- Don't hide information users need to make decisions (e.g., hiding that a session will use Codex instead of Claude)\n- Don't over-simplify settings that have security implications (API keys, permission modes)\n- If sales/engagement drops after hiding something, bring it back — trust trumps minimalism\n\n## Visual Design Principles\n\n### Reduce Visual Noise\n- Fewer colors, fewer animations, fewer competing elements\n- Use whitespace to create hierarchy\n- Sequential content flow (top-to-bottom)\n- One primary action per view\n\n### Animations\n- Only animate to communicate state changes (not for decoration)\n- Respect `prefers-reduced-motion` OS setting\n- Debounce status updates (300ms minimum)\n- Subtle > flashy. Text > spinners. Dots > bouncing balls.\n\n### Color\n- Use color sparingly and consistently for status:\n - Green: running/success\n - Yellow: waiting/attention needed\n - Red: error\n - Blue: new/unviewed activity\n - Gray: completed/inactive\n\n## References\n\n- [The Linear Method: Opinionated Software](https://www.figma.com/blog/the-linear-method-opinionated-software/)\n- [How we redesigned the Linear UI](https://linear.app/now/how-we-redesigned-the-linear-ui)\n- [Progressive disclosure in UX: Types and use cases (LogRocket)](https://blog.logrocket.com/ux-design/progressive-disclosure-ux-types-use-cases/)\n- [Designing for Progressive Disclosure (UXmatters)](https://www.uxmatters.com/mt/archives/2020/05/designing-for-progressive-disclosure.php)\n- [The Default Effect in UX](https://www.ux-bulletin.com/default-effect-in-ux/)\n- [Claude Code CLI Status Line Docs](https://code.claude.com/docs/en/statusline)\n"},{"path":"docs/assets/visual-style.md","title":"dcouple README visual language","text":"# dcouple README visual language\n\nUse a shared pixel-art world with a distinct setting for each product. The family\nreference is the [Agent Farm banner](https://github.com/greenfield-inc/agent-farm/blob/19f1fba8ef12050eef39c1b44e725c697b57b87b/docs/assets/agent-farm-banner.png).\n\n## Shared rules\n\n- Crisp coherent pixels, readable silhouettes, and isometric miniature scenes.\n- Charcoal midnight backgrounds, warm cream lettering, and soft lantern light.\n- Cream robots with dark faceplates and green eyes; clothes and props reflect their role.\n- Restrained foliage, wood, stone, and floating islands connect the settings.\n- Wide 3:1 banners: title left, scene right. Supporting images are roughly 2:1.\n- Use each project's own setting, props, and accent colors. Avoid repeating the same farm under a new title.\n- Keep technical labels and arrows accurate. Preserve real screenshots, demos, commands, and detailed diagrams.\n- Do not depict planned features as shipped, implicit sharing as public, or required human approval as automatic.\n- Keep alt text and text equivalents for information-bearing diagrams.\n\n## This repository\n\nA multilevel terminal workshop with three separate well-lit work booths, each holding a cream robot and terminal; a human coordinator views the booths from a central desk. Tiny branch-shaped paths lead to each booth, suggesting separate workspaces. Teal and copper accents, no musical props or farmer hats.\n\nLocal reference: [pane-banner.png](pane-banner.png).\n\nSupply that banner as an image reference for future generation. Match its pixel\ntreatment, characters, lighting, and palette while adapting the scene to the\nsection. Save final assets in `docs/assets/` and add the prompt to the\ngeneration record below; inspect labels and readability at README display width\nbefore embedding. Use new filenames for experiments rather than overwriting\nchosen art.\n\n## Generation record\n\nGenerated with the built-in image-generation tool, using the Agent Farm banner as\nthe style reference.\n\n### pane-banner.png\n\nUse case: stylized-concept\nAsset type: GitHub README hero banner, 3:1 landscape.\nUse the provided Agent Farm banner as STYLE REFERENCE only. Same crisp coherent pixel-art game world, charcoal midnight opaque backdrop, warm cream chunky title type, soft lantern light, cream robots with dark faceplates and green eyes, restrained foliage on isometric floating islands. Keep readable silhouettes and generous negative space. Give this product its own props and accent palette. No blur, smooth 3D, photorealism, watermarks, fake screenshots, tiny illegible text or extra slogans.\nScene: a multilevel terminal workshop with three separate well-lit work booths, each holding a cream robot and terminal; a human coordinator views the booths from a central desk. Tiny branch-shaped paths lead to each booth, suggesting separate workspaces. Teal and copper accents, no musical props or farmer hats.\nComposition: large title on left 45%, detailed but tidy scene on right 55%; safe margins. Exact title: \"PANE\". Use two lines for long titles. Only this title as text. Make a polished sibling identity within the dcouple family.\n\n### pane-workspaces.png\n\nUse case: infographic-diagram\nAsset type: GitHub README section illustration, 2:1 landscape.\nUse the provided Agent Farm banner as STYLE REFERENCE only. Same crisp coherent pixel-art game world, charcoal midnight opaque backdrop, warm cream chunky title type, soft lantern light, cream robots with dark faceplates and green eyes, restrained foliage on isometric floating islands. Keep readable silhouettes and generous negative space. Give this product its own props and accent palette. No blur, smooth 3D, photorealism, watermarks, fake screenshots, tiny illegible text or extra slogans.\nLocal identity: a multilevel terminal workshop with three separate well-lit work booths, each holding a cream robot and terminal; a human coordinator views the booths from a central desk. Tiny branch-shaped paths lead to each booth, suggesting separate workspaces. Teal and copper accents, no musical props or farmer hats.\nDiagram: Three panels left to right with exact headings and body lines: \"Repository\" / \"Saved base repo\"; \"Pane\" / \"Worktree and branch\"; \"Panels\" / \"Terminals share the worktree\". Arrows from Repository to Pane to Panels. Illustrate a project folder, a separate workshop booth, then two small terminals within that same booth. All panels are conceptual, not an app screenshot. No implication that each panel gets its own worktree.\nLarge highly legible cream text on uncluttered dark areas; graphical precision and factual readability take priority over scenery. No large product title. Match the supplied family reference.\nInput images for the section illustration: first is the family style reference; second is this project's generated banner. Match the second image's character, props and accents.\n"},{"path":"docs/lint/anti-slop.md","title":"Anti-slop policy","text":"# Anti-slop policy\n\nPane vendors the 15 Oxlint rules from `dmmulroy/anti-slop` at commit\n`cd064fe602b5915ff35e1e1c20836ca9bcb3729a` in\n`tools/oxlint/anti-slop`. The snapshot is intentionally local so the rules run\ndeterministically and can be reviewed alongside the code they gate.\n\nThe original full-repository scan covered `frontend`, `main`,\n`packages/runpane`, `shared`, `tests`, `scripts`, all non-vendored `tools`, and\nevery Playwright config. It found 2,463 findings in 240 files. The cleanup\ncampaign reduced every rule to a zero baseline, so all 15 rules now block\nregressions in the root lint command.\n\n| Rule | Baseline findings/files | Lane | Reason |\n| --- | ---: | --- | --- |\n| `no-object-parameters` | 0/0 | blocking | Prevent new positional object bags without an explicit contract. |\n| `no-reflect-apply` | 0/0 | blocking | Prefer direct, typed calls over reflective dispatch. |\n| `no-unknown-type-aliases` | 0/0 | blocking | Prevent aliases that rename uncertainty instead of parsing it. |\n| `no-widen-then-assert` | 0/0 | blocking | Prevent discarding known types and recovering them with assertions. |\n| `no-reflect-get` | 4/1 | blocking | Reflective reads were replaced with declared or descriptor-based access. |\n| `no-chained-type-assertions` | 89/22 | blocking | Boundary contracts avoid type laundering through chained assertions. |\n| `no-conditional-empty-object-spread` | 34/16 | blocking | Optional fields are assembled explicitly. |\n| `no-known-value-widening` | 142/65 | blocking | Known values retain inferred or named domain types. |\n| `no-module-mocking` | 31/13 | blocking | Tests use explicit seams or real modules. |\n| `no-runtime-typeof` | 417/94 | blocking | Runtime boundaries use parsers, feature checks, or domain guards. |\n| `no-shape-in-symbol-names` | 18/1 | blocking | Symbols describe domain intent rather than structural shape. |\n| `no-unknown-parameters` | 250/64 | blocking | Uncertain input is parsed at its boundary. |\n| `no-unknown-returns` | 37/21 | blocking | Adapters return parsed domain values. |\n| `no-unsafe-dictionary-type` | 192/51 | blocking | Dictionary contracts use bounded keys or validated JSON objects. |\n| `require-safety-comment-for-type-assertion` | 1,249/177 | blocking | Remaining necessary assertions document their checked invariant. |\n\nRun `pnpm lint` for the blocking policy. `pnpm lint:ox:extra` and\n`pnpm lint:ox:extra:details` remain available as harness checks, but no rules\nremain in the advisory lane.\n\n## Root cause policy\n\nFor agent-written Pane code, the most important class is uncertainty that leaks\npast an I/O boundary and later becomes a widening or assertion. Ask: **is this\nchange fixing the root cause or a symptom?** The root fix parses IPC, daemon,\nfilesystem, process, or browser input once into a named domain type. A late\n`unknown` alias, representation ladder, dictionary, or assertion usually treats\nthe symptom. Blocking findings should guide that review.\n\nAny future anti-slop rule must reach a zero baseline for the full scope before\njoining the blocking configuration. Do not silence a class globally to make a\npromotion appear clean.\n\nThe root development toolchain requires Node 22.18 or newer because Oxlint's\nTypeScript plugin runs under the developer Node process. This does not change\nElectron 41's bundled Node 24 runtime, and the published `runpane` wrapper keeps\nits Node 20 runtime floor.\n"},{"path":"docs/lint/oxlint-overlap.md","title":"Oxlint and ESLint overlap","text":"# Oxlint and ESLint overlap\n\n`pnpm lint:ox` is the first blocking lane. ESLint remains blocking for rules\nwhose current Pane semantics are not reproduced by Oxlint 1.76.0, and continues\nto report the repository's existing warning-level checks. An ESLint rule is\ndisabled in `tools/eslint/oxlint-overlap.cjs` only when the explicit blocking\nOxlint rule below replaces it.\n\nThe 79 unique ESLint error-level rules were resolved from the frontend, main,\nand RunPane flat configs. Seventy-eight have blocking equivalents. `no-octal`\nhas no Oxlint 1.76.0 equivalent and therefore stays in ESLint.\n\n| ESLint rule | Blocking Oxlint rule | Scope/disposition |\n| --- | --- | --- |\n| `for-direction` | `eslint/for-direction` | common; moved |\n| `no-async-promise-executor` | `eslint/no-async-promise-executor` | common; moved |\n| `no-case-declarations` | `eslint/no-case-declarations` | common; moved |\n| `no-compare-neg-zero` | `eslint/no-compare-neg-zero` | common; moved |\n| `no-cond-assign` | `eslint/no-cond-assign` | common; moved |\n| `no-constant-binary-expression` | `eslint/no-constant-binary-expression` | common; moved |\n| `no-constant-condition` | `eslint/no-constant-condition` | common; moved |\n| `no-control-regex` | `eslint/no-control-regex` | common; moved |\n| `no-debugger` | `eslint/no-debugger` | common; moved |\n| `no-delete-var` | `eslint/no-delete-var` | common; moved |\n| `no-dupe-else-if` | `eslint/no-dupe-else-if` | common; moved |\n| `no-duplicate-case` | `eslint/no-duplicate-case` | common; moved |\n| `no-empty` | `eslint/no-empty` | frontend only; moved (main/RunPane warning retained) |\n| `no-empty-character-class` | `eslint/no-empty-character-class` | common; moved |\n| `no-empty-pattern` | `eslint/no-empty-pattern` | common; moved |\n| `no-empty-static-block` | `eslint/no-empty-static-block` | common; moved |\n| `no-ex-assign` | `eslint/no-ex-assign` | common; moved |\n| `no-extra-boolean-cast` | `eslint/no-extra-boolean-cast` | common; moved |\n| `no-fallthrough` | `eslint/no-fallthrough` | common; moved |\n| `no-global-assign` | `eslint/no-global-assign` | common; moved |\n| `no-invalid-regexp` | `eslint/no-invalid-regexp` | common; moved |\n| `no-irregular-whitespace` | `eslint/no-irregular-whitespace` | common; moved |\n| `no-loss-of-precision` | `eslint/no-loss-of-precision` | common; moved |\n| `no-misleading-character-class` | `eslint/no-misleading-character-class` | common; moved |\n| `no-nonoctal-decimal-escape` | `eslint/no-nonoctal-decimal-escape` | common; moved |\n| `no-octal` | — | common; retained in ESLint |\n| `no-prototype-builtins` | `eslint/no-prototype-builtins` | common; moved |\n| `no-regex-spaces` | `eslint/no-regex-spaces` | common; moved |\n| `no-self-assign` | `eslint/no-self-assign` | common; moved |\n| `no-shadow-restricted-names` | `eslint/no-shadow-restricted-names` | common; moved |\n| `no-sparse-arrays` | `eslint/no-sparse-arrays` | common; moved |\n| `no-unexpected-multiline` | `eslint/no-unexpected-multiline` | common; moved |\n| `no-unsafe-finally` | `eslint/no-unsafe-finally` | common; moved |\n| `no-unsafe-optional-chaining` | `eslint/no-unsafe-optional-chaining` | common; moved |\n| `no-unused-labels` | `eslint/no-unused-labels` | common; moved |\n| `no-unused-private-class-members` | `eslint/no-unused-private-class-members` | common; moved |\n| `no-useless-backreference` | `eslint/no-useless-backreference` | common; moved |\n| `no-useless-catch` | `eslint/no-useless-catch` | common; moved |\n| `no-var` | `eslint/no-var` | common; moved |\n| `prefer-const` | `eslint/prefer-const` | frontend only; moved (main/RunPane warning retained) |\n| `prefer-rest-params` | `eslint/prefer-rest-params` | common; moved |\n| `prefer-spread` | `eslint/prefer-spread` | common; moved |\n| `require-yield` | `eslint/require-yield` | common; moved |\n| `use-isnan` | `eslint/use-isnan` | common; moved |\n| `valid-typeof` | `eslint/valid-typeof` | common; moved |\n| `@typescript-eslint/ban-ts-comment` | `typescript/ban-ts-comment` | common; moved |\n| `@typescript-eslint/no-array-constructor` | `typescript/no-array-constructor` | common; moved |\n| `@typescript-eslint/no-duplicate-enum-values` | `typescript/no-duplicate-enum-values` | common; moved |\n| `@typescript-eslint/no-empty-object-type` | `typescript/no-empty-object-type` | common; moved |\n| `@typescript-eslint/no-explicit-any` | `typescript/no-explicit-any` | common; moved |\n| `@typescript-eslint/no-extra-non-null-assertion` | `typescript/no-extra-non-null-assertion` | common; moved |\n| `@typescript-eslint/no-misused-new` | `typescript/no-misused-new` | common; moved |\n| `@typescript-eslint/no-namespace` | `typescript/no-namespace` | common; moved |\n| `@typescript-eslint/no-non-null-asserted-optional-chain` | `typescript/no-non-null-asserted-optional-chain` | common; moved |\n| `@typescript-eslint/no-require-imports` | `typescript/no-require-imports` | frontend only; moved (main/RunPane warning retained) |\n| `@typescript-eslint/no-this-alias` | `typescript/no-this-alias` | common; moved |\n| `@typescript-eslint/no-unnecessary-type-constraint` | `typescript/no-unnecessary-type-constraint` | common; moved |\n| `@typescript-eslint/no-unsafe-declaration-merging` | `typescript/no-unsafe-declaration-merging` | common; moved |\n| `@typescript-eslint/no-unsafe-function-type` | `typescript/no-unsafe-function-type` | common; moved |\n| `@typescript-eslint/no-unused-expressions` | `typescript/no-unused-expressions` | common; moved |\n| `@typescript-eslint/no-wrapper-object-types` | `typescript/no-wrapper-object-types` | common; moved |\n| `@typescript-eslint/prefer-as-const` | `typescript/prefer-as-const` | common; moved |\n| `@typescript-eslint/prefer-namespace-keyword` | `typescript/prefer-namespace-keyword` | common; moved |\n| `@typescript-eslint/triple-slash-reference` | `typescript/triple-slash-reference` | common; moved |\n| `react-hooks/rules-of-hooks` | `react/rules-of-hooks` | frontend only; moved |\n| `jsx-a11y/alt-text` | `jsx-a11y/alt-text` | frontend only; moved |\n| `jsx-a11y/anchor-has-content` | `jsx-a11y/anchor-has-content` | frontend only; moved |\n| `jsx-a11y/aria-props` | `jsx-a11y/aria-props` | frontend only; moved |\n| `jsx-a11y/aria-proptypes` | `jsx-a11y/aria-proptypes` | frontend only; moved |\n| `jsx-a11y/aria-unsupported-elements` | `jsx-a11y/aria-unsupported-elements` | frontend only; moved |\n| `jsx-a11y/click-events-have-key-events` | `jsx-a11y/click-events-have-key-events` | frontend only; moved |\n| `jsx-a11y/heading-has-content` | `jsx-a11y/heading-has-content` | frontend only; moved |\n| `jsx-a11y/iframe-has-title` | `jsx-a11y/iframe-has-title` | frontend only; moved |\n| `jsx-a11y/no-access-key` | `jsx-a11y/no-access-key` | frontend only; moved |\n| `jsx-a11y/no-distracting-elements` | `jsx-a11y/no-distracting-elements` | frontend only; moved |\n| `jsx-a11y/no-static-element-interactions` | `jsx-a11y/no-static-element-interactions` | frontend only; moved with `onClick`/expression options |\n| `jsx-a11y/role-has-required-aria-props` | `jsx-a11y/role-has-required-aria-props` | frontend only; moved |\n| `jsx-a11y/role-supports-aria-props` | `jsx-a11y/role-supports-aria-props` | frontend only; moved |\n| `jsx-a11y/tabindex-no-positive` | `jsx-a11y/tabindex-no-positive` | frontend only; moved |\n\nBoth Oxlint configs disable the broad `correctness` category. This is\nintentional: the blocking contract is the explicit list above plus the 15\nzero-baseline anti-slop rules, rather than an unstable category preset.\n\nA Profiler-based render counter is deliberately deferred. Add one with the\nfirst named React subscription/render regression and a component test renderer\nthat can mount the affected Pane surface; React Scan supplies exploratory\nruntime evidence but is not a deterministic unit-test assertion.\n"},{"path":"docs/remote-daemon-lifecycle.md","title":"Remote Daemon Lifecycle","text":"# Remote Daemon Lifecycle\n\nThis is the implementation checklist for Remote Pane setup, teardown, and runtime switching. It exists to keep config writes, runtime controller actions, and renderer refreshes in sync.\n\n## Runtime Roles\n\n- Host lifecycle is owned by `PaneRemoteTransportController` and `remoteHostRuntimeStateStore`.\n- Client lifecycle is owned by `RemotePaneClientController`.\n- IPC handlers in `main/src/ipc/remoteDaemon.ts` orchestrate config writes and controller calls.\n- Renderer runtime changes are reconciled through `remote-daemon:resync-required`. Main sends `{ hostChanged: true }` when the active runtime changes; a reconnect to the same host sends it without a payload.\n- A resync refetches config, Panes, the active Pane's panels, repositories, Sessions and pinned Sessions, pending permission prompts, loaded archived Panes, archive progress, the host's terminal shells, and an open Usage view. When the host changed, it also leaves an open repository view, since repository ids are per host, loads that host's expanded repositories, opened Sessions and open Archived groups, and asks again about that host's interrupted Panes.\n- Expanded repositories are saved per host: this computer under `treeView.expandedProjects`, a remote host under `treeView.expandedProjects@<profile id>`.\n- Opened Sessions and open Archived groups are saved per host the same way, under `treeView.expandedSessions[@<profile id>]` and `treeView.expandedArchivedProjects[@<profile id>]`. A Session not in the opened list shows collapsed. Older `treeView.collapsedSessions[@<profile id>]` rows are no longer read and stay in `ui_state` unused.\n- Where the user was is saved per host too, under `navigation.lastLocation` / `navigation.lastLocation@<profile id>`: the active view, the open repository view's project id, and the open Pane. A switch clears the outgoing host's selection as above and then restores the incoming host's, validating every id against that host's freshly loaded repositories and Panes — anything archived or gone degrades to the home view, and the dead memory is overwritten rather than retried. `frontend/src/utils/hostNavigationMemory.ts` owns both halves; the renderer names the host on each read and write, because it can still be showing the outgoing host while main has already switched runtimes. Restoring selects one Pane, as a click would, and never revives every terminal once open on that host. The memory also holds this desktop's selected Session: launch and host switches both restore it, and the host's last-used Session is only the fallback when there is none. Another client's selection never moves it. The tabs and split inside each Pane are a separate per-Pane memory, `paneLayout.<pane id>` / `paneLayout.<pane id>@<profile id>` (`frontend/src/utils/paneLayoutMemory.ts`); the host's stored layout and active tab are the last used by any client and seed a Pane this desktop has not shown. Archiving or deleting a Pane drops its memory. Apart from the Session, the location memory is not applied at app startup; only a host switch reads it.\n- The terminal shell picker reads and writes the active host's shell (`terminal:get-shell-settings`, `terminal:set-preferred-shell`). It appears only when that host runs Windows.\n- `pane:focus-requested` and `pane:open-link` are host events. `runpane panes focus` and `runpane panes create --focus` move only the desktop window attached to that host as its local host; a remote-mode client ignores them. A host- or agent-initiated tab activation (`panel:activeChanged`) moves only clients already showing that Pane, and a new Pane switches only the client whose request created it (`clientRequestId`, echoed on `session:created`). A pane:// link opened on a remote-mode client is resolved by its renderer against the active host.\n- Remote terminals ack output with `terminal:ack`. This window's ptyHost port reaches only local terminals.\n- The Remote Pane PWA keeps the same rules in the browser. When its event stream returns after a drop (network loss, or the native app resuming), it refetches Panes, the selected Pane's panels, Sessions, and loaded archived Panes. Connecting to a host or disconnecting clears all host-scoped state first: Panes, panels, Sessions, the open Session, archived Panes, open dialogs, and notification settings.\n\n## Lifecycle Matrix\n\n| Action | Main-process side effect | Renderer side effect |\n| --- | --- | --- |\n| Import connection and connect succeeds | Activate profile, save/dedupe profile, set remote mode | Resync config, sessions, panels |\n| Import connection and connect fails | Save/dedupe profile, keep current active runtime | No runtime resync |\n| Connect saved profile | Activate profile before persisting remote mode | Resync config, sessions, panels |\n| Switch to local runtime | Disconnect active remote client, save local mode | Resync config, sessions, panels, clear stale active session |\n| Delete inactive profile | Remove profile | No runtime resync |\n| Delete active profile | Switch local, remove profile, save local mode | Resync config, sessions, panels |\n| Enable or update host | Save host config, transport controller syncs to live or error | Host-state event updates UI |\n| Stop host | Save disabled host config, transport controller stops server | Host-state event updates UI |\n| Disconnect host clients | Drop matching SSE clients | Host-state event updates client count |\n| Revoke host client | Remove saved client record, drop matching SSE clients | Host-state event updates client count |\n\n## Guardrails\n\n- Do not persist remote mode until the selected profile has successfully activated.\n- Failed import-connect saves the profile but must not switch runtime or emit a renderer resync.\n- Connected remote clients are runtime state, not saved client records.\n- Current Pane Data hosting is live only while that Pane app is running.\n- Isolated daemon data can install a background service; Current Pane Data should not.\n\n## Terminal Input\n\nDesktop and browser clients send at most one terminal input request per panel at a time. Keys typed while a request is pending are combined into the next request, preserving their order without a separate round trip for every buffered key. A bare Escape always ends a combined request, because terminal apps read Escape followed by another key in the same write as an Alt shortcut. Different panels and other remote commands remain independent.\n\nInput requests are not retried. On failure, disconnect, or a ten-second input timeout, queued input is discarded and outstanding callers are rejected. An interrupted request may already have reached the host, so its input must not be replayed after reconnecting. This uses the existing HTTP API and requires no host protocol upgrade.\n\n## Desktop media previews\n\nThe explorer's previews (images, PDFs, fonts, video, audio and the text formats) work from a remote desktop. The renderer still gets a `pane-media://preview/<token>` URL from its own main process; the grant records which host issued it, and a request after a host switch, or after a switch to local mode, gets 403. Each request goes to the host's `GET /media?sessionId=&filePath=` with the connection's usual authentication on that door, and its `Range` header passes through, so video and audio seek without downloading the whole file. The host checks that the path has a preview kind and resolves it through `file:getPath`, the same worktree and symlink boundary the host uses for its own previews, and answers 404 otherwise. Archive and SQLite listings use the daemon-owned `file:preview-list` command, so they run on the host too. \"Open with system app\" on a remote desktop downloads the whole file through the same route into a new temporary folder on that desktop and opens the copy, so edits in the system app do not reach the host. The copy keeps the host file name when that name is valid on every desktop OS, and is otherwise named `preview` plus the extension. A failed download or open removes the folder. While remote mode has no connected host, previews and Open fail; they never read the desktop's own files. \"Reveal in folder\" is available only on the host.\n\n## Desktop browser file previews\n\nBefore mounting a `file://` webview, the desktop renderer asks the local main process to prepare its session. In remote mode, an isolated in-memory Electron partition handles every file request through `panels:read-browser-file` on the existing authenticated remote connection. The original URL, including its filename, remains intact, so relative CSS, images, scripts and linked HTML pages resolve normally. Local-mode previews, like every other browser tab, use the shared `persist:pane-browser` store; Settings → Privacy → Clear browser data clears it on the computer showing the browser and is never forwarded to the host. This does not add browser panels to the Remote PWA.\n\nThe host reader requires an existing browser panel whose saved entry URL identifies a regular file. It serves regular files within that entry page's directory and descendants, with a 16 MiB limit per file. It checks canonical paths to reject traversal and symlinks/junctions outside that directory at validation time; it never lists directories. The host filesystem is trusted: these checks do not guarantee atomic containment against a host process maliciously replacing files or directories during a read. Thus opening a file shares its containing bundle directory with connected clients, not the entire worktree or disk. This endpoint has no caller-supplied root. File entry URLs are host-owned: HTTP command provenance is carried through async handlers, and the panel manager rejects remote attempts to create or retarget file previews (including through RunPane). Clients can update an unchanged entry but must open a new file on the host. The one exception is previewing a worktree HTML file: a remote desktop names the file and the host creates the browser tab for it, so links within that file's folder load through the reader above. Arbitrary `file://` URLs from a client stay refused.\n\nA host switch removes cached browser panels before asynchronous resync, so outgoing URLs and partitions cannot survive even when both hosts have identical panel IDs. Panel-list requests carry a host generation and refetch if their response belongs to an outgoing host. Incoming panel data recreates the guest and prepares its file session anew. While file preparation is unresolved, navigation does not mount a guest or persist URLs; starting preparation also cancels earlier pending URL writes. Remote file navigation is client-local and does not overwrite the host's saved entry URL. This keeps nested page navigation from changing the bundle boundary. If the host replaces the file entry with an HTTP(S) URL, the client returns to the shared browser store and ordinary navigation persistence, including when it was already viewing that HTTP page. Missing/deleted panels revoke reads; changing the connected profile or its base URL invalidates the previous session's requests, including responses already in flight. A failed fetch renders an error message and never falls back to the client's filesystem. For a file outside the entry page's directory, the message is the host's refusal: it names the file, that directory and the ways to fix the page. While the client is still connected to that host, the tab also shows that refusal under its address bar until its next page, so a refused stylesheet or script that the page never displays is still visible. Both desktop client and host need this version; an older host reports an unavailable preview.\n\nRun `pnpm build:main`, start the frontend with `pnpm --filter frontend dev`, then set `PANE_ELECTRON_E2E=1` and run `pnpm exec playwright test tests/remote-browser-files.spec.ts`. Set `PLAYWRIGHT_PORT` if using a different dev-server port. This opt-in Electron regression uses isolated temporary Pane data and a loopback host with the production HTTP/SSE transport. It verifies the actual BrowserPanel, CSS/image loads, linked-page navigation, the preserved filename, access denial and disconnect behavior, and saves `remote-client-rendered.png`. Linux requires a display (for example Xvfb).\n"},{"path":"docs/remote-terminal-flow-control.md","title":"Remote terminal output flow control","text":"# Remote terminal output flow control\n\nTerminal panels begin hidden until a client registers visibility with\n`terminal:setVisibility`. Hidden panels still stream to daemon subscribers and\nretain scrollback, but do not wait for renderer acknowledgements. A raw SSE\nsubscriber does not register visibility by merely opening `/events`.\n\nVisible panels pause their PTY at 100,000 unacknowledged characters and resume\nat 5,000. The five-second safety timeout abandons the stale acknowledgement\ncount before resuming; fresh output can still cross the high watermark and\npause again. This escape hatch prevents one lost acknowledgement from causing\nevery subsequent flush to wait five seconds. It does not disable backpressure\nfor a visible client that consistently fails to acknowledge output.\n\nAcknowledgements have no sequence or generation. After timeout recovery, a\ndelayed acknowledgement can temporarily reduce the fresh backlog count. Keeping\nforgiven debt would instead discard valid new acknowledgements when the old ones\nwere lost. Strict accounting across recovery would require a protocol change.\n\nDesktop clients connected to a remote host acknowledge processed output through\n`terminal:ack`, including when a PTY ID is known. Only local terminals can use\nthe local PTY host MessagePort. Clients attached to an already-running panel\ncan acknowledge by panel ID without receiving a new `terminal:ptyReady` event.\n\nAcknowledgement accounting is per terminal, not per viewer: one responsive\nviewer can release a pause even if another viewer is silent. Removing the last\nviewer clears pending flow control and resumes the PTY. Registered viewers\nsurvive a PTY host respawn, and attach/detach changes during an asynchronous spawn\nare applied to the replacement terminal.\n\nFocused regression checks:\n\n```sh\npnpm --filter main test run src/ptyHost/flowControl.test.ts src/services/terminalPanelManager.persistence.test.ts src/services/terminalPanelManager.test.ts\npnpm --filter frontend test src/utils/terminalAck.test.ts\n```\n"},{"path":"docs/troubleshooting/SETUP_TROUBLESHOOTING.md","title":"Setup Troubleshooting Guide","text":"# Setup Troubleshooting Guide\n\n## Python distutils Error\n\nIf you encounter this error during `pnpm run setup`:\n```\nModuleNotFoundError: No module named 'distutils'\n```\n\nThis happens because Python 3.12+ removed the `distutils` module that `node-gyp` depends on.\n\n### Quick Fix:\n```bash\nbrew install python-setuptools\n```\n\n### Alternative: Use Python 3.11 with pyenv\n\n```bash\nbrew install pyenv\npyenv install 3.11.9\npyenv global 3.11.9\n```\n\nThen run `pnpm run setup` again.\n\n## Other Common Issues\n\n### Modified terminal keys appear as escape-sequence text on Windows/WSL\n\nIf Alt+Up inserts `[1;3A`, Shift+Left inserts `[1;2D`, or F6 inserts `[17~`,\ncheck whether the application launched from WSL is actually a Windows executable.\nA Windows npm launcher can select `node.exe` even when invoked from a Linux shell.\n\nPane must honor ConPTY's `CSI ? 9001 h` request using xterm's\n`vtExtensions.win32InputMode`. Without it, a Windows console application reached\nthrough WSL can receive a complete VT sequence as individual character key\nrecords. A Linux raw-stdin reader can still receive the exact bytes correctly;\nthis symptom alone does not demonstrate split IPC messages or PTY writes.\nThe negotiated mode must also survive terminal snapshot restoration. The `@`\ninterceptor interprets Win32 key records locally and forwards unconsumed records\nunchanged. This handling applies to all terminal applications.\n\nThe browser regression suite is `tests/terminal-keyboard-input.spec.ts`; it checks\nwhole input messages for Alt+Up, Shift+Left, Shift+Up, and F6 with and without\nnegotiated Windows input mode. For transport diagnosis, compare a Linux raw-stdin\nreader with a Windows `ReadConsoleInputW` reader launched through WSL: compare\nbytes for the former and virtual keys/modifiers for the latter.\n\nSee Microsoft's [Win32 input protocol specification](https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md).\n\n### electron-rebuild failures\n- Ensure Xcode Command Line Tools are installed: `xcode-select --install`\n- Clear node_modules and reinstall: `rm -rf node_modules && pnpm install`\n\n### pnpm permission errors\n- Never use `sudo` with pnpm\n- Fix npm permissions: `npm config set prefix ~/.npm-global`\n\n## Windows Build Requirements\n\n### Spectre-mitigated Libraries Error\n\nIf you encounter this error during `pnpm run setup` on Windows:\n```\nLINK : fatal error LNK1181: cannot open input file 'MSVCRT.lib'\n```\n\nOr similar errors mentioning missing `.lib` files, this means you need to install the Spectre-mitigated libraries in Visual Studio.\n\n### Solution:\n\n1. **Install Visual Studio 2022** (Community Edition is free):\n - Download from [https://visualstudio.microsoft.com/](https://visualstudio.microsoft.com/)\n \n2. **Install Required Components**:\n - Open **Visual Studio Installer**\n - Click **Modify** on your Visual Studio 2022 installation\n - Go to the **Individual components** tab\n - Search for \"Spectre\" in the search box\n - Check the following components:\n - `MSVC v143 - VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)`\n - `MSVC v143 - VS 2022 C++ ARM64/ARM64EC Spectre-mitigated libs (Latest)` (if building for ARM64)\n - Click **Modify** to install\n\n3. **Restart your terminal** and run `pnpm run setup` again\n\n### Why is this needed?\n\nNode.js native modules on Windows are built with Visual Studio's C++ compiler. Recent security updates require Spectre-mitigated libraries to be installed separately. These libraries provide protection against Spectre vulnerability exploits.\n\n### Alternative: Use Pre-built Binaries\n\nIf you only need to run Pane, download the Windows installer from [GitHub Releases](https://github.com/greenfield-inc/Pane/releases/latest) instead of building from source.\n"},{"path":"docs/troubleshooting/WINDOWS_APP_HANG.md","title":"Windows freezes on startup or window focus","text":"# Windows freezes on startup or window focus\n\nA September 3, 2026 report described the latest version as \"lagging\" and \"freezing all the time.\" The reported investigation found a 250 MB `sessions.db`, approximately 200 MB of terminal history across 92 stopped sessions, an accidental Git repository at the Windows user profile root, and the isolated PTY host disabled. Windows recorded `AppHangTransient`.\n\n## Prevention\n\n- Startup loads only logs and browser panels that need restart cleanup. Terminal state is restored on demand when accessed, including history for stopped and archived sessions.\n- Workspace summaries exclude raw and serialized terminal buffers in SQLite before returning rows to JavaScript. Summary reads do not replace complete cached panel state or write reduced state back to disk.\n- Automatic Git status checks and filesystem watching skip the home directory, log a diagnostic, and report unknown Git status. Windows path comparison ignores case and normalizes separators and dot segments. Choose the actual project folder instead of `C:\\Users\\<user>`. Existing repository records and `.git` directories are not deleted.\n- New configurations no longer use the home directory as an implicit Git repository. Legacy home-directory `gitRepoPath` values are rejected by the migration accessor.\n- The isolated PTY host defaults to enabled on Windows, including existing configurations without a `usePtyHost` value. Explicit `false` values are preserved. Enable **Settings > Advanced > Use isolated PTY host** and restart Pane to change an existing opt-out. `PANE_USE_PTY_HOST=1` still forces it on for development.\n\nTerminal history remains on disk. The existing 21-day retention policy for archived sessions is unchanged, and no full `VACUUM` runs during startup. A large database file does not by itself mean all history is resident in the JavaScript heap.\n\n## Manual Windows verification\n\n1. Use an isolated `PANE_DIR` with a copy of a database containing many stopped and archived terminal panels. Launch Pane, inspect responsiveness and heap use, then reopen an old terminal and verify its history.\n2. In an isolated test profile with a home-directory Git repository, select a session whose worktree path is that home directory. Repeatedly blur/focus Pane. Confirm no recursive watcher or Git status scan starts, and check the diagnostic in the main-process log. Verify a normal project beneath the profile still refreshes.\n3. Start with a config missing `usePtyHost`: confirm the settings toggle is enabled and the supervisor forks on Windows. Repeat with explicit `false`, then enable the setting and restart.\n\nThe automated tests cover persistence, summary projections, path normalization, Git scan entry points, and platform defaults. They do not reproduce Windows `AppHangTransient` or validate a packaged Windows PTY host.\n"},{"path":"docs/troubleshooting/WINDOWS_CLAUDE_CODE_SPAWN.md","title":"Windows Claude Code Spawn Issue - RESOLVED","text":"# Windows Claude Code Spawn Issue - RESOLVED\n\n## Status: ✅ FIXED\n\nBoth issues have been resolved. Claude Code now spawns correctly on Windows.\n\n## Note: the original reproduction no longer applies\n\nThe failure described below was reproduced against a copy of Claude Code installed **inside\nPane's own `node_modules`** — Pane declared `@anthropic-ai/claude-code` as a dependency, so\n`Pane/node_modules/.bin/claude` existed and could win PATH resolution during development.\nThat dependency has since been removed (nothing ever imported it; Pane spawns the user's\nown CLI), so the specific `MODULE_NOT_FOUND` path shown below — resolving into Pane's pnpm\nstore — can no longer occur.\n\n**The fixes described here are still live and still required.** Both apply to any\nglobally installed `claude`:\n\n- npm/pnpm bin stubs are shell scripts on Windows, so `node-pty` cannot execute them\n directly (error code 193). The proactive Node.js fallback in `AbstractCliManager.ts`\n handles this.\n- Those stubs resolve their target through `$basedir`-relative paths, which break when the\n process is spawned with a worktree as `cwd`. `findCliNodeScript()` in `nodeFinder.ts`\n parses the stub and invokes the resolved `.js` file directly.\n\nRead the sections below as the rationale for that code. Only the reproduction environment\nis out of date.\n\n## Problem Summary\n\nClaude Code was failing to spawn properly on Windows when Pane runs it in a worktree directory different from where `@anthropic-ai/claude-code` is installed.\n\n## Root Cause Analysis\n\n### Issue 1: Error Code 193 (Fixed)\n\n**Error:** `Failed to spawn Claude Code: Cannot create process, error code: 193`\n\n**Cause:** Error code 193 on Windows means \"not a valid Win32 application\". This occurs when trying to execute a shell script (like npm bin stubs) directly via `node-pty` instead of through a shell or Node.js.\n\n**Fix Applied:** The PTY host classifies the spawn failure as `E193` (`classifySpawnError()` in `main/src/ptyHost/ptyHostMain.ts`). `AbstractCliManager.ts` matches the error message (`error code: 193` or `not a valid Win32 application`) and retries with the Node.js fallback.\n\n**Location:** `main/src/ptyHost/ptyHostMain.ts` and `main/src/services/panels/cli/AbstractCliManager.ts`\n\n### Issue 2: Module Not Found (Fixed)\n\n**Error:**\n```\nError: Cannot find module 'C:\\Users\\<user>\\<repo>\\worktrees\\@anthropic-ai\\claude-code\\cli.js'\n```\n\n**Cause:** The npm bin stub at `node_modules/.bin/claude` uses a relative path:\n```bash\nexec node \"$basedir/../@anthropic-ai/claude-code/cli.js\" \"$@\"\n```\n\nWhen the `cwd` is set to the worktree directory, the relative path resolves incorrectly.\n\n## Solution Applied\n\n### Fix 1: Proactive Windows Node.js Fallback\n\nOn Windows, we now proactively use the Node.js fallback instead of waiting for failures:\n\n```typescript\n// In spawnPtyProcess() at main/src/services/panels/cli/AbstractCliManager.ts\nif (os.platform() === 'win32') {\n this.logger?.verbose(`[${this.getCliToolName()}] Windows detected, using Node.js fallback proactively`);\n nodeFallbackTools.add(needsNodeFallbackKey);\n}\n```\n\nThis bypasses the problematic shell script execution entirely.\n\n### Fix 2: Enhanced pnpm Path Resolution\n\nThe `findCliNodeScript()` function in `main/src/utils/nodeFinder.ts` was enhanced to properly handle pnpm's node_modules structure:\n\n1. **Detects pnpm directory**: Checks for `node_modules/.pnpm` directory\n2. **Known package mappings**: Has specific mappings for Claude Code and Codex packages\n3. **Generic pnpm search**: Falls back to searching for any matching package\n4. **Proper path construction**: Correctly builds paths like:\n ```\n node_modules/.pnpm/@anthropic-ai+claude-code@2.0.0/node_modules/@anthropic-ai/claude-code/cli.js\n ```\n\n## Technical Details\n\n### npm Bin Stub Structure (Windows)\n\nThe file at `Pane/node_modules/.bin/claude` is a shell script that:\n1. Sets `NODE_PATH` with pnpm-specific paths\n2. Executes: `node \"$basedir/../@anthropic-ai/claude-code/cli.js\" \"$@\"`\n\nThe `$basedir` is the directory containing the script, but when node-pty spawns with a different `cwd`, the relative path resolution breaks.\n\n### Why the Proactive Approach?\n\nThe reactive fallback (catching error code 193) wasn't enough because:\n- The spawn could \"succeed\" from node-pty's perspective\n- But the process would exit immediately with code 1 (MODULE_NOT_FOUND)\n- By the time we knew it failed, it was too late to retry\n\nBy proactively using Node.js on Windows, we avoid the shell script entirely and directly invoke the CLI script with the correct absolute path.\n\n## Files Modified\n\n1. `main/src/services/panels/cli/AbstractCliManager.ts` - Added:\n - Proactive Windows Node.js fallback detection\n - Error code 193 message matching (reactive fallback)\n2. `main/src/ptyHost/ptyHostMain.ts` - `classifySpawnError()` reports error 193 as `E193`\n3. `main/src/utils/nodeFinder.ts` - Enhanced `findCliNodeScript()` with:\n - pnpm structure detection\n - Known package mappings for Claude Code and Codex\n - Generic pnpm package search\n - Standard npm path resolution\n\n## Debug Logs to Check\n\n- `backend-debug.log` - Look for:\n - `Windows detected, using Node.js fallback proactively`\n - `Detected pnpm structure`\n - `Found pnpm CLI script at:`\n - `Using Node.js:`\n\n## Test Commands\n\n```bash\n# Rebuild main process after changes\npnpm build:main\n\n# Run in dev mode\npnpm electron-dev\n\n# Check logs (PowerShell)\nSelect-String -Path backend-debug.log -Pattern \"Claude|spawn|pnpm|NodeFinder\"\n```\n"}]
|