runpane 2.4.133 → 2.4.136

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.
@@ -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 a token-boundary regex to\n `AGENT_COMMAND_PATTERNS` (order matters when one binary name contains another).\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\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, and unsupported environments\n (`AGENT_UNSUPPORTED_ENVIRONMENTS`).\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` (`CLI_BRAND_ICONS`) 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"},{"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## 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- `analytics_identity_source`\n\nThe event should also set person properties when the identity is known:\n\n- `email`\n- `github_email`\n- `git_email`\n- `install_id`\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:\n\n- `identity`\n- `installId`\n- `attribution`\n- `hasTrackedFirstOpen`\n- `hasTrackedWebAttribution`\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\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_code_copied` | `frontend/src/components/Settings.tsx` | Host code copied |\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## 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.\n"},{"path":"docs/ANALYTICS_UI_EVENTS_INTEGRATION.md","title":"Analytics UI Events Integration Guide","text":"# Analytics UI Events Integration Guide\n\nThis document describes how to integrate UI interaction event tracking into Pane's frontend components.\n\n## Overview\n\nBefore changing UI event tracking, read\n[Analytics Invariants](ANALYTICS_INVARIANTS.md). First-run disclosure and\nchoice events must be captured only after identity is resolved, and settings\nsaves must preserve analytics identity and attribution config.\n\nThe analytics system has been implemented with:\n1. Backend IPC handlers in `/main/src/ipc/analytics.ts`\n2. AnalyticsManager service enhancements to support string arrays\n3. Frontend service utility in `/frontend/src/services/analyticsService.ts`\n4. Preload script exposure for secure IPC communication\n\n## Backend Implementation\n\n### IPC Handlers\n- **File**: `/main/src/ipc/analytics.ts`\n- **Registered**: In `/main/src/ipc/index.ts`\n- **Service Integration**: AnalyticsManager added to AppServices in `/main/src/ipc/types.ts`\n\n### IPC Endpoints\n\n1. `analytics:track-ui-event` - Track UI interaction events\n2. `analytics:categorize-result-count` - Helper to categorize search result counts\n3. `analytics:hash-session-id` - Helper to hash session IDs for privacy\n\n## Frontend Integration Points\n\n### Using the AnalyticsService\n\nImport the service:\n```typescript\nimport { AnalyticsService } from '@/services/analyticsService';\n```\n\n### Event 1: view_switched\n\n**Track when users switch between tabs (Output, Diff, Logs, Editor, etc.)**\n\n**Where to implement:**\n- Component: `/frontend/src/components/SessionView.tsx` or `/frontend/src/hooks/useSessionView.ts`\n- Trigger: When user clicks on a tab to switch views\n- Implementation example:\n\n```typescript\nconst handleViewSwitch = async (fromView: string, toView: string, sessionId?: string) => {\n // Hash the session ID for privacy\n const sessionIdHash = sessionId\n ? await AnalyticsService.hashSessionId(sessionId)\n : undefined;\n\n await AnalyticsService.trackViewSwitched({\n from_view: fromView,\n to_view: toView,\n session_id_hash: sessionIdHash,\n });\n};\n```\n\n**View names to use:**\n- `output` - Main output view\n- `diff` - Git diff view\n- `logs` - Logs/terminal view\n- `editor` - Editor view\n- `dashboard` - Dashboard view\n\n### Event 2: help_dialog_opened\n\n**Track when users open the help dialog**\n\n**Where to implement:**\n- Component: `/frontend/src/components/Help.tsx` (or wherever the help dialog trigger is)\n- Trigger: When the help button/icon is clicked\n- Implementation example:\n\n```typescript\nconst handleHelpOpen = async (location: string) => {\n await AnalyticsService.trackHelpDialogOpened({\n from_location: location,\n });\n\n // Then open the help dialog\n setShowHelp(true);\n};\n```\n\n**Location values to use:**\n- `sidebar` - From sidebar help button\n- `toolbar` - From toolbar help button\n- `settings` - From settings page\n- `keyboard_shortcut` - From keyboard shortcut\n\n### Event 3: settings_opened\n\n**Track when users open settings**\n\n**Where to implement:**\n- Component: `/frontend/src/components/Settings.tsx` (or wherever settings trigger is)\n- Trigger: When settings dialog/page is opened\n- Implementation example:\n\n```typescript\nconst handleSettingsOpen = async (location: string) => {\n await AnalyticsService.trackSettingsOpened({\n from_location: location,\n });\n\n // Then open settings\n setShowSettings(true);\n};\n```\n\n**Location values to use:**\n- `sidebar` - From sidebar settings button\n- `menu` - From menu bar\n- `keyboard_shortcut` - From keyboard shortcut\n\n### Event 4: settings_saved\n\n**Track when users save settings changes**\n\n**Where to implement:**\n- Component: `/frontend/src/components/Settings.tsx`\n- Trigger: When user clicks \"Save\" or settings are persisted\n- Implementation example:\n\n```typescript\nconst handleSettingsSave = async (category: string, changedKeys: string[]) => {\n await AnalyticsService.trackSettingsSaved({\n category: category,\n setting_keys: changedKeys,\n });\n\n // Then save the settings\n await window.electronAPI.config.update(updatedSettings);\n};\n```\n\n**Category values to use:**\n- `general` - General settings\n- `notifications` - Notification settings\n- `project` - Project-specific settings\n- `appearance` - UI/appearance settings\n- `advanced` - Advanced settings\n\n**Setting keys examples:**\n- `verbose_logging`\n- `api_key`\n- `notification_enabled`\n- `notification_sound`\n- `custom_prompt`\n- `main_branch`\n\n### Event 5: sidebar_toggled\n\n**Track when users show/hide the sidebar**\n\n**Where to implement:**\n- Component: Main layout component (likely in `/frontend/src/App.tsx` or similar)\n- Trigger: When sidebar visibility changes\n- Implementation example:\n\n```typescript\nconst handleSidebarToggle = async (isVisible: boolean) => {\n await AnalyticsService.trackSidebarToggled({\n is_visible: isVisible,\n });\n\n // Then toggle sidebar\n setSidebarVisible(isVisible);\n};\n```\n\n### Event 6: search_used\n\n**Track when users use search functionality**\n\n**Where to implement:**\n- Component: Search components (likely in `/frontend/src/components/DraggableProjectTreeView.tsx` or search bar component)\n- Trigger: When search is executed (on Enter or search button click)\n- Implementation example:\n\n```typescript\nconst handleSearch = async (searchTerm: string, searchType: string, resultCount: number) => {\n // Categorize the result count for privacy (don't track exact numbers)\n const resultCategory = await AnalyticsService.categorizeResultCount(resultCount);\n\n await AnalyticsService.trackSearchUsed({\n search_type: searchType,\n result_count_category: resultCategory,\n });\n\n // Note: Do NOT track the actual search term for privacy\n};\n```\n\n**Search type values to use:**\n- `sessions` - Searching sessions\n- `prompts` - Searching prompts\n- `files` - Searching files\n- `projects` - Searching projects\n\n**Result count categories** (automatically categorized by backend):\n- `0-0` - No results\n- `1-5` - Few results\n- `6-10` - Some results\n- `11-25` - Many results\n- `26-50` - Lots of results\n- `51-100` - Very many results\n- `101+` - Tons of results\n\n## Privacy Considerations\n\n### Do NOT Track:\n- ❌ Actual search terms\n- ❌ File names or paths\n- ❌ Prompt content\n- ❌ User identifiable information\n- ❌ Exact counts (use categories)\n- ❌ Raw session IDs (always hash them)\n\n### DO Track:\n- ✅ Event types (which button was clicked)\n- ✅ Categories and types\n- ✅ Hashed/anonymized identifiers\n- ✅ Boolean states (visible/hidden, enabled/disabled)\n- ✅ Categorized counts (0-5, 6-10, etc.)\n\n## Testing\n\nTo test analytics integration:\n\n1. **Enable analytics** in settings\n2. **Enable verbose logging** to see analytics events in the console\n3. **Perform actions** that should trigger events\n4. **Check logs** for messages like:\n ```\n [Analytics] Tracked event: view_switched { from_view: 'output', to_view: 'diff', ... }\n ```\n\n## Type Safety\n\nThe `AnalyticsService` class provides type-safe methods for all events. TypeScript will enforce:\n- Correct event names\n- Required properties for each event type\n- Proper value types for each property\n\nIf you try to pass invalid data, you'll get a TypeScript error at compile time.\n\n## Error Handling\n\nAll analytics methods:\n- Are non-blocking (use `async/await` but don't wait for responses)\n- Fail silently (errors are logged but don't break functionality)\n- Check if analytics is enabled before tracking\n- Handle cases where the electronAPI is not available\n\n## Example: Complete Integration\n\nHere's a complete example of integrating view switching in a component:\n\n```typescript\nimport React, { useState } from 'react';\nimport { AnalyticsService } from '@/services/analyticsService';\n\nexport function SessionView({ sessionId }: { sessionId: string }) {\n const [currentView, setCurrentView] = useState<'output' | 'diff' | 'logs' | 'editor'>('output');\n\n const switchView = async (newView: typeof currentView) => {\n // Track the view switch\n const sessionIdHash = await AnalyticsService.hashSessionId(sessionId);\n await AnalyticsService.trackViewSwitched({\n from_view: currentView,\n to_view: newView,\n session_id_hash: sessionIdHash,\n });\n\n // Update the view\n setCurrentView(newView);\n };\n\n return (\n <div>\n <div className=\"tabs\">\n <button onClick={() => switchView('output')}>Output</button>\n <button onClick={() => switchView('diff')}>Diff</button>\n <button onClick={() => switchView('logs')}>Logs</button>\n <button onClick={() => switchView('editor')}>Editor</button>\n </div>\n {/* View content here */}\n </div>\n );\n}\n```\n\n## Summary\n\nAll backend infrastructure is complete. Frontend developers should:\n\n1. Import `AnalyticsService` from `/frontend/src/services/analyticsService.ts`\n2. Call the appropriate tracking method when user actions occur\n3. Follow the privacy guidelines (no PII, use hashing/categorization)\n4. Test with verbose logging enabled to verify events are tracked\n\nProduct analytics are enabled by default for new and previously undecided\ninstalls. Privacy Settings discloses the default and provides the visible\nopt-out. Pane does not collect prompts, code, or file paths, and capture stops\nafter an opt-out.\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/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:** As of February 2026, the Windows build works without major issues. 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** - v20.x or later (v22.x recommended)\n2. **pnpm** - v8.x or later\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`) that handles several compatibility issues:\n\n### Known Issues & Workarounds\n\n#### 1. winpty.gyp Batch File Path Issue\n\n**Problem:** The `@homebridge/node-pty-prebuilt-multiarch` package uses batch files in its build process:\n```\ncmd /c \"cd shared && GetCommitHash.bat\"\n```\n\nOn Windows, batch files need an explicit `.\\` prefix to run from the current directory.\n\n**Solution:** The build script patches `winpty.gyp` to use:\n```\ncmd /c \"cd shared && .\\GetCommitHash.bat\"\n```\n\n#### 2. pnpm + node-gyp Path Resolution\n\n**Problem:** pnpm's nested `node_modules` structure causes node-gyp to fail when resolving relative paths for dependencies like `node-addon-api`. The error looks like:\n```\nFileNotFoundError: [Errno 2] No such file or directory: '...node-addon-api\\node_addon_api_maybe.vcxproj.filters'\n```\n\n**Solution:** The build script copies `node-addon-api` files to the location pnpm/node-gyp expects.\n\n#### 3. 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#### 4. Native Module Rebuild\n\n**Problem:** Even with the above fixes, 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- Pre-bundled ConPTY binaries for `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: Patch winpty.gyp\n\nEdit `node_modules/.pnpm/@homebridge+node-pty-prebuilt-multiarch@*/node_modules/@homebridge/node-pty-prebuilt-multiarch/deps/winpty/src/winpty.gyp`:\n\nChange:\n```python\n'WINPTY_COMMIT_HASH%': '<!(cmd /c \"cd shared && GetCommitHash.bat\")',\n```\nTo:\n```python\n'WINPTY_COMMIT_HASH%': '<!(cmd /c \"cd shared && .\\\\GetCommitHash.bat\")',\n```\n\nAlso change:\n```python\n'<!(cmd /c \"cd shared && UpdateGenVersion.bat <(WINPTY_COMMIT_HASH)\")',\n```\nTo:\n```python\n'<!(cmd /c \"cd shared && .\\\\UpdateGenVersion.bat <(WINPTY_COMMIT_HASH)\")',\n```\n\n### Step 3: Copy node-addon-api\n\n```bash\n# Find the source and target directories (paths may vary by version)\nmkdir -p \"node_modules/.pnpm/@homebridge+node-pty-prebuilt-multiarch@*/node-addon-api@*/node_modules/node-addon-api/\"\ncp -r \"node_modules/.pnpm/node-addon-api@*/node_modules/node-addon-api/\"* \\\n \"node_modules/.pnpm/@homebridge+node-pty-prebuilt-multiarch@*/node-addon-api@*/node_modules/node-addon-api/\"\n```\n\n### Step 4: 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 5: 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: `GetCommitHash.bat is not recognized`\n\nThe winpty.gyp file hasn't been patched. Run the build script or apply the manual patch.\n\n### Error: `FileNotFoundError: node_addon_api_maybe.vcxproj.filters`\n\nThe node-addon-api files haven't been copied to the expected location. Run the build script or copy manually.\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 handles all the necessary patches automatically.\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\n**Examples:**\n```json\n\"run\": \"pnpm dev\"\n\"run\": \"npm run dev -- --port $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\"run\": \"npm run db:teardown\"\n\"run\": \"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` for clarity, but both are detected with the same priority.\n"},{"path":"docs/CRYSTAL_ARCHITECTURE.md","title":"Pane Architecture Documentation","text":"# Pane Architecture Documentation\n\nThe filename is historical (the product was once called Crystal). There is no Express HTTP API; the renderer talks to the main process over Electron IPC, and RunPane talks to the same process over the local daemon socket.\n\n## Overview\n\nPane is an Electron desktop application that manages multiple agent CLI processes (Claude Code, Codex, Cursor, and custom tools) in git worktrees. This document visualizes the architecture using Mermaid diagrams.\n\n## High-Level Architecture\n\n```mermaid\ngraph TB\n subgraph \"Electron Application\"\n subgraph \"Renderer Process\"\n UI[React UI]\n Store[Zustand Store]\n Terminal[XTerm.js Terminal]\n end\n \n subgraph \"Main Process\"\n IPC[IPC Handlers]\n Daemon[RunPane Daemon]\n DB[(SQLite Database)]\n Queue[Bull Task Queue]\n Sessions[Session Manager]\n Git[Git Manager]\n end\n \n UI <--> IPC\n Store <--> IPC\n Terminal <--> IPC\n \n IPC <--> Daemon\n IPC <--> Sessions\n IPC <--> Git\n \n Daemon <--> DB\n Daemon <--> Queue\n Sessions <--> DB\n Git <--> DB\n end\n \n subgraph \"External Processes\"\n Agent1[Agent CLI 1]\n Agent2[Agent CLI 2]\n Agent3[Agent CLI N]\n \n Worktree1[Git Worktree 1]\n Worktree2[Git Worktree 2]\n Worktree3[Git Worktree N]\n end\n \n Sessions --> Agent1\n Sessions --> Agent2\n Sessions --> Agent3\n \n Agent1 --> Worktree1\n Agent2 --> Worktree2\n Agent3 --> Worktree3\n```\n\n## Component Flow\n\n```mermaid\nflowchart LR\n User[User] --> UI[UI Components]\n UI --> Action[User Action]\n \n Action --> IPC{IPC Router}\n \n IPC -->|Session Ops| SessionHandler[Session Handler]\n IPC -->|Git Ops| GitHandler[Git Handler]\n IPC -->|Config| ConfigHandler[Config Handler]\n \n SessionHandler --> DB[(Database)]\n GitHandler --> Git[Git Commands]\n ConfigHandler --> Store[Config Manager]\n \n SessionHandler --> Agent[Agent Process]\n Agent --> Output[Terminal Output]\n Output --> Stream[IPC Stream]\n Stream --> UI\n```\n\n## Database Schema\n\n```mermaid\nerDiagram\n projects ||--o{ sessions : has\n sessions ||--o{ session_outputs : generates\n sessions ||--o{ conversation_messages : contains\n sessions ||--o{ execution_diffs : tracks\n sessions ||--o{ prompt_markers : marks\n \n projects {\n int id PK\n string name\n string path\n string system_prompt\n string run_script\n string main_branch\n timestamp created_at\n timestamp updated_at\n }\n \n sessions {\n int id PK\n int project_id FK\n string name\n string prompt\n string status\n string worktree_path\n string branch_name\n boolean archived\n timestamp created_at\n timestamp updated_at\n }\n \n session_outputs {\n int id PK\n int session_id FK\n string type\n text data\n timestamp timestamp\n }\n \n conversation_messages {\n int id PK\n int session_id FK\n string role\n text content\n int message_order\n timestamp timestamp\n }\n \n execution_diffs {\n int id PK\n int session_id FK\n text diff_content\n json file_stats\n timestamp timestamp\n }\n \n prompt_markers {\n int id PK\n int session_id FK\n string prompt_text\n int output_index\n timestamp timestamp\n timestamp completion_timestamp\n }\n```\n\n## Session Lifecycle\n\n```mermaid\nstateDiagram-v2\n [*] --> Created: User creates session\n Created --> Initializing: Start worktree setup\n Initializing --> Running: Agent CLI started\n Running --> Waiting: Needs user input\n Waiting --> Running: User provides input\n Running --> Completed: Task finished\n Running --> Error: Process failed\n Waiting --> Stopped: User stops\n Running --> Stopped: User stops\n Completed --> Archived: User archives\n Error --> Archived: User archives\n Stopped --> Archived: User archives\n Archived --> [*]\n```\n\n## IPC Communication Flow\n\n```mermaid\nsequenceDiagram\n participant UI as Frontend UI\n participant IPC as IPC Handler\n participant Main as Main Process\n participant DB as Database\n participant Agent as Agent Process\n \n UI->>IPC: Create Session Request\n IPC->>Main: Validate & Process\n Main->>DB: Store Session Data\n Main->>Main: Create Git Worktree\n Main->>Agent: Spawn Agent Process\n Agent-->>Main: Process Started\n Main->>DB: Update Session Status\n Main-->>IPC: Session Created\n IPC-->>UI: Update UI\n \n loop Real-time Output\n Agent->>Main: Output Data\n Main->>DB: Store Output\n Main->>IPC: Stream Output\n IPC->>UI: Display in Terminal\n end\n```\n\n## Frontend Component Hierarchy\n\n```mermaid\ngraph TD\n App[App Component]\n App --> Sidebar[Sidebar]\n App --> Main[Main Content Area]\n \n Sidebar --> ProjectSelector[Project Selector]\n Sidebar --> SessionList[Session List]\n Sidebar --> PromptHistory[Prompt History]\n \n SessionList --> SessionItem[Session Item]\n SessionItem --> StatusBadge[Status Badge]\n SessionItem --> Actions[Session Actions]\n \n Main --> SessionView[Session View]\n SessionView --> Tabs[View Tabs]\n SessionView --> Terminal[Terminal Display]\n SessionView --> Input[Input Area]\n \n Tabs --> OutputView[Output View]\n Tabs --> MessagesView[Messages View]\n Tabs --> DiffView[Diff View]\n Tabs --> TerminalView[Terminal View]\n```\n\n## Data Flow for Session Output\n\n```mermaid\nflowchart TD\n Agent[Agent Process] --> Raw[Raw Output]\n Raw --> Parse{Parse Output}\n \n Parse -->|JSON Message| Format[Format for Display]\n Parse -->|Stdout/Stderr| Direct[Direct Display]\n \n Format --> Store[(Database)]\n Direct --> Store\n \n Store --> Load{Load Session}\n Load --> Transform[Transform JSON to Stdout]\n Transform --> Display[Display in Terminal]\n \n subgraph \"Real-time Updates\"\n Raw --> Stream[IPC Stream]\n Stream --> LiveDisplay[Live Terminal Update]\n end\n```\n\n## Git Operations Flow\n\n```mermaid\nflowchart LR\n UI[User Interface] --> GitOp{Git Operation}\n \n GitOp -->|Rebase| RebaseFlow\n GitOp -->|Squash| SquashFlow\n GitOp -->|Diff| DiffFlow\n \n subgraph RebaseFlow[Rebase from Main]\n Fetch[git fetch origin]\n Fetch --> Rebase[git rebase origin/main]\n Rebase --> UpdateDB[Update Database]\n end\n \n subgraph SquashFlow[Squash and Rebase]\n Reset[git reset --soft origin/main]\n Reset --> Commit[git commit]\n Commit --> UpdateDB2[Update Database]\n end\n \n subgraph DiffFlow[View Diff]\n DiffCmd[git diff origin/main]\n DiffCmd --> ParseDiff[Parse Diff Output]\n ParseDiff --> StoreDiff[Store in Database]\n StoreDiff --> DisplayDiff[Display in UI]\n end\n```\n\n## Task Queue Processing\n\n```mermaid\nflowchart TD\n Request[IPC Request] --> Queue{Task Queue}\n \n Queue --> Job1[Session Creation Job]\n Queue --> Job2[Git Operation Job]\n Queue --> Job3[Cleanup Job]\n \n Job1 --> Process1[Create Worktree]\n Process1 --> Start1[Start Agent]\n Start1 --> Update1[Update Database]\n \n Job2 --> Process2[Execute Git Command]\n Process2 --> Update2[Update Database]\n \n Job3 --> Process3[Remove Worktree]\n Process3 --> Update3[Archive Session]\n \n Update1 --> Event1[Emit Event]\n Update2 --> Event2[Emit Event]\n Update3 --> Event3[Emit Event]\n \n Event1 --> UI[Update UI]\n Event2 --> UI\n Event3 --> UI\n```\n\n## Module Dependencies\n\n```mermaid\ngraph TD\n subgraph \"Main Process Modules\"\n Index[index.ts] --> IPC[IPC Handlers]\n Index --> Events[events.ts]\n \n IPC --> GitIPC[ipc/git.ts]\n IPC --> SessionIPC[ipc/session.ts]\n \n GitIPC --> GitManager[GitManager]\n SessionIPC --> SessionManager[SessionManager]\n \n SessionManager --> DB[Database]\n SessionManager --> AgentManager[Terminal / Agent Managers]\n \n GitManager --> DB\n \n Events --> SessionManager\n Events --> GitManager\n end\n \n subgraph \"Frontend Modules\"\n App[App.tsx] --> SessionView[SessionView.tsx]\n SessionView --> UseSessionView[useSessionView.ts]\n \n UseSessionView --> Store[Zustand Stores]\n UseSessionView --> Utils[Utils]\n \n App --> Sidebar[Sidebar Components]\n Sidebar --> Store\n end\n```\n\n## Security & Isolation\n\n```mermaid\nflowchart TB\n subgraph \"Security Boundaries\"\n subgraph \"Renderer Process\"\n UI[UI Components]\n UI -.->|Sandboxed| External[External Resources]\n end\n \n subgraph \"Main Process\"\n IPC[IPC Handlers]\n FS[File System Access]\n Process[Process Spawning]\n end\n \n subgraph \"Isolation\"\n W1[Worktree 1]\n W2[Worktree 2]\n W3[Worktree N]\n \n C1[Agent 1]\n C2[Agent 2]\n C3[Agent N]\n end\n end\n \n UI -->|Controlled IPC| IPC\n IPC --> FS\n IPC --> Process\n \n Process --> C1\n Process --> C2\n Process --> C3\n \n C1 --> W1\n C2 --> W2\n C3 --> W3\n \n W1 -.->|Isolated| W2\n W2 -.->|Isolated| W3\n```\n\n## Performance Optimizations\n\n```mermaid\nflowchart LR\n subgraph \"Optimization Strategies\"\n subgraph \"Frontend\"\n Debounce[Debounced Updates]\n Lazy[Lazy Loading]\n Virtual[Virtual Scrolling]\n Cache[State Caching]\n end\n \n subgraph \"Backend\"\n Batch[Batch Operations]\n Index[DB Indexing]\n Pool[Connection Pooling]\n Queue[Async Queue]\n end\n \n subgraph \"Communication\"\n Targeted[Targeted Updates]\n Compress[Data Compression]\n Stream[Streaming]\n end\n end\n \n Updates[State Updates] --> Debounce\n LargeData[Large Datasets] --> Lazy\n Terminal[Terminal Output] --> Virtual\n \n Multiple[Multiple Ops] --> Batch\n Queries[DB Queries] --> Index\n \n Changes[Data Changes] --> Targeted\n Output[Agent Output] --> Stream\n```\n\nThis comprehensive set of Mermaid diagrams illustrates the various aspects of Pane's architecture, from high-level component relationships to detailed data flows and module dependencies."},{"path":"docs/DATABASE_DOCUMENTATION.md","title":"Pane Database Documentation","text":"# Pane Database Documentation\n\nThis document provides a comprehensive overview of all SQLite database tables and local storage mechanisms used in the Pane application.\n\n## Overview\n\nPane uses a SQLite database located at `~/.pane/pane.db` for persistent data storage. The database is managed using Better-SQLite3 for synchronous operations and includes a migration system for schema evolution.\n\n## Database Tables\n\n### 1. `projects` Table\n\nStores project configurations and metadata.\n\n| Column | Type | Description | Constraints |\n|--------|------|-------------|-------------|\n| `id` | INTEGER | Unique project identifier | PRIMARY KEY AUTOINCREMENT |\n| `name` | TEXT | Project display name | NOT NULL |\n| `path` | TEXT | Absolute path to project directory | NOT NULL UNIQUE |\n| `created_at` | DATETIME | Project creation timestamp | DEFAULT CURRENT_TIMESTAMP |\n| `is_active` | INTEGER | Whether this is the active project (0 or 1) | DEFAULT 0 |\n| `custom_prompt` | TEXT | Project-specific system prompt | Optional |\n| `run_script` | TEXT | Script command for testing/building | Optional |\n| `main_branch` | TEXT | Main branch name (e.g., 'main', 'master') | DEFAULT 'main' |\n\n**Purpose**: Manages multiple project directories, allowing users to switch between different codebases. Only one project can be active at a time.\n\n### 2. `sessions` Table\n\nCore session metadata for Claude Code instances.\n\n| Column | Type | Description | Constraints |\n|--------|------|-------------|-------------|\n| `id` | INTEGER | Unique session identifier | PRIMARY KEY AUTOINCREMENT |\n| `project_id` | INTEGER | Associated project | FOREIGN KEY REFERENCES projects(id) |\n| `worktree_name` | TEXT | Git worktree branch name | NOT NULL |\n| `prompt` | TEXT | Initial user prompt | NOT NULL |\n| `status` | TEXT | Session state | NOT NULL |\n| `created_at` | DATETIME | Session creation timestamp | DEFAULT CURRENT_TIMESTAMP |\n| `updated_at` | DATETIME | Last update timestamp | DEFAULT CURRENT_TIMESTAMP |\n| `archived` | INTEGER | Whether session is archived (0 or 1) | DEFAULT 0 |\n| `ai_generated_name` | TEXT | AI-generated descriptive name | Optional |\n| `worktree_path` | TEXT | Absolute path to git worktree | Optional |\n| `last_execution_index` | INTEGER | Index of last command execution | DEFAULT 0 |\n\n**Status Values**:\n- `initializing` - Setting up git worktree\n- `running` - Claude is actively processing\n- `waiting` - Needs user input\n- `completed` - Task finished successfully\n- `error` - Something went wrong\n- `stopped` - Session was manually stopped\n\n**Purpose**: Tracks all Claude Code sessions, their states, and metadata for session management and recovery.\n\n### 3. `session_outputs` Table\n\nStores terminal output history for each session.\n\n| Column | Type | Description | Constraints |\n|--------|------|-------------|-------------|\n| `id` | INTEGER | Unique output identifier | PRIMARY KEY AUTOINCREMENT |\n| `session_id` | INTEGER | Associated session | FOREIGN KEY REFERENCES sessions(id) |\n| `type` | TEXT | Output type | NOT NULL |\n| `data` | TEXT | Output content | NOT NULL |\n| `timestamp` | DATETIME | When output was generated | DEFAULT CURRENT_TIMESTAMP |\n| `formatted_data` | TEXT | Pre-formatted version for display | Optional |\n\n**Type Values**:\n- `stdout` - Standard output text\n- `stderr` - Error output text\n- `json` - JSON messages from Claude\n- `system` - System messages\n\n**Purpose**: Maintains complete history of all session outputs for replay and debugging. Raw JSON messages are stored as-is, with formatting done on-the-fly during retrieval.\n\n### 4. `conversation_messages` Table\n\nStores conversation history for session continuation.\n\n| Column | Type | Description | Constraints |\n|--------|------|-------------|-------------|\n| `id` | INTEGER | Unique message identifier | PRIMARY KEY AUTOINCREMENT |\n| `session_id` | INTEGER | Associated session | FOREIGN KEY REFERENCES sessions(id) |\n| `role` | TEXT | Message sender | NOT NULL |\n| `content` | TEXT | Message content | NOT NULL |\n| `timestamp` | DATETIME | Message timestamp | DEFAULT CURRENT_TIMESTAMP |\n\n**Role Values**:\n- `user` - User input messages\n- `assistant` - Claude's responses\n- `system` - System prompts\n\n**Purpose**: Enables conversation continuation by preserving the full message history context when resuming a session.\n\n### 5. `execution_diffs` Table\n\nTracks git diffs for each execution round.\n\n| Column | Type | Description | Constraints |\n|--------|------|-------------|-------------|\n| `id` | INTEGER | Unique diff identifier | PRIMARY KEY AUTOINCREMENT |\n| `session_id` | INTEGER | Associated session | FOREIGN KEY REFERENCES sessions(id) |\n| `execution_index` | INTEGER | Execution round number | NOT NULL |\n| `diff_data` | TEXT | Git diff output (JSON) | NOT NULL |\n| `created_at` | DATETIME | When diff was captured | DEFAULT CURRENT_TIMESTAMP |\n\n**Purpose**: Maintains a history of code changes per execution round, allowing users to see what changed at each step of the session.\n\n### 6. `prompt_markers` Table\n\nNavigation markers for prompts within session output.\n\n| Column | Type | Description | Constraints |\n|--------|------|-------------|-------------|\n| `id` | INTEGER | Unique marker identifier | PRIMARY KEY AUTOINCREMENT |\n| `session_id` | INTEGER | Associated session | FOREIGN KEY REFERENCES sessions(id) |\n| `prompt_text` | TEXT | The prompt content | NOT NULL |\n| `output_index` | INTEGER | Position in session_outputs | NOT NULL |\n| `timestamp` | DATETIME | When prompt was sent | DEFAULT CURRENT_TIMESTAMP |\n| `completion_timestamp` | DATETIME | When prompt finished processing | Optional |\n\n**Purpose**: Enables quick navigation to specific prompts within long session outputs and tracks execution duration.\n\n### 7. `migrations` Table\n\nTracks applied database migrations.\n\n| Column | Type | Description | Constraints |\n|--------|------|-------------|-------------|\n| `id` | INTEGER | Migration version number | PRIMARY KEY |\n| `applied_at` | DATETIME | When migration was applied | DEFAULT CURRENT_TIMESTAMP |\n\n**Purpose**: Ensures database schema can be safely evolved over time by tracking which migrations have been applied.\n\n## Database Indexes\n\nThe database includes several indexes for performance optimization:\n\n- `idx_sessions_project_id` - Fast lookup of sessions by project\n- `idx_session_outputs_session_id` - Fast retrieval of outputs for a session\n- `idx_conversation_messages_session_id` - Quick access to conversation history\n- `idx_execution_diffs_session_id_execution_index` - Efficient diff lookups\n- `idx_prompt_markers_session_id` - Fast prompt navigation\n\n## Local Storage (Non-Database)\n\n### 1. Electron Store\n\nLocation: Platform-specific (handled by electron-store)\n- macOS: `~/Library/Application Support/Pane/config.json`\n- Windows: `%APPDATA%/Pane/config.json`\n- Linux: `~/.config/Pane/config.json`\n\n**Stored Data**:\n```json\n{\n \"verboseLogging\": boolean,\n \"anthropicApiKey\": \"string (encrypted)\",\n \"globalSystemPrompt\": \"string\",\n \"claudeExecutablePath\": \"string\",\n \"notifications\": {\n \"enabled\": boolean,\n \"sound\": boolean,\n \"statusChanges\": boolean,\n \"waiting\": boolean,\n \"completion\": boolean,\n \"errors\": boolean\n }\n}\n```\n\n### 2. Application Directory\n\nLocation: `~/.pane/`\n\n**Contents**:\n- `pane.db` - Main SQLite database\n- Automatically created on first application run\n\n### 3. Git Worktrees\n\nLocation: `<project_path>/.git/worktrees/`\n\n**Structure**:\n- Each session creates a worktree named after its branch\n- Worktrees are cleaned up when sessions are deleted\n- Contains isolated git working directory for each Claude session\n\n## Data Flow and Relationships\n\n```\nprojects (1) ─────┬──── (∞) sessions\n │ │\n │ ├──── (∞) session_outputs\n │ ├──── (∞) conversation_messages\n │ ├──── (∞) execution_diffs\n │ └──── (∞) prompt_markers\n │\n └──── git worktrees (file system)\n```\n\n## Important Implementation Notes\n\n1. **Timestamp Handling**: All timestamps are stored in UTC format. The frontend utilities handle proper timezone conversion for display.\n\n2. **Transaction Safety**: Database operations use transactions where appropriate to maintain data integrity.\n\n3. **Cascade Deletion**: When a project is deleted, all associated sessions and their related data are automatically removed through foreign key constraints.\n\n4. **Archive vs Delete**: Sessions are typically archived (`archived = 1`) rather than deleted to preserve history. True deletion only occurs when explicitly requested.\n\n5. **Performance Considerations**: \n - Session outputs can grow large; pagination may be needed for very long sessions\n - Indexes are crucial for responsive UI with many sessions\n - The `formatted_data` column in session_outputs is used sparingly to balance storage vs computation\n\n## Migration System\n\nThe application uses a simple migration system located in `main/src/database/migrations/`. Each migration is a SQL file numbered sequentially (e.g., `001_initial_schema.sql`, `002_add_prompt_markers.sql`).\n\nMigrations are applied automatically on application startup if the database version is behind the latest migration.\n\n## Backup Recommendations\n\nUsers should periodically backup:\n1. `~/.pane/pane.db` - Contains all session data\n2. The Electron Store config file - Contains application settings\n3. Project directories - Contains the actual code and git history\n\n## Security Considerations\n\n1. **API Keys**: The Anthropic API key is stored in the Electron Store with platform-specific encryption\n2. **File Paths**: All file paths are stored as absolute paths; care should be taken when sharing databases\n3. **Sensitive Data**: Session outputs may contain sensitive information and should be treated accordingly"},{"path":"docs/FEATURE_USAGE_TRACKING_INTEGRATION.md","title":"Feature Usage Tracking Integration Guide","text":"# Feature Usage Tracking Integration Guide\n\nThis document provides guidance on integrating the remaining feature usage tracking events into Pane's frontend.\n\n## Overview\n\nBefore adding or changing tracking, read\n[Analytics Invariants](ANALYTICS_INVARIANTS.md). Consent, identity, attribution,\nand version fields have ordering requirements that keep PostHog funnels\nperson-stitchable.\n\nThe following feature usage events have been fully implemented:\n- ✅ `run_script_executed` - Tracked in backend (logsManager)\n- ✅ `notification_shown` - Tracked in frontend (useNotifications hook)\n\nThe following events require frontend integration:\n- ⏳ `keyboard_shortcut_used` - Integration points documented below\n- ⏳ `drag_drop_used` - Integration points documented below\n\n## Keyboard Shortcut Tracking\n\n### Event Properties\n```typescript\n{\n shortcut_key: string; // e.g., \"Cmd+Enter\", \"Ctrl+K\", \"Escape\"\n action: string; // e.g., \"submit_input\", \"focus_search\", \"close_dialog\"\n}\n```\n\n### Integration Points\n\n#### 1. AI Input Panels (Cmd/Ctrl+Enter)\n**File**: `/frontend/src/hooks/useAIInputPanel.ts` (line ~311)\n\n**Current Code**:\n```typescript\nconst handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n handleSubmit();\n }\n};\n```\n\n**Add Tracking**:\n```typescript\nimport { AnalyticsService } from '../services/analyticsService';\n\nconst handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n handleSubmit();\n\n // Track keyboard shortcut usage\n AnalyticsService.trackKeyboardShortcut({\n shortcut_key: `${e.metaKey ? 'Cmd' : 'Ctrl'}+Enter`,\n action: 'submit_ai_input',\n }).catch(console.error);\n }\n};\n```\n\n#### 2. Session Input (Cmd/Ctrl+Enter)\n**File**: `/frontend/src/components/session/SessionInput.tsx` (line ~50-60)\n\n**Similar pattern** - Add tracking when Cmd/Ctrl+Enter is pressed to submit session input.\n\n#### 3. File Editor Shortcuts\n**File**: `/frontend/src/components/panels/editor/FileEditor.tsx`\n\n**Keyboard shortcuts to track**:\n- Save file: `Cmd/Ctrl+S`\n- Close editor: `Escape`\n- Any other Monaco editor keyboard shortcuts\n\n**Add event listener**:\n```typescript\n// In useEffect or Monaco configuration\neditor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {\n handleSave();\n AnalyticsService.trackKeyboardShortcut({\n shortcut_key: 'Cmd+S',\n action: 'save_file',\n }).catch(console.error);\n});\n```\n\n#### 4. Dialog Shortcuts (Escape to close)\n**Files to check**:\n- `/frontend/src/components/CreateSessionDialog.tsx`\n- `/frontend/src/components/CommitDialog.tsx`\n- `/frontend/src/components/ConfirmDialog.tsx`\n- `/frontend/src/components/Help.tsx`\n\n**Pattern**:\n```typescript\nconst handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n handleClose();\n AnalyticsService.trackKeyboardShortcut({\n shortcut_key: 'Escape',\n action: 'close_dialog',\n }).catch(console.error);\n }\n};\n```\n\n#### 5. Search/Navigation Shortcuts\n**File**: `/frontend/src/components/DraggableProjectTreeView.tsx`\n\nIf implementing keyboard shortcuts for search or navigation (e.g., `/` to focus search, arrow keys for navigation), track them here.\n\n### Analytics Service Method to Add\n\n**File**: `/frontend/src/services/analyticsService.ts`\n\n```typescript\nexport interface KeyboardShortcutProperties {\n shortcut_key: string;\n action: string;\n}\n\n// In UIEventName type\nexport type UIEventName =\n | 'view_switched'\n | 'help_dialog_opened'\n | 'settings_opened'\n | 'settings_saved'\n | 'sidebar_toggled'\n | 'search_used'\n | 'notification_shown'\n | 'keyboard_shortcut_used'; // Add this\n\n// Add method to class\nstatic async trackKeyboardShortcut(properties: KeyboardShortcutProperties): Promise<void> {\n if (!this.isEnabled()) return;\n\n try {\n await window.electronAPI.analytics.trackUIEvent({\n event: 'keyboard_shortcut_used',\n properties,\n });\n } catch (error) {\n console.error('[Analytics] Failed to track keyboard_shortcut_used:', error);\n }\n}\n```\n\n### Backend IPC Handler Update\n\n**File**: `/main/src/ipc/analytics.ts`\n\n```typescript\ninterface KeyboardShortcutUsedEvent extends Record<string, string | number | boolean | string[] | undefined> {\n shortcut_key: string;\n action: string;\n}\n\ntype AnalyticsEventData =\n | { event: 'view_switched'; properties: ViewSwitchedEvent }\n | { event: 'help_dialog_opened'; properties: HelpDialogOpenedEvent }\n | { event: 'settings_opened'; properties: SettingsOpenedEvent }\n | { event: 'settings_saved'; properties: SettingsSavedEvent }\n | { event: 'sidebar_toggled'; properties: SidebarToggledEvent }\n | { event: 'search_used'; properties: SearchUsedEvent }\n | { event: 'notification_shown'; properties: NotificationShownEvent }\n | { event: 'keyboard_shortcut_used'; properties: KeyboardShortcutUsedEvent };\n```\n\n---\n\n## Drag and Drop Tracking\n\n### Event Properties\n```typescript\n{\n item_type: string; // \"session\" | \"folder\" | \"project\"\n target_type: string; // \"session\" | \"folder\" | \"project\" | \"root\"\n}\n```\n\n### Integration Point\n\n**File**: `/frontend/src/components/DraggableProjectTreeView.tsx`\n\nThis component already has comprehensive drag-and-drop functionality. The drag state is managed in the `dragState` state variable.\n\n#### Key Handlers to Instrument\n\n**1. handleDrop handler** (line ~800-1000)\n\nThis is the main drop handler where drag-and-drop actions are completed. Add tracking here.\n\n**Current pattern**:\n```typescript\nconst handleDrop = async (e: React.DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n\n // ... existing drop logic ...\n\n if (dragState.type && dragState.overType) {\n // Handle different drop scenarios\n // ... move session, move folder, etc. ...\n }\n\n // Reset drag state\n setDragState({ ... });\n};\n```\n\n**Add Tracking**:\n```typescript\nimport { AnalyticsService } from '../services/analyticsService';\n\nconst handleDrop = async (e: React.DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n\n const sourceType = dragState.type;\n const targetType = dragState.overType;\n\n // ... existing drop logic ...\n\n if (sourceType && targetType) {\n // Track successful drag and drop\n AnalyticsService.trackDragDrop({\n item_type: sourceType,\n target_type: targetType,\n }).catch(console.error);\n }\n\n // Reset drag state\n setDragState({ ... });\n};\n```\n\n**2. Track only successful drops**\n\nMake sure to only track when the drop is successful and changes are saved to the database. Place the tracking call after successful API calls:\n\n```typescript\n// After successful session move\nawait API.sessions.update(dragState.sessionId, { folder_id: targetFolderId });\n\n// Track the drag-drop action\nAnalyticsService.trackDragDrop({\n item_type: 'session',\n target_type: targetType || 'root',\n}).catch(console.error);\n```\n\n### Analytics Service Method to Add\n\n**File**: `/frontend/src/services/analyticsService.ts`\n\n```typescript\nexport interface DragDropUsedProperties {\n item_type: string;\n target_type: string;\n}\n\n// In UIEventName type\nexport type UIEventName =\n | 'view_switched'\n | 'help_dialog_opened'\n | 'settings_opened'\n | 'settings_saved'\n | 'sidebar_toggled'\n | 'search_used'\n | 'notification_shown'\n | 'keyboard_shortcut_used'\n | 'drag_drop_used'; // Add this\n\n// Add method to class\nstatic async trackDragDrop(properties: DragDropUsedProperties): Promise<void> {\n if (!this.isEnabled()) return;\n\n try {\n await window.electronAPI.analytics.trackUIEvent({\n event: 'drag_drop_used',\n properties,\n });\n } catch (error) {\n console.error('[Analytics] Failed to track drag_drop_used:', error);\n }\n}\n```\n\n### Backend IPC Handler Update\n\n**File**: `/main/src/ipc/analytics.ts`\n\n```typescript\ninterface DragDropUsedEvent extends Record<string, string | number | boolean | string[] | undefined> {\n item_type: string;\n target_type: string;\n}\n\ntype AnalyticsEventData =\n | { event: 'view_switched'; properties: ViewSwitchedEvent }\n | { event: 'help_dialog_opened'; properties: HelpDialogOpenedEvent }\n | { event: 'settings_opened'; properties: SettingsOpenedEvent }\n | { event: 'settings_saved'; properties: SettingsSavedEvent }\n | { event: 'sidebar_toggled'; properties: SidebarToggledEvent }\n | { event: 'search_used'; properties: SearchUsedEvent }\n | { event: 'notification_shown'; properties: NotificationShownEvent }\n | { event: 'keyboard_shortcut_used'; properties: KeyboardShortcutUsedEvent }\n | { event: 'drag_drop_used'; properties: DragDropUsedEvent };\n```\n\n---\n\n## Testing\n\nAfter implementing the above tracking:\n\n1. **Enable analytics** in Pane settings\n2. **Configure PostHog API key** in settings\n3. **Use the features** that trigger the events:\n - Press keyboard shortcuts\n - Drag and drop sessions/folders\n4. **Verify in PostHog dashboard** that events are being tracked\n5. **Check console logs** for any tracking errors\n\n## Privacy Considerations\n\n- ✅ No user-identifiable information is tracked\n- ✅ All session IDs are hashed before sending\n- ✅ Only event counts and categorical data are tracked\n- ✅ Users can disable analytics in settings\n- ✅ Analytics respect the user's privacy preferences\n\n## Summary\n\n| Event | Status | Implementation |\n|-------|--------|---------------|\n| `run_script_executed` | ✅ Complete | Backend (logsManager.ts) |\n| `notification_shown` | ✅ Complete | Frontend (useNotifications.ts) |\n| `keyboard_shortcut_used` | ⏳ Documentation Ready | Follow integration guide above |\n| `drag_drop_used` | ⏳ Documentation Ready | Follow integration guide above |\n"},{"path":"docs/IMPLEMENTING_NEW_CLI_AGENTS.md","title":"Implementing New CLI Agents","text":"# Implementing New CLI Agents\n\nThis document described the removed `AbstractCliManager`/`cliToolRegistry`\narchitecture and is retired. See [ADDING_NEW_CLI_TOOLS.md](./ADDING_NEW_CLI_TOOLS.md)\nfor the current integration recipe.\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, removes, or recreates an external directory. Archiving or permanently deleting an adopted Pane only removes Pane's record. 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\nThe tool that owns the worktree remains responsible for its archive and deletion lifecycle. `runpane panes archive --dry-run --pane <id> --json` reports worktree cleanup as not applicable.\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/LICENSE-COMPATIBILITY.md","title":"License Compatibility Guide","text":"# License Compatibility Guide\n\nPane is licensed under the MIT License, which is a permissive open-source license. This document explains our approach to dependency license compatibility.\n\n## Compatible Licenses\n\nThe following licenses are compatible with MIT for use in Pane:\n\n### Permissive Licenses (Fully Compatible)\n- **MIT**: Same as Pane's license\n- **Apache-2.0**: Permissive with patent grant\n- **BSD** (2-clause, 3-clause): Permissive with attribution\n- **ISC**: Simplified BSD-style license\n- **CC0**: Public domain dedication\n- **Unlicense**: Public domain\n- **0BSD**: Zero-clause BSD\n\n### Weak Copyleft Licenses (Compatible with Conditions)\n- **LGPL-2.1, LGPL-3.0**: Compatible when used as dynamically linked libraries\n- **MPL-2.0**: File-level copyleft, doesn't affect MIT code\n- **EPL**: Similar to LGPL in practical terms\n\n## Incompatible Licenses\n\nThe following licenses are **not** compatible with MIT distribution:\n\n### Strong Copyleft Licenses\n- **GPL-2.0, GPL-3.0** (without \"or later\" or dual licensing)\n- **AGPL** (any version): Network copyleft\n- **SSPL**: Server Side Public License\n\n### Other Restrictive Licenses\n- Proprietary licenses\n- Custom licenses with distribution restrictions\n\n## How We Check\n\nThe `check-license-compatibility.sh` script automatically validates that all dependencies use compatible licenses. It:\n\n1. Checks for truly incompatible licenses (GPL, AGPL, SSPL)\n2. Notes LGPL dependencies (compatible but worth tracking)\n3. Summarizes all permissive licenses found\n\n## LGPL Special Note\n\nLGPL (Lesser GPL) dependencies are compatible with MIT when:\n- They are used as libraries (not forked/modified)\n- They are dynamically linked (which is the case for npm packages)\n- We comply with LGPL requirements (providing source of the LGPL component)\n\nThe LGPL-3.0-or-later license specifically allows users to choose any later version of LGPL, providing additional flexibility.\n\n## Current Status\n\nRun `pnpm build:notices` to generate a current NOTICES file listing all dependency licenses."},{"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 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. 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 generic text and routing metadata; see the payload details below.\n\nFor iOS, register `com.dcouple.pane.mobile` (or your signed bundle identifier), enable Push Notifications, add the capability in Xcode, and set these only in the host service environment:\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\nFor Android, create a Firebase Android app with the same application ID, download its real `google-services.json` to `mobile/android/app/google-services.json` (the checked-in `.example` is only a shape guide), and create a narrowly scoped service account with Firebase Cloud Messaging send permission. Set `PANE_FCM_SERVICE_ACCOUNT_PATH=/secure/path/service-account.json` in the host service environment. Do not commit either provider 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\nThe daemon sends generic APNs/FCM alert text for both a newly blocked turn and a `working → idle` completion. 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\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 protect the service-account file outside the app build. iOS Simulator builds do not prove APNs delivery; Android emulators similarly do not prove a real FCM token path. 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 also keeps a short `AGENTS.md` pointer in saved repositories so agents that don't have the MCP server yet 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\nPane registers only with the CLIs that are installed, 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.\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. 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 and Codex**. Pane then removes its entry from each config.\n\nRestart a running `claude` or `codex` session to pick up a new registration. Check it with `claude mcp list` or `codex mcp list`.\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_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 |\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`: creates a Pane in a repository, starts the agent with the task, waits until it is ready, and returns the pane and panel ids and a `pane://` link.\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 Pane saw it leave the composer.\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\": { \"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\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 and Codex installed.\n2. Run `claude mcp list` and `codex mcp list`. Each shows `pane` as connected.\n3. In a fresh `claude` session, ask \"Use Pane to list my repos\". It calls `repos_list`.\n4. Quit and relaunch Pane. The Claude and Codex configs are unchanged.\n5. Turn off **Settings → AI & Agents → Register Pane tools with Claude Code and Codex**. `pane` is gone from both lists, 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 and Codex installed inside that distro.\n2. Relaunch Pane. Inside the distro, `claude mcp list` and `codex 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` isn'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 the output as a tool error (`isError: true`), and so do bad arguments, 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` is omitted because a streaming 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## AGENTS.md block\n\nPane still writes a short managed `<!-- pane-agent-context -->` block into each active repository's `AGENTS.md`. The block points agents at the RunPane CLI (`npm i -g runpane`, then `runpane doctor --json`) and at the `pane` MCP server, including Claude Code and Codex config. The full command catalog lives in MCP tools and `runpane agent-context`, not in the file.\n\nTurning **Publish Pane instructions to AGENTS.md** off stops Pane from updating the block. It does not delete existing blocks.\n"},{"path":"docs/RELEASE_INSTRUCTIONS.md","title":"Pane Release Instructions","text":"# Pane Release Instructions\n\nPane releases are cut from a clean `main` checkout with `scripts/release.js`.\nThe script updates `package.json`, commits `release: vX.Y.Z`, tags the same\ncommit, pushes `HEAD:main`, and pushes the tag.\n\nThe same release commit also keeps the npm and PyPI wrapper package versions in\nsync through `scripts/sync-runpane-package-versions.js`.\n\n## Mechanical Invariants\n\nBefore a release, these facts must be true:\n\n- The worktree is clean.\n- `HEAD` matches `origin/main`.\n- For inferred bumps (`patch`, `minor`, `major`), `package.json` version matches\n the latest `v*` semver tag.\n- `packages/runpane/package.json`, `packages/runpane-py/pyproject.toml`, and\n `packages/runpane-py/src/runpane/__init__.py` match the root release version.\n- The release tag does not already exist locally or on `origin`.\n- Dependencies are installed before running the release script so commit hooks can\n run successfully.\n\nIf `package.json` and the latest release tag disagree, do not run an inferred\npatch. Decide the intended next version and run an explicit release instead:\n\n```bash\npnpm run release 2.2.1\n```\n\n## Happy Path\n\n```bash\ngit switch main\ngit pull --ff-only origin main\ngit status --porcelain\ngit tag --list 'v*' --sort=-v:refname | head -5\nnode -p \"require('./package.json').version\"\n\npnpm typecheck\npnpm lint\npnpm run check:runpane-package-versions\npnpm run test:runpane-contract\npnpm run test:runpane-package-smoke\npnpm test:ci:minimal\n\npnpm run release patch\n```\n\nIf the release commit fails, the script aborts before creating or pushing a tag\nas long as any changes remain in the worktree. Fix the hook, dependency, or\ntypecheck failure, restore the worktree, and rerun the release.\n\nUse `minor`, `major`, or an explicit version when that is the intended release:\n\n```bash\npnpm run release minor\npnpm run release major\npnpm run release 2.3.0\n```\n\n## GitHub Workflows\n\nPull requests to `main` run:\n\n- `Code Quality`\n - typecheck\n - lint\n - runpane wrapper compatibility tests across Node, Python, Linux, macOS, and Windows\n - main process tests on Linux, macOS, and Windows\n - frontend unit tests\n - maintained Playwright smoke tests\n\nPushes to `main` run:\n\n- `Code Quality`\n- `Deploy Remote PWA Preview`\n\n`v*` tag pushes run:\n\n- `Build & Release`\n - macOS Apple Silicon and Intel installers\n - Linux installer artifacts\n - Windows x64 installer\n - Windows arm64 installer\n - GitHub release publishing\n - `SHA256SUMS.txt`\n - npm `runpane` publish\n - PyPI `runpane` publish\n- `Notify website on release`\n\nThe release is not considered complete until the tag-triggered `Build & Release`\nrun succeeds and the GitHub release is published.\n\n## Verification\n\nAfter `pnpm run release ...` finishes:\n\n```bash\ngit fetch origin main --tags\ngit rev-parse HEAD\ngit rev-parse origin/main\ngit tag --points-at HEAD\ngh run list --limit 10\ngh release view vX.Y.Z\n```\n\nConfirm:\n\n- `HEAD` and `origin/main` point at the release commit.\n- The release commit has the expected `vX.Y.Z` tag.\n- `Build & Release` succeeded for the tag.\n- `npm view runpane version` reports `X.Y.Z`.\n- `python3 -m pip index versions runpane` includes `X.Y.Z`.\n- `Notify website on release` succeeded for the tag.\n- `Code Quality` succeeded for the release commit on `main`.\n- `Deploy Remote PWA Preview` succeeded for the release commit on `main`.\n\n## Required Secrets\n\nGitHub Actions provides `GITHUB_TOKEN` automatically.\n\nThe npm and PyPI packages should publish through trusted publishing:\n\n- npm: configure a trusted publisher for package `runpane` on npmjs.com with\n repository `greenfield-inc/Pane`, workflow filename `build.yml`, and `npm publish`\n permission. The workflow installs npm `11.5.1` or newer for OIDC support.\n- PyPI: configure a trusted publisher for project `runpane` with repository\n `greenfield-inc/Pane`, workflow filename `build.yml`, and GitHub environment `pypi`.\n\nFallback token publishing is allowed only for first package reservation or\nmanual recovery. Use `NPM_TOKEN` or `PYPI_API_TOKEN` as local environment\nvariables or GitHub Actions secrets, do not commit token files such as `.npmrc`\nor `.pypirc`, and revoke or rotate the tokens after use.\n\nIf the source repository remains private, do not promise npm provenance:\nnpm trusted publishing can still be used, but npm provenance attestations are\nnot generated for private repositories.\n\nThe release and preview workflows also depend on repository secrets and\nvariables configured in GitHub Actions. Relevant examples include:\n\n- `SITE_REPO_DISPATCH_TOKEN` for website release notification.\n- Google Cloud workload identity, service account, project, and region values\n for the remote PWA preview deploy.\n- Platform signing or publishing credentials if signing is re-enabled.\n\n## Auto-Update Files\n\nThe build process generates update metadata and installers under\n`dist-electron/` in the release workflow:\n\n- `latest-mac.yml`\n- `latest-linux.yml`\n- `latest-linux-arm64.yml`\n- `latest.yml`\n- macOS `.dmg` and `.zip`\n- Linux `.deb` and `.AppImage`\n- Windows `.exe`\n- npm package `runpane`\n- PyPI package `runpane`\n\n## Rollback\n\nDo not retag an existing version. If a release has a critical issue:\n\n1. Fix the issue on `main`.\n2. Cut a new patch version.\n3. Leave the broken tag/release history intact unless maintainers explicitly\n decide to remove it.\n\nUsers can always manually download the latest good release from GitHub Releases.\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 watch --self-test\nrunpane watch --follow\nrunpane watch --follow --kinds agent.ready,agent.blocked,agent.idle,panel.exited,pane.gone --settle 180000 --blocked-settle 30000 --min-interval 600000 --idle-backoff\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 agents start --repo active --name fix-login --agent claude --prompt \"Fix the login redirect\" --yes --json\nrunpane agents status --pane <pane-id> --json\nrunpane agents send --pane <pane-id> --text \"Also add a test\" --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\n```\n\n`runpane` with no arguments and `runpane setup` open an interactive wizard when stdin and stdout are TTYs. 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` uses the same release resolution and installer path as `install client`.\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.\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; pass `--no-pinned` to opt out. Panes created interactively in the Pane UI are unaffected.\n\nFor `panes create --wait-ready`, `initialInput.verifiedSubmitted: true` is reported only after argument attachment or composer-clear plus activity evidence. Routing input does not by itself verify submission.\n\n`runpane panes archive` refreshes the configured upstream, reports exact unpushed commit evidence, and refuses unsafe archive operations unless `--force` is used. Add `--dry-run` to inspect the same evidence without archiving. Successful archives wait for worktree removal and report `worktreeCleanup`.\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 list` lists tool panels inside one Pane session.\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\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 workspace transitions from the daemon journal without polling. `--follow` keeps waiting and prints one line per event: READY, BLOCKED, IDLE, STUCK, NEW, GONE, EXIT, plus HEARTBEAT every 60 seconds as proof of life. Defaults are responsive: no settle, no batching, all kinds, IDLE every `--idle-after`. Expensive consumers opt into `--kinds` (drop `agent.busy`; BUSY carries no action), `--settle <ms>` (READY only after a quiet window; a BUSY inside it cancels the line), `--blocked-settle <ms>`, `--min-interval <ms>` (batch non-urgent lines; BLOCKED bypasses it), and `--idle-backoff` (10m, 30m, 1h, 3h, then daily). The recommended orchestrator invocation is `runpane watch --follow --kinds agent.ready,agent.blocked,agent.idle,panel.exited,pane.gone --settle 180000 --blocked-settle 30000 --min-interval 600000 --idle-backoff`, which budgets about 6 wake-ups per active pane per hour worst case, usually 1-3. Pane Chat arms it automatically through its skill; only your own scripts need the flags. STUCK means real unsubmitted composer text, never an agent prompt suggestion. Judge a dead watch by a non-zero exit or a WATCH ERROR line, not by silence.\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 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.\n\n`runpane agents start|status|send` finish the three common agent jobs in one call each: start an agent on a task in a repository, check on it, and send it a follow-up.\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## 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`: Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\n- `panes archive`: Archive a Pane exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\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`: Create reviewer/helper terminal tabs inside an existing Pane; they share that Pane's worktree.\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 correct key sequence, including Ctrl+Enter for Codex.\n- `panels wait`: Wait for terminal initialized, ready, idle, or text state with compact output.\n- `watch`: Wait for workspace transitions (READY, BLOCKED, IDLE, STUCK, NEW, GONE, EXIT) from the daemon journal without polling; responsive by default, with opt-in cadence flags for expensive consumers.\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\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 and Codex. Check the connection with `claude mcp list` or `codex mcp list`. 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\"]`; any other stdio client uses the same command and args.\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--base-branch <branch>\n--folder <name>\n--resume <agent-session-id>\n--agent <codex|claude|cursor>\n--tool-command <command>\n--title <title>\n--initial-input <text> (aliases: --prompt)\n--initial-input-file <path|->\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>\n--as <consumer-name>\n--since <generation>\n--from <now|earliest>\n--kinds <kind,...>\n--name-contains <text>\n--exclude-pane <pane-id>\n--format <lines|json>\n--heartbeat <seconds>\n--idle-after <milliseconds>\n--settle <milliseconds>\n--blocked-settle <milliseconds>\n--min-interval <milliseconds>\n--body-file <path|->\n--session <id|name>\n--message <message>\n--query <text>\n--doc <path>\n--url <pane-url>\n--toolsets <name,...>\n--keys <name,...>\n--json\n--wait-ready\n--no-focus\n--focus\n--pinned\n--no-pinned\n--force\n--launch\n--follow\n--ack-now\n--include-held-input\n--agents-only\n--all-managed\n--include-shells\n--no-held-input\n--self-test\n--idle-backoff\n--report\n--read-only\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. In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22, for example `PATH=/opt/homebrew/opt/node@22/bin:$PATH 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`.\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 also emit best-effort lifecycle telemetry to `https://runpane.com/api/runpane/telemetry` for command start/success/failure, download request/success/failure, and GitHub fallback usage.\nWrapper telemetry uses a persisted anonymous `install_id` in the form `install_<uuid>` and PostHog `distinct_id = install:<install_id>` so distinct wrapper users can be counted with `count(DISTINCT properties.install_id)`.\nWrapper telemetry must be disabled in CI and when `RUNPANE_TELEMETRY_DISABLED` is set, and must not include raw paths, labels, prompts, raw error text, or environment values.\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/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 Pane`, 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## 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\nRecommended package-manager commands:\n\n```bash\nnpx --yes runpane@latest install daemon --label \"VM\"\npnpm dlx runpane@latest install daemon --label \"VM\"\npipx run runpane install daemon --label \"VM\"\nuvx runpane@latest install daemon --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 --label \"VM\"\n\npython -m pip install runpane\nrunpane install daemon --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 Pane`.\n3. Paste the full `pane-remote://...` code into `Import Remote Connection`.\n4. Click `Import & Connect`.\n\nIf the tunnel is not reachable yet, Pane still saves the profile and shows the connection error. Start the printed SSH/Tailscale tunnel and click `Connect` on the saved profile.\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 terminal-backed remote sessions:\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### 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## 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\nTailscale Serve example generated by setup:\n\n```bash\ntailscale serve --bg http://127.0.0.1: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 Pane`:\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. Save host settings.\n\n### 3. Create a Paired Connection\n\nFrom the same settings section on the host:\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 tcp:127.0.0.1:42137\n```\n\n### 5. Connect the Desktop Client\n\nUse `Import Remote Connection` for a generated code, or use `Save Existing Remote Profile` with:\n\n- label\n- base URL\n- bearer token\n\nThen 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## 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 generic attention alerts for `blocked` and settled `working → idle` transitions; 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 and an optional chat name field. Pane\nremembers the chosen agent as the default for future Sessions while existing\nSessions keep their own agent. A blank name receives a generated name such as\n`New chat` or `New chat 2`; each new Session opens as one Pane Chat and can be\nrenamed 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\nThe Session owns discussion, read-only code exploration and investigation,\nclarification, and ticket creation or revision. After a ticket is ready and\nthe user explicitly authorizes implementation, the Session dispatches an\nimplementation session through RunPane in an appropriate existing Pane or\ntab, or creates one when needed. It chooses the agent and the skills that fit\nthe work and names those skills by absolute path. The Session keeps its own\nselected agent, profile, and tool configuration.\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 to pin or unpin it. The pin preference survives\nrestarts; an archived Session stays out of Pinned until it is restored.\n\nClick a Session row to open its chat and expand or collapse its associated\nPanes. Child Pane rows are indented beyond the Session chat icon and keep the\nordinary Pane actions.\n\nRight-click a Session to archive it. 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; active navigation\nfilters them. 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## Session startup\n\nSession setup is quiet. After routine context and liveness checks, a new\nSession greets the user with a short invitation such as `Ready when you are.\nWhat would you like to work on?`; a resumed Session briefly surfaces its saved\nnext step. Routine diagnostics, machine details, watcher output, and\nworkspace-wide Pane inventory stay internal unless the user asks or a relevant\nfailure needs attention. If one human action blocks progress, the Session\nsurfaces that single action clearly.\n\nUnattended resilience is offered only when the user requests unattended work\nor delegated work makes the choice relevant. The offer states its effect,\nincluding keep-awake and automatic resume behavior. An explicit yes or no is\nremembered for the Session; silence and unrelated prompts are not consent. A\nnew explicit no revokes resilience, while an enabled choice persists across\nordinary resumes and unrelated prompts.\n\n## Pane association before delegation\n\nManagement is a Pane-level relationship; tabs inherit the relationship and\nshare the Pane's worktree. The orchestrator reads its own stable identity from\n`PANE_ORCHESTRATION_SESSION_ID` and associates a Pane immediately after\ncreating it, or before delegating to an existing Pane. The supported command\nis:\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. Prefer creating a Pane without\nan implementation prompt, associating and verifying it, then submitting the\nprompt. Keep a Pane attached through idle and completion; do not detach on\ncompletion. Archive behavior remains a separate #654 follow-up.\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## 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: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 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## Resume and refresh persisted context\n\nRead `PANE_ORCHESTRATION_SESSION_ID` from the current environment whenever a\nSession conversation starts or resumes. 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, reload the saved record and 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`\nafter a resume or mutation. If the variable is missing, use `runpane sessions\nlist --json` to resolve a Session explicitly; never guess an identity. 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\nWhen legacy Pane Chat contains history for more than one agent, each existing\nagent history is imported as its own named Session. The imported rows retain\nthe original hidden Session, fixed panel, buffers, and resume ID; generated\nunused panel slots keep those owners distinct across agent switches and\nrestarts.\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## Session watcher\n\nUse one durable, named watcher per Session, scoped to every associated Pane:\n\n```text\nrunpane watch --as session-<session-id> --follow --pane <pane-id> \\\n --kinds agent.ready,agent.blocked,agent.idle,panel.exited,pane.gone \\\n --settle 180000 --blocked-settle 30000 --min-interval 600000 \\\n --idle-backoff --json\n```\n\nRepeat `--pane` for each associated Pane. A discussion-only Session has no\nfollow watcher; never omit `--pane` to watch all Panes. After an associate or\ndetach mutation, refresh `sessions overview` and re-arm the same cursor with\nthe current Pane set. On restart, retain the `session-<session-id>` cursor and\ncapture a fresh output baseline before interpreting notifications. Return\nblocked and decision findings to the Session conversation. Terminal idle or\nexit remains activity evidence only.\n\n## Skill contract\n\n`main/src/services/skillCacheManager.ts` installs the skills that ship in\n`main/src/services/paneChatBundle/` once per run: 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, the runtime context, and the helper subagents\nin `.claude/agents/` and `.codex/agents/`. A manifest records what it\ninstalled, so later installs replace only those entries.\n\nThe Session route depends on `pane-orchestrator`, `runpane`,\n`orchestrate-sessions`, `create-ticket`, and `pane-work`. Agents dispatched to\nother repositories get skills by absolute path from the Session's prompt.\n"},{"path":"docs/SESSION_OUTPUT_PERFORMANCE.md","title":"Session output audit","text":"# Session output audit\n\nThis audit builds on the startup and Git-scan fixes in #591. It targets work\nrepeated during terminal activity and history loading, without changing\nterminal rendering, scrollback limits, or database schema.\n\n## Changes\n\n- `App`, `HomePage`, and `useIPCEvents` subscribe to the session fields/actions\n they use. Updating the separate terminal output buffer no longer schedules\n either top-level component to render. The event hook's unused fake socket\n return value and unused action dependency are removed.\n- Prompt markers use an indexed SQLite `COUNT(*)` instead of materializing\n every output payload to read the array length. Both existing marker offset\n conventions are preserved, and the count includes every output type.\n- Session history loading reads backward, retaining the newest 300 text\n outputs and 100 JSON messages independently, then restores chronological\n order. The old synchronous “batching” loop never yielded and could stop\n before reaching the newest messages. Normalization now runs only for\n retained JSON messages. Late history for a deleted session is ignored.\n- Panel JSON is decoded at the database boundary, including legacy\n string-wrapped state/metadata. This also fixes legacy state throwing before\n the active-panel flag could be assigned. The manager no longer performs\n two exception-driven string checks per panel or repeats JSON parsing.\n Malformed legacy serialized values retain a fallback, while archived-panel\n cache guards and summary reads that omit scrollback are preserved.\n\n## Measurements\n\nLocal Linux measurements with Node 22.18.0, compared with `80b8b621` (v2.4.100).\nThese are isolated measurements, not an end-to-end application speedup claim.\n\n| Workload | Before | After |\n| --- | ---: | ---: |\n| Count 10,000 output rows with 78.125 MiB of payload, median of 9 reads | 253.476 ms | 0.353 ms |\n| Heap growth during those reads, median of 9 | 83.772 MiB | 0.001 MiB |\n| App renders recorded during 100 background output chunks | 100 | 0 |\n| HomePage renders recorded during the same burst | 64 | 0 |\n| Output chunks retained after the burst | 100 | 100 |\n\nThe SQL benchmark warms both paths and requests GC before each measured read.\nHeap growth measures temporary JavaScript allocation, not retained memory or\ntotal process RSS. React Scan records component renders in a development\nChromium renderer with mocked Electron IPC; it does not measure native PTY,\nxterm/WebGL, or packaged Windows performance.\n\n## Reproduce\n\nUse the repository's supported Node/pnpm toolchain and a SQLite native module\nbuilt for that Node version.\n\n```bash\npnpm build:main\nnode --expose-gc scripts/benchmark-session-output.js\npnpm --filter frontend test src/stores/sessionStore.test.ts\npnpm --filter main test --run src/database/database.panel-loading.test.ts src/services/sessionManager.output-count.test.ts\nPANE_REACT_SCAN=1 pnpm test -- tests/session-output-perf.spec.ts --workers=1\n```\n\nThe render check requires a fresh dev server started with `PANE_REACT_SCAN=1`;\nan already-running server without that option cannot produce the evidence.\nThe check is opt-in so ordinary Playwright runs do not require React Scan.\nThe benchmark creates and removes its own temporary database.\n\n## Verification\n\n- All 334 frontend unit tests passed.\n- Full main unit run: 910 passed, 2 skipped, 15 failed. All 15 failures were\n Unix-socket `EPERM` errors in `daemon/server.test.ts` and\n `permissionIpcServer.test.ts`; the same 15 failures reproduced on unchanged\n `80b8b621` in this environment.\n- Chromium: render regression and two renderer startup smoke checks passed.\n The render regression failed on the baseline as expected. The new history\n and legacy-panel assertions also failed on the baseline and passed with\n these changes.\n- Root lint and typecheck passed. Lint reports five existing unused-variable\n warnings in `skillCacheManager.ts`.\n\nThe browser checks used a Vite-only Playwright web server with the repository's\nElectron API mock. Native desktop/Windows interaction was not exercised.\n"},{"path":"docs/SESSION_OUTPUT_SYSTEM.md","title":"Session Output Handling System","text":"# Session Output Handling System\n\n⚠️ **WARNING**: The session output handling system is complex and fragile. Modifying it frequently causes issues like duplicate messages, disappearing content, or blank screens. Any changes require explicit user permission.\n\n## How It Works\n\n### 1. Database Storage\n\n- Raw JSON messages from Claude are stored as-is in the database\n- Stdout/stderr outputs are stored directly\n- No formatting or transformation happens at storage time\n\n### 2. Real-time Streaming\n\n- When Claude outputs data, it's saved to the database immediately\n- For JSON messages, a formatted stdout version is sent to the frontend for the Output view\n- The original JSON is also sent for the Messages view\n- This provides immediate visual feedback during active sessions\n\n### 3. Session Loading\n\n- When navigating to a session, outputs are loaded from the database\n- The `sessions:get-output` handler transforms JSON messages to formatted stdout on-the-fly\n- Uses `setSessionOutputs` for atomic updates to prevent race conditions\n\n### 4. Frontend Display\n\n- The useSessionView hook manages session view logic and state (extracted from SessionView component)\n- A mutex lock (`loadingRef`) prevents concurrent loads\n- Timing is carefully managed with `requestAnimationFrame` and delays\n- The `formattedOutput` state is NOT cleared on session switch - it updates naturally\n\n### 5. Key Principles\n\n- Database is the single source of truth\n- Transformations happen on-the-fly, not at storage time\n- Real-time updates supplement but don't replace database data\n- Session switches always reload from database to ensure consistency\n\n## Common Issues and Solutions\n\n- **Duplicate messages**: Usually caused by sending both formatted and raw versions\n- **Disappearing content**: Often due to clearing output states at the wrong time\n- **Black screens**: Typically from race conditions during session switching\n- **Content only loads once**: Results from improper state management or missing dependencies\n\nThe current implementation carefully balances real-time updates with data persistence to provide a smooth user experience.\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 (if implemented)\n\n- `project:created` - Add new project to list\n- `project:updated` - Update specific project properties\n- `project:deleted` - Remove project from list\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, getCurrentTimestamp } from '../utils/timestampUtils';\n\n// For database storage\nconst timestamp = formatForDatabase(); // Returns ISO string\nconst now = getCurrentTimestamp(); // Alias for formatForDatabase()\n\n// For display formatting\nconst displayTime = formatForDisplay(timestamp);\n```\n\n### Frontend (`frontend/src/utils/timestampUtils.ts`)\n\n```typescript\nimport { parseTimestamp, formatDuration, getTimeDifference } from '../utils/timestampUtils';\n\n// Parse SQLite timestamps correctly\nconst date = parseTimestamp(\"2024-01-01 12:00:00\"); // Handles UTC conversion\n\n// Calculate durations\nconst durationMs = getTimeDifference(startTime, endTime);\nconst formatted = formatDuration(durationMs); // \"2m 34s\"\n\n// Display relative time\nconst ago = formatDistanceToNow(timestamp); // \"5 minutes ago\"\n```\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 - uses parseTimestamp utility\nconst date = parseTimestamp(\"2024-01-01 12:00:00\");\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 = parseTimestamp(dbTime); // Correctly handled 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\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` tracks the currently active panel per session\n- Foreign key constraints ensure panels are cleaned up when sessions are deleted\n\n## Event System\n\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 currently supports 8 panel types:\n\n1. **Terminal** (`terminal`) - Multiple PTY shells per session\n - Independent processes with command history and scrollback\n - File operation detection triggers `files:changed` events\n\n2. **Claude** (`claude`) - Multiple Claude Code instances\n - Managed by `claudePanelManager.ts`\n - Permission mode: approve/ignore\n - Automatic context tracking with token usage display\n\n3. **Codex** (`codex`) - Multiple Codex CLI instances\n - Managed by `codexPanelManager.ts`\n - Configurable model provider and approval policy\n - Sandbox mode and web search support\n\n4. **Diff** (`diff`) - Git diff viewer (singleton, permanent)\n - View modes: split/unified\n - Whitespace and context line configuration\n - Auto-refreshes on file changes\n\n5. **Editor** (`editor`) - In-app file editor\n - Syntax highlighting with Monaco\n - File tree with expanded directory tracking\n - Cursor position and scroll persistence\n\n6. **Logs** (`logs`) - Script execution panel (singleton)\n - Process management with PID tracking\n - Output buffer with error/warning counts\n - Start/end time and exit code tracking\n\n7. **Dashboard** (`dashboard`) - Project overview (singleton, permanent, projects-only)\n - Session health monitoring\n - Filter by status: all/stale/changes/pr\n - Cached data persistence\n\n8. **Setup Tasks** (`setup-tasks`) - Project setup checklist (singleton, permanent, projects-only)\n - Task completion tracking\n - Dismissible tasks\n - Last check timestamp\n\n## Panel Capabilities\n\nEach panel type has specific capabilities defined in `PANEL_CAPABILITIES`:\n\n- **requiresProcess**: Whether panel needs background process (terminal, claude, codex, 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)\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"},{"path":"docs/assets/image-prompts.md","title":"README image prompts","text":"# README image prompts\n\nGenerated using the built-in image-generation tool. Supplied style reference: the approved [Agent Farm banner](https://github.com/dcouple/agent-farm/blob/19f1fba8ef12050eef39c1b44e725c697b57b87b/docs/assets/agent-farm-banner.png).\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/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/dcouple/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 prompts and final assets in `docs/assets/`; inspect labels and\nreadability at README display width before embedding. Use new filenames for\nexperiments rather than overwriting chosen art.\n\nGeneration record: [image-prompts.md](image-prompts.md).\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`.\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"},{"path":"docs/remote-pwa-safari-debug-handoff.md","title":"Remote PWA Safari Debug Handoff","text":"# Remote PWA Safari Debug Handoff\n\n## Goal\n\nRoot-cause why Remote Pane connects from Chrome but fails from Safari/iOS Safari. Safari matters because iPhone Home Screen PWA install uses Safari/WebKit.\n\nDo not paste connection tokens into tickets, screenshots, or logs. When sharing output, redact `access_token`, `token`, and the full `pane-remote://...` code.\n\n## Current Known State\n\n- Preview app URL: `https://pane-remote-pwa-preview-sg622dpt4q-uc.a.run.app/app/?v=ca722cc`\n- Current preview revision verified on Cloud Run: `pane-remote-pwa-preview-00011-t4c`\n- Current preview JS bundle verified: `assets/remote-BwkHgECz.js`\n- Chrome can connect with the same code.\n- Safari/iOS Safari fails during connection or shortly after.\n- Direct `/health` on the Tailscale host has been reachable from iPhone, sometimes slowly.\n- Local curl from WSL to the Tailscale host returned CORS headers for `/health`, `OPTIONS /invoke`, and an unauthenticated `/events` error response:\n - `Access-Control-Allow-Origin: *`\n - `Access-Control-Allow-Methods: GET, POST, OPTIONS`\n - `Access-Control-Allow-Headers: Authorization, Content-Type, X-Pane-Remote-Runtime-Id, X-Pane-Remote-Client-Label, X-Pane-Client-Label, X-Pane-Client-Device-Label`\n\nThis points away from a simple missing CORS header and toward Safari-specific behavior in one of the browser transport steps.\n\n## Code Path To Inspect\n\nPrimary client file:\n\n- `frontend/src/remote/runtime/remoteDaemonBrowserClient.ts`\n\nConnection sequence:\n\n1. `connect()`\n2. `checkHealth()` calls `fetch(<baseUrl>/health, { cache: 'no-store' })`\n3. `openEventStream()` calls `fetch(<baseUrl>/events?...token...)`\n4. `consumeEventStream()` reads `response.body.getReader()`\n5. After connection, app state loads with POST `fetch(<baseUrl>/invoke, { method: 'POST', headers: { Content-Type: 'text/plain;charset=UTF-8' }, body: JSON.stringify(...) })`\n\nServer file:\n\n- `main/src/daemon/httpApiServer.ts`\n\nEndpoints:\n\n- `GET /health`\n- `GET /events`\n- `POST /invoke`\n- `OPTIONS *`\n\nImportant detail: the app currently consumes SSE through streaming `fetch()` plus `ReadableStream.getReader()`, not native `EventSource`.\n\n## Ranked Hypotheses\n\n1. Safari accepts `/health` but fails or stalls on streaming `fetch()` for `/events`.\n - Evidence: code uses `response.body.getReader()` for a long-lived `text/event-stream`; Chrome works; Safari/iOS is the only failing browser.\n - Supporting references:\n - MDN documents `Response.body` as a `ReadableStream` and shows `getReader()` stream consumption.\n - MDN documents `EventSource` as the native browser API for receiving `text/event-stream` SSE.\n\n2. Safari Home Screen PWA has different service-worker/storage/network behavior than Safari tab.\n - Need to compare Safari tab, Safari private/non-private, and Home Screen app with Web Inspector.\n\n3. Safari blocks or mishandles the cross-origin POST `/invoke` even though CORS looks correct from curl.\n - Need Network tab evidence for preflight and POST.\n\n4. Tailscale DNS/Tunnel readiness is slow on iOS Safari, causing the health or event connection to time out/abort.\n - User observed `/health` could take around 10 seconds earlier.\n\n## Best Reproduction Setup\n\nPhysical iPhone is preferred because it exactly matches the failing PWA environment. iOS Simulator is still useful for quick Safari Web Inspector iteration, but if the simulator cannot reach the `.ts.net` host through the Mac's Tailscale routing, use the physical iPhone.\n\nRequirements:\n\n- Mac with Safari and Xcode installed.\n- iPhone signed into the same Tailscale tailnet as the Pane host.\n- Fresh `pane-remote://...` code from the current Pane nightly host.\n- Preview URL above.\n- Safari Develop menu enabled on macOS Safari.\n\nApple's docs say iOS/iPadOS pages, Home Screen web apps, service workers, and simulators can be inspected from Safari on a Mac. They also note Web Inspector is always enabled for simulators and booted simulators appear in Safari's Develop menu.\n\nReferences:\n\n- Apple: https://developer.apple.com/documentation/safari-developer-tools/inspecting-ios\n- MDN `Response.body`: https://developer.mozilla.org/en-US/docs/Web/API/Response/body\n- MDN `EventSource`: https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource\n\n## Reproduction Steps\n\n### Physical iPhone\n\n1. On iPhone, open Settings > Apps > Safari > Advanced.\n2. Enable Web Inspector.\n3. Connect iPhone to Mac with a cable and trust the Mac when prompted.\n4. On Mac Safari, enable Develop menu if needed: Safari > Settings > Advanced > Show features for web developers.\n5. On iPhone Safari, open:\n - `https://pane-remote-pwa-preview-sg622dpt4q-uc.a.run.app/app/?v=ca722cc`\n6. On Mac Safari, open Develop > iPhone > the preview URL.\n7. Open Console and Network tabs.\n8. Paste a fresh connection code into the iPhone page and tap connect.\n9. Save screenshots or copied rows for:\n - Console error\n - `/health`\n - `/events`\n - `/invoke`\n - Any preflight `OPTIONS`\n10. Add the page to Home Screen and repeat from the installed web app:\n - With the Home Screen app in foreground, inspect it from Develop > iPhone > Home Screen Web Apps > preview URL.\n\n### iOS Simulator\n\n1. Install/open Xcode.\n2. Launch an iPhone simulator.\n3. Open Safari in the simulator.\n4. Open the preview URL.\n5. On Mac Safari, use Develop > Simulator > preview URL.\n6. Repeat the same connection steps.\n7. If `.ts.net` does not resolve or `/health` fails only in the simulator, switch to a physical iPhone.\n\n## Network Evidence To Capture\n\nFor each request, capture:\n\n- URL path, not full tokenized URL.\n- HTTP status.\n- Request method.\n- Request headers:\n - `Origin`\n - `Access-Control-Request-Method`\n - `Access-Control-Request-Headers`\n - `Content-Type`\n- Response headers:\n - `Access-Control-Allow-Origin`\n - `Access-Control-Allow-Methods`\n - `Access-Control-Allow-Headers`\n - `Content-Type`\n - `Cache-Control`\n- Timing: DNS, connect, SSL, waiting, total.\n- Whether Safari labels the request as blocked, cancelled, failed, or CORS error.\n\nExpected shape:\n\n- `/health`: `200`, JSON `{ \"ok\": true, \"status\": \"ready\", \"transport\": \"http+sse\" }`\n- `/events`: `200`, `Content-Type: text/event-stream; charset=utf-8`, stays open, first event should be `ready`\n- `/invoke`: `200`, JSON `{ \"ok\": true, \"result\": ... }`\n\n## Console Probes\n\nRun these in Web Inspector Console after importing the connection code. They read the saved profile from localStorage without printing the token.\n\n### 1. Confirm Saved Profile\n\n```js\nconst profiles = JSON.parse(localStorage.getItem('pane.remotePwa.savedProfiles') || '[]');\nprofiles.map((p) => ({\n id: p.id,\n label: p.label,\n baseUrl: p.baseUrl,\n transport: p.transport,\n hasToken: Boolean(p.token),\n tunnelKind: p.tunnel?.kind,\n}));\n```\n\n### 2. Test Health\n\n```js\nconst p = JSON.parse(localStorage.getItem('pane.remotePwa.savedProfiles') || '[]')[0];\nconst t0 = performance.now();\nawait fetch(`${p.baseUrl}/health`, { cache: 'no-store' })\n .then(async (r) => ({\n ok: r.ok,\n status: r.status,\n ms: Math.round(performance.now() - t0),\n headers: Object.fromEntries(r.headers.entries()),\n text: await r.text(),\n }))\n .catch((e) => ({ name: e.name, message: e.message, ms: Math.round(performance.now() - t0) }));\n```\n\n### 3. Test Streaming Fetch SSE\n\n```js\nconst p = JSON.parse(localStorage.getItem('pane.remotePwa.savedProfiles') || '[]')[0];\nconst url = new URL(`${p.baseUrl}/events`);\nurl.searchParams.set('access_token', p.token);\nurl.searchParams.set('runtime_id', 'safari-fetch-debug');\nurl.searchParams.set('client_label', 'Safari fetch debug');\n\nconst ac = new AbortController();\nsetTimeout(() => ac.abort(), 12000);\nconst t0 = performance.now();\ntry {\n const r = await fetch(url.toString(), { signal: ac.signal });\n console.log('fetch events response', {\n ok: r.ok,\n status: r.status,\n ms: Math.round(performance.now() - t0),\n contentType: r.headers.get('content-type'),\n hasBody: Boolean(r.body),\n getReaderType: typeof r.body?.getReader,\n });\n const reader = r.body?.getReader();\n const decoder = new TextDecoder();\n const first = await reader.read();\n console.log('fetch events first chunk', {\n done: first.done,\n text: first.value ? decoder.decode(first.value) : null,\n ms: Math.round(performance.now() - t0),\n });\n reader.releaseLock();\n} catch (e) {\n console.log('fetch events error', {\n name: e.name,\n message: e.message,\n ms: Math.round(performance.now() - t0),\n });\n}\n```\n\n### 4. Test Native EventSource SSE\n\n```js\nconst p = JSON.parse(localStorage.getItem('pane.remotePwa.savedProfiles') || '[]')[0];\nconst url = new URL(`${p.baseUrl}/events`);\nurl.searchParams.set('access_token', p.token);\nurl.searchParams.set('runtime_id', 'safari-eventsource-debug');\nurl.searchParams.set('client_label', 'Safari EventSource debug');\n\nconst t0 = performance.now();\nconst es = new EventSource(url.toString());\nes.addEventListener('open', () => console.log('eventsource open', Math.round(performance.now() - t0)));\nes.addEventListener('ready', (event) => console.log('eventsource ready', Math.round(performance.now() - t0), event.data));\nes.addEventListener('heartbeat', (event) => console.log('eventsource heartbeat', Math.round(performance.now() - t0), event.data));\nes.onerror = (event) => console.log('eventsource error', Math.round(performance.now() - t0), event);\nsetTimeout(() => {\n es.close();\n console.log('eventsource closed');\n}, 15000);\n```\n\n### 5. Test Invoke POST\n\n```js\nconst p = JSON.parse(localStorage.getItem('pane.remotePwa.savedProfiles') || '[]')[0];\nconst t0 = performance.now();\nawait fetch(`${p.baseUrl}/invoke`, {\n method: 'POST',\n headers: { 'Content-Type': 'text/plain;charset=UTF-8' },\n body: JSON.stringify({\n channel: 'sessions:get-all-with-projects',\n args: [],\n token: p.token,\n runtimeId: 'safari-invoke-debug',\n clientLabel: 'Safari invoke debug',\n }),\n})\n .then(async (r) => ({\n ok: r.ok,\n status: r.status,\n ms: Math.round(performance.now() - t0),\n headers: Object.fromEntries(r.headers.entries()),\n text: (await r.text()).slice(0, 1000),\n }))\n .catch((e) => ({ name: e.name, message: e.message, ms: Math.round(performance.now() - t0) }));\n```\n\n## Decision Matrix\n\nUse this to identify the root cause quickly.\n\n| Result | Likely Cause | Next Fix |\n| --- | --- | --- |\n| `/health` fails in Safari and Chrome works | iOS/Tailscale/DNS/TLS path | Capture Safari Network error and Tailscale state; not a PWA app bug yet |\n| `/health` passes, streaming `fetch /events` fails, `EventSource /events` works | Safari streaming `fetch` incompatibility | Change PWA client SSE transport from streaming fetch to native `EventSource` |\n| `/health` passes, both `fetch /events` and `EventSource /events` fail | SSE endpoint/header/proxy behavior incompatible with Safari | Compare `/events` response headers/timing; consider server header changes or heartbeat priming |\n| `/events` works, `/invoke` fails | POST/CORS/body handling issue | Capture preflight and POST headers/status; adjust server/client request shape |\n| Safari tab works, Home Screen PWA fails | PWA/service-worker/storage mode issue | Inspect Home Screen Web App and service worker separately; clear web app data and compare storage |\n\n## Useful Local Commands\n\nRun from any machine that has Tailscale access to the host. Redact tokens before sharing output.\n\n```bash\ncurl -sS -D - -o /tmp/pane-health.txt \\\n -H 'Origin: https://pane-remote-pwa-preview-sg622dpt4q-uc.a.run.app' \\\n '<BASE_URL>/health'\n```\n\n```bash\ncurl -sS -D - -o /tmp/pane-options.txt \\\n -X OPTIONS \\\n -H 'Origin: https://pane-remote-pwa-preview-sg622dpt4q-uc.a.run.app' \\\n -H 'Access-Control-Request-Method: POST' \\\n -H 'Access-Control-Request-Headers: content-type' \\\n '<BASE_URL>/invoke'\n```\n\n## What To Send Back\n\nSend:\n\n- Browser and OS version.\n- Safari tab vs Home Screen PWA result.\n- Physical iPhone vs simulator result.\n- Redacted Network rows for `/health`, `/events`, `/invoke`, and `OPTIONS`.\n- Console output from the five probes above.\n- Whether native `EventSource` works when streaming `fetch` fails.\n\nDo not send:\n\n- Full `pane-remote://...` code.\n- `access_token` query string.\n- `token` body value.\n"},{"path":"docs/troubleshooting/DIFF_VIEWER_CSS.md","title":"Diff Viewer CSS Troubleshooting","text":"# Diff Viewer CSS Troubleshooting\n\n⚠️ **IMPORTANT**: The diff viewer (react-diff-viewer-continued) has specific CSS requirements that can be tricky to debug.\n\n## Common Issue: No Scrollbars on Diff Viewer\n\nIf the diff viewer content is cut off and scrollbars don't appear:\n\n1. **DO NOT add complex CSS overrides** - This often makes the problem worse\n2. **Check parent containers for `overflow-hidden`** - This is usually the root cause\n3. **Use simple `overflow: 'auto'`** on the immediate diff container\n4. **Remove any forced widths or min-widths** unless absolutely necessary\n\n## The Solution That Works\n\n```tsx\n// In CombinedDiffView.tsx - the panel is a flex column and ChangesList owns scrolling.\n<div className=\"diff-panel flex min-w-0 flex-1 flex-col overflow-hidden\">\n <ChangesList rows={rows} {...listProps} />\n</div>\n```\n\n## What NOT to Do\n\n- Don't add multiple wrapper divs with conflicting overflow settings\n- Don't use CSS-in-JS to override react-diff-viewer's internal styles\n- Don't add global CSS selectors targeting generated class names\n- Don't use JavaScript hacks to force reflows\n\n## Root Cause\n\nThe issue is typically caused by parent containers having `overflow-hidden` which prevents child scrollbars from appearing. Check these files:\n\n- `SessionView.tsx` - Look for `overflow-hidden` classes\n- `CombinedDiffView.tsx` - Check the manifest/list host and its flex containers\n- `App.tsx` - Sometimes the issue starts at the app root level\n\nThe react-diff-viewer-continued library uses emotion/styled-components internally, which makes CSS overrides unreliable. The best approach is to ensure proper overflow handling in parent containers and keep the diff viewer wrapper simple.\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 Solutions:\n\n1. **Use Python 3.11 with pyenv**:\n ```bash\n brew install pyenv\n pyenv install 3.11.9\n pyenv global 3.11.9\n ```\n\n2. **Use the setup script**:\n ```bash\n ./setup-dev.sh\n ```\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're having trouble with the build process, consider waiting for official Windows binaries to be released in future versions of Pane.\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:** Added detection for error code 193 in `AbstractCliManager.ts` to trigger the Node.js fallback.\n\n**Location:** `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\\khaza\\allGitHubRepos\\notetake\\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(`Windows detected, using Node.js fallback proactively`);\n (global as typeof global & Record<string, boolean>)[needsNodeFallbackKey] = true;\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 detection (reactive fallback)\n2. `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 a token-boundary regex to\n `AGENT_COMMAND_PATTERNS` (order matters when one binary name contains another).\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\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, and unsupported environments\n (`AGENT_UNSUPPORTED_ENVIRONMENTS`).\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` (`CLI_BRAND_ICONS`) 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"},{"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## 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- `analytics_identity_source`\n\nThe event should also set person properties when the identity is known:\n\n- `email`\n- `github_email`\n- `git_email`\n- `install_id`\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:\n\n- `identity`\n- `installId`\n- `attribution`\n- `hasTrackedFirstOpen`\n- `hasTrackedWebAttribution`\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\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_code_copied` | `frontend/src/components/Settings.tsx` | Host code copied |\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## 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.\n"},{"path":"docs/ANALYTICS_UI_EVENTS_INTEGRATION.md","title":"Analytics UI Events Integration Guide","text":"# Analytics UI Events Integration Guide\n\nThis document describes how to integrate UI interaction event tracking into Pane's frontend components.\n\n## Overview\n\nBefore changing UI event tracking, read\n[Analytics Invariants](ANALYTICS_INVARIANTS.md). First-run disclosure and\nchoice events must be captured only after identity is resolved, and settings\nsaves must preserve analytics identity and attribution config.\n\nThe analytics system has been implemented with:\n1. Backend IPC handlers in `/main/src/ipc/analytics.ts`\n2. AnalyticsManager service enhancements to support string arrays\n3. Frontend service utility in `/frontend/src/services/analyticsService.ts`\n4. Preload script exposure for secure IPC communication\n\n## Backend Implementation\n\n### IPC Handlers\n- **File**: `/main/src/ipc/analytics.ts`\n- **Registered**: In `/main/src/ipc/index.ts`\n- **Service Integration**: AnalyticsManager added to AppServices in `/main/src/ipc/types.ts`\n\n### IPC Endpoints\n\n1. `analytics:track-ui-event` - Track UI interaction events\n2. `analytics:categorize-result-count` - Helper to categorize search result counts\n3. `analytics:hash-session-id` - Helper to hash session IDs for privacy\n\n## Frontend Integration Points\n\n### Using the AnalyticsService\n\nImport the service:\n```typescript\nimport { AnalyticsService } from '@/services/analyticsService';\n```\n\n### Event 1: view_switched\n\n**Track when users switch between tabs (Output, Diff, Logs, Editor, etc.)**\n\n**Where to implement:**\n- Component: `/frontend/src/components/SessionView.tsx` or `/frontend/src/hooks/useSessionView.ts`\n- Trigger: When user clicks on a tab to switch views\n- Implementation example:\n\n```typescript\nconst handleViewSwitch = async (fromView: string, toView: string, sessionId?: string) => {\n // Hash the session ID for privacy\n const sessionIdHash = sessionId\n ? await AnalyticsService.hashSessionId(sessionId)\n : undefined;\n\n await AnalyticsService.trackViewSwitched({\n from_view: fromView,\n to_view: toView,\n session_id_hash: sessionIdHash,\n });\n};\n```\n\n**View names to use:**\n- `output` - Main output view\n- `diff` - Git diff view\n- `logs` - Logs/terminal view\n- `editor` - Editor view\n- `dashboard` - Dashboard view\n\n### Event 2: help_dialog_opened\n\n**Track when users open the help dialog**\n\n**Where to implement:**\n- Component: `/frontend/src/components/Help.tsx` (or wherever the help dialog trigger is)\n- Trigger: When the help button/icon is clicked\n- Implementation example:\n\n```typescript\nconst handleHelpOpen = async (location: string) => {\n await AnalyticsService.trackHelpDialogOpened({\n from_location: location,\n });\n\n // Then open the help dialog\n setShowHelp(true);\n};\n```\n\n**Location values to use:**\n- `sidebar` - From sidebar help button\n- `toolbar` - From toolbar help button\n- `settings` - From settings page\n- `keyboard_shortcut` - From keyboard shortcut\n\n### Event 3: settings_opened\n\n**Track when users open settings**\n\n**Where to implement:**\n- Component: `/frontend/src/components/Settings.tsx` (or wherever settings trigger is)\n- Trigger: When settings dialog/page is opened\n- Implementation example:\n\n```typescript\nconst handleSettingsOpen = async (location: string) => {\n await AnalyticsService.trackSettingsOpened({\n from_location: location,\n });\n\n // Then open settings\n setShowSettings(true);\n};\n```\n\n**Location values to use:**\n- `sidebar` - From sidebar settings button\n- `menu` - From menu bar\n- `keyboard_shortcut` - From keyboard shortcut\n\n### Event 4: settings_saved\n\n**Track when users save settings changes**\n\n**Where to implement:**\n- Component: `/frontend/src/components/Settings.tsx`\n- Trigger: When user clicks \"Save\" or settings are persisted\n- Implementation example:\n\n```typescript\nconst handleSettingsSave = async (category: string, changedKeys: string[]) => {\n await AnalyticsService.trackSettingsSaved({\n category: category,\n setting_keys: changedKeys,\n });\n\n // Then save the settings\n await window.electronAPI.config.update(updatedSettings);\n};\n```\n\n**Category values to use:**\n- `general` - General settings\n- `notifications` - Notification settings\n- `project` - Project-specific settings\n- `appearance` - UI/appearance settings\n- `advanced` - Advanced settings\n\n**Setting keys examples:**\n- `verbose_logging`\n- `api_key`\n- `notification_enabled`\n- `notification_sound`\n- `custom_prompt`\n- `main_branch`\n\n### Event 5: sidebar_toggled\n\n**Track when users show/hide the sidebar**\n\n**Where to implement:**\n- Component: Main layout component (likely in `/frontend/src/App.tsx` or similar)\n- Trigger: When sidebar visibility changes\n- Implementation example:\n\n```typescript\nconst handleSidebarToggle = async (isVisible: boolean) => {\n await AnalyticsService.trackSidebarToggled({\n is_visible: isVisible,\n });\n\n // Then toggle sidebar\n setSidebarVisible(isVisible);\n};\n```\n\n### Event 6: search_used\n\n**Track when users use search functionality**\n\n**Where to implement:**\n- Component: Search components (likely in `/frontend/src/components/DraggableProjectTreeView.tsx` or search bar component)\n- Trigger: When search is executed (on Enter or search button click)\n- Implementation example:\n\n```typescript\nconst handleSearch = async (searchTerm: string, searchType: string, resultCount: number) => {\n // Categorize the result count for privacy (don't track exact numbers)\n const resultCategory = await AnalyticsService.categorizeResultCount(resultCount);\n\n await AnalyticsService.trackSearchUsed({\n search_type: searchType,\n result_count_category: resultCategory,\n });\n\n // Note: Do NOT track the actual search term for privacy\n};\n```\n\n**Search type values to use:**\n- `sessions` - Searching sessions\n- `prompts` - Searching prompts\n- `files` - Searching files\n- `projects` - Searching projects\n\n**Result count categories** (automatically categorized by backend):\n- `0-0` - No results\n- `1-5` - Few results\n- `6-10` - Some results\n- `11-25` - Many results\n- `26-50` - Lots of results\n- `51-100` - Very many results\n- `101+` - Tons of results\n\n## Privacy Considerations\n\n### Do NOT Track:\n- ❌ Actual search terms\n- ❌ File names or paths\n- ❌ Prompt content\n- ❌ User identifiable information\n- ❌ Exact counts (use categories)\n- ❌ Raw session IDs (always hash them)\n\n### DO Track:\n- ✅ Event types (which button was clicked)\n- ✅ Categories and types\n- ✅ Hashed/anonymized identifiers\n- ✅ Boolean states (visible/hidden, enabled/disabled)\n- ✅ Categorized counts (0-5, 6-10, etc.)\n\n## Testing\n\nTo test analytics integration:\n\n1. **Enable analytics** in settings\n2. **Enable verbose logging** to see analytics events in the console\n3. **Perform actions** that should trigger events\n4. **Check logs** for messages like:\n ```\n [Analytics] Tracked event: view_switched { from_view: 'output', to_view: 'diff', ... }\n ```\n\n## Type Safety\n\nThe `AnalyticsService` class provides type-safe methods for all events. TypeScript will enforce:\n- Correct event names\n- Required properties for each event type\n- Proper value types for each property\n\nIf you try to pass invalid data, you'll get a TypeScript error at compile time.\n\n## Error Handling\n\nAll analytics methods:\n- Are non-blocking (use `async/await` but don't wait for responses)\n- Fail silently (errors are logged but don't break functionality)\n- Check if analytics is enabled before tracking\n- Handle cases where the electronAPI is not available\n\n## Example: Complete Integration\n\nHere's a complete example of integrating view switching in a component:\n\n```typescript\nimport React, { useState } from 'react';\nimport { AnalyticsService } from '@/services/analyticsService';\n\nexport function SessionView({ sessionId }: { sessionId: string }) {\n const [currentView, setCurrentView] = useState<'output' | 'diff' | 'logs' | 'editor'>('output');\n\n const switchView = async (newView: typeof currentView) => {\n // Track the view switch\n const sessionIdHash = await AnalyticsService.hashSessionId(sessionId);\n await AnalyticsService.trackViewSwitched({\n from_view: currentView,\n to_view: newView,\n session_id_hash: sessionIdHash,\n });\n\n // Update the view\n setCurrentView(newView);\n };\n\n return (\n <div>\n <div className=\"tabs\">\n <button onClick={() => switchView('output')}>Output</button>\n <button onClick={() => switchView('diff')}>Diff</button>\n <button onClick={() => switchView('logs')}>Logs</button>\n <button onClick={() => switchView('editor')}>Editor</button>\n </div>\n {/* View content here */}\n </div>\n );\n}\n```\n\n## Summary\n\nAll backend infrastructure is complete. Frontend developers should:\n\n1. Import `AnalyticsService` from `/frontend/src/services/analyticsService.ts`\n2. Call the appropriate tracking method when user actions occur\n3. Follow the privacy guidelines (no PII, use hashing/categorization)\n4. Test with verbose logging enabled to verify events are tracked\n\nProduct analytics are enabled by default for new and previously undecided\ninstalls. Privacy Settings discloses the default and provides the visible\nopt-out. Pane does not collect prompts, code, or file paths, and capture stops\nafter an opt-out.\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/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:** As of February 2026, the Windows build works without major issues. 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** - v20.x or later (v22.x recommended)\n2. **pnpm** - v8.x or later\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`) that handles several compatibility issues:\n\n### Known Issues & Workarounds\n\n#### 1. winpty.gyp Batch File Path Issue\n\n**Problem:** The `@homebridge/node-pty-prebuilt-multiarch` package uses batch files in its build process:\n```\ncmd /c \"cd shared && GetCommitHash.bat\"\n```\n\nOn Windows, batch files need an explicit `.\\` prefix to run from the current directory.\n\n**Solution:** The build script patches `winpty.gyp` to use:\n```\ncmd /c \"cd shared && .\\GetCommitHash.bat\"\n```\n\n#### 2. pnpm + node-gyp Path Resolution\n\n**Problem:** pnpm's nested `node_modules` structure causes node-gyp to fail when resolving relative paths for dependencies like `node-addon-api`. The error looks like:\n```\nFileNotFoundError: [Errno 2] No such file or directory: '...node-addon-api\\node_addon_api_maybe.vcxproj.filters'\n```\n\n**Solution:** The build script copies `node-addon-api` files to the location pnpm/node-gyp expects.\n\n#### 3. 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#### 4. Native Module Rebuild\n\n**Problem:** Even with the above fixes, 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- Pre-bundled ConPTY binaries for `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: Patch winpty.gyp\n\nEdit `node_modules/.pnpm/@homebridge+node-pty-prebuilt-multiarch@*/node_modules/@homebridge/node-pty-prebuilt-multiarch/deps/winpty/src/winpty.gyp`:\n\nChange:\n```python\n'WINPTY_COMMIT_HASH%': '<!(cmd /c \"cd shared && GetCommitHash.bat\")',\n```\nTo:\n```python\n'WINPTY_COMMIT_HASH%': '<!(cmd /c \"cd shared && .\\\\GetCommitHash.bat\")',\n```\n\nAlso change:\n```python\n'<!(cmd /c \"cd shared && UpdateGenVersion.bat <(WINPTY_COMMIT_HASH)\")',\n```\nTo:\n```python\n'<!(cmd /c \"cd shared && .\\\\UpdateGenVersion.bat <(WINPTY_COMMIT_HASH)\")',\n```\n\n### Step 3: Copy node-addon-api\n\n```bash\n# Find the source and target directories (paths may vary by version)\nmkdir -p \"node_modules/.pnpm/@homebridge+node-pty-prebuilt-multiarch@*/node-addon-api@*/node_modules/node-addon-api/\"\ncp -r \"node_modules/.pnpm/node-addon-api@*/node_modules/node-addon-api/\"* \\\n \"node_modules/.pnpm/@homebridge+node-pty-prebuilt-multiarch@*/node-addon-api@*/node_modules/node-addon-api/\"\n```\n\n### Step 4: 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 5: 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: `GetCommitHash.bat is not recognized`\n\nThe winpty.gyp file hasn't been patched. Run the build script or apply the manual patch.\n\n### Error: `FileNotFoundError: node_addon_api_maybe.vcxproj.filters`\n\nThe node-addon-api files haven't been copied to the expected location. Run the build script or copy manually.\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 handles all the necessary patches automatically.\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\n**Examples:**\n```json\n\"run\": \"pnpm dev\"\n\"run\": \"npm run dev -- --port $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\"run\": \"npm run db:teardown\"\n\"run\": \"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` for clarity, but both are detected with the same priority.\n"},{"path":"docs/CRYSTAL_ARCHITECTURE.md","title":"Pane Architecture Documentation","text":"# Pane Architecture Documentation\n\nThe filename is historical (the product was once called Crystal). There is no Express HTTP API; the renderer talks to the main process over Electron IPC, and RunPane talks to the same process over the local daemon socket.\n\n## Overview\n\nPane is an Electron desktop application that manages multiple agent CLI processes (Claude Code, Codex, Cursor, and custom tools) in git worktrees. This document visualizes the architecture using Mermaid diagrams.\n\n## High-Level Architecture\n\n```mermaid\ngraph TB\n subgraph \"Electron Application\"\n subgraph \"Renderer Process\"\n UI[React UI]\n Store[Zustand Store]\n Terminal[XTerm.js Terminal]\n end\n \n subgraph \"Main Process\"\n IPC[IPC Handlers]\n Daemon[RunPane Daemon]\n DB[(SQLite Database)]\n Queue[Bull Task Queue]\n Sessions[Session Manager]\n Git[Git Manager]\n end\n \n UI <--> IPC\n Store <--> IPC\n Terminal <--> IPC\n \n IPC <--> Daemon\n IPC <--> Sessions\n IPC <--> Git\n \n Daemon <--> DB\n Daemon <--> Queue\n Sessions <--> DB\n Git <--> DB\n end\n \n subgraph \"External Processes\"\n Agent1[Agent CLI 1]\n Agent2[Agent CLI 2]\n Agent3[Agent CLI N]\n \n Worktree1[Git Worktree 1]\n Worktree2[Git Worktree 2]\n Worktree3[Git Worktree N]\n end\n \n Sessions --> Agent1\n Sessions --> Agent2\n Sessions --> Agent3\n \n Agent1 --> Worktree1\n Agent2 --> Worktree2\n Agent3 --> Worktree3\n```\n\n## Component Flow\n\n```mermaid\nflowchart LR\n User[User] --> UI[UI Components]\n UI --> Action[User Action]\n \n Action --> IPC{IPC Router}\n \n IPC -->|Session Ops| SessionHandler[Session Handler]\n IPC -->|Git Ops| GitHandler[Git Handler]\n IPC -->|Config| ConfigHandler[Config Handler]\n \n SessionHandler --> DB[(Database)]\n GitHandler --> Git[Git Commands]\n ConfigHandler --> Store[Config Manager]\n \n SessionHandler --> Agent[Agent Process]\n Agent --> Output[Terminal Output]\n Output --> Stream[IPC Stream]\n Stream --> UI\n```\n\n## Database Schema\n\n```mermaid\nerDiagram\n projects ||--o{ sessions : has\n sessions ||--o{ session_outputs : generates\n sessions ||--o{ conversation_messages : contains\n sessions ||--o{ execution_diffs : tracks\n sessions ||--o{ prompt_markers : marks\n \n projects {\n int id PK\n string name\n string path\n string system_prompt\n string run_script\n string main_branch\n timestamp created_at\n timestamp updated_at\n }\n \n sessions {\n int id PK\n int project_id FK\n string name\n string prompt\n string status\n string worktree_path\n string branch_name\n boolean archived\n timestamp created_at\n timestamp updated_at\n }\n \n session_outputs {\n int id PK\n int session_id FK\n string type\n text data\n timestamp timestamp\n }\n \n conversation_messages {\n int id PK\n int session_id FK\n string role\n text content\n int message_order\n timestamp timestamp\n }\n \n execution_diffs {\n int id PK\n int session_id FK\n text diff_content\n json file_stats\n timestamp timestamp\n }\n \n prompt_markers {\n int id PK\n int session_id FK\n string prompt_text\n int output_index\n timestamp timestamp\n timestamp completion_timestamp\n }\n```\n\n## Session Lifecycle\n\n```mermaid\nstateDiagram-v2\n [*] --> Created: User creates session\n Created --> Initializing: Start worktree setup\n Initializing --> Running: Agent CLI started\n Running --> Waiting: Needs user input\n Waiting --> Running: User provides input\n Running --> Completed: Task finished\n Running --> Error: Process failed\n Waiting --> Stopped: User stops\n Running --> Stopped: User stops\n Completed --> Archived: User archives\n Error --> Archived: User archives\n Stopped --> Archived: User archives\n Archived --> [*]\n```\n\n## IPC Communication Flow\n\n```mermaid\nsequenceDiagram\n participant UI as Frontend UI\n participant IPC as IPC Handler\n participant Main as Main Process\n participant DB as Database\n participant Agent as Agent Process\n \n UI->>IPC: Create Session Request\n IPC->>Main: Validate & Process\n Main->>DB: Store Session Data\n Main->>Main: Create Git Worktree\n Main->>Agent: Spawn Agent Process\n Agent-->>Main: Process Started\n Main->>DB: Update Session Status\n Main-->>IPC: Session Created\n IPC-->>UI: Update UI\n \n loop Real-time Output\n Agent->>Main: Output Data\n Main->>DB: Store Output\n Main->>IPC: Stream Output\n IPC->>UI: Display in Terminal\n end\n```\n\n## Frontend Component Hierarchy\n\n```mermaid\ngraph TD\n App[App Component]\n App --> Sidebar[Sidebar]\n App --> Main[Main Content Area]\n \n Sidebar --> ProjectSelector[Project Selector]\n Sidebar --> SessionList[Session List]\n Sidebar --> PromptHistory[Prompt History]\n \n SessionList --> SessionItem[Session Item]\n SessionItem --> StatusBadge[Status Badge]\n SessionItem --> Actions[Session Actions]\n \n Main --> SessionView[Session View]\n SessionView --> Tabs[View Tabs]\n SessionView --> Terminal[Terminal Display]\n SessionView --> Input[Input Area]\n \n Tabs --> OutputView[Output View]\n Tabs --> MessagesView[Messages View]\n Tabs --> DiffView[Diff View]\n Tabs --> TerminalView[Terminal View]\n```\n\n## Data Flow for Session Output\n\n```mermaid\nflowchart TD\n Agent[Agent Process] --> Raw[Raw Output]\n Raw --> Parse{Parse Output}\n \n Parse -->|JSON Message| Format[Format for Display]\n Parse -->|Stdout/Stderr| Direct[Direct Display]\n \n Format --> Store[(Database)]\n Direct --> Store\n \n Store --> Load{Load Session}\n Load --> Transform[Transform JSON to Stdout]\n Transform --> Display[Display in Terminal]\n \n subgraph \"Real-time Updates\"\n Raw --> Stream[IPC Stream]\n Stream --> LiveDisplay[Live Terminal Update]\n end\n```\n\n## Git Operations Flow\n\n```mermaid\nflowchart LR\n UI[User Interface] --> GitOp{Git Operation}\n \n GitOp -->|Rebase| RebaseFlow\n GitOp -->|Squash| SquashFlow\n GitOp -->|Diff| DiffFlow\n \n subgraph RebaseFlow[Rebase from Main]\n Fetch[git fetch origin]\n Fetch --> Rebase[git rebase origin/main]\n Rebase --> UpdateDB[Update Database]\n end\n \n subgraph SquashFlow[Squash and Rebase]\n Reset[git reset --soft origin/main]\n Reset --> Commit[git commit]\n Commit --> UpdateDB2[Update Database]\n end\n \n subgraph DiffFlow[View Diff]\n DiffCmd[git diff origin/main]\n DiffCmd --> ParseDiff[Parse Diff Output]\n ParseDiff --> StoreDiff[Store in Database]\n StoreDiff --> DisplayDiff[Display in UI]\n end\n```\n\n## Task Queue Processing\n\n```mermaid\nflowchart TD\n Request[IPC Request] --> Queue{Task Queue}\n \n Queue --> Job1[Session Creation Job]\n Queue --> Job2[Git Operation Job]\n Queue --> Job3[Cleanup Job]\n \n Job1 --> Process1[Create Worktree]\n Process1 --> Start1[Start Agent]\n Start1 --> Update1[Update Database]\n \n Job2 --> Process2[Execute Git Command]\n Process2 --> Update2[Update Database]\n \n Job3 --> Process3[Remove Worktree]\n Process3 --> Update3[Archive Session]\n \n Update1 --> Event1[Emit Event]\n Update2 --> Event2[Emit Event]\n Update3 --> Event3[Emit Event]\n \n Event1 --> UI[Update UI]\n Event2 --> UI\n Event3 --> UI\n```\n\n## Module Dependencies\n\n```mermaid\ngraph TD\n subgraph \"Main Process Modules\"\n Index[index.ts] --> IPC[IPC Handlers]\n Index --> Events[events.ts]\n \n IPC --> GitIPC[ipc/git.ts]\n IPC --> SessionIPC[ipc/session.ts]\n \n GitIPC --> GitManager[GitManager]\n SessionIPC --> SessionManager[SessionManager]\n \n SessionManager --> DB[Database]\n SessionManager --> AgentManager[Terminal / Agent Managers]\n \n GitManager --> DB\n \n Events --> SessionManager\n Events --> GitManager\n end\n \n subgraph \"Frontend Modules\"\n App[App.tsx] --> SessionView[SessionView.tsx]\n SessionView --> UseSessionView[useSessionView.ts]\n \n UseSessionView --> Store[Zustand Stores]\n UseSessionView --> Utils[Utils]\n \n App --> Sidebar[Sidebar Components]\n Sidebar --> Store\n end\n```\n\n## Security & Isolation\n\n```mermaid\nflowchart TB\n subgraph \"Security Boundaries\"\n subgraph \"Renderer Process\"\n UI[UI Components]\n UI -.->|Sandboxed| External[External Resources]\n end\n \n subgraph \"Main Process\"\n IPC[IPC Handlers]\n FS[File System Access]\n Process[Process Spawning]\n end\n \n subgraph \"Isolation\"\n W1[Worktree 1]\n W2[Worktree 2]\n W3[Worktree N]\n \n C1[Agent 1]\n C2[Agent 2]\n C3[Agent N]\n end\n end\n \n UI -->|Controlled IPC| IPC\n IPC --> FS\n IPC --> Process\n \n Process --> C1\n Process --> C2\n Process --> C3\n \n C1 --> W1\n C2 --> W2\n C3 --> W3\n \n W1 -.->|Isolated| W2\n W2 -.->|Isolated| W3\n```\n\n## Performance Optimizations\n\n```mermaid\nflowchart LR\n subgraph \"Optimization Strategies\"\n subgraph \"Frontend\"\n Debounce[Debounced Updates]\n Lazy[Lazy Loading]\n Virtual[Virtual Scrolling]\n Cache[State Caching]\n end\n \n subgraph \"Backend\"\n Batch[Batch Operations]\n Index[DB Indexing]\n Pool[Connection Pooling]\n Queue[Async Queue]\n end\n \n subgraph \"Communication\"\n Targeted[Targeted Updates]\n Compress[Data Compression]\n Stream[Streaming]\n end\n end\n \n Updates[State Updates] --> Debounce\n LargeData[Large Datasets] --> Lazy\n Terminal[Terminal Output] --> Virtual\n \n Multiple[Multiple Ops] --> Batch\n Queries[DB Queries] --> Index\n \n Changes[Data Changes] --> Targeted\n Output[Agent Output] --> Stream\n```\n\nThis comprehensive set of Mermaid diagrams illustrates the various aspects of Pane's architecture, from high-level component relationships to detailed data flows and module dependencies."},{"path":"docs/DATABASE_DOCUMENTATION.md","title":"Pane Database Documentation","text":"# Pane Database Documentation\n\nThis document provides a comprehensive overview of all SQLite database tables and local storage mechanisms used in the Pane application.\n\n## Overview\n\nPane uses a SQLite database located at `~/.pane/pane.db` for persistent data storage. The database is managed using Better-SQLite3 for synchronous operations and includes a migration system for schema evolution.\n\n## Database Tables\n\n### 1. `projects` Table\n\nStores project configurations and metadata.\n\n| Column | Type | Description | Constraints |\n|--------|------|-------------|-------------|\n| `id` | INTEGER | Unique project identifier | PRIMARY KEY AUTOINCREMENT |\n| `name` | TEXT | Project display name | NOT NULL |\n| `path` | TEXT | Absolute path to project directory | NOT NULL UNIQUE |\n| `created_at` | DATETIME | Project creation timestamp | DEFAULT CURRENT_TIMESTAMP |\n| `is_active` | INTEGER | Whether this is the active project (0 or 1) | DEFAULT 0 |\n| `custom_prompt` | TEXT | Project-specific system prompt | Optional |\n| `run_script` | TEXT | Script command for testing/building | Optional |\n| `main_branch` | TEXT | Main branch name (e.g., 'main', 'master') | DEFAULT 'main' |\n\n**Purpose**: Manages multiple project directories, allowing users to switch between different codebases. Only one project can be active at a time.\n\n### 2. `sessions` Table\n\nCore session metadata for Claude Code instances.\n\n| Column | Type | Description | Constraints |\n|--------|------|-------------|-------------|\n| `id` | INTEGER | Unique session identifier | PRIMARY KEY AUTOINCREMENT |\n| `project_id` | INTEGER | Associated project | FOREIGN KEY REFERENCES projects(id) |\n| `worktree_name` | TEXT | Git worktree branch name | NOT NULL |\n| `prompt` | TEXT | Initial user prompt | NOT NULL |\n| `status` | TEXT | Session state | NOT NULL |\n| `created_at` | DATETIME | Session creation timestamp | DEFAULT CURRENT_TIMESTAMP |\n| `updated_at` | DATETIME | Last update timestamp | DEFAULT CURRENT_TIMESTAMP |\n| `archived` | INTEGER | Whether session is archived (0 or 1) | DEFAULT 0 |\n| `ai_generated_name` | TEXT | AI-generated descriptive name | Optional |\n| `worktree_path` | TEXT | Absolute path to git worktree | Optional |\n| `last_execution_index` | INTEGER | Index of last command execution | DEFAULT 0 |\n\n**Status Values**:\n- `initializing` - Setting up git worktree\n- `running` - Claude is actively processing\n- `waiting` - Needs user input\n- `completed` - Task finished successfully\n- `error` - Something went wrong\n- `stopped` - Session was manually stopped\n\n**Purpose**: Tracks all Claude Code sessions, their states, and metadata for session management and recovery.\n\n### 3. `session_outputs` Table\n\nStores terminal output history for each session.\n\n| Column | Type | Description | Constraints |\n|--------|------|-------------|-------------|\n| `id` | INTEGER | Unique output identifier | PRIMARY KEY AUTOINCREMENT |\n| `session_id` | INTEGER | Associated session | FOREIGN KEY REFERENCES sessions(id) |\n| `type` | TEXT | Output type | NOT NULL |\n| `data` | TEXT | Output content | NOT NULL |\n| `timestamp` | DATETIME | When output was generated | DEFAULT CURRENT_TIMESTAMP |\n| `formatted_data` | TEXT | Pre-formatted version for display | Optional |\n\n**Type Values**:\n- `stdout` - Standard output text\n- `stderr` - Error output text\n- `json` - JSON messages from Claude\n- `system` - System messages\n\n**Purpose**: Maintains complete history of all session outputs for replay and debugging. Raw JSON messages are stored as-is, with formatting done on-the-fly during retrieval.\n\n### 4. `conversation_messages` Table\n\nStores conversation history for session continuation.\n\n| Column | Type | Description | Constraints |\n|--------|------|-------------|-------------|\n| `id` | INTEGER | Unique message identifier | PRIMARY KEY AUTOINCREMENT |\n| `session_id` | INTEGER | Associated session | FOREIGN KEY REFERENCES sessions(id) |\n| `role` | TEXT | Message sender | NOT NULL |\n| `content` | TEXT | Message content | NOT NULL |\n| `timestamp` | DATETIME | Message timestamp | DEFAULT CURRENT_TIMESTAMP |\n\n**Role Values**:\n- `user` - User input messages\n- `assistant` - Claude's responses\n- `system` - System prompts\n\n**Purpose**: Enables conversation continuation by preserving the full message history context when resuming a session.\n\n### 5. `execution_diffs` Table\n\nTracks git diffs for each execution round.\n\n| Column | Type | Description | Constraints |\n|--------|------|-------------|-------------|\n| `id` | INTEGER | Unique diff identifier | PRIMARY KEY AUTOINCREMENT |\n| `session_id` | INTEGER | Associated session | FOREIGN KEY REFERENCES sessions(id) |\n| `execution_index` | INTEGER | Execution round number | NOT NULL |\n| `diff_data` | TEXT | Git diff output (JSON) | NOT NULL |\n| `created_at` | DATETIME | When diff was captured | DEFAULT CURRENT_TIMESTAMP |\n\n**Purpose**: Maintains a history of code changes per execution round, allowing users to see what changed at each step of the session.\n\n### 6. `prompt_markers` Table\n\nNavigation markers for prompts within session output.\n\n| Column | Type | Description | Constraints |\n|--------|------|-------------|-------------|\n| `id` | INTEGER | Unique marker identifier | PRIMARY KEY AUTOINCREMENT |\n| `session_id` | INTEGER | Associated session | FOREIGN KEY REFERENCES sessions(id) |\n| `prompt_text` | TEXT | The prompt content | NOT NULL |\n| `output_index` | INTEGER | Position in session_outputs | NOT NULL |\n| `timestamp` | DATETIME | When prompt was sent | DEFAULT CURRENT_TIMESTAMP |\n| `completion_timestamp` | DATETIME | When prompt finished processing | Optional |\n\n**Purpose**: Enables quick navigation to specific prompts within long session outputs and tracks execution duration.\n\n### 7. `migrations` Table\n\nTracks applied database migrations.\n\n| Column | Type | Description | Constraints |\n|--------|------|-------------|-------------|\n| `id` | INTEGER | Migration version number | PRIMARY KEY |\n| `applied_at` | DATETIME | When migration was applied | DEFAULT CURRENT_TIMESTAMP |\n\n**Purpose**: Ensures database schema can be safely evolved over time by tracking which migrations have been applied.\n\n## Database Indexes\n\nThe database includes several indexes for performance optimization:\n\n- `idx_sessions_project_id` - Fast lookup of sessions by project\n- `idx_session_outputs_session_id` - Fast retrieval of outputs for a session\n- `idx_conversation_messages_session_id` - Quick access to conversation history\n- `idx_execution_diffs_session_id_execution_index` - Efficient diff lookups\n- `idx_prompt_markers_session_id` - Fast prompt navigation\n\n## Local Storage (Non-Database)\n\n### 1. Electron Store\n\nLocation: Platform-specific (handled by electron-store)\n- macOS: `~/Library/Application Support/Pane/config.json`\n- Windows: `%APPDATA%/Pane/config.json`\n- Linux: `~/.config/Pane/config.json`\n\n**Stored Data**:\n```json\n{\n \"verboseLogging\": boolean,\n \"anthropicApiKey\": \"string (encrypted)\",\n \"globalSystemPrompt\": \"string\",\n \"claudeExecutablePath\": \"string\",\n \"notifications\": {\n \"enabled\": boolean,\n \"sound\": boolean,\n \"statusChanges\": boolean,\n \"waiting\": boolean,\n \"completion\": boolean,\n \"errors\": boolean\n }\n}\n```\n\n### 2. Application Directory\n\nLocation: `~/.pane/`\n\n**Contents**:\n- `pane.db` - Main SQLite database\n- Automatically created on first application run\n\n### 3. Git Worktrees\n\nLocation: `<project_path>/.git/worktrees/`\n\n**Structure**:\n- Each session creates a worktree named after its branch\n- Worktrees are cleaned up when sessions are deleted\n- Contains isolated git working directory for each Claude session\n\n## Data Flow and Relationships\n\n```\nprojects (1) ─────┬──── (∞) sessions\n │ │\n │ ├──── (∞) session_outputs\n │ ├──── (∞) conversation_messages\n │ ├──── (∞) execution_diffs\n │ └──── (∞) prompt_markers\n │\n └──── git worktrees (file system)\n```\n\n## Important Implementation Notes\n\n1. **Timestamp Handling**: All timestamps are stored in UTC format. The frontend utilities handle proper timezone conversion for display.\n\n2. **Transaction Safety**: Database operations use transactions where appropriate to maintain data integrity.\n\n3. **Cascade Deletion**: When a project is deleted, all associated sessions and their related data are automatically removed through foreign key constraints.\n\n4. **Archive vs Delete**: Sessions are typically archived (`archived = 1`) rather than deleted to preserve history. True deletion only occurs when explicitly requested.\n\n5. **Performance Considerations**: \n - Session outputs can grow large; pagination may be needed for very long sessions\n - Indexes are crucial for responsive UI with many sessions\n - The `formatted_data` column in session_outputs is used sparingly to balance storage vs computation\n\n## Migration System\n\nThe application uses a simple migration system located in `main/src/database/migrations/`. Each migration is a SQL file numbered sequentially (e.g., `001_initial_schema.sql`, `002_add_prompt_markers.sql`).\n\nMigrations are applied automatically on application startup if the database version is behind the latest migration.\n\n## Backup Recommendations\n\nUsers should periodically backup:\n1. `~/.pane/pane.db` - Contains all session data\n2. The Electron Store config file - Contains application settings\n3. Project directories - Contains the actual code and git history\n\n## Security Considerations\n\n1. **API Keys**: The Anthropic API key is stored in the Electron Store with platform-specific encryption\n2. **File Paths**: All file paths are stored as absolute paths; care should be taken when sharing databases\n3. **Sensitive Data**: Session outputs may contain sensitive information and should be treated accordingly"},{"path":"docs/FEATURE_USAGE_TRACKING_INTEGRATION.md","title":"Feature Usage Tracking Integration Guide","text":"# Feature Usage Tracking Integration Guide\n\nThis document provides guidance on integrating the remaining feature usage tracking events into Pane's frontend.\n\n## Overview\n\nBefore adding or changing tracking, read\n[Analytics Invariants](ANALYTICS_INVARIANTS.md). Consent, identity, attribution,\nand version fields have ordering requirements that keep PostHog funnels\nperson-stitchable.\n\nThe following feature usage events have been fully implemented:\n- ✅ `run_script_executed` - Tracked in backend (logsManager)\n- ✅ `notification_shown` - Tracked in frontend (useNotifications hook)\n\nThe following events require frontend integration:\n- ⏳ `keyboard_shortcut_used` - Integration points documented below\n- ⏳ `drag_drop_used` - Integration points documented below\n\n## Keyboard Shortcut Tracking\n\n### Event Properties\n```typescript\n{\n shortcut_key: string; // e.g., \"Cmd+Enter\", \"Ctrl+K\", \"Escape\"\n action: string; // e.g., \"submit_input\", \"focus_search\", \"close_dialog\"\n}\n```\n\n### Integration Points\n\n#### 1. AI Input Panels (Cmd/Ctrl+Enter)\n**File**: `/frontend/src/hooks/useAIInputPanel.ts` (line ~311)\n\n**Current Code**:\n```typescript\nconst handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n handleSubmit();\n }\n};\n```\n\n**Add Tracking**:\n```typescript\nimport { AnalyticsService } from '../services/analyticsService';\n\nconst handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n handleSubmit();\n\n // Track keyboard shortcut usage\n AnalyticsService.trackKeyboardShortcut({\n shortcut_key: `${e.metaKey ? 'Cmd' : 'Ctrl'}+Enter`,\n action: 'submit_ai_input',\n }).catch(console.error);\n }\n};\n```\n\n#### 2. Session Input (Cmd/Ctrl+Enter)\n**File**: `/frontend/src/components/session/SessionInput.tsx` (line ~50-60)\n\n**Similar pattern** - Add tracking when Cmd/Ctrl+Enter is pressed to submit session input.\n\n#### 3. File Editor Shortcuts\n**File**: `/frontend/src/components/panels/editor/FileEditor.tsx`\n\n**Keyboard shortcuts to track**:\n- Save file: `Cmd/Ctrl+S`\n- Close editor: `Escape`\n- Any other Monaco editor keyboard shortcuts\n\n**Add event listener**:\n```typescript\n// In useEffect or Monaco configuration\neditor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {\n handleSave();\n AnalyticsService.trackKeyboardShortcut({\n shortcut_key: 'Cmd+S',\n action: 'save_file',\n }).catch(console.error);\n});\n```\n\n#### 4. Dialog Shortcuts (Escape to close)\n**Files to check**:\n- `/frontend/src/components/CreateSessionDialog.tsx`\n- `/frontend/src/components/CommitDialog.tsx`\n- `/frontend/src/components/ConfirmDialog.tsx`\n- `/frontend/src/components/Help.tsx`\n\n**Pattern**:\n```typescript\nconst handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n handleClose();\n AnalyticsService.trackKeyboardShortcut({\n shortcut_key: 'Escape',\n action: 'close_dialog',\n }).catch(console.error);\n }\n};\n```\n\n#### 5. Search/Navigation Shortcuts\n**File**: `/frontend/src/components/DraggableProjectTreeView.tsx`\n\nIf implementing keyboard shortcuts for search or navigation (e.g., `/` to focus search, arrow keys for navigation), track them here.\n\n### Analytics Service Method to Add\n\n**File**: `/frontend/src/services/analyticsService.ts`\n\n```typescript\nexport interface KeyboardShortcutProperties {\n shortcut_key: string;\n action: string;\n}\n\n// In UIEventName type\nexport type UIEventName =\n | 'view_switched'\n | 'help_dialog_opened'\n | 'settings_opened'\n | 'settings_saved'\n | 'sidebar_toggled'\n | 'search_used'\n | 'notification_shown'\n | 'keyboard_shortcut_used'; // Add this\n\n// Add method to class\nstatic async trackKeyboardShortcut(properties: KeyboardShortcutProperties): Promise<void> {\n if (!this.isEnabled()) return;\n\n try {\n await window.electronAPI.analytics.trackUIEvent({\n event: 'keyboard_shortcut_used',\n properties,\n });\n } catch (error) {\n console.error('[Analytics] Failed to track keyboard_shortcut_used:', error);\n }\n}\n```\n\n### Backend IPC Handler Update\n\n**File**: `/main/src/ipc/analytics.ts`\n\n```typescript\ninterface KeyboardShortcutUsedEvent extends Record<string, string | number | boolean | string[] | undefined> {\n shortcut_key: string;\n action: string;\n}\n\ntype AnalyticsEventData =\n | { event: 'view_switched'; properties: ViewSwitchedEvent }\n | { event: 'help_dialog_opened'; properties: HelpDialogOpenedEvent }\n | { event: 'settings_opened'; properties: SettingsOpenedEvent }\n | { event: 'settings_saved'; properties: SettingsSavedEvent }\n | { event: 'sidebar_toggled'; properties: SidebarToggledEvent }\n | { event: 'search_used'; properties: SearchUsedEvent }\n | { event: 'notification_shown'; properties: NotificationShownEvent }\n | { event: 'keyboard_shortcut_used'; properties: KeyboardShortcutUsedEvent };\n```\n\n---\n\n## Drag and Drop Tracking\n\n### Event Properties\n```typescript\n{\n item_type: string; // \"session\" | \"folder\" | \"project\"\n target_type: string; // \"session\" | \"folder\" | \"project\" | \"root\"\n}\n```\n\n### Integration Point\n\n**File**: `/frontend/src/components/DraggableProjectTreeView.tsx`\n\nThis component already has comprehensive drag-and-drop functionality. The drag state is managed in the `dragState` state variable.\n\n#### Key Handlers to Instrument\n\n**1. handleDrop handler** (line ~800-1000)\n\nThis is the main drop handler where drag-and-drop actions are completed. Add tracking here.\n\n**Current pattern**:\n```typescript\nconst handleDrop = async (e: React.DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n\n // ... existing drop logic ...\n\n if (dragState.type && dragState.overType) {\n // Handle different drop scenarios\n // ... move session, move folder, etc. ...\n }\n\n // Reset drag state\n setDragState({ ... });\n};\n```\n\n**Add Tracking**:\n```typescript\nimport { AnalyticsService } from '../services/analyticsService';\n\nconst handleDrop = async (e: React.DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n\n const sourceType = dragState.type;\n const targetType = dragState.overType;\n\n // ... existing drop logic ...\n\n if (sourceType && targetType) {\n // Track successful drag and drop\n AnalyticsService.trackDragDrop({\n item_type: sourceType,\n target_type: targetType,\n }).catch(console.error);\n }\n\n // Reset drag state\n setDragState({ ... });\n};\n```\n\n**2. Track only successful drops**\n\nMake sure to only track when the drop is successful and changes are saved to the database. Place the tracking call after successful API calls:\n\n```typescript\n// After successful session move\nawait API.sessions.update(dragState.sessionId, { folder_id: targetFolderId });\n\n// Track the drag-drop action\nAnalyticsService.trackDragDrop({\n item_type: 'session',\n target_type: targetType || 'root',\n}).catch(console.error);\n```\n\n### Analytics Service Method to Add\n\n**File**: `/frontend/src/services/analyticsService.ts`\n\n```typescript\nexport interface DragDropUsedProperties {\n item_type: string;\n target_type: string;\n}\n\n// In UIEventName type\nexport type UIEventName =\n | 'view_switched'\n | 'help_dialog_opened'\n | 'settings_opened'\n | 'settings_saved'\n | 'sidebar_toggled'\n | 'search_used'\n | 'notification_shown'\n | 'keyboard_shortcut_used'\n | 'drag_drop_used'; // Add this\n\n// Add method to class\nstatic async trackDragDrop(properties: DragDropUsedProperties): Promise<void> {\n if (!this.isEnabled()) return;\n\n try {\n await window.electronAPI.analytics.trackUIEvent({\n event: 'drag_drop_used',\n properties,\n });\n } catch (error) {\n console.error('[Analytics] Failed to track drag_drop_used:', error);\n }\n}\n```\n\n### Backend IPC Handler Update\n\n**File**: `/main/src/ipc/analytics.ts`\n\n```typescript\ninterface DragDropUsedEvent extends Record<string, string | number | boolean | string[] | undefined> {\n item_type: string;\n target_type: string;\n}\n\ntype AnalyticsEventData =\n | { event: 'view_switched'; properties: ViewSwitchedEvent }\n | { event: 'help_dialog_opened'; properties: HelpDialogOpenedEvent }\n | { event: 'settings_opened'; properties: SettingsOpenedEvent }\n | { event: 'settings_saved'; properties: SettingsSavedEvent }\n | { event: 'sidebar_toggled'; properties: SidebarToggledEvent }\n | { event: 'search_used'; properties: SearchUsedEvent }\n | { event: 'notification_shown'; properties: NotificationShownEvent }\n | { event: 'keyboard_shortcut_used'; properties: KeyboardShortcutUsedEvent }\n | { event: 'drag_drop_used'; properties: DragDropUsedEvent };\n```\n\n---\n\n## Testing\n\nAfter implementing the above tracking:\n\n1. **Enable analytics** in Pane settings\n2. **Configure PostHog API key** in settings\n3. **Use the features** that trigger the events:\n - Press keyboard shortcuts\n - Drag and drop sessions/folders\n4. **Verify in PostHog dashboard** that events are being tracked\n5. **Check console logs** for any tracking errors\n\n## Privacy Considerations\n\n- ✅ No user-identifiable information is tracked\n- ✅ All session IDs are hashed before sending\n- ✅ Only event counts and categorical data are tracked\n- ✅ Users can disable analytics in settings\n- ✅ Analytics respect the user's privacy preferences\n\n## Summary\n\n| Event | Status | Implementation |\n|-------|--------|---------------|\n| `run_script_executed` | ✅ Complete | Backend (logsManager.ts) |\n| `notification_shown` | ✅ Complete | Frontend (useNotifications.ts) |\n| `keyboard_shortcut_used` | ⏳ Documentation Ready | Follow integration guide above |\n| `drag_drop_used` | ⏳ Documentation Ready | Follow integration guide above |\n"},{"path":"docs/IMPLEMENTING_NEW_CLI_AGENTS.md","title":"Implementing New CLI Agents","text":"# Implementing New CLI Agents\n\nThis document described the removed `AbstractCliManager`/`cliToolRegistry`\narchitecture and is retired. See [ADDING_NEW_CLI_TOOLS.md](./ADDING_NEW_CLI_TOOLS.md)\nfor the current integration recipe.\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, removes, or recreates an external directory. Archiving or permanently deleting an adopted Pane only removes Pane's record. 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\nThe tool that owns the worktree remains responsible for its archive and deletion lifecycle. `runpane panes archive --dry-run --pane <id> --json` reports worktree cleanup as not applicable.\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/LICENSE-COMPATIBILITY.md","title":"License Compatibility Guide","text":"# License Compatibility Guide\n\nPane is licensed under the MIT License, which is a permissive open-source license. This document explains our approach to dependency license compatibility.\n\n## Compatible Licenses\n\nThe following licenses are compatible with MIT for use in Pane:\n\n### Permissive Licenses (Fully Compatible)\n- **MIT**: Same as Pane's license\n- **Apache-2.0**: Permissive with patent grant\n- **BSD** (2-clause, 3-clause): Permissive with attribution\n- **ISC**: Simplified BSD-style license\n- **CC0**: Public domain dedication\n- **Unlicense**: Public domain\n- **0BSD**: Zero-clause BSD\n\n### Weak Copyleft Licenses (Compatible with Conditions)\n- **LGPL-2.1, LGPL-3.0**: Compatible when used as dynamically linked libraries\n- **MPL-2.0**: File-level copyleft, doesn't affect MIT code\n- **EPL**: Similar to LGPL in practical terms\n\n## Incompatible Licenses\n\nThe following licenses are **not** compatible with MIT distribution:\n\n### Strong Copyleft Licenses\n- **GPL-2.0, GPL-3.0** (without \"or later\" or dual licensing)\n- **AGPL** (any version): Network copyleft\n- **SSPL**: Server Side Public License\n\n### Other Restrictive Licenses\n- Proprietary licenses\n- Custom licenses with distribution restrictions\n\n## How We Check\n\nThe `check-license-compatibility.sh` script automatically validates that all dependencies use compatible licenses. It:\n\n1. Checks for truly incompatible licenses (GPL, AGPL, SSPL)\n2. Notes LGPL dependencies (compatible but worth tracking)\n3. Summarizes all permissive licenses found\n\n## LGPL Special Note\n\nLGPL (Lesser GPL) dependencies are compatible with MIT when:\n- They are used as libraries (not forked/modified)\n- They are dynamically linked (which is the case for npm packages)\n- We comply with LGPL requirements (providing source of the LGPL component)\n\nThe LGPL-3.0-or-later license specifically allows users to choose any later version of LGPL, providing additional flexibility.\n\n## Current Status\n\nRun `pnpm build:notices` to generate a current NOTICES file listing all dependency licenses."},{"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 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. 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 generic text and routing metadata; see the payload details below.\n\nFor iOS, register `com.dcouple.pane.mobile` (or your signed bundle identifier), enable Push Notifications, add the capability in Xcode, and set these only in the host service environment:\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\nFor Android, create a Firebase Android app with the same application ID, download its real `google-services.json` to `mobile/android/app/google-services.json` (the checked-in `.example` is only a shape guide), and create a narrowly scoped service account with Firebase Cloud Messaging send permission. Set `PANE_FCM_SERVICE_ACCOUNT_PATH=/secure/path/service-account.json` in the host service environment. Do not commit either provider 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\nThe daemon sends generic APNs/FCM alert text for both a newly blocked turn and a `working → idle` completion. 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\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 protect the service-account file outside the app build. iOS Simulator builds do not prove APNs delivery; Android emulators similarly do not prove a real FCM token path. 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_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 |\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`: creates a Pane in a repository, starts the agent with the task, waits until it is ready, and returns the pane and panel ids and a `pane://` link.\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 Pane saw it leave the composer.\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` isn'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 the output as a tool error (`isError: true`), and so do bad arguments, 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` is omitted because a streaming 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/RELEASE_INSTRUCTIONS.md","title":"Pane Release Instructions","text":"# Pane Release Instructions\n\nPane releases are cut from a clean `main` checkout with `scripts/release.js`.\nThe script updates `package.json`, commits `release: vX.Y.Z`, tags the same\ncommit, pushes `HEAD:main`, and pushes the tag.\n\nThe same release commit also keeps the npm and PyPI wrapper package versions in\nsync through `scripts/sync-runpane-package-versions.js`.\n\n## Mechanical Invariants\n\nBefore a release, these facts must be true:\n\n- The worktree is clean.\n- `HEAD` matches `origin/main`.\n- For inferred bumps (`patch`, `minor`, `major`), `package.json` version matches\n the latest `v*` semver tag.\n- `packages/runpane/package.json`, `packages/runpane-py/pyproject.toml`, and\n `packages/runpane-py/src/runpane/__init__.py` match the root release version.\n- The release tag does not already exist locally or on `origin`.\n- Dependencies are installed before running the release script so commit hooks can\n run successfully.\n\nIf `package.json` and the latest release tag disagree, do not run an inferred\npatch. Decide the intended next version and run an explicit release instead:\n\n```bash\npnpm run release 2.2.1\n```\n\n## Happy Path\n\n```bash\ngit switch main\ngit pull --ff-only origin main\ngit status --porcelain\ngit tag --list 'v*' --sort=-v:refname | head -5\nnode -p \"require('./package.json').version\"\n\npnpm typecheck\npnpm lint\npnpm run check:runpane-package-versions\npnpm run test:runpane-contract\npnpm run test:runpane-package-smoke\npnpm test:ci:minimal\n\npnpm run release patch\n```\n\nIf the release commit fails, the script aborts before creating or pushing a tag\nas long as any changes remain in the worktree. Fix the hook, dependency, or\ntypecheck failure, restore the worktree, and rerun the release.\n\nUse `minor`, `major`, or an explicit version when that is the intended release:\n\n```bash\npnpm run release minor\npnpm run release major\npnpm run release 2.3.0\n```\n\n## GitHub Workflows\n\nPull requests to `main` run:\n\n- `Code Quality`\n - typecheck\n - lint\n - runpane wrapper compatibility tests across Node, Python, Linux, macOS, and Windows\n - main process tests on Linux, macOS, and Windows\n - frontend unit tests\n - maintained Playwright smoke tests\n\nPushes to `main` run:\n\n- `Code Quality`\n- `Deploy Remote PWA Preview`\n\n`v*` tag pushes run:\n\n- `Build & Release`\n - macOS Apple Silicon and Intel installers\n - Linux installer artifacts\n - Windows x64 installer\n - Windows arm64 installer\n - GitHub release publishing\n - `SHA256SUMS.txt`\n - npm `runpane` publish\n - PyPI `runpane` publish\n- `Notify website on release`\n\nThe release is not considered complete until the tag-triggered `Build & Release`\nrun succeeds and the GitHub release is published.\n\n## Verification\n\nAfter `pnpm run release ...` finishes:\n\n```bash\ngit fetch origin main --tags\ngit rev-parse HEAD\ngit rev-parse origin/main\ngit tag --points-at HEAD\ngh run list --limit 10\ngh release view vX.Y.Z\n```\n\nConfirm:\n\n- `HEAD` and `origin/main` point at the release commit.\n- The release commit has the expected `vX.Y.Z` tag.\n- `Build & Release` succeeded for the tag.\n- `npm view runpane version` reports `X.Y.Z`.\n- `python3 -m pip index versions runpane` includes `X.Y.Z`.\n- `Notify website on release` succeeded for the tag.\n- `Code Quality` succeeded for the release commit on `main`.\n- `Deploy Remote PWA Preview` succeeded for the release commit on `main`.\n\n## Required Secrets\n\nGitHub Actions provides `GITHUB_TOKEN` automatically.\n\nThe npm and PyPI packages should publish through trusted publishing:\n\n- npm: configure a trusted publisher for package `runpane` on npmjs.com with\n repository `greenfield-inc/Pane`, workflow filename `build.yml`, and `npm publish`\n permission. The workflow installs npm `11.5.1` or newer for OIDC support.\n- PyPI: configure a trusted publisher for project `runpane` with repository\n `greenfield-inc/Pane`, workflow filename `build.yml`, and GitHub environment `pypi`.\n\nFallback token publishing is allowed only for first package reservation or\nmanual recovery. Use `NPM_TOKEN` or `PYPI_API_TOKEN` as local environment\nvariables or GitHub Actions secrets, do not commit token files such as `.npmrc`\nor `.pypirc`, and revoke or rotate the tokens after use.\n\nIf the source repository remains private, do not promise npm provenance:\nnpm trusted publishing can still be used, but npm provenance attestations are\nnot generated for private repositories.\n\nThe release and preview workflows also depend on repository secrets and\nvariables configured in GitHub Actions. Relevant examples include:\n\n- `SITE_REPO_DISPATCH_TOKEN` for website release notification.\n- Google Cloud workload identity, service account, project, and region values\n for the remote PWA preview deploy.\n- Platform signing or publishing credentials if signing is re-enabled.\n\n## Auto-Update Files\n\nThe build process generates update metadata and installers under\n`dist-electron/` in the release workflow:\n\n- `latest-mac.yml`\n- `latest-linux.yml`\n- `latest-linux-arm64.yml`\n- `latest.yml`\n- macOS `.dmg` and `.zip`\n- Linux `.deb` and `.AppImage`\n- Windows `.exe`\n- npm package `runpane`\n- PyPI package `runpane`\n\n## Rollback\n\nDo not retag an existing version. If a release has a critical issue:\n\n1. Fix the issue on `main`.\n2. Cut a new patch version.\n3. Leave the broken tag/release history intact unless maintainers explicitly\n decide to remove it.\n\nUsers can always manually download the latest good release from GitHub Releases.\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 watch --self-test\nrunpane watch --follow\nrunpane watch --follow --kinds agent.ready,agent.blocked,agent.idle,panel.exited,pane.gone --settle 180000 --blocked-settle 30000 --min-interval 600000 --idle-backoff\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 agents start --repo active --name fix-login --agent claude --prompt \"Fix the login redirect\" --yes --json\nrunpane agents status --pane <pane-id> --json\nrunpane agents send --pane <pane-id> --text \"Also add a test\" --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\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` uses the same release resolution and installer path as `install client`.\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.\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; pass `--no-pinned` to opt out. 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`, `initialInput.verifiedSubmitted: true` is reported only after argument attachment or composer-clear plus activity evidence. Routing input does not by itself verify submission.\n\n`runpane panes archive` refreshes the configured upstream, reports exact unpushed commit evidence, and refuses unsafe archive operations unless `--force` is used. Add `--dry-run` to inspect the same evidence without archiving. Successful archives wait for worktree removal and report `worktreeCleanup`.\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 list` lists tool panels inside one Pane session.\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\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 workspace transitions from the daemon journal without polling. `--follow` keeps waiting and prints one line per event: READY, BLOCKED, IDLE, STUCK, NEW, GONE, EXIT, plus HEARTBEAT every 60 seconds as proof of life. Defaults are responsive: no settle, no batching, all kinds, IDLE every `--idle-after`. Expensive consumers opt into `--kinds` (drop `agent.busy`; BUSY carries no action), `--settle <ms>` (READY only after a quiet window; a BUSY inside it cancels the line), `--blocked-settle <ms>`, `--min-interval <ms>` (batch non-urgent lines; BLOCKED bypasses it), and `--idle-backoff` (10m, 30m, 1h, 3h, then daily). The recommended orchestrator invocation is `runpane watch --follow --kinds agent.ready,agent.blocked,agent.idle,panel.exited,pane.gone --settle 180000 --blocked-settle 30000 --min-interval 600000 --idle-backoff`, which budgets about 6 wake-ups per active pane per hour worst case, usually 1-3. Pane Chat arms it automatically through its skill; only your own scripts need the flags. STUCK means real unsubmitted composer text, never an agent prompt suggestion. Judge a dead watch by a non-zero exit or a WATCH ERROR line, not by silence.\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 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.\n\n`runpane agents start|status|send` finish the three common agent jobs in one call each: start an agent on a task in a repository, check on it, and send it a follow-up.\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## 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`: Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\n- `panes archive`: Archive a Pane exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\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`: Create reviewer/helper terminal tabs inside an existing Pane; they share that Pane's worktree.\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- `watch`: Wait for workspace transitions (READY, BLOCKED, IDLE, STUCK, NEW, GONE, EXIT) from the daemon journal without polling; responsive by default, with opt-in cadence flags for expensive consumers.\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\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--base-branch <branch>\n--folder <name>\n--resume <agent-session-id>\n--agent <codex|claude|cursor>\n--tool-command <command>\n--title <title>\n--initial-input <text> (aliases: --prompt)\n--initial-input-file <path|->\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--format <lines|json>\n--heartbeat <seconds>\n--idle-after <milliseconds>\n--settle <milliseconds>\n--blocked-settle <milliseconds>\n--min-interval <milliseconds>\n--body-file <path|->\n--session <id|name>\n--message <message>\n--query <text>\n--doc <path>\n--url <pane-url>\n--toolsets <name,...>\n--keys <name,...>\n--json\n--wait-ready\n--no-focus\n--focus\n--pinned\n--no-pinned\n--no-associate\n--force\n--launch\n--follow\n--ack-now\n--include-held-input\n--agents-only\n--all-managed\n--include-shells\n--no-held-input\n--self-test\n--idle-backoff\n--report\n--read-only\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. In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22, for example `PATH=/opt/homebrew/opt/node@22/bin:$PATH 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`.\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 also emit best-effort lifecycle telemetry to `https://runpane.com/api/runpane/telemetry` for command start/success/failure, download request/success/failure, and GitHub fallback usage.\nWrapper telemetry uses a persisted anonymous `install_id` in the form `install_<uuid>` and PostHog `distinct_id = install:<install_id>` so distinct wrapper users can be counted with `count(DISTINCT properties.install_id)`.\nWrapper telemetry must be disabled in CI and when `RUNPANE_TELEMETRY_DISABLED` is set, and must not include raw paths, labels, prompts, raw error text, or environment values.\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/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 Pane`, 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## 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\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 Pane`.\n3. Paste the full `pane-remote://...` code into `Import Remote Connection`.\n4. Click `Import & Connect`.\n\nIf the tunnel is not reachable yet, Pane still saves the profile and shows the connection error. Start the printed SSH/Tailscale tunnel and click `Connect` on the saved profile.\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 terminal-backed remote sessions:\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### 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## 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\nTailscale Serve example generated by setup:\n\n```bash\ntailscale serve --bg http://127.0.0.1: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 Pane`:\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. Save host settings.\n\n### 3. Create a Paired Connection\n\nFrom the same settings section on the host:\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 tcp:127.0.0.1:42137\n```\n\n### 5. Connect the Desktop Client\n\nUse `Import Remote Connection` for a generated code, or use `Save Existing Remote Profile` with:\n\n- label\n- base URL\n- bearer token\n\nThen 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## 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 generic attention alerts for `blocked` and settled `working → idle` transitions; 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 and an optional chat name field. Pane\nremembers the chosen agent as the default for future Sessions while existing\nSessions keep their own agent. A blank name receives a generated name such as\n`New chat` or `New chat 2`; each new Session opens as one Pane Chat and can be\nrenamed 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\nThe Session owns discussion, read-only code exploration and investigation,\nclarification, and ticket creation or revision. After a ticket is ready and\nthe user explicitly authorizes implementation, the Session dispatches an\nimplementation session through RunPane in an appropriate existing Pane or\ntab, or creates one when needed. It chooses the agent and the skills that fit\nthe work and names those skills by absolute path. The Session keeps its own\nselected agent, profile, and tool configuration.\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 to pin or unpin it. The pin preference survives\nrestarts; an archived Session stays out of Pinned until it is restored.\n\nClick a Session row to open its chat and expand or collapse its associated\nPanes. Child Pane rows are indented beyond the Session chat icon and keep the\nordinary Pane actions.\n\nRight-click a Session to archive it. 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; active navigation\nfilters them. 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## Session startup\n\nSession setup is quiet. After routine context and liveness checks, a new\nSession greets the user with a short invitation such as `Ready when you are.\nWhat would you like to work on?`; a resumed Session briefly surfaces its saved\nnext step. Routine diagnostics, machine details, watcher output, and\nworkspace-wide Pane inventory stay internal unless the user asks or a relevant\nfailure needs attention. If one human action blocks progress, the Session\nsurfaces that single action clearly.\n\nUnattended resilience is offered only when the user requests unattended work\nor delegated work makes the choice relevant. The offer states its effect,\nincluding keep-awake and automatic resume behavior. An explicit yes or no is\nremembered for the Session; silence and unrelated prompts are not consent. A\nnew explicit no revokes resilience, while an enabled choice persists across\nordinary resumes and unrelated prompts.\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. Archive behavior remains a separate #654 follow-up.\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## 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: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 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## Resume and refresh persisted context\n\nRead `PANE_ORCHESTRATION_SESSION_ID` from the current environment whenever a\nSession conversation starts or resumes. 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, reload the saved record and 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`\nafter a resume or mutation. If the variable is missing, use `runpane sessions\nlist --json` to resolve a Session explicitly; never guess an identity. 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\nWhen legacy Pane Chat contains history for more than one agent, each existing\nagent history is imported as its own named Session. The imported rows retain\nthe original hidden Session, fixed panel, buffers, and resume ID; generated\nunused panel slots keep those owners distinct across agent switches and\nrestarts.\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## Session watcher\n\nUse one durable, named watcher per Session, scoped to every associated Pane:\n\n```text\nrunpane watch --as session-<session-id> --follow --pane <pane-id> \\\n --kinds agent.ready,agent.blocked,agent.idle,panel.exited,pane.gone \\\n --settle 180000 --blocked-settle 30000 --min-interval 600000 \\\n --idle-backoff --json\n```\n\nRepeat `--pane` for each associated Pane. A discussion-only Session has no\nfollow watcher; never omit `--pane` to watch all Panes. After an associate or\ndetach mutation, refresh `sessions overview` and re-arm the same cursor with\nthe current Pane set. On restart, retain the `session-<session-id>` cursor and\ncapture a fresh output baseline before interpreting notifications. Return\nblocked and decision findings to the Session conversation. Terminal idle or\nexit remains activity evidence only.\n\n## Skill contract\n\n`main/src/services/skillCacheManager.ts` installs the skills that ship in\n`main/src/services/paneChatBundle/` once per run: 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, the runtime context, and the helper subagents\nin `.claude/agents/` and `.codex/agents/`. A manifest records what it\ninstalled, so later installs replace only those entries.\n\nThe Session route depends on `pane-orchestrator`, `runpane`,\n`orchestrate-sessions`, `create-ticket`, and `pane-work`. Agents dispatched to\nother repositories get skills by absolute path from the Session's prompt.\n"},{"path":"docs/SESSION_OUTPUT_PERFORMANCE.md","title":"Session output audit","text":"# Session output audit\n\nThis audit builds on the startup and Git-scan fixes in #591. It targets work\nrepeated during terminal activity and history loading, without changing\nterminal rendering, scrollback limits, or database schema.\n\n## Changes\n\n- `App`, `HomePage`, and `useIPCEvents` subscribe to the session fields/actions\n they use. Updating the separate terminal output buffer no longer schedules\n either top-level component to render. The event hook's unused fake socket\n return value and unused action dependency are removed.\n- Prompt markers use an indexed SQLite `COUNT(*)` instead of materializing\n every output payload to read the array length. Both existing marker offset\n conventions are preserved, and the count includes every output type.\n- Session history loading reads backward, retaining the newest 300 text\n outputs and 100 JSON messages independently, then restores chronological\n order. The old synchronous “batching” loop never yielded and could stop\n before reaching the newest messages. Normalization now runs only for\n retained JSON messages. Late history for a deleted session is ignored.\n- Panel JSON is decoded at the database boundary, including legacy\n string-wrapped state/metadata. This also fixes legacy state throwing before\n the active-panel flag could be assigned. The manager no longer performs\n two exception-driven string checks per panel or repeats JSON parsing.\n Malformed legacy serialized values retain a fallback, while archived-panel\n cache guards and summary reads that omit scrollback are preserved.\n\n## Measurements\n\nLocal Linux measurements with Node 22.18.0, compared with `80b8b621` (v2.4.100).\nThese are isolated measurements, not an end-to-end application speedup claim.\n\n| Workload | Before | After |\n| --- | ---: | ---: |\n| Count 10,000 output rows with 78.125 MiB of payload, median of 9 reads | 253.476 ms | 0.353 ms |\n| Heap growth during those reads, median of 9 | 83.772 MiB | 0.001 MiB |\n| App renders recorded during 100 background output chunks | 100 | 0 |\n| HomePage renders recorded during the same burst | 64 | 0 |\n| Output chunks retained after the burst | 100 | 100 |\n\nThe SQL benchmark warms both paths and requests GC before each measured read.\nHeap growth measures temporary JavaScript allocation, not retained memory or\ntotal process RSS. React Scan records component renders in a development\nChromium renderer with mocked Electron IPC; it does not measure native PTY,\nxterm/WebGL, or packaged Windows performance.\n\n## Reproduce\n\nUse the repository's supported Node/pnpm toolchain and a SQLite native module\nbuilt for that Node version.\n\n```bash\npnpm build:main\nnode --expose-gc scripts/benchmark-session-output.js\npnpm --filter frontend test src/stores/sessionStore.test.ts\npnpm --filter main test --run src/database/database.panel-loading.test.ts src/services/sessionManager.output-count.test.ts\nPANE_REACT_SCAN=1 pnpm test -- tests/session-output-perf.spec.ts --workers=1\n```\n\nThe render check requires a fresh dev server started with `PANE_REACT_SCAN=1`;\nan already-running server without that option cannot produce the evidence.\nThe check is opt-in so ordinary Playwright runs do not require React Scan.\nThe benchmark creates and removes its own temporary database.\n\n## Verification\n\n- All 334 frontend unit tests passed.\n- Full main unit run: 910 passed, 2 skipped, 15 failed. All 15 failures were\n Unix-socket `EPERM` errors in `daemon/server.test.ts` and\n `permissionIpcServer.test.ts`; the same 15 failures reproduced on unchanged\n `80b8b621` in this environment.\n- Chromium: render regression and two renderer startup smoke checks passed.\n The render regression failed on the baseline as expected. The new history\n and legacy-panel assertions also failed on the baseline and passed with\n these changes.\n- Root lint and typecheck passed. Lint reports five existing unused-variable\n warnings in `skillCacheManager.ts`.\n\nThe browser checks used a Vite-only Playwright web server with the repository's\nElectron API mock. Native desktop/Windows interaction was not exercised.\n"},{"path":"docs/SESSION_OUTPUT_SYSTEM.md","title":"Session Output Handling System","text":"# Session Output Handling System\n\n⚠️ **WARNING**: The session output handling system is complex and fragile. Modifying it frequently causes issues like duplicate messages, disappearing content, or blank screens. Any changes require explicit user permission.\n\n## How It Works\n\n### 1. Database Storage\n\n- Raw JSON messages from Claude are stored as-is in the database\n- Stdout/stderr outputs are stored directly\n- No formatting or transformation happens at storage time\n\n### 2. Real-time Streaming\n\n- When Claude outputs data, it's saved to the database immediately\n- For JSON messages, a formatted stdout version is sent to the frontend for the Output view\n- The original JSON is also sent for the Messages view\n- This provides immediate visual feedback during active sessions\n\n### 3. Session Loading\n\n- When navigating to a session, outputs are loaded from the database\n- The `sessions:get-output` handler transforms JSON messages to formatted stdout on-the-fly\n- Uses `setSessionOutputs` for atomic updates to prevent race conditions\n\n### 4. Frontend Display\n\n- The useSessionView hook manages session view logic and state (extracted from SessionView component)\n- A mutex lock (`loadingRef`) prevents concurrent loads\n- Timing is carefully managed with `requestAnimationFrame` and delays\n- The `formattedOutput` state is NOT cleared on session switch - it updates naturally\n\n### 5. Key Principles\n\n- Database is the single source of truth\n- Transformations happen on-the-fly, not at storage time\n- Real-time updates supplement but don't replace database data\n- Session switches always reload from database to ensure consistency\n\n## Common Issues and Solutions\n\n- **Duplicate messages**: Usually caused by sending both formatted and raw versions\n- **Disappearing content**: Often due to clearing output states at the wrong time\n- **Black screens**: Typically from race conditions during session switching\n- **Content only loads once**: Results from improper state management or missing dependencies\n\nThe current implementation carefully balances real-time updates with data persistence to provide a smooth user experience.\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 (if implemented)\n\n- `project:created` - Add new project to list\n- `project:updated` - Update specific project properties\n- `project:deleted` - Remove project from list\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, getCurrentTimestamp } from '../utils/timestampUtils';\n\n// For database storage\nconst timestamp = formatForDatabase(); // Returns ISO string\nconst now = getCurrentTimestamp(); // Alias for formatForDatabase()\n\n// For display formatting\nconst displayTime = formatForDisplay(timestamp);\n```\n\n### Frontend (`frontend/src/utils/timestampUtils.ts`)\n\n```typescript\nimport { parseTimestamp, formatDuration, getTimeDifference } from '../utils/timestampUtils';\n\n// Parse SQLite timestamps correctly\nconst date = parseTimestamp(\"2024-01-01 12:00:00\"); // Handles UTC conversion\n\n// Calculate durations\nconst durationMs = getTimeDifference(startTime, endTime);\nconst formatted = formatDuration(durationMs); // \"2m 34s\"\n\n// Display relative time\nconst ago = formatDistanceToNow(timestamp); // \"5 minutes ago\"\n```\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 - uses parseTimestamp utility\nconst date = parseTimestamp(\"2024-01-01 12:00:00\");\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 = parseTimestamp(dbTime); // Correctly handled 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\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` tracks the currently active panel per session\n- Foreign key constraints ensure panels are cleaned up when sessions are deleted\n\n## Event System\n\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 currently supports 8 panel types:\n\n1. **Terminal** (`terminal`) - Multiple PTY shells per session\n - Independent processes with command history and scrollback\n - File operation detection triggers `files:changed` events\n\n2. **Claude** (`claude`) - Multiple Claude Code instances\n - Managed by `claudePanelManager.ts`\n - Permission mode: approve/ignore\n - Automatic context tracking with token usage display\n\n3. **Codex** (`codex`) - Multiple Codex CLI instances\n - Managed by `codexPanelManager.ts`\n - Configurable model provider and approval policy\n - Sandbox mode and web search support\n\n4. **Diff** (`diff`) - Git diff viewer (singleton, permanent)\n - View modes: split/unified\n - Whitespace and context line configuration\n - Auto-refreshes on file changes\n\n5. **Editor** (`editor`) - In-app file editor\n - Syntax highlighting with Monaco\n - File tree with expanded directory tracking\n - Cursor position and scroll persistence\n\n6. **Logs** (`logs`) - Script execution panel (singleton)\n - Process management with PID tracking\n - Output buffer with error/warning counts\n - Start/end time and exit code tracking\n\n7. **Dashboard** (`dashboard`) - Project overview (singleton, permanent, projects-only)\n - Session health monitoring\n - Filter by status: all/stale/changes/pr\n - Cached data persistence\n\n8. **Setup Tasks** (`setup-tasks`) - Project setup checklist (singleton, permanent, projects-only)\n - Task completion tracking\n - Dismissible tasks\n - Last check timestamp\n\n## Panel Capabilities\n\nEach panel type has specific capabilities defined in `PANEL_CAPABILITIES`:\n\n- **requiresProcess**: Whether panel needs background process (terminal, claude, codex, 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)\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"},{"path":"docs/assets/image-prompts.md","title":"README image prompts","text":"# README image prompts\n\nGenerated using the built-in image-generation tool. Supplied style reference: the approved [Agent Farm banner](https://github.com/dcouple/agent-farm/blob/19f1fba8ef12050eef39c1b44e725c697b57b87b/docs/assets/agent-farm-banner.png).\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/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/dcouple/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 prompts and final assets in `docs/assets/`; inspect labels and\nreadability at README display width before embedding. Use new filenames for\nexperiments rather than overwriting chosen art.\n\nGeneration record: [image-prompts.md](image-prompts.md).\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`.\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"},{"path":"docs/remote-pwa-safari-debug-handoff.md","title":"Remote PWA Safari Debug Handoff","text":"# Remote PWA Safari Debug Handoff\n\n## Goal\n\nRoot-cause why Remote Pane connects from Chrome but fails from Safari/iOS Safari. Safari matters because iPhone Home Screen PWA install uses Safari/WebKit.\n\nDo not paste connection tokens into tickets, screenshots, or logs. When sharing output, redact `access_token`, `token`, and the full `pane-remote://...` code.\n\n## Current Known State\n\n- Preview app URL: `https://pane-remote-pwa-preview-sg622dpt4q-uc.a.run.app/app/?v=ca722cc`\n- Current preview revision verified on Cloud Run: `pane-remote-pwa-preview-00011-t4c`\n- Current preview JS bundle verified: `assets/remote-BwkHgECz.js`\n- Chrome can connect with the same code.\n- Safari/iOS Safari fails during connection or shortly after.\n- Direct `/health` on the Tailscale host has been reachable from iPhone, sometimes slowly.\n- Local curl from WSL to the Tailscale host returned CORS headers for `/health`, `OPTIONS /invoke`, and an unauthenticated `/events` error response:\n - `Access-Control-Allow-Origin: *`\n - `Access-Control-Allow-Methods: GET, POST, OPTIONS`\n - `Access-Control-Allow-Headers: Authorization, Content-Type, X-Pane-Remote-Runtime-Id, X-Pane-Remote-Client-Label, X-Pane-Client-Label, X-Pane-Client-Device-Label`\n\nThis points away from a simple missing CORS header and toward Safari-specific behavior in one of the browser transport steps.\n\n## Code Path To Inspect\n\nPrimary client file:\n\n- `frontend/src/remote/runtime/remoteDaemonBrowserClient.ts`\n\nConnection sequence:\n\n1. `connect()`\n2. `checkHealth()` calls `fetch(<baseUrl>/health, { cache: 'no-store' })`\n3. `openEventStream()` calls `fetch(<baseUrl>/events?...token...)`\n4. `consumeEventStream()` reads `response.body.getReader()`\n5. After connection, app state loads with POST `fetch(<baseUrl>/invoke, { method: 'POST', headers: { Content-Type: 'text/plain;charset=UTF-8' }, body: JSON.stringify(...) })`\n\nServer file:\n\n- `main/src/daemon/httpApiServer.ts`\n\nEndpoints:\n\n- `GET /health`\n- `GET /events`\n- `POST /invoke`\n- `OPTIONS *`\n\nImportant detail: the app currently consumes SSE through streaming `fetch()` plus `ReadableStream.getReader()`, not native `EventSource`.\n\n## Ranked Hypotheses\n\n1. Safari accepts `/health` but fails or stalls on streaming `fetch()` for `/events`.\n - Evidence: code uses `response.body.getReader()` for a long-lived `text/event-stream`; Chrome works; Safari/iOS is the only failing browser.\n - Supporting references:\n - MDN documents `Response.body` as a `ReadableStream` and shows `getReader()` stream consumption.\n - MDN documents `EventSource` as the native browser API for receiving `text/event-stream` SSE.\n\n2. Safari Home Screen PWA has different service-worker/storage/network behavior than Safari tab.\n - Need to compare Safari tab, Safari private/non-private, and Home Screen app with Web Inspector.\n\n3. Safari blocks or mishandles the cross-origin POST `/invoke` even though CORS looks correct from curl.\n - Need Network tab evidence for preflight and POST.\n\n4. Tailscale DNS/Tunnel readiness is slow on iOS Safari, causing the health or event connection to time out/abort.\n - User observed `/health` could take around 10 seconds earlier.\n\n## Best Reproduction Setup\n\nPhysical iPhone is preferred because it exactly matches the failing PWA environment. iOS Simulator is still useful for quick Safari Web Inspector iteration, but if the simulator cannot reach the `.ts.net` host through the Mac's Tailscale routing, use the physical iPhone.\n\nRequirements:\n\n- Mac with Safari and Xcode installed.\n- iPhone signed into the same Tailscale tailnet as the Pane host.\n- Fresh `pane-remote://...` code from the current Pane nightly host.\n- Preview URL above.\n- Safari Develop menu enabled on macOS Safari.\n\nApple's docs say iOS/iPadOS pages, Home Screen web apps, service workers, and simulators can be inspected from Safari on a Mac. They also note Web Inspector is always enabled for simulators and booted simulators appear in Safari's Develop menu.\n\nReferences:\n\n- Apple: https://developer.apple.com/documentation/safari-developer-tools/inspecting-ios\n- MDN `Response.body`: https://developer.mozilla.org/en-US/docs/Web/API/Response/body\n- MDN `EventSource`: https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource\n\n## Reproduction Steps\n\n### Physical iPhone\n\n1. On iPhone, open Settings > Apps > Safari > Advanced.\n2. Enable Web Inspector.\n3. Connect iPhone to Mac with a cable and trust the Mac when prompted.\n4. On Mac Safari, enable Develop menu if needed: Safari > Settings > Advanced > Show features for web developers.\n5. On iPhone Safari, open:\n - `https://pane-remote-pwa-preview-sg622dpt4q-uc.a.run.app/app/?v=ca722cc`\n6. On Mac Safari, open Develop > iPhone > the preview URL.\n7. Open Console and Network tabs.\n8. Paste a fresh connection code into the iPhone page and tap connect.\n9. Save screenshots or copied rows for:\n - Console error\n - `/health`\n - `/events`\n - `/invoke`\n - Any preflight `OPTIONS`\n10. Add the page to Home Screen and repeat from the installed web app:\n - With the Home Screen app in foreground, inspect it from Develop > iPhone > Home Screen Web Apps > preview URL.\n\n### iOS Simulator\n\n1. Install/open Xcode.\n2. Launch an iPhone simulator.\n3. Open Safari in the simulator.\n4. Open the preview URL.\n5. On Mac Safari, use Develop > Simulator > preview URL.\n6. Repeat the same connection steps.\n7. If `.ts.net` does not resolve or `/health` fails only in the simulator, switch to a physical iPhone.\n\n## Network Evidence To Capture\n\nFor each request, capture:\n\n- URL path, not full tokenized URL.\n- HTTP status.\n- Request method.\n- Request headers:\n - `Origin`\n - `Access-Control-Request-Method`\n - `Access-Control-Request-Headers`\n - `Content-Type`\n- Response headers:\n - `Access-Control-Allow-Origin`\n - `Access-Control-Allow-Methods`\n - `Access-Control-Allow-Headers`\n - `Content-Type`\n - `Cache-Control`\n- Timing: DNS, connect, SSL, waiting, total.\n- Whether Safari labels the request as blocked, cancelled, failed, or CORS error.\n\nExpected shape:\n\n- `/health`: `200`, JSON `{ \"ok\": true, \"status\": \"ready\", \"transport\": \"http+sse\" }`\n- `/events`: `200`, `Content-Type: text/event-stream; charset=utf-8`, stays open, first event should be `ready`\n- `/invoke`: `200`, JSON `{ \"ok\": true, \"result\": ... }`\n\n## Console Probes\n\nRun these in Web Inspector Console after importing the connection code. They read the saved profile from localStorage without printing the token.\n\n### 1. Confirm Saved Profile\n\n```js\nconst profiles = JSON.parse(localStorage.getItem('pane.remotePwa.savedProfiles') || '[]');\nprofiles.map((p) => ({\n id: p.id,\n label: p.label,\n baseUrl: p.baseUrl,\n transport: p.transport,\n hasToken: Boolean(p.token),\n tunnelKind: p.tunnel?.kind,\n}));\n```\n\n### 2. Test Health\n\n```js\nconst p = JSON.parse(localStorage.getItem('pane.remotePwa.savedProfiles') || '[]')[0];\nconst t0 = performance.now();\nawait fetch(`${p.baseUrl}/health`, { cache: 'no-store' })\n .then(async (r) => ({\n ok: r.ok,\n status: r.status,\n ms: Math.round(performance.now() - t0),\n headers: Object.fromEntries(r.headers.entries()),\n text: await r.text(),\n }))\n .catch((e) => ({ name: e.name, message: e.message, ms: Math.round(performance.now() - t0) }));\n```\n\n### 3. Test Streaming Fetch SSE\n\n```js\nconst p = JSON.parse(localStorage.getItem('pane.remotePwa.savedProfiles') || '[]')[0];\nconst url = new URL(`${p.baseUrl}/events`);\nurl.searchParams.set('access_token', p.token);\nurl.searchParams.set('runtime_id', 'safari-fetch-debug');\nurl.searchParams.set('client_label', 'Safari fetch debug');\n\nconst ac = new AbortController();\nsetTimeout(() => ac.abort(), 12000);\nconst t0 = performance.now();\ntry {\n const r = await fetch(url.toString(), { signal: ac.signal });\n console.log('fetch events response', {\n ok: r.ok,\n status: r.status,\n ms: Math.round(performance.now() - t0),\n contentType: r.headers.get('content-type'),\n hasBody: Boolean(r.body),\n getReaderType: typeof r.body?.getReader,\n });\n const reader = r.body?.getReader();\n const decoder = new TextDecoder();\n const first = await reader.read();\n console.log('fetch events first chunk', {\n done: first.done,\n text: first.value ? decoder.decode(first.value) : null,\n ms: Math.round(performance.now() - t0),\n });\n reader.releaseLock();\n} catch (e) {\n console.log('fetch events error', {\n name: e.name,\n message: e.message,\n ms: Math.round(performance.now() - t0),\n });\n}\n```\n\n### 4. Test Native EventSource SSE\n\n```js\nconst p = JSON.parse(localStorage.getItem('pane.remotePwa.savedProfiles') || '[]')[0];\nconst url = new URL(`${p.baseUrl}/events`);\nurl.searchParams.set('access_token', p.token);\nurl.searchParams.set('runtime_id', 'safari-eventsource-debug');\nurl.searchParams.set('client_label', 'Safari EventSource debug');\n\nconst t0 = performance.now();\nconst es = new EventSource(url.toString());\nes.addEventListener('open', () => console.log('eventsource open', Math.round(performance.now() - t0)));\nes.addEventListener('ready', (event) => console.log('eventsource ready', Math.round(performance.now() - t0), event.data));\nes.addEventListener('heartbeat', (event) => console.log('eventsource heartbeat', Math.round(performance.now() - t0), event.data));\nes.onerror = (event) => console.log('eventsource error', Math.round(performance.now() - t0), event);\nsetTimeout(() => {\n es.close();\n console.log('eventsource closed');\n}, 15000);\n```\n\n### 5. Test Invoke POST\n\n```js\nconst p = JSON.parse(localStorage.getItem('pane.remotePwa.savedProfiles') || '[]')[0];\nconst t0 = performance.now();\nawait fetch(`${p.baseUrl}/invoke`, {\n method: 'POST',\n headers: { 'Content-Type': 'text/plain;charset=UTF-8' },\n body: JSON.stringify({\n channel: 'sessions:get-all-with-projects',\n args: [],\n token: p.token,\n runtimeId: 'safari-invoke-debug',\n clientLabel: 'Safari invoke debug',\n }),\n})\n .then(async (r) => ({\n ok: r.ok,\n status: r.status,\n ms: Math.round(performance.now() - t0),\n headers: Object.fromEntries(r.headers.entries()),\n text: (await r.text()).slice(0, 1000),\n }))\n .catch((e) => ({ name: e.name, message: e.message, ms: Math.round(performance.now() - t0) }));\n```\n\n## Decision Matrix\n\nUse this to identify the root cause quickly.\n\n| Result | Likely Cause | Next Fix |\n| --- | --- | --- |\n| `/health` fails in Safari and Chrome works | iOS/Tailscale/DNS/TLS path | Capture Safari Network error and Tailscale state; not a PWA app bug yet |\n| `/health` passes, streaming `fetch /events` fails, `EventSource /events` works | Safari streaming `fetch` incompatibility | Change PWA client SSE transport from streaming fetch to native `EventSource` |\n| `/health` passes, both `fetch /events` and `EventSource /events` fail | SSE endpoint/header/proxy behavior incompatible with Safari | Compare `/events` response headers/timing; consider server header changes or heartbeat priming |\n| `/events` works, `/invoke` fails | POST/CORS/body handling issue | Capture preflight and POST headers/status; adjust server/client request shape |\n| Safari tab works, Home Screen PWA fails | PWA/service-worker/storage mode issue | Inspect Home Screen Web App and service worker separately; clear web app data and compare storage |\n\n## Useful Local Commands\n\nRun from any machine that has Tailscale access to the host. Redact tokens before sharing output.\n\n```bash\ncurl -sS -D - -o /tmp/pane-health.txt \\\n -H 'Origin: https://pane-remote-pwa-preview-sg622dpt4q-uc.a.run.app' \\\n '<BASE_URL>/health'\n```\n\n```bash\ncurl -sS -D - -o /tmp/pane-options.txt \\\n -X OPTIONS \\\n -H 'Origin: https://pane-remote-pwa-preview-sg622dpt4q-uc.a.run.app' \\\n -H 'Access-Control-Request-Method: POST' \\\n -H 'Access-Control-Request-Headers: content-type' \\\n '<BASE_URL>/invoke'\n```\n\n## What To Send Back\n\nSend:\n\n- Browser and OS version.\n- Safari tab vs Home Screen PWA result.\n- Physical iPhone vs simulator result.\n- Redacted Network rows for `/health`, `/events`, `/invoke`, and `OPTIONS`.\n- Console output from the five probes above.\n- Whether native `EventSource` works when streaming `fetch` fails.\n\nDo not send:\n\n- Full `pane-remote://...` code.\n- `access_token` query string.\n- `token` body value.\n"},{"path":"docs/troubleshooting/DIFF_VIEWER_CSS.md","title":"Diff Viewer CSS Troubleshooting","text":"# Diff Viewer CSS Troubleshooting\n\n⚠️ **IMPORTANT**: The diff viewer (react-diff-viewer-continued) has specific CSS requirements that can be tricky to debug.\n\n## Common Issue: No Scrollbars on Diff Viewer\n\nIf the diff viewer content is cut off and scrollbars don't appear:\n\n1. **DO NOT add complex CSS overrides** - This often makes the problem worse\n2. **Check parent containers for `overflow-hidden`** - This is usually the root cause\n3. **Use simple `overflow: 'auto'`** on the immediate diff container\n4. **Remove any forced widths or min-widths** unless absolutely necessary\n\n## The Solution That Works\n\n```tsx\n// In CombinedDiffView.tsx - the panel is a flex column and ChangesList owns scrolling.\n<div className=\"diff-panel flex min-w-0 flex-1 flex-col overflow-hidden\">\n <ChangesList rows={rows} {...listProps} />\n</div>\n```\n\n## What NOT to Do\n\n- Don't add multiple wrapper divs with conflicting overflow settings\n- Don't use CSS-in-JS to override react-diff-viewer's internal styles\n- Don't add global CSS selectors targeting generated class names\n- Don't use JavaScript hacks to force reflows\n\n## Root Cause\n\nThe issue is typically caused by parent containers having `overflow-hidden` which prevents child scrollbars from appearing. Check these files:\n\n- `SessionView.tsx` - Look for `overflow-hidden` classes\n- `CombinedDiffView.tsx` - Check the manifest/list host and its flex containers\n- `App.tsx` - Sometimes the issue starts at the app root level\n\nThe react-diff-viewer-continued library uses emotion/styled-components internally, which makes CSS overrides unreliable. The best approach is to ensure proper overflow handling in parent containers and keep the diff viewer wrapper simple.\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 Solutions:\n\n1. **Use Python 3.11 with pyenv**:\n ```bash\n brew install pyenv\n pyenv install 3.11.9\n pyenv global 3.11.9\n ```\n\n2. **Use the setup script**:\n ```bash\n ./setup-dev.sh\n ```\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're having trouble with the build process, consider waiting for official Windows binaries to be released in future versions of Pane.\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:** Added detection for error code 193 in `AbstractCliManager.ts` to trigger the Node.js fallback.\n\n**Location:** `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\\khaza\\allGitHubRepos\\notetake\\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(`Windows detected, using Node.js fallback proactively`);\n (global as typeof global & Record<string, boolean>)[needsNodeFallbackKey] = true;\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 detection (reactive fallback)\n2. `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"}]