Agent-readable docs

Build Documentation

Reference for the Build Electron IDE: multi-harness agent orchestration, sessions, IPC APIs, provider configuration, semantic search, browser preview, and build/release workflows for contributors.

Pages

  1. OverviewWhat Build exposes as a desktop IDE, primary entry points (sessions, harness picker, Auto Build), BYOC/BYOK assumptions, and the shortest path from install to first agent turn.
  2. InstallationmacOS release download, from-source prerequisites (Node, npm, Electron native deps), npm install, and environment separation between production and dev user data.
  3. QuickstartOpen a project folder, store an API key, pick a harness or Auto Build, send the first message, and verify success via chat stream, terminal output, or status bar version.
  4. Electron process modelMain vs renderer boundaries, service layout under src/main/services, webpack packaging, custom protocols (monaco-asset), CDP ports, and dev vs production userData paths.
  5. Harnesses and modelsSupported harness identifiers (claude, codex, cursor, gemini, opencode, custom), model picker strings, permission modes per harness, and harness capability limits for injection and multi-turn.
  6. Sessions and workspacesSession schema, lifecycle statuses, Docker vs local dev vs SSH vs OpenClaw, worktrees, forks, transcript resumption, and electron-store persistence (claudette-sessions).
  7. Auto Build routingPlan/build/verify/refine tiers, heuristic vs Flue controller routing, orchestration plans, mission control policies, failure cooldowns, and CLAUDE_AUTO_ROUTE_DECISION events.
  8. IPC and preload bridgeHow ipcMain handlers map to renderer window.electronAPI, typed channel constants, event vs invoke patterns, and secure contextBridge exposure in preload.ts.
  9. Configure API keys and providersStore Anthropic, OpenAI, Google, Foundry, Cursor, DeepSeek, and custom proxy models; provider CLI detection; secure key interception; and optional PostHog analytics keys.
  10. Manage coding sessionsCreate, start, stop, fork, and rewind sessions; teleport/import flows; message queue coalescing; permission and plan approval dialogs; and session switcher UX.
  11. SSH remote sessionsSSHConfig fields, connection test, remote workdir setup scripts, resume candidates, download/teleport session flows, and detached bridge recovery.
  12. Git workflows in BuildPer-session worktree git operations: status, diff, commit, push/pull, branch watch events, and GitExplorer UI bindings to git IPC channels.

Complete Markdown

# Build Documentation

> Reference for the Build Electron IDE: multi-harness agent orchestration, sessions, IPC APIs, provider configuration, semantic search, browser preview, and build/release workflows for contributors.

## Context Links

- [Agent index](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/llms.txt)
- [Human interactive docs](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b)
- [GitHub repository](https://github.com/Parcha-ai/build)

## Repository Metadata

- Repository: Parcha-ai/build

- Generated: 2026-06-02T00:27:44.703Z
- Updated: 2026-06-02T02:29:22.684Z
- Runtime: Grok CLI
- Format: Documentation
- Pages: 24

## Page Index

- 01. [Overview](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/01-overview.md) - What Build exposes as a desktop IDE, primary entry points (sessions, harness picker, Auto Build), BYOC/BYOK assumptions, and the shortest path from install to first agent turn.
- 02. [Installation](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/02-installation.md) - macOS release download, from-source prerequisites (Node, npm, Electron native deps), npm install, and environment separation between production and dev user data.
- 03. [Quickstart](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/03-quickstart.md) - Open a project folder, store an API key, pick a harness or Auto Build, send the first message, and verify success via chat stream, terminal output, or status bar version.
- 04. [Electron process model](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/04-electron-process-model.md) - Main vs renderer boundaries, service layout under src/main/services, webpack packaging, custom protocols (monaco-asset), CDP ports, and dev vs production userData paths.
- 05. [Harnesses and models](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/05-harnesses-and-models.md) - Supported harness identifiers (claude, codex, cursor, gemini, opencode, custom), model picker strings, permission modes per harness, and harness capability limits for injection and multi-turn.
- 06. [Sessions and workspaces](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/06-sessions-and-workspaces.md) - Session schema, lifecycle statuses, Docker vs local dev vs SSH vs OpenClaw, worktrees, forks, transcript resumption, and electron-store persistence (claudette-sessions).
- 07. [Auto Build routing](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/07-auto-build-routing.md) - Plan/build/verify/refine tiers, heuristic vs Flue controller routing, orchestration plans, mission control policies, failure cooldowns, and CLAUDE_AUTO_ROUTE_DECISION events.
- 08. [IPC and preload bridge](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/08-ipc-and-preload-bridge.md) - How ipcMain handlers map to renderer window.electronAPI, typed channel constants, event vs invoke patterns, and secure contextBridge exposure in preload.ts.
- 09. [Configure API keys and providers](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/09-configure-api-keys-and-providers.md) - Store Anthropic, OpenAI, Google, Foundry, Cursor, DeepSeek, and custom proxy models; provider CLI detection; secure key interception; and optional PostHog analytics keys.
- 10. [Manage coding sessions](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/10-manage-coding-sessions.md) - Create, start, stop, fork, and rewind sessions; teleport/import flows; message queue coalescing; permission and plan approval dialogs; and session switcher UX.
- 11. [SSH remote sessions](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/11-ssh-remote-sessions.md) - SSHConfig fields, connection test, remote workdir setup scripts, resume candidates, download/teleport session flows, and detached bridge recovery.
- 12. [Git workflows in Build](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/12-git-workflows-in-build.md) - Per-session worktree git operations: status, diff, commit, push/pull, branch watch events, and GitExplorer UI bindings to git IPC channels.
- 13. [Browser preview and inspection](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/13-browser-preview-and-inspection.md) - In-window webview navigation, CDP attachment per session, DOM inspector injection, snapshots, console/network capture, and Stagehand integration for agent-driven browsing.
- 14. [MCP servers](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/14-mcp-servers.md) - Install stdio/http/sse MCP servers from the registry marketplace, claudette-mcp-servers store schema, harness sync to Cursor/Gemini/Codex/OpenCode, and runtime loading into Claude Agent SDK.
- 15. [Semantic search (QMD)](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/15-semantic-search-qmd.md) - Opt-in QMD indexing via Bun bundle, collection creation, embedding generation, search IPC, project preferences, and setup-qmd dev/build integration.
- 16. [Voice and audio](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/16-voice-and-audio.md) - ElevenLabs conversational voice mode, OpenAI realtime transcription, TTS streaming IPC, microphone permissions on macOS, and optional API keys for audio providers.
- 17. [Extensions, skills, and slash commands](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/17-extensions-skills-and-slash-commands.md) - Scan project/user commands, skills, and agents; install skills from marketplace; plugin marketplaces; GStack workflow modes; and command autocomplete in chat.
- 18. [Settings reference](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/18-settings-reference.md) - AppSettings and electron-store keys in claudette-settings: theme, fonts, QMD, ultra plan, reminders, GStack, Foundry, focus tasks, customModels, and autoRouterConfig defaults.
- 19. [IPC channels reference](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/19-ipc-channels-reference.md) - Complete IPC_CHANNELS catalog grouped by domain (auth, session, claude, browser, git, mcp, qmd, ssh, voice, analytics) with invoke vs push event semantics.
- 20. [Auto Build configuration reference](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/20-auto-build-configuration-reference.md) - AutoRouterConfig fields (planModel, buildModel, verifyModel, refineModel, categories, costAware), MetaHarnessPolicy options, and npm verify:auto-router:* regression scripts.
- 21. [Shared types reference](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/21-shared-types-reference.md) - Core exported interfaces: Session, ChatMessage, RoutingDecision, OrchestrationPlan, Harness, PermissionRequest, MCP types, and message-queue HarnessCapabilities.
- 22. [npm scripts reference](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/22-npm-scripts-reference.md) - package.json scripts: start, lint, make, setup-qmd, verify:auto-router:* suite, and when to use ./scripts/dev.sh vs npm run start directly.
- 23. [Build and release](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/23-build-and-release.md) - electron-forge make output paths, version bump rules, git tagging, dual remotes (origin/public), /build and /release slash-command workflows, and GitHub release artifacts.
- 24. [Troubleshooting](https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/24-troubleshooting.md) - Dev instance port conflicts, PATH/node discovery in packaged apps, CDP timeouts, MCP harness sync errors, SSH bridge recovery, production main.log location, and security reporting.

## Source File Index

- `.claude/commands/build.md`
- `.claude/commands/dev.md`
- `.claude/commands/release.md`
- `.notes/cdp-navigation-timeout-investigation.md`
- `.notes/cdp-proxy-hardening-summary.md`
- `.notes/ssh-download-implementation-summary.md`
- `CLAUDE.md`
- `entitlements.mac.plist`
- `forge.config.ts`
- `package.json`
- `README.md`
- `scripts/build.sh`
- `scripts/dev.sh`
- `scripts/setup-qmd.ts`
- `scripts/verify-auto-router-goal-orchestration.ts`
- `scripts/verify-auto-router-meta-harness.ts`
- `scripts/verify-auto-router-plan-mode.ts`
- `scripts/verify-mcp-harness-sync.ts`
- `scripts/verify-mcp-runtime-matrix.js`
- `scripts/verify-ssh-detached-bridge-resume.js`
- `SECURITY.md`
- `src/main/index.ts`
- `src/main/ipc/analytics.ipc.ts`
- `src/main/ipc/audio.ipc.ts`
- `src/main/ipc/browser.ipc.ts`
- `src/main/ipc/claude.ipc.ts`
- `src/main/ipc/extension.ipc.ts`
- `src/main/ipc/git.ipc.ts`
- `src/main/ipc/mcp.ipc.ts`
- `src/main/ipc/qmd.ipc.ts`
- `src/main/ipc/queue.ipc.ts`
- `src/main/ipc/secure-keys.ipc.ts`
- `src/main/ipc/session.ipc.ts`
- `src/main/ipc/settings.ipc.ts`
- `src/main/ipc/ssh.ipc.ts`
- `src/main/ipc/voice.ipc.ts`
- `src/main/preload.ts`
- `src/main/services/audio.service.ts`
- `src/main/services/auto-router.service.ts`
- `src/main/services/browser.service.ts`
- `src/main/services/cdp-proxy.service.ts`
- `src/main/services/codex.service.ts`
- `src/main/services/docker.service.ts`
- `src/main/services/elevenlabs-voice.service.ts`
- `src/main/services/extension.service.ts`
- `src/main/services/flue-meta-router.service.ts`
- `src/main/services/git.service.ts`
- `src/main/services/gstack.service.ts`
- `src/main/services/harness-capabilities.ts`
- `src/main/services/harness-policy.service.ts`
- `src/main/services/mcp.service.ts`
- `src/main/services/message-queue.service.ts`
- `src/main/services/opencode.service.ts`
- `src/main/services/plugin.service.ts`
- `src/main/services/qmd.service.ts`
- `src/main/services/realtime.service.ts`
- `src/main/services/secure-keys.service.ts`
- `src/main/services/session.service.ts`
- `src/main/services/settings.service.ts`
- `src/main/services/ssh.service.ts`
- `src/main/services/stagehand.service.ts`
- `src/main/services/transcript.service.ts`
- `src/renderer/App.tsx`
- `src/renderer/components/chat/AutoRouteBadge.tsx`
- `src/renderer/components/chat/CommandAutocomplete.tsx`
- `src/renderer/components/chat/ForkTabs.tsx`
- `src/renderer/components/chat/InputArea.tsx`
- `src/renderer/components/chat/MessageQueuePanel.tsx`
- `src/renderer/components/chat/VoiceMode.tsx`
- `src/renderer/components/extensions/MCPInstallDialog.tsx`
- `src/renderer/components/extensions/MCPMarketplace.tsx`
- `src/renderer/components/extensions/UnifiedMarketplace.tsx`
- `src/renderer/components/git/GitExplorer.tsx`
- `src/renderer/components/onboarding/ApiKeyOnboarding.tsx`
- `src/renderer/components/preview/BrowserPreview.tsx`
- `src/renderer/components/qmd/QMDPrompt.tsx`
- `src/renderer/components/session/DownloadSessionDialog.tsx`
- `src/renderer/components/session/NewSessionDialog.tsx`
- `src/renderer/components/session/SessionList.tsx`
- `src/renderer/components/session/SSHConfigForm.tsx`
- `src/renderer/components/settings/SettingsDialog.tsx`
- `src/renderer/monaco-config.ts`
- `src/renderer/stores/session.store.ts`
- `src/shared/constants/channels.ts`
- `src/shared/types/index.ts`
- `src/shared/types/message-queue.ts`
- `src/shared/utils/message-recovery.ts`
- `src/shared/utils/prompt-truncation.ts`
- `TEST_RESULTS_v0043.md`
- `webpack.main.config.ts`
- `webpack.renderer.config.ts`

---

## 01. Overview

> What Build exposes as a desktop IDE, primary entry points (sessions, harness picker, Auto Build), BYOC/BYOK assumptions, and the shortest path from install to first agent turn.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/01-overview.md
- Generated: 2026-06-02T00:19:38.895Z

### Source Files

- `README.md`
- `package.json`
- `src/main/index.ts`
- `src/renderer/App.tsx`
- `src/shared/types/index.ts`
- `CLAUDE.md`

---
title: "Overview"
description: "What Build exposes as a desktop IDE, primary entry points (sessions, harness picker, Auto Build), BYOC/BYOK assumptions, and the shortest path from install to first agent turn."
---

Build (`productName`: **Build**, npm package `build`, current version **0.5.27**) is an Electron 38 desktop IDE that orchestrates external coding-agent harnesses (Claude Code, Cursor Agent, Codex, Gemini CLI, OpenCode, and custom proxy models) inside one window. The main process (`src/main/`) owns services, IPC, and subprocess spawning; the React renderer (`src/renderer/`) drives chat, sessions, terminal, browser preview, git, and settings through `window.electronAPI` exposed from `preload.ts` with `contextIsolation: true`.

## Desktop surfaces

| Surface | Location | Role |
|---------|----------|------|
| Session sidebar | `Sidebar` + `session.store` | List, create, switch, and star sessions bound to `repoPath` / `worktreePath` |
| Chat + input | `ChatContainer` / `InputArea` | Send prompts, stream harness output, permission/plan dialogs |
| Model / harness picker | `InputArea` two-level menu | Select `auto` (Auto Build) or a prefixed model id per harness |
| Terminal | `TerminalContainer` | PTY via `node-pty`; mirrors agent shell activity |
| Browser preview | `BrowserPreview` | In-window `webview`, CDP proxy, DOM inspector |
| Git | `GitExplorer` | Status, diff, commit, push/pull per session worktree |
| Monaco editor | `EditorPanel` | Multi-tab file editing with `monaco-asset://` protocol |
| Settings | `SettingsDialog` | API keys, Auto Build tiers, QMD, MCP, extensions |
| Status bar | `StatusBar` | Clock, session state, packaged **version** string |
| Onboarding | `ApiKeyOnboarding` | Provider CLI scan + optional Anthropic API key |

`MainContent` composes chat with optional terminal, browser, git, plan, extensions, command center, and agent view panels. With no active session, `EmptyState` prompts sidebar selection or **⌘N** for a new session.

## Process model

```mermaid
flowchart TB
  subgraph renderer["Renderer (React + Zustand)"]
    App["App.tsx"]
    Stores["session / ui / auth stores"]
    UI["Sidebar · Chat · Terminal · Browser · Git"]
    App --> Stores --> UI
  end

  subgraph bridge["IPC bridge"]
    Preload["preload.ts → electronAPI"]
    Channels["shared/constants/channels.ts"]
    Preload --> Channels
  end

  subgraph main["Main process (Node)"]
    Index["index.ts"]
    IPC["ipc/*.ipc.ts handlers"]
    Svc["services/*.service.ts"]
    Index --> IPC --> Svc
  end

  subgraph external["External (BYOC)"]
    Harnesses["Claude / Codex / Cursor / Gemini / OpenCode CLIs"]
    Providers["Anthropic · OpenAI · Google · proxies"]
    Harnesses --> Providers
  end

  UI <-->|invoke + events| Preload
  Preload <-->|ipcMain| IPC
  Svc --> Harnesses
```

On `app.ready`, `index.ts` runs `migrateFromGrepBuild()`, `registerIPCHandlers()` (auth, session, git, terminal, claude, settings, browser, SSH, QMD, MCP, codex, queue, and others), starts `cdpProxyService`, and opens the primary `BrowserWindow`. Packaged macOS builds append logs to `userData/main.log`; dev builds set `GREP_DEV_USER_DATA` so settings and sessions do not overwrite production data.

## Sessions

A **session** is the unit of work: one project folder (or remote target) with its own branch/worktree, chat transcript, model selection, and optional SSH or OpenClaw gateway config.

<ParamField body="id" type="string" required>Stable session identifier</ParamField>
<ParamField body="repoPath" type="string" required>User-selected project root</ParamField>
<ParamField body="worktreePath" type="string" required>Git worktree used for edits and terminal cwd</ParamField>
<ParamField body="status" type="SessionStatus" required>Lifecycle: `creating` → `starting` → `setup` → `running` (or `stopping` / `stopped` / `error`)</ParamField>
<ParamField body="model" type="string">Per-session model id (e.g. `claude-sonnet-4-6`, `codex:gpt-5.4`, `auto`)</ParamField>

Sessions persist in electron-store (`claudette-sessions`; dev may use a copied file under `/tmp/grep-build-dev`). After auth/dev gate, `ElectronApp` calls `loadSessions()`, subscribes to IPC session/claude/codex streams, and may `checkAndAutoResume()` interrupted work.

Session kinds implied by `Session` fields:

- **Local dev** — `isDevMode: true`, no Docker container
- **Docker** — `containerId` allocated with port map (`web`, `api`, `debug`)
- **SSH remote** — `sshConfig` with host, key, `remoteWorkdir`, optional worktree script
- **OpenClaw** — `openclawConfig.gatewayUrl` + bearer token

Forks, teleports, and conversation forks are tracked via `parentSessionId`, `forkName`, `teleportedFrom`, and related fields.

## Harness picker

Build does not embed model weights; it **drives installed harness CLIs** and maps UI model strings to harness ids:

| Harness id | Model id prefix | UI group label (picker) |
|------------|-----------------|-------------------------|
| `claude` | (none) | Claude |
| `codex` | `codex:` | Codex |
| `cursor` | `cursor:` | Cursor |
| `gemini` | `gemini:` | Gemini CLI |
| `opencode` | `opencode:` | DeepSeek (OpenCode BYO endpoint) |
| `custom` | `custom:` | Custom (settings `customModels` proxy) |

`harnessFromModel()` in `session.store.ts` performs prefix resolution. Permission modes differ for Codex vs Claude harnesses (`getSupportedPermissionModes`).

The picker is a **two-level menu**: harness list (left) → models for that harness (right), plus **Recent** quick picks in `localStorage` (`grep-recent-models`). Hot-swapping mid-session updates `selectedModel[sessionId]` without clearing files or branch context.

Default Anthropic-facing models are listed in `claude.service.ts` (`getModels`), including Sonnet/Opus/Haiku ids and Codex-prefixed entries; Foundry and `customModels` append at runtime.

## Auto Build

Selecting model id **`auto`** enables **Auto Build**: tiered routing across `plan`, `build`, `verify`, and `refine` (`TaskTier` in shared types). The router produces a `RoutingDecision` (tier, domain, `resolvedModel`, `resolvedHarness`, confidence, `method: 'heuristic' | 'controller'`, optional `orchestration` / `missionControl`).

<Info>
Auto Build’s meta-harness controller (`flue-meta-router.service.ts`) returns routing decisions only; it does not execute shell, read, or write in the sandbox—execution delegates to the resolved harness.
</Info>

Configure fixed tier → harness/model mappings under Settings → **Auto Build** (`autoRouterConfig` on `AppSettings`: `planModel`, `buildModel`, `verifyModel`, `refineModel`, categories, `costAware`, workflow/speed/verification policies). While `auto` is active, `AutoRouteBadge` and chat copy surface the resolved scope (e.g. `plan:frontend`).

## BYOC / BYOK assumptions

<Check>
**Bring your own credentials.** API keys and CLI logins are stored locally (electron-store / secure key IPC), not on a Build-hosted inference proxy for normal chat turns.
</Check>

| Expectation | Behavior in repo |
|-------------|------------------|
| No Build account for local folders | README: open a project folder without signing in |
| Anthropic key | `claudette-settings` → `anthropicApiKey`; onboarding can set via `settings.setApiKey` |
| Other providers | `googleApiKey`, `foundryApiKey`, `cursorApiKey`, OpenAI/Codex via CLI login, `customModels` for OpenAI-compatible proxies |
| Harness presence | Onboarding runs `auth.checkProviders()` for Claude, Codex, Cursor, Gemini, OpenCode install/login state |
| Optional analytics | `posthogApiKey` / `posthogHost` in settings; PostHog client only active when env/key configured |
| GitHub OAuth | `LoginScreen` + `auth.store` exist; current `checkAuth()` sets `isDevMode: true` and skips the gate so local workflow proceeds immediately |

Provider calls are issued from the main process to **your** CLIs and provider endpoints. Voice (ElevenLabs / OpenAI Realtime) and QMD embeddings are likewise opt-in with separate keys.

## Renderer bootstrap

`App.tsx` root routing:

1. Non-Electron → `PreviewMode` (UI only, no IPC)
2. `?mode=browser` → `BrowserOnlyApp` (command-center browser pop-out)
3. Else → `ElectronApp`

`ElectronApp` initialization sequence:

1. `initializeTTSListeners()` / audio settings
2. `auth.checkAuth()` (dev gate)
3. `checkApiKey()` → open `ApiKeyOnboarding` if missing and not `onboardingSkipped`
4. `loadSessions()` + IPC subscriptions when `user || isDevMode`

## Shortest path: install → first agent turn

<Steps>
<Step title="Install Build">
<Tabs>
<Tab title="macOS release">
Download the signed `.dmg` from [GitHub Releases](https://github.com/Parcha-ai/build/releases) and move **Build** to Applications.
</Tab>
<Tab title="From source">
```bash
git clone https://github.com/Parcha-ai/build.git
cd build
npm install
./scripts/dev.sh
```
Note the printed **DEV INSTANCE** name (e.g. `bouncy-penguin`) in the terminal; dev uses port **9001** and `GREP_DEV_USER_DATA=/tmp/grep-build-dev`.
</Tab>
</Tabs>
</Step>

<Step title="Open a project">
Launch Build. Create or select a session (**⌘N** or sidebar) and choose a local folder (`repoPath`). Wait for `status: running` (and setup script completion for container/SSH sessions).
</Step>

<Step title="Configure credentials">
Complete **ApiKeyOnboarding** (Anthropic API key and/or install+log in harness CLIs), or open Settings → providers. Skip is allowed if at least one harness is already logged in via CLI.
</Step>

<Step title="Pick harness or Auto Build">
In the chat input model menu, choose **Auto Build** (`auto`) or a specific harness model (e.g. `claude-sonnet-4-6`, `codex:gpt-5.4`). Set permission mode if prompted.
</Step>

<Step title="Send the first message">
Type a prompt and press Enter. Verify: streaming assistant content in chat, tool/terminal activity in the terminal panel, and no session `error` status. Packaged builds show the new version in the status bar after upgrades.
</Step>
</Steps>

<Warning>
Use `./scripts/dev.sh` for development—not bare `npm run start`—so QMD setup, port cleanup, dev `userData` separation, and instance naming run correctly.
</Warning>

## Persistence and environments

| Store file | Contents |
|------------|----------|
| `claudette-settings.json` | `AppSettings`, `anthropicApiKey`, provider keys, `autoRouterConfig`, theme, QMD flags |
| `claudette-sessions.json` | Map of `Session` records |
| `claudette-mcp-servers.json` | MCP server definitions |
| `claudette-identity.json` | Anonymous device id for analytics |

Production macOS path: `~/Library/Application Support/Build` (migrated from legacy **G-Build** / **Grep Build** names). Dev copies settings once into `/tmp/grep-build-dev` without symlinks to avoid corrupting prod on flush.

Remote debugging: CDP port **9222** (production) or **9223** (development / `DEV_INSTANCE_NAME`), overridable via `ELECTRON_CDP_PORT`.

## Related pages

<CardGroup>
<Card title="Installation" href="/installation">
macOS release, prerequisites, npm install, and dev vs production user data.
</Card>
<Card title="Quickstart" href="/quickstart">
Folder open, API key, first message, and verification signals.
</Card>
<Card title="Electron architecture" href="/electron-architecture">
Main vs renderer, services layout, webpack, protocols, CDP.
</Card>
<Card title="Harnesses and models" href="/harnesses-and-models">
Harness ids, model prefixes, permission modes, capability limits.
</Card>
<Card title="Sessions and workspaces" href="/sessions-and-workspaces">
Schema, lifecycle, SSH/Docker/OpenClaw, worktrees, persistence.
</Card>
<Card title="Auto Build routing" href="/auto-build-routing">
Plan/build/verify/refine tiers, controller routing, orchestration.
</Card>
<Card title="Configure providers" href="/configure-providers">
API keys, CLI detection, secure storage.
</Card>
</CardGroup>

---

## 02. Installation

> macOS release download, from-source prerequisites (Node, npm, Electron native deps), npm install, and environment separation between production and dev user data.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/02-installation.md
- Generated: 2026-06-02T00:19:35.618Z

### Source Files

- `README.md`
- `package.json`
- `forge.config.ts`
- `entitlements.mac.plist`
- `scripts/dev.sh`
- `webpack.main.config.ts`

---
title: "Installation"
description: "macOS release download, from-source prerequisites (Node, npm, Electron native deps), npm install, and environment separation between production and dev user data."
---

Build ships as a signed macOS `.app` from GitHub Releases and as an Electron Forge workspace you run with `npm install` plus `./scripts/dev.sh`. Production state lives under `~/Library/Application Support/Build`; development overrides `userData` to `/tmp/grep-build-dev` so settings, sessions, and MCP configs do not collide with a release install.

## macOS release install

<Tabs>
<Tab title="Download release">

<Steps>
<Step title="Download the artifact">

Download the latest macOS build from [GitHub Releases](https://github.com/Parcha-ai/build/releases). Release builds are signed and notarized when Apple credentials are present at package time.

</Step>
<Step title="Install to Applications">

Drag **Build.app** into `/Applications` (or open the DMG/ZIP artifact and move the app bundle).

</Step>
<Step title="Verify the install">

Launch **Build** from Applications. The status bar shows the app version from `package.json` (for example `0.5.27`). No account is required for local-folder mode; API keys are configured after launch.

</Step>
</Steps>

</Tab>
<Tab title="Build locally">

Use `npm run make` when you need a local distributable. Output lands under a versioned directory:

```text
out/v{version}/Build-darwin-arm64/Build.app
```

`./scripts/build.sh` wraps `npm run make`, tags `v{version}`, and opens the built app on macOS.

</Tab>
</Tabs>

<Note>
README notes that **building from source** is supported on macOS, Linux, and Windows; **prebuilt release downloads** target macOS.
</Note>

| Artifact | Typical path |
|----------|----------------|
| Packaged app (arm64) | `out/v{version}/Build-darwin-arm64/Build.app` |
| Forge output root | `out/v{version}/` |
| Bundle ID | `com.parcha.build` |
| Executable name | `build` |

## From-source prerequisites

| Requirement | Version / notes |
|-------------|-----------------|
| **Node.js** | Node **18+** (Electron Forge 7 and dependencies expect modern Node; no `engines` field in root `package.json`) |
| **npm** | Bundled with Node; used for install and scripts |
| **Electron** | `^38.7.2` (devDependency; Forge downloads the matching runtime) |
| **TypeScript** | `~4.5.4` (compile-time only) |
| **macOS toolchain** | Xcode Command Line Tools for native module compile and optional codesign |
| **Git** | Clone and contribution workflow |

### Native and externalized dependencies

Build relies on native Node addons and packages that stay **outside** the webpack bundle:

| Package | Role |
|---------|------|
| `node-pty` | Integrated terminal (PTY) |
| `@anthropic-ai/claude-agent-sdk` (+ platform binary package) | Claude Code harness |
| `@cursor/sdk` (+ platform binary) | Cursor harness |
| `@anthropic-ai/sdk`, `docx`, `monaco-editor` | SDK and editor assets copied at package time |

Webpack **externals** in `webpack.main.config.ts` match the `postPackage` copy list in `forge.config.ts`. `@electron-forge/plugin-auto-unpack-natives` unpacks `.node` binaries into the app layout. `webpack.rules.ts` routes `node-pty` and `@anthropic-ai` through `node-loader` / asset relocator rules instead of bundling them.

<Warning>
Main-process `webPreferences` set `sandbox: false` because **node-pty** and **webview** tags require it. Do not enable sandbox without replacing those subsystems.
</Warning>

### Optional: QMD semantic search bundle

`./scripts/dev.sh` runs `npm run setup-qmd`, which downloads Bun and QMD platform bundles into `resources/qmd/{platform-arch}/`. Production `npm run make` copies the matching bundle into `Build.app/Contents/Resources/qmd` when present; otherwise packaging logs a warning to run `npx ts-node scripts/setup-qmd.ts {platform-arch}`.

## Clone and `npm install`

<CodeGroup>
```bash title="Clone"
git clone https://github.com/Parcha-ai/build.git
cd build
```

```bash title="Install dependencies"
npm install
```
</CodeGroup>

`npm install` pulls Electron Forge, webpack tooling, React, and application dependencies from `package.json`. There is no root `postinstall` script; Forge plugins handle native unpacking on package.

<Tip>
If `node-pty` or SDK binaries fail to load after install, run `npm run make` or `npx electron-rebuild` against the pinned Electron version, then retry `./scripts/dev.sh`.
</Tip>

## Development launch (`./scripts/dev.sh`)

<Warning>
Start development with **`./scripts/dev.sh`**, not `npm run start` alone. The dev script sets instance name, QMD setup, port isolation, userData override, and cleanup of orphaned dev Electron processes.
</Warning>

<Steps>
<Step title="Run the dev script">

```bash
./scripts/dev.sh
```

The script prints a **DEV INSTANCE** name (for example `bouncy-penguin`) used in the status bar to distinguish dev builds.

</Step>
<Step title="What the script configures">

| Variable / action | Value / behavior |
|-------------------|------------------|
| `DEV_INSTANCE_NAME` | Random adjective–noun label |
| `npm run setup-qmd` | Ensures QMD/Bun bundle for current platform |
| Process cleanup | Kills `node_modules/electron` dev instances and `electron-forge`; does **not** kill `out/` production builds |
| `DEV_WEBPACK_PORT` | `9001` (frees port `9000` for production logger) |
| `GREP_DEV_USER_DATA` | `/tmp/grep-build-dev` |
| Settings sync | One-time copy from production dirs if dev files missing |
| Launch | `npm run start` → `electron-forge start` |

</Step>
<Step title="Verify dev is isolated">

Confirm the console line `Using dev userData: /tmp/grep-build-dev` and that DevTools open automatically when `GREP_DEV_USER_DATA` is set.

</Step>
</Steps>

Other common scripts after install:

| Script | Purpose |
|--------|---------|
| `npm run lint` | ESLint across `.ts` / `.tsx` |
| `npm run package` | Forge package without full make |
| `npm run make` | Clean `.webpack` and `out/v{version}`, then `electron-forge make` |
| `npm run setup-qmd` | QMD bundle for current platform only |
| `npm run setup-qmd:all` | QMD bundles for all supported platforms |

## Production vs development user data

Electron persists app state under `app.getPath('userData')`. Build separates environments by **directory**, not by renaming store files in current code (`getSessionStoreName()` returns `claudette-sessions` in both modes; files simply live in different folders).

```text
Production (release .app)
  ~/Library/Application Support/Build/
    claudette-settings.json      # electron-store (API keys, theme, QMD flags, …)
    claudette-sessions.json      # CachedStore session index
    claudette-mcp-servers.json
    claudette-mcp-harness-sync.json
    claudette-identity.json
    claudette-memory.json
    claudette-qmd.json
    main.log                     # production main-process log
    sessions/                    # per-session artifacts
    qmd/                         # copied/runtime QMD files
    Local Storage/               # renderer localStorage

Development (./scripts/dev.sh)
  /tmp/grep-build-dev/           # GREP_DEV_USER_DATA via app.setPath
    (same store filenames)
```

```mermaid
flowchart LR
  subgraph prod["Production Build.app"]
    PUD["~/Library/Application Support/Build"]
  end
  subgraph dev["Dev electron-forge"]
    DEV["/tmp/grep-build-dev"]
  end
  subgraph legacy["Legacy dirs - migrated once"]
    GB["G-Build"]
    GR["Grep Build"]
  end
  legacy -->|migrateFromGrepBuild on first launch| PUD
  GB -.->|dev.sh one-time copy if missing| DEV
  GR -.->|dev.sh one-time copy if missing| DEV
  PUD -.->|dev.sh one-time copy if missing| DEV
```

### How separation is enforced

| Mechanism | Production | Development |
|-----------|------------|-------------|
| `userData` path | Default Electron path for `productName: "Build"` | `GREP_DEV_USER_DATA=/tmp/grep-build-dev` set before `app.ready` |
| CDP remote debugging | Port `9222` (default) | Port `9223` when `DEV_INSTANCE_NAME` or `NODE_ENV=development` |
| Webpack logger port | `9000` (Forge default) | `9001` via `DEV_WEBPACK_PORT` |
| Main log file | `main.log` under userData | Skipped when `DEV_INSTANCE_NAME` is set |
| DevTools | Closed by default | Opened when `GREP_DEV_USER_DATA` is set |

On first production launch, `migrateFromGrepBuild()` copies store files and `Local Storage` from `~/Library/Application Support/G-Build` or `Grep Build` into `Build`, then writes `.migrated-from-grep-build`.

`scripts/dev.sh` sync policy:

- **Settings / MCP**: copy from production only if the dev file does **not** exist (never overwrite dev keys or custom models).
- **Sessions**: copy `claudette-sessions.json` when present (optional convenience).
- **Never symlink** dev → prod; `CachedStore` debounced writes would corrupt production data.

<Info>
Store files retain the `claudette-*` prefix from earlier product names. Paths on disk use **Build**; identifiers in JSON are unchanged for compatibility.
</Info>

### Persistence keys (electron-store)

Settings use `claudette-settings` (`SettingsService`). Sessions and related IPC services use `CachedStore` with name `claudette-sessions`. MCP configuration uses `claudette-mcp-servers` and `claudette-mcp-harness-sync`. All resolve under the active `userData` directory.

## macOS signing and entitlements

Distribution signing in `forge.config.ts` `postPackage` uses `entitlements.plist` (JIT, unsigned executable memory, network client, user-selected read-write). When `APPLE_ID`, `APPLE_PASSWORD`, and `APPLE_TEAM_ID` are set, the hook runs `@electron/osx-sign` and `@electron/notarize`; otherwise it falls back to adhoc `codesign`.

`entitlements.mac.plist` adds microphone access (`com.apple.security.device.audio-input`) for voice mode alongside standard Electron JIT entitlements.

Offline or air-gapped packaging knobs:

| Environment variable | Effect |
|---------------------|--------|
| `GREP_ELECTRON_ZIP_DIR` | Use a local Electron zip (`electronZipDir`) |
| `GREP_ELECTRON_CACHE_ROOT` | Cache root (default `~/Library/Caches/electron`) |
| `GREP_SKIP_ELECTRON_CHECKSUMS=1` | Skip checksum verification on download |

## Environment variables reference

<ParamField body="GREP_DEV_USER_DATA" type="string">
Overrides Electron `userData`. Set by `scripts/dev.sh` to `/tmp/grep-build-dev`.
</ParamField>

<ParamField body="DEV_INSTANCE_NAME" type="string">
Human-readable dev instance label; disables production `main.log` redirection when set.
</ParamField>

<ParamField body="DEV_WEBPACK_PORT" type="number">
Webpack dev server port. Dev script sets `9001` to avoid conflicting with production on `9000`.
</ParamField>

<ParamField body="DEV_WEBPACK_LOGGER_PORT" type="number">
Forge logger port (default `9000` in `forge.config.ts`).
</ParamField>

<ParamField body="ELECTRON_CDP_PORT" type="string">
Chrome DevTools Protocol port override for browser preview / Stagehand.
</ParamField>

<ParamField body="NODE_ENV" type="string">
When `development`, CDP defaults to port `9223` alongside `DEV_INSTANCE_NAME`.
</ParamField>

## Verification checklist

| Check | Expected signal |
|-------|-----------------|
| Release app opens | No Gatekeeper block on notarized builds from Releases |
| Dev script banner | `DEV INSTANCE: {adjective}-{noun}` in terminal |
| Isolated settings | Edits under `/tmp/grep-build-dev` do not change `~/Library/Application Support/Build` |
| Terminal works | PTY sessions start (confirms `node-pty` native module loaded) |
| Status bar version | Matches `package.json` `version` field |

<Warning>
Run only **one** dev Electron instance at a time. `dev.sh` kills prior `node_modules/electron` processes to release the single-instance lock; production apps under `out/` are left running.
</Warning>

## Related pages

<CardGroup>
<Card title="Quickstart" href="/quickstart">
Open a project, store API keys, pick a harness, and send the first agent message after install.
</Card>
<Card title="Electron architecture" href="/electron-architecture">
Main vs renderer boundaries, services layout, webpack output, and userData path behavior in depth.
</Card>
<Card title="Build and release" href="/build-and-release">
Version bumps, `npm run make`, git tags, and GitHub release artifacts.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Port conflicts, PATH/node discovery in packaged apps, and log file locations.
</Card>
</CardGroup>

---

## 03. Quickstart

> Open a project folder, store an API key, pick a harness or Auto Build, send the first message, and verify success via chat stream, terminal output, or status bar version.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/03-quickstart.md
- Generated: 2026-06-02T00:19:51.444Z

### Source Files

- `README.md`
- `src/renderer/components/onboarding/ApiKeyOnboarding.tsx`
- `src/renderer/components/session/NewSessionDialog.tsx`
- `src/renderer/components/chat/InputArea.tsx`
- `src/main/services/settings.service.ts`
- `scripts/dev.sh`

---
title: "Quickstart"
description: "Open a project folder, store an API key, pick a harness or Auto Build, send the first message, and verify success via chat stream, terminal output, or status bar version."
---

Build is an Electron desktop IDE that opens local project folders as **dev-mode sessions** (`isDevMode: true`), persists credentials in `claudette-settings` via `electron-store`, and routes chat through harness-specific backends (Claude Agent SDK, Codex, Cursor, Gemini CLI, OpenCode) or **Auto Build** when the per-session model is `auto`. The shortest path from install to a verified agent turn: open a folder → satisfy onboarding (CLI harness and/or Anthropic API key) → create or activate a session → pick a model in `InputArea` → send a message → confirm streaming in chat, shell output in the terminal panel, and `G-BUILD v{version}` in the status bar.

## Prerequisites

| Requirement | Notes |
|-------------|-------|
| macOS build (recommended) | Signed release from [GitHub Releases](https://github.com/Parcha-ai/build/releases), or from-source with Node/npm (see [Installation](/installation)) |
| Project folder | Any directory; git is optional but enables branch UI and worktrees |
| Provider credentials | At least one of: installed harness CLI with login, or `sk-ant-…` Anthropic API key stored in settings |
| Harness CLIs (optional per harness) | Claude Code, Codex, Cursor Agent, Gemini CLI, OpenCode — detected at onboarding via `auth.checkProviders` |

<Note>
Local-folder mode does not require a GitHub account. `LoginScreen` offers **LOCAL FOLDER** alongside GitHub login; choosing a folder sets `isDevMode` and skips the GitHub gate in `App.tsx`.
</Note>

## End-to-end flow

```mermaid
sequenceDiagram
  participant User
  participant UI as Renderer (LoginScreen / NewSessionDialog)
  participant Main as Main (dev.ipc / settings)
  participant Store as session.store
  participant Agent as claude.service + harness

  User->>UI: Open local folder
  UI->>Main: dev.openLocalRepo → dev.createSession
  Main-->>UI: Session record (isDevMode)
  UI->>Store: addSession + setActiveSession
  User->>UI: Onboarding: CLI and/or setApiKey
  UI->>Main: settings.setApiKey (optional)
  User->>UI: InputArea: model + message + Enter
  UI->>Store: sendMessage(sessionId, text)
  Store->>Agent: claude.sendMessage(model, mode, thinking)
  Agent-->>Store: onStreamChunk / onStreamEnd
  Store-->>UI: Chat stream + terminal tool output
```

## Open a project folder

<Steps>
<Step title="Production app">
Download **Build** from [GitHub Releases](https://github.com/Parcha-ai/build/releases) and open the app. On first launch you see `LoginScreen` unless you already have a GitHub session or dev mode.

</Step>
<Step title="First launch — local folder (fastest)">
On `LoginScreen`, click **LOCAL FOLDER**. The native folder picker runs `dev.openLocalRepo`. Build creates a session with `dev.createSession({ name, repoPath, branch })`, adds it to the sidebar, activates it, and sets `isDevMode: true`. Non-git folders prompt to initialize git or **OPEN WITHOUT GIT**.

</Step>
<Step title="Additional sessions from the IDE">
After you are in the main UI, use the sidebar **+** control (`openNewSessionDialog`) → **NEW SESSION** → **Local Folder**. Same picker and config step (`CONFIGURE SESSION`) with optional git branch and worktree options.

</Step>
<Step title="From-source development">
Run `./scripts/dev.sh` (not `npm run start` directly). The script assigns `DEV_INSTANCE_NAME` (e.g. `bouncy-penguin`), uses webpack port **9001**, and isolates user data at `/tmp/grep-build-dev` so production settings under `~/Library/Application Support/Build` are not overwritten. Confirm dev mode in the status bar: `G-BUILD v…` plus `[DEV_INSTANCE_NAME]` in amber.

</Step>
</Steps>

<Tip>
`NewSessionDialog` also supports GitHub clone, Teleport, SSH remote, and OpenClaw gateway sources. For a minimal quickstart, stay on **Local Folder**.
</Tip>

## Store API keys and provider readiness

On startup, `App.tsx` calls `checkApiKey()` (reads `anthropicApiKey` from settings). If no key and `onboardingSkipped` is not set, **Welcome to Build** (`ApiKeyOnboarding`) opens.

| Onboarding signal | Meaning |
|-------------------|---------|
| Green check on a harness row | `auth.checkProviders` reports `loggedIn: true` (CLI install + auth) |
| Amber / install or login command | CLI missing or sign-in required — copy commands from the row |
| Anthropic API key field | Required only when **no** harness is ready; optional when any harness is logged in |
| **Continue to Build** | Sets `onboardingSkipped: true` or saves `sk-ant-…` via `settings.setApiKey` |

<ParamField body="anthropicApiKey" type="string">
Stored in `claudette-settings` (top-level key, not inside `settings`). Onboarding validates prefix `sk-ant-` before save.
</ParamField>

<Warning>
Build does not proxy API traffic. Keys and CLI credentials stay on your machine (BYOC/BYOK). Use **Advanced Settings →** in onboarding for OpenAI, Google, Foundry, Cursor, DeepSeek, and custom proxy models.
</Warning>

## Pick a harness or Auto Build

The chat input footer hosts the **model picker** (`InputArea`). Models load from `claude.getModels()` into `session.store` `availableModels`.

| Picker entry | Model id | Behavior |
|--------------|----------|----------|
| **Auto Build** | `auto` | Default when no per-session override; orchestrates plan/build/verify/refine tiers and shows `AutoRouteBadge` while streaming |
| Claude | `claude-opus-4-8`, `claude-sonnet-4-6`, … | Anthropic models via Claude Code harness |
| Codex | `codex:gpt-5.4`, `codex:o3`, … | OpenAI Codex harness |
| Cursor | `cursor:…` | Cursor Agent harness |
| Gemini CLI | `gemini:…` | Google Gemini harness |
| OpenCode / custom | `opencode:…`, `custom:…` | Community or proxy-defined models |

**Default for new turns:** `selectedModel[sessionId] || 'auto'`.

**Permission mode** (prompt glyph next to input): cycles `auto`, `acceptEdits` (`>>`), `default` / ASK (`>`), `bypassPermissions`, `plan`, `dontAsk`. Default permission mode is `default` (approval required).

**Effort / thinking:** defaults to `high` when unset; adjust via the effort control in the input chrome.

To pin a specific harness model instead of Auto Build, open the picker → choose harness group → select a model. Recent picks persist in `localStorage` key `grep-recent-models`.

## Send the first message

1. Select the session in the sidebar so `activeSessionId` is set.
2. Focus the chat input (`InputArea`).
3. Type a concrete task (for example: “List the top-level directories and summarize `package.json` scripts”).
4. Press **Enter** to send (or **Cmd+Enter** / **Ctrl+Enter** to force-send during an active stream).

`handleSubmit` → `session.store.sendMessage` → `electronAPI.claude.sendMessage` with:

- `model` — `auto` or explicit id
- `permissionMode` — normalized per model
- `thinkingMode` — effort level (default `high`)
- Optional attachments (images, `@` file mentions)

If a stream is already running, new text is **queued** in the main-process queue and shown in `MessageQueuePanel` until the prior turn finishes.

## Verify success

Confirm the first agent turn with at least one of these signals:

### Chat stream

| Signal | Expected |
|--------|----------|
| `isStreaming[sessionId]` | `true` while the harness runs |
| Assistant content | Chunks arrive via `onStreamChunk`; final message on `onStreamEnd` |
| Auto Build | Purple **AUTO** label or `AutoRouteBadge` with tier/domain; tooltip shows resolved harness + model |
| Tool cards | Read, Bash, Edit, etc. appear in the thread |

### Terminal output

Open the **terminal** panel for the active session. Agent shell commands from tool use should appear in the integrated PTY (xterm + `node-pty`). Long-running setup during session create also streams to `NewSessionDialog` progress log (`dev.onSetupProgress` / `ssh.onSetupProgress`).

### Status bar version

Bottom-right status bar (`StatusBar.tsx`):

| Display | Meaning |
|---------|---------|
| `G-BUILD v0.5.27` (example) | `app.getVersion()` from packaged `package.json` — confirms you are on the build you expect |
| `[fuzzy-penguin]` (dev only) | `DEV_INSTANCE_NAME` from `./scripts/dev.sh` — distinguishes dev from production |
| Branch name | Git watcher active for the session worktree |
| `PORT:` | Shown when `session.status === 'running'` (container/dev server sessions) |

<Check>
Quickstart success checklist: onboarding harness or API key saved → session active in sidebar → model shows **AUTO** or chosen harness → first message streams in chat → terminal shows command activity (if tools run) → status bar version matches your install.
</Check>

## Persistence and paths

| Store file | Purpose |
|------------|---------|
| `claudette-settings.json` | `settings.*`, `anthropicApiKey`, `onboardingSkipped`, provider keys |
| `claudette-sessions.json` | Session list and metadata |
| Production userData | `~/Library/Application Support/Build` (also legacy `G-Build`, `Grep Build`) |
| Dev userData | `/tmp/grep-build-dev` when using `./scripts/dev.sh` |

Sessions created from a local folder are flagged `isDevMode: true` (no Docker container required for the default quickstart path).

## Common blockers

<AccordionGroup>
<Accordion title="Onboarding will not dismiss">
Install or log in to at least one harness CLI, **or** enter a valid `sk-ant-…` key, **or** click **Continue to Build** (sets `onboardingSkipped`). Open **Advanced Settings** for non-Anthropic keys.
</Accordion>
<Accordion title="Send does nothing">
Confirm `activeSessionId` is set, input is not `disabled`, and onboarding closed. Check main-process logs: production `~/Library/Application Support/Build/main.log`; dev uses console from `./scripts/dev.sh`.
</Accordion>
<Accordion title="Auto Build stays on AUTO with no stream">
Ensure a harness backing Auto Build is configured (Claude API key and/or logged-in CLI). See [Auto Build routing](/auto-build-routing) and [Harnesses and models](/harnesses-and-models).
</Accordion>
<Accordion title="Wrong build version in status bar">
Production should match the downloaded release tag. Dev builds show the repo `package.json` version plus `[DEV_INSTANCE_NAME]` — restart with `./scripts/dev.sh` after pulling changes.
</Accordion>
</AccordionGroup>

## Related pages

<CardGroup>
<Card title="Installation" href="/installation">
macOS download, from-source setup, and dev vs production user data separation.
</Card>
<Card title="Overview" href="/overview">
Product surfaces: sessions, harness picker, Auto Build, and BYOC/BYOK assumptions.
</Card>
<Card title="Configure providers" href="/configure-providers">
Full API key and provider matrix beyond onboarding.
</Card>
<Card title="Harnesses and models" href="/harnesses-and-models">
Harness ids, model strings, and capability limits.
</Card>
<Card title="Sessions and workspaces" href="/sessions-and-workspaces">
Session schema, dev vs Docker vs SSH, and `claudette-sessions` persistence.
</Card>
<Card title="Auto Build routing" href="/auto-build-routing">
Plan/build/verify/refine tiers when model id is `auto`.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Port conflicts, PATH/node in packaged apps, and log locations.
</Card>
</CardGroup>

---

## 04. Electron process model

> Main vs renderer boundaries, service layout under src/main/services, webpack packaging, custom protocols (monaco-asset), CDP ports, and dev vs production userData paths.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/04-electron-process-model.md
- Generated: 2026-06-02T00:19:31.935Z

### Source Files

- `src/main/index.ts`
- `src/main/preload.ts`
- `webpack.main.config.ts`
- `webpack.renderer.config.ts`
- `forge.config.ts`
- `src/renderer/monaco-config.ts`

---
title: "Electron process model"
description: "Main vs renderer boundaries, service layout under src/main/services, webpack packaging, custom protocols (monaco-asset), CDP ports, and dev vs production userData paths."
---

Build is an Electron Forge application: a Node.js **main process** owns system integration, IPC, and agent services; a React **renderer** runs the IDE UI behind `contextBridge`; optional **webview** renderers host browser preview. Webpack bundles three entry surfaces (main, preload, renderer), and packaged builds externalize native modules into `Resources/node_modules`.

## Process boundaries

| Process | Entry | Node integration | Primary role |
|---------|-------|------------------|--------------|
| Main | `src/main/index.ts` → `.webpack/main` | Full Node.js | Window lifecycle, IPC, services, protocols, CDP proxy |
| Preload | `src/main/preload.ts` (webpack preload entry) | Limited (`ipcRenderer`) | Typed `window.electronAPI` via `contextBridge` |
| Renderer | `src/renderer/index.tsx` | Disabled (`nodeIntegration: false`, `contextIsolation: true`) | React UI, Zustand stores, Monaco |
| Webview | Created by renderer `<webview>` | Disabled; `sandbox: false` | In-app browser preview (`persist:browser` partition) |

```mermaid
flowchart TB
  subgraph main["Main process (index.ts)"]
    IPC["ipc/*.ts handlers"]
    SVC["services/*.ts"]
    CDP["cdp-proxy.service"]
  end
  subgraph bridge["Preload (preload.ts)"]
    API["contextBridge → electronAPI"]
  end
  subgraph ui["Renderer (index.tsx)"]
    React["React + Zustand"]
    Monaco["monaco-config.ts"]
  end
  subgraph preview["Webview renderers"]
    WV["persist:browser partition"]
  end
  React -->|invoke / on| API
  API -->|IPC| IPC
  IPC --> SVC
  SVC --> CDP
  React --> WV
  WV -->|register-webview IPC| SVC
  CDP -->|debugger API| WV
```

Main-window `webPreferences` set `sandbox: false` (required for `node-pty`), `webviewTag: true`, and load the webpack-generated preload script. The renderer imports `./monaco-config` before React so Monaco paths resolve to `monaco-asset://` in Electron.

## Main process layout

### Bootstrap (`src/main/index.ts`)

Startup runs before `app.ready`:

- **Dev instance name** — `DEV_INSTANCE_NAME` from `./scripts/dev.sh` (shown in UI).
- **Production logging** — When not in dev, `console.*` is tee’d to `{userData}/main.log`.
- **Dev userData override** — `GREP_DEV_USER_DATA` calls `app.setPath('userData', …)` so dev never writes production settings.
- **Chromium remote debugging** — `remote-debugging-port` from `ELECTRON_CDP_PORT`, or `9223` in development / named dev instance, else `9222`.
- **PATH fix** — `fix-path` plus Homebrew/nvm fallbacks so packaged Finder launches can spawn `node`.
- **Privileged scheme** — `monaco-asset` registered via `protocol.registerSchemesAsPrivileged` before ready.
- **Single instance** — Unless `GREP_DISABLE_SINGLE_INSTANCE=1`, a second launch focuses the existing window.

On `app.ready`: migrate legacy app-support folders, register all IPC handlers, start `powerService`, sync MCP harness configs, `createWindow()`, and `cdpProxyService.start()`.

### IPC layer (`src/main/ipc/`)

Handlers are thin adapters over services. Domains include auth, session, git, terminal, claude/codex, settings, dev, fs, audio/realtime/voice, extension, browser, ssh, memory, secure-keys, qmd, mcp, plugin, openclaw, analytics, and queue. Registration is centralized in `registerIPCHandlers()` inside `index.ts`.

### Services (`src/main/services/`)

Business logic lives in services; IPC files delegate to them. Grouped by concern:

| Domain | Representative modules |
|--------|-------------------------|
| Agent harnesses | `claude.service`, `codex.service`, `cursor.service`, `cursor-cli.service`, `gemini.service`, `opencode.service` |
| Auto Build routing | `auto-router.service`, `flue-meta-router.service`, `harness-policy.service`, `harness-capabilities.ts` |
| Sessions & workspaces | `session.service`, `docker.service`, `ssh.service`, `transcript.service`, `message-queue.service` |
| Browser & CDP | `browser.service`, `cdp-proxy.service`, `stagehand.service`, `computer-use.service`, `renderer-cdp.service` |
| Git & terminal | `git.service`, `terminal.service` |
| Settings & memory | `settings.service`, `memory.service`, `qmd.service`, `secure-keys.service` |
| MCP & extensions | `mcp.service`, `mcp-stdio-bridge.service`, `extension.service`, `plugin.service`, `gstack.service` |
| Voice & audio | `audio.service`, `realtime.service`, `elevenlabs-voice.service` |
| Auth & analytics | `auth.service`, `analytics.service`, `posthog.service` |
| Utilities | `auth`, `document`, `openclaw`, `power`, `wakeup` |

`browser.service` maps coding sessions to webview `webContentsId` values (via renderer `browser:register-webview`) and issues CDP commands through Electron’s debugger API. `cdp-proxy.service` exposes Playwright-compatible HTTP/WebSocket endpoints that bridge external tools (e.g. Stagehand) to those webviews.

Persistence uses `electron-store` JSON files under `userData`, with `CachedStore` debouncing large session writes. Store names include `claudette-settings`, `claudette-sessions`, `claudette-mcp-servers`, `claudette-memory`, `claudette-qmd`, and related files.

## Preload and renderer bridge

`preload.ts` builds a single `electronAPI` object (auth, sessions, terminal, git, claude, browser, settings, etc.) and exposes it with:

```typescript
contextBridge.exposeInMainWorld('electronAPI', electronAPI);
```

The renderer types this as `window.electronAPI` in `index.tsx`. Invoke handlers use `ipcRenderer.invoke`; streaming and events use `ipcRenderer.on` with unsubscribe helpers. No Node APIs are exposed to the renderer.

<Note>
Packaged builds intercept global shortcuts in the main process (`before-input-event`) and forward them to the renderer via `IPC_CHANNELS.APP_SHORTCUT_TRIGGERED`, so keyboard commands work even when focus is inconsistent.
</Note>

## Webpack and Electron Forge packaging

### Webpack entries

| Config | Entry / outputs | Notes |
|--------|-----------------|-------|
| `webpack.main.config.ts` | `./src/main/index.ts` | Externals: `node-pty`, `@anthropic-ai/claude-agent-sdk`, `@anthropic-ai/sdk`, `@cursor/sdk`, `docx` |
| `webpack.renderer.config.ts` | Forge entry `src/renderer/index.tsx` | PostCSS/Tailwind via shared rules |
| Forge `WebpackPlugin` | Preload: `./src/main/preload.ts` | Injected globals: `MAIN_WINDOW_WEBPACK_ENTRY`, `MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY` |

Shared `webpack.plugins.ts` adds `ForkTsCheckerWebpackPlugin`, `MonacoWebpackPlugin`, and optional `DefinePlugin` keys from `.env.production`.

`package.json` sets `"main": ".webpack/main"`. Production artifacts land under `./out/v{version}/` (see `forge.config.ts`).

### Packaged native dependencies

`forge.config.ts` `postPackage` copies webpack-externalized packages into `Build.app/Contents/Resources/node_modules` (including `monaco-editor` for the custom protocol). QMD platform bundles copy to `Resources/qmd`. Fuses disable Node-in-renderer patterns and enable ASAR integrity.

Dev server ports (Forge):

| Variable | Default in Forge | Dev script override |
|----------|------------------|---------------------|
| `DEV_WEBPACK_PORT` | `3000` | `9001` (`scripts/dev.sh`) |
| `DEV_WEBPACK_LOGGER_PORT` | `9000` | unchanged |

Use `./scripts/dev.sh` for development (not bare `npm run start`): it sets `DEV_INSTANCE_NAME`, runs `setup-qmd`, kills stale dev Electron processes, assigns `/tmp/grep-build-dev` userData, and syncs settings from production only when dev files are missing.

## Custom protocols

### `monaco-asset://`

Registered as a privileged scheme before `app.ready`. After ready, `protocol.handle('monaco-asset', …)` serves files from:

- **Dev** — `{projectRoot}/node_modules/...` (resolved from `.webpack/main` up two levels).
- **Packaged** — `{process.resourcesPath}/node_modules/...` (copied at package time).

The renderer configures Monaco in `monaco-config.ts`:

```typescript
loader.config({
  paths: { vs: 'monaco-asset://app/node_modules/monaco-editor/min/vs' },
});
```

CSP headers on the default session explicitly allow `monaco-asset:` for scripts, styles, workers, and connections.

### `grep://` (OAuth)

`protocol.registerHttpProtocol('grep', …)` handles `grep://oauth/callback` and forwards the authorization code to the renderer on `auth:oauth-callback`. `auth.service` uses redirect URI `grep://oauth/callback`.

## CDP ports and debugging surfaces

Build uses **two** CDP-related listeners:

| Surface | Purpose | Default port | Override |
|---------|---------|--------------|----------|
| Chromium `remote-debugging-port` | DevTools protocol on the Electron app (main + renderer debugging) | `9223` dev / `9222` production | `ELECTRON_CDP_PORT` |
| `cdp-proxy.service` | HTTP `/json/*` + WebSocket bridge for webview targets (Playwright / Stagehand) | `9223` (increments on `EADDRINUSE`) | `CDP_PROXY_PORT` |

The CDP proxy binds **localhost only** (`127.0.0.1`). Discovery endpoints include `/json/version` and browser WebSocket `ws://localhost:{port}/devtools/browser`. Webviews register with `browser.service`; the proxy attaches via `webContents.debugger` and forwards Target-domain events to connected clients.

Optional automation: set `GREP_RENDERER_CDP_SCRIPT` to run a module via `webContents.debugger` after the main window loads (`renderer-cdp.service`).

<Tip>
If Stagehand or Playwright cannot connect, check main logs for `[CDP Proxy]` port selection and confirm the browser panel registered the webview (`browser:register-webview`).
</Tip>

## userData: development vs production

| Mode | Path | How it is set |
|------|------|----------------|
| Production (macOS) | `~/Library/Application Support/Build` | Electron default for app name **Build** |
| Legacy names | `~/Library/Application Support/G-Build`, `Grep Build` | One-time migration into **Build** on first launch |
| Development | `/tmp/grep-build-dev` | `GREP_DEV_USER_DATA` from `scripts/dev.sh` |

<Warning>
Dev settings are copied from production only when missing in `/tmp/grep-build-dev`; dev never symlinks production files (avoids `CachedStore` flush corrupting production data).
</Warning>

Common files in `userData`:

| File | Role |
|------|------|
| `claudette-settings.json` | App settings, API keys, router config |
| `claudette-sessions.json` | Session records |
| `claudette-mcp-servers.json` | MCP server definitions |
| `claudette-memory.json`, `claudette-qmd.json` | Memory and QMD preferences |
| `main.log` | Main-process console tee (production only) |
| `sessions/` | Per-session paths used by `session.service` |

Dev opens DevTools automatically when `GREP_DEV_USER_DATA` is set. Production does not.

## Webview session partition

Browser preview uses `session.fromPartition('persist:browser')` with permissive handlers for preview OAuth. Webviews use `nodeIntegration: false`, `contextIsolation: false`, `webSecurity: false`, and `sandbox: false`. Ctrl+Tab in a webview is forwarded to the main renderer for session switching.

## Multi-window behavior

`createNewWindow()` spawns additional `BrowserWindow` instances sharing the same preload and webpack entry. `getMainWindow()` prefers the focused window; `broadcastToAll()` sends IPC events to every open window (renderer filters by `sessionId`).

## Related pages

<CardGroup>
  <Card title="IPC and preload bridge" href="/ipc-bridge">
    Typed channels, invoke vs push events, and secure preload exposure.
  </Card>
  <Card title="Browser preview and inspection" href="/browser-preview">
    Webview navigation, CDP attachment, DOM inspector, and Stagehand.
  </Card>
  <Card title="Installation" href="/installation">
    Dev vs production user data separation and prerequisites.
  </Card>
  <Card title="npm scripts reference" href="/npm-scripts-reference">
    When to use ./scripts/dev.sh vs npm run start.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    CDP timeouts, port conflicts, and main.log location.
  </Card>
</CardGroup>

---

## 05. Harnesses and models

> Supported harness identifiers (claude, codex, cursor, gemini, opencode, custom), model picker strings, permission modes per harness, and harness capability limits for injection and multi-turn.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/05-harnesses-and-models.md
- Generated: 2026-06-02T00:20:50.296Z

### Source Files

- `README.md`
- `src/shared/types/index.ts`
- `src/main/services/harness-capabilities.ts`
- `src/main/services/harness-policy.service.ts`
- `src/renderer/stores/session.store.ts`
- `src/main/services/codex.service.ts`
- `src/main/services/opencode.service.ts`

---
title: "Harnesses and models"
description: "Supported harness identifiers (claude, codex, cursor, gemini, opencode, custom), model picker strings, permission modes per harness, and harness capability limits for injection and multi-turn."
---

Build routes every chat turn through a **harness** (the CLI or SDK backend) selected by a **model picker string**. The shared `Harness` union, prefix-based model IDs, `claude:get-models` catalog, per-session permission modes, and `HarnessCapabilities` queue limits are the contracts that tie the renderer, `ClaudeService.streamMessage`, and the message queue together.

## Harness identifiers

The canonical harness type is exported from shared types:

```typescript
export type Harness = 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode' | 'custom';
```

| Harness | Backend | How Build selects it |
|---------|---------|----------------------|
| `claude` | Claude Agent SDK (`@anthropic-ai/claude-agent-sdk`) | Model ID has **no** `codex:`, `cursor:`, `gemini:`, `opencode:`, or `custom:` prefix (e.g. `claude-sonnet-4-6`) |
| `codex` | OpenAI Codex CLI (`codex` binary) | `codex:<model>` |
| `cursor` | Cursor Agent CLI and/or `@cursor/sdk` | `cursor:<model>` |
| `gemini` | Google Gemini CLI | `gemini:<model>` |
| `opencode` | OpenCode CLI (`opencode` or `npx opencode-ai`) | `opencode:<model>` |
| `custom` | Claude Agent SDK with proxy env overrides | `custom:<settings-id>` from `AppSettings.customModels` |

Resolution is prefix-based in both renderer and main process (`harnessFromModel`):

```typescript
if (model.startsWith('codex:')) return 'codex';
if (model.startsWith('cursor:')) return 'cursor';
if (model.startsWith('gemini:')) return 'gemini';
if (model.startsWith('opencode:')) return 'opencode';
if (model.startsWith('custom:')) return 'custom';
return 'claude';
```

<Note>
`custom` models still execute through the **Claude Agent SDK** path in `ClaudeService.streamMessage`, with `ANTHROPIC_BASE_URL`, `ANTHROPIC_API_KEY`, and `ANTHROPIC_DEFAULT_SONNET_MODEL` overridden from `CustomModelConfig`. The `custom` harness label applies to transcripts, analytics, and queue capability lookup—not a separate runtime.
</Note>

### Special picker value: `auto`

`auto` is the **Auto Build** entry. It is not a harness; routing resolves a concrete model and harness per turn via `AutoRouterService` / Flue meta-router. Resolved harness appears on `RoutingDecision.resolvedHarness` and on assistant messages when `model === 'auto'`.

## Model picker catalog

Models are loaded once per session via IPC `claude:get-models` → `ClaudeService.getAvailableModels()`. Each entry is `{ id, name, description }`.

### Prefix convention

| Prefix | Example `id` | Stripped backend model |
|--------|----------------|------------------------|
| (none) | `claude-sonnet-4-6` | Full string sent to SDK |
| `codex:` | `codex:gpt-5.4` | `gpt-5.4` |
| `cursor:` | `cursor:composer-2.5` | `composer-2.5` |
| `gemini:` | `gemini:gemini-2.5-pro` | `gemini-2.5-pro` |
| `opencode:` | `opencode:deepseek-v4-pro` | `deepseek/deepseek-v4-pro` if no `/` in suffix |
| `custom:` | `custom:kimi-k26` | Resolved to `CustomModelConfig.modelId` for API calls |

OpenCode resolution (`resolveOpenCodeModel`): bare suffix `deepseek-v4-pro` becomes `deepseek/deepseek-v4-pro`.

### Default catalog (when Foundry is disabled)

**Claude / Auto Build**

| `id` | Display name |
|------|----------------|
| `auto` | Auto Build |
| `claude-opus-4-8` | Opus 4.8 |
| `claude-opus-4-7` | Opus 4.7 |
| `claude-opus-4-6` | Opus 4.6 |
| `claude-opus-4-5-20251101` | Opus 4.5 |
| `claude-sonnet-4-6` | Sonnet 4.6 |
| `claude-sonnet-4-5-20250929` | Sonnet 4.5 |
| `claude-sonnet-4-20250514` | Sonnet 4 |
| `claude-haiku-4-5-20251001` | Haiku 4.5 |

**Codex** (always listed)

| `id` |
|------|
| `codex:gpt-5.5` |
| `codex:gpt-5.4` |
| `codex:gpt-5.4-mini` |
| `codex:gpt-5.3-codex` |
| `codex:o3` |

**Cursor** — dynamic list when `cursorApiKey` is set and `@cursor/sdk` `models.list` succeeds; otherwise hardcoded:

| `id` |
|------|
| `cursor:composer-2.5` |
| `cursor:claude-3.5-sonnet` |
| `cursor:gpt-4o` |
| `cursor:gemini-3.5-flash` |
| `cursor:o3` |

**OpenCode (DeepSeek)** — appended only when `deepseekApiKey` is set:

| `id` |
|------|
| `opencode:deepseek-v4-pro` |
| `opencode:deepseek-v4-flash` |
| `opencode:deepseek-reasoner` |

**Gemini** — always listed (CLI uses `GEMINI_API_KEY` / settings):

| `id` |
|------|
| `gemini:gemini-3.5-flash` |
| `gemini:gemini-2.5-pro` |
| `gemini:gemini-2.5-flash` |

**Custom** — one row per `settings.customModels[]` entry: `custom:${id}`.

### Foundry override

When `foundryEnabled` is true and Foundry default model strings are configured, `getAvailableModels()` returns **only** those Foundry IDs (no prefixed harness models in that list).

### Auto Build tier defaults (settings UI)

Default tier bindings in settings (overridable via `autoRouterConfig`):

| Tier | Default model |
|------|----------------|
| plan | `claude-sonnet-4-6` |
| build | `codex:gpt-5.5` |
| verify | `codex:gpt-5.5` |
| refine | `cursor:composer-2.5` |
| fallback | `claude-sonnet-4-6` |

## Routing flow

`ClaudeService.streamMessage` is the single dispatch point. Non-Claude prefixes branch to dedicated services; everything else uses the Claude Agent SDK (including `custom:*` and bare Claude IDs).

```mermaid
flowchart TB
  subgraph UI["Renderer"]
    Picker["Model picker id"]
    Store["session.store selectedModel"]
  end
  subgraph IPC["Main IPC"]
    Send["claude:send-message"]
  end
  subgraph Dispatch["ClaudeService.streamMessage"]
    Auto{"id === auto?"}
    Codex{"codex:*"}
    Cursor{"cursor:*"}
    Gemini{"gemini:*"}
    OC{"opencode:*"}
    SDK["Claude Agent SDK\n(claude + custom)"]
  end
  Picker --> Store --> Send --> Auto
  Auto -->|router| Codex & Cursor & Gemini & OC & SDK
  Codex --> CodexSvc["codex.service"]
  Cursor --> CursorSvc["cursor-cli / cursor.service"]
  Gemini --> GeminiSvc["gemini.service"]
  OC --> OpenCodeSvc["opencode.service"]
```

Cross-harness context: when switching harnesses, Build injects unified transcript context via `buildUnifiedContextForHarness` (and Cursor may start a fresh CLI chat if the last assistant harness was not `cursor`).

Auto Build delegate stages (`isExecutableDelegateHarness`): `codex`, `cursor`, `gemini`, `opencode` only—not `claude` or `custom`.

## Permission modes

Build-wide permission mode type (renderer `session.store`):

```typescript
export type PermissionMode =
  | 'auto' | 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'dontAsk';
```

Default persisted mode: `bypassPermissions`.

### Modes offered in the UI

| Model family | Modes in picker |
|--------------|-----------------|
| Claude, Cursor, Gemini, OpenCode, `custom`, `auto` | `auto`, `acceptEdits`, `default`, `bypassPermissions`, `plan`, `dontAsk` |
| Codex (`codex:*`) | `auto`, `acceptEdits`, `bypassPermissions`, `plan` only |

`normalizePermissionModeForModel` clamps unsupported modes to the nearest allowed value for the active model.

Auto Build mutating stages (`canRunMutatingStages`): blocked when permission is `plan` or `dontAsk`.

### Per-harness translation

`translateHarnessPolicy` maps Build permission modes and `MetaHarnessPolicy` into harness-specific execution settings.

**Codex** (`getExecutionMode`):

| Build mode | Codex CLI behavior |
|------------|-------------------|
| `plan` | Read-only sandbox; plan-only preamble; `approval_policy=never` |
| `acceptEdits` | `workspace-write`; approvals never |
| `default` | `workspace-write`; `approval_policy=on-request` |
| `dontAsk` | Read-only sandbox |
| `bypassPermissions` (default) | `--dangerously-bypass-approvals-and-sandbox` |

**Cursor** (policy translation):

| Build mode | Cursor SDK/CLI |
|------------|----------------|
| `plan`, `dontAsk` | `mode: 'plan'`, `sandbox: 'enabled'` |
| `dontAsk` | `mode: 'ask'` |
| Others | No mode override from permission |

**Gemini** (CLI `--approval-mode`):

| Build mode | Gemini `approvalMode` |
|------------|----------------------|
| `plan`, `dontAsk` | `plan` |
| `acceptEdits` | `auto_edit` |
| `default` | `default` |
| `bypassPermissions` | `yolo` (CLI default when no policy) |

**OpenCode** (`buildPermissionConfig` JSON):

| Build mode | Permission JSON |
|------------|-----------------|
| `plan`, `dontAsk` | `edit` and `external_directory` denied |
| Other | Permissive `*` allow; `question: deny` |

**Claude SDK** uses permission mode directly on the Agent SDK query (plus effort/thinking from policy).

### Effort / thinking (Claude and Auto Build policy)

Renderer **effort** levels: `low` | `medium` | `high` | `xhigh` | `max` (legacy `ThinkingMode` values `off` / `thinking` / `ultrathink` migrate via `migrateThinkingMode`).

`HarnessPolicyTranslation` for `claude` maps meta effort to SDK `effort`, adaptive thinking (Opus/Sonnet 4.6+), or `maxThinkingTokens`, and `fastMode` when `speed === 'fast'`.

Codex maps effort to `model_reasoning_effort`: `minimal` | `low` | `medium` | `high` | `xhigh`.

## Harness capabilities (queue and injection)

`HarnessCapabilities` (shared `message-queue` types):

| Field | Meaning |
|-------|---------|
| `supportsAsyncInjection` | Mid-stream inject via `claude:inject-message` / SDK `streamInput` |
| `supportsMultiTurn` | Harness maintains conversation state across queued turns without full context replay |
| `minTurnGapMs` | Delay after stream end before dequeuing next message |
| `maxCoalesceWindowMs` | Declared coalesce window (3000 ms for all harnesses; drain scheduling uses `minTurnGapMs` today) |

Static table in `harness-capabilities.ts`:

| Harness | Async injection | Multi-turn | `minTurnGapMs` |
|---------|-----------------|------------|----------------|
| `claude` | yes | yes | 0 |
| `cursor` | no | yes | 500 |
| `codex` | no | no | 500 |
| `gemini` | no | no | 500 |
| `opencode` | no | no | 500 |
| `custom` | no* | no* | 500 |

\*Capability row is `custom`; runtime still uses Claude SDK. Renderer treats `custom:*` like Claude for injection (`isNonClaudeHarness` is false), so **queued messages during an active `custom` stream can use `injectMessage`** when a Claude query is active.

### Queue behavior

1. `messageQueueService.enqueue` — if not streaming, schedules immediate drain.
2. `onStreamStart(sessionId, harness)` — marks streaming; records active harness.
3. `onStreamEnd` — schedules drain after `getHarnessCapabilities(harness).minTurnGapMs`.
4. `drain-ready` → renderer sends next queued message through normal `sendMessage`.

**Injection path (Claude only):** After a tool completes, if the queue has messages and the active stream is **not** a non-Claude harness (`codex:`, `cursor:`, `gemini:`, `opencode:`), the store calls `claude.injectMessage`. Non-Claude streams wait until `stream end`, then send the next message as a **new turn** (often with rebuilt cross-harness context).

### Multi-turn semantics by harness

| Harness | Multi-turn mechanism |
|---------|---------------------|
| `claude` | SDK session resume (`sdkSessionId`); Auto Build may skip resume if last harness ≠ `claude` |
| `cursor` | CLI `chatId` resume when last assistant harness was `cursor`; else new chat + context blob; local SDK agent map when API key and no chatId |
| `codex`, `gemini`, `opencode` | Effectively single-turn CLI invocations; prior turns rehydrated via `buildUnifiedContextForHarness` |
| `custom` | Same as Claude SDK session semantics with proxy env |

## Persistence and messages

- Per-session `model` and `permissionMode` persist on `sessions.update` and `claude.setPermissionMode`.
- `ChatMessage.harness` tags which backend produced assistant output.
- Preferred compaction fallbacks: Claude models list vs Codex list in `session.store` depending on source model prefix.

## SSH remote harness availability

`RemoteCliCapabilities` tracks which CLIs are installed on the remote host: `claude`, `codex`, `cursor`, `gemini`, `opencode`. SSH resume candidates are typed for `backend: 'claude'` only; other harnesses run over SSH when the remote binary exists and the session routes to that prefix.

## Verification checklist

<Steps>
<Step title="Load model list">
Open a session and confirm the picker shows expected groups (Claude, Codex, Cursor, Gemini, OpenCode, Custom) after keys are configured.
</Step>
<Step title="Confirm harness routing">
Select `codex:gpt-5.4` and send a message; stream should log Codex routing and assistant messages should carry `harness: 'codex'`.
</Step>
<Step title="Permission clamping">
Switch to a Codex model and cycle permission modes; `default` and `dontAsk` should not appear.
</Step>
<Step title="Queue vs injection">
With Claude selected, queue a second message while streaming; after a tool completes, injection should run. Repeat with `gemini:*`; message should send only after stream ends.
</Step>
</Steps>

## Related pages

<CardGroup>
<Card title="Configure API keys and providers" href="/configure-providers">
Keys and CLI detection that gate Codex, Cursor, Gemini, OpenCode, and custom proxy models in the picker.
</Card>
<Card title="Auto Build routing" href="/auto-build-routing">
How `auto` resolves tier, harness, and orchestration plans across the same model strings.
</Card>
<Card title="Shared types reference" href="/shared-types-reference">
`Harness`, `HarnessCapabilities`, `RoutingDecision`, and `CustomModelConfig` field definitions.
</Card>
<Card title="Manage coding sessions" href="/manage-sessions">
Session model persistence, permission dialogs, and message queue UX.
</Card>
</CardGroup>

---

## 06. Sessions and workspaces

> Session schema, lifecycle statuses, Docker vs local dev vs SSH vs OpenClaw, worktrees, forks, transcript resumption, and electron-store persistence (claudette-sessions).

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/06-sessions-and-workspaces.md
- Generated: 2026-06-02T00:20:55.788Z

### Source Files

- `src/shared/types/index.ts`
- `src/main/services/session.service.ts`
- `src/main/services/docker.service.ts`
- `src/main/services/transcript.service.ts`
- `src/main/ipc/session.ipc.ts`
- `src/renderer/components/session/SessionList.tsx`
- `src/renderer/stores/session.store.ts`

---
title: "Sessions and workspaces"
description: "Session schema, lifecycle statuses, Docker vs local dev vs SSH vs OpenClaw, worktrees, forks, transcript resumption, and electron-store persistence (claudette-sessions)."
---

Build models every coding conversation as a `Session` record in the main process, keyed by UUID and persisted under `claudette-sessions` via `CachedStore`. The active workspace path (`worktreePath`), optional remote or gateway configuration, lifecycle `status`, and transcript identity (`sdkSessionId` / `sdkSessionMappings`) determine where harnesses run, which files agents see, and how chat history resumes after restart.

## Session schema

The canonical type lives in `src/shared/types/index.ts`. A session always has filesystem anchors and ports; mode-specific fields are optional.

| Field | Role |
| --- | --- |
| `id` | Build-local UUID (or transcript-derived id for discovered sessions) |
| `name` | Display name; may be overridden by `sessionNames.{id}` in the store |
| `repoPath` | Primary repo path (local disk or SSH remote path string) |
| `worktreePath` | Directory harnesses and terminals use as `cwd` |
| `branch` | Git branch label (best-effort) |
| `status` | Lifecycle state (see below) |
| `ports` | `PortAllocation`: `web`, `api`, `debug` |
| `isDevMode` | `true` for local folder, SSH, OpenClaw, teleport, and discovered sessions (no Docker container lifecycle on start/stop) |
| `sshConfig` | Present for SSH remote workspaces |
| `openclawConfig` | Present for OpenClaw Gateway sessions (`gatewayUrl`, `gatewayPassword`) |
| `containerId` | Optional Docker container when a GitHub-clone session still has container infrastructure |
| `sdkSessionId` | Claude Agent SDK / Claude Code transcript id for resume |
| `relatedSessionIds` | Alternate ids mapping to the same conversation |
| `continuedFromSessionId` | Local session resumed when creating SSH from a local transcript |
| `isWorktree` / `parentRepoPath` / `forkName` | Git worktree fork metadata |
| `parentSessionId` / `childSessionIds` / `forkPoint` / `aiGeneratedName` | Conversation fork graph (separate from git worktrees) |
| `isTeleported` | Imported from claude.ai via CLI teleport |
| `worktreeInstructions` / `worktreeInstructionsSent` | `.claudette/worktree-setup.md` content injected on first turn |
| `setupOutput` / `errorMessage` | SSH setup logs or failure text |
| `isStarred` / `starredAt` / `tabHidden` | Sidebar ordering and visibility |

<ParamField body="openclawConfig.gatewayUrl" type="string" required>
Base URL of the OpenClaw gateway (for example `http://localhost:18789`).
</ParamField>

<ParamField body="sshConfig.remoteWorkdir" type="string" required>
Remote working directory; also used as initial `worktreePath` until setup scripts rewrite it.
</ParamField>

## Lifecycle statuses

```typescript
export type SessionStatus =
  'creating' | 'starting' | 'setup' | 'running' | 'stopping' | 'stopped' | 'error';
```

```mermaid
stateDiagram-v2
  [*] --> creating: GitHub clone / SSH create
  creating --> stopped: Clone finished (GitHub)
  creating --> running: Local dev / OpenClaw / teleport
  creating --> setup: SSH async setup
  setup --> running: Remote connect OK
  setup --> error: SSH setup failed
  stopped --> running: startSession()
  running --> stopped: stopSession()
  creating --> error: Clone or setup failure
  error --> [*]
```

<Note>
`startSession` and `stopSession` today only flip `status` between `running` and `stopped`; they do not start Docker containers. SSH creation uses `creating` → async connect/setup → `running` or `error`, with `SetupProgressEvent` pushed on `SSH_SETUP_PROGRESS`.
</Note>

Local dev sessions created through `DEV_CREATE_SESSION` are stored immediately as `running`. GitHub `SESSION_CREATE` ends in `stopped` after clone; the user starts explicitly.

## Workspace modes

Build supports four distinct workspace backends. All share the same `Session` shape; presence of flags/config selects behavior in services and UI.

```text
                    ┌─────────────────────────────────────┐
                    │         SessionService + IPC        │
                    │   sessions.{id}  sdkSessionMappings │
                    └─────────────────────────────────────┘
           ┌────────┴────────┬────────────┬───────────────┴────────┐
           ▼                 ▼            ▼                        ▼
    Local dev (folder)   GitHub clone   SSH remote            OpenClaw Gateway
    isDevMode: true      ports + clone  sshConfig             openclawConfig
    worktree optional    userData/      isDevMode: true       repoPath: ""
    ~/.claudette/        sessions/      remote workdir        isDevMode: true
    worktrees/           + Docker*      async setup
```

### Local dev (open project folder)

Entry: **New session → Local folder** → `IPC_CHANNELS.DEV_CREATE_SESSION` in `dev.ipc.ts`.

- `repoPath` and default `worktreePath` are the chosen folder.
- `isDevMode: true`, `status: 'running'`, fixed placeholder ports (`3000` / `8080` / `9229`).
- Optional **git worktree fork**: when `createWorktree` is set, Build resolves the main repo, runs `git worktree add` into `~/.claudette/worktrees/{repo-hash}/wt-{id-prefix}`, sets `isWorktree`, `parentRepoPath`, and a memorable `forkName`.
- Project hooks: `.claudette/worktree-setup.sh` (executed in the worktree) or `.claudette/worktree-setup.md` (stored on the session and sent as the first agent message when not yet sent).

Discovered sessions (see below) also set `isDevMode: true` when scanned from Claude Code transcripts under `~/.claude/projects` (or `CLAUDE_PROJECTS_DIR`).

### Docker / GitHub clone

Entry: **New session → GitHub** → `SESSION_CREATE` → `SessionService.createSession`.

- Clones into `{userData}/sessions/{sessionId}/{repoName}`.
- Allocates host ports via `DockerService.allocatePorts` (`BASE_PORT` 10000, 10 ports per session).
- Writes `.grep/setup.sh` from `setupScript` (default installs npm/pip deps).
- Ends at `stopped` (or `error` with `errorMessage`).
- `deleteSession` still stops/removes `containerId` and releases ports if present.

`DockerService` can build images, bind `worktreePath` to `/workspace`, and expose 3000/8000/9229 — used when `containerId` exists (terminals use `docker exec`). Current `startSession` does **not** call `startContainer`; container lifecycle is legacy/cleanup-oriented unless another path sets `containerId`.

### SSH remote

Entry: **New session → SSH** → `SSH_CREATE_SESSION` in `ssh.ipc.ts`.

- Session persisted immediately with `status: 'creating'`, `isDevMode: true`, zero ports.
- `sdkSessionMappings.{id}` is `resumeSessionId`, `'new'` (fresh remote), or mapped on resume.
- Optional `continuedFromSessionId` when resuming from a matching local session.
- Background: `sshService.connect`, optional `worktreeScript` / `syncSettings` pre-setup, then `running` or `error` with `setupOutput`.
- `worktreePath` / `repoPath` may be rewritten to the directory returned by remote setup.
- `SESSION_SCAN_REMOTE` lists orphan remote transcripts and materializes hidden child sessions (`tabHidden: true`) linked via `parentSessionId`.

Resume candidates: `SSH_LIST_RESUME_CANDIDATES` probes `~/.claude/projects/...` on the remote workdir.

### OpenClaw Gateway

Entry: **New session → OpenClaw** → `OPENCLAW_CREATE_SESSION` in `openclaw.ipc.ts`.

- Empty `repoPath` / `worktreePath`, `openclawConfig` required, `isDevMode: true`, `status: 'running'`.
- `sdkSessionMappings.{id}` = `'new'` so message load does not pull unrelated transcripts.
- `claude.service` routes streaming to `openclawService.streamAsChat` when `session.openclawConfig` is set (BYOC: any gateway URL and bearer token you configure).

## Git worktrees vs conversation forks

These are **different** concepts in the schema.

| Concept | Fields | Mechanism |
| --- | --- | --- |
| **Git worktree fork** | `isWorktree`, `parentRepoPath`, `forkName` | `git worktree add` under `~/.claudette/worktrees/`; sidebar nests under parent repo in `SessionList` |
| **Conversation fork** | `parentSessionId`, `childSessionIds`, `forkPoint`, `aiGeneratedName` | SDK `forkSession()` locally; SSH uses `resume` + `forkSession: true` on first query |
| **Rewind fork** | New `id`, truncated transcript | `SESSION_REWIND_FORK` copies Claude JSONL + `transcriptService.cloneTranscript`; clears `sdkSessionId` on the fork to avoid resume conflicts |

`SESSION_CREATE_FORK` and `SESSION_GET_FORK_GROUP` implement the conversation fork graph. `ForkTabs` in the renderer shows tabs; the sidebar hides `parentSessionId` forks unless that fork is the active session.

## Transcript resumption

Build keeps **three** related stores:

1. **Claude Code / SDK JSONL** — `~/.claude/projects/{encoded-path}/{sessionId}.jsonl` (override root with `CLAUDE_PROJECTS_DIR`).
2. **Build canonical transcript** — `~/.build/transcripts/{sessionId}.jsonl` via `TranscriptService` (all harnesses, append-only JSONL).
3. **Session store mappings** — `sdkSessionMappings` in `claudette-sessions`: local Build id → SDK transcript id.

On list/load, `SessionService.getMergedSessions()` merges **stored** sessions (`sessions.{id}`) with **discovered** cache (`discoveredSessions` + scan), attaches `sdkSessionId` from mappings, and deduplicates ids through `resolveBuildSessionIdForDiscoveredSession`.

Resume behavior in `claude.service`:

- Resolves SDK id from `session.sdkSessionId` or `sdkSessionMappings`.
- Sentinel `'new'` means no resume until the first `system_id` stores the real id.
- Auto Build (`model === 'auto'`) may skip SDK resume when the last harness was not Claude.
- SSH forks may keep `forkFromSdkSessionId` until the first query forks server-side.

Teleport (`DEV_CREATE_TELEPORT_SESSION`) runs `claude --teleport` for web `session_*` ids, then stores the session with `isTeleported: true` and mapping to the remote/local transcript id.

## `claudette-sessions` persistence

| Store key | Contents |
| --- | --- |
| `sessions.{sessionId}` | Full `Session` objects (requires `name` and `repoPath` to appear in `getSessions`) |
| `sdkSessionMappings.{localId}` | SDK/transcript id, or `'new'` |
| `sessionNames.{sessionId}` | AI-generated display names (Haiku) |
| `discoveredSessions` | Cached discovery snapshot for fast startup |
| `lastDiscoveryTimestamp` | Cache freshness |

Implementation: `CachedStore` wraps `electron-store` with in-memory cache and debounced disk writes (2s) to avoid blocking the main process on large session files.

**Dev vs production separation** is by **userData directory**, not store file name: `./scripts/dev.sh` sets `GREP_DEV_USER_DATA=/tmp/grep-build-dev`, and `src/main/index.ts` calls `app.setPath('userData', …)` so the same `claudette-sessions` filename lives in an isolated folder. Production uses `~/Library/Application Support/Build` (with migration from legacy `Grep Build` / `G-Build` dirs).

Startup maintenance:

- **Prune**: sessions older than 14 days (non-starred) removed from `sessions.*`.
- **Worktree migration**: paths under `.claudette/worktrees/` backfill `isWorktree` / `parentRepoPath`.

Active session id for the renderer is stored separately in `claudette-settings` via `dev.getActiveSession` / `dev.setActiveSession`.

## Renderer integration

`session.store.ts` loads `window.electronAPI.sessions.list()`, restores active session from dev settings, auto-selects the newest `running` session if none active, and calls `loadMessages` for the active id. `withMaterializedSession` re-invokes `sessions.update(sessionId, {})` when IPC returns "session not found" so discovered ids can be promoted to stored records.

`SessionList.tsx` groups sessions into:

- **SSH hosts** (`sshConfig.host`),
- **Local projects** by `worktreePath` (with nested **worktree forks** under `parentRepoPath`),
- **Recent** non-fork sessions,
- **Starred** reorderable list.

Conversation forks are omitted from the tree unless active (so fork tabs carry navigation).

## IPC surface (session domain)

| Channel | Behavior |
| --- | --- |
| `SESSION_CREATE` | GitHub clone session |
| `SESSION_START` / `SESSION_STOP` | Status toggle |
| `SESSION_DELETE` | Remove store, optional Docker cleanup |
| `SESSION_LIST` / `SESSION_GET` / `SESSION_UPDATE` | CRUD + merge |
| `SESSION_REWIND_FORK` | Truncated transcript fork |
| `SESSION_CREATE_FORK` / `SESSION_GET_FORK_GROUP` | Conversation forks |
| `SESSION_SCAN_REMOTE` | SSH orphan transcript import |
| `DEV_CREATE_SESSION` | Local / worktree session |
| `DEV_CREATE_TELEPORT_SESSION` | claude.ai import |
| `SSH_CREATE_SESSION` | Remote workspace |
| `OPENCLAW_CREATE_SESSION` | Gateway workspace |

Events: `SESSION_STATUS_CHANGED`, `SESSION_LIST_UPDATED` broadcast to all windows.

## Verification signals

| Check | Expected |
| --- | --- |
| New local session | `sessions.{uuid}` in dev userData; `isDevMode: true`; `status: 'running'` |
| SSH connect | Setup progress in chat; `status: 'running'`; `worktreePath` matches remote cwd |
| Resume | `sdkSessionMappings` points to existing JSONL; next message continues transcript |
| Worktree fork | Path under `~/.claudette/worktrees/{hash}/wt-*`; `isWorktree: true` |
| List after restart | `discoveredSessions` + merge shows Claude Code sessions without re-scan blocking UI |

<Warning>
Updating fields on a **discovered-only** session (no `sessions.{id}` row) is ignored by `updateSession` except via materialization. Starred SSH sessions should be stored rows so metadata survives merge.
</Warning>

## Related pages

<CardGroup>
  <Card title="Manage coding sessions" href="/manage-sessions">
    Create, start, stop, fork, rewind, teleport, and permission flows from the UI.
  </Card>
  <Card title="SSH remote sessions" href="/ssh-remote-sessions">
    SSHConfig, resume candidates, download/teleport, and bridge recovery.
  </Card>
  <Card title="Git workflows" href="/git-workflows">
    Per-session worktree git operations and GitExplorer bindings.
  </Card>
  <Card title="Shared types reference" href="/shared-types-reference">
    Full `Session`, `SSHConfig`, `OpenClawConfig`, and message-queue types.
  </Card>
  <Card title="IPC channels reference" href="/ipc-channels-reference">
    Complete `SESSION_*`, `DEV_*`, `SSH_*`, and `OPENCLAW_*` channel catalog.
  </Card>
</CardGroup>

---

## 07. Auto Build routing

> Plan/build/verify/refine tiers, heuristic vs Flue controller routing, orchestration plans, mission control policies, failure cooldowns, and CLAUDE_AUTO_ROUTE_DECISION events.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/07-auto-build-routing.md
- Generated: 2026-06-02T00:20:24.933Z

### Source Files

- `src/main/services/auto-router.service.ts`
- `src/main/services/flue-meta-router.service.ts`
- `src/shared/types/index.ts`
- `src/renderer/components/chat/AutoRouteBadge.tsx`
- `src/shared/constants/channels.ts`
- `scripts/verify-auto-router-plan-mode.ts`

---
title: "Auto Build routing"
description: "Plan/build/verify/refine tiers, heuristic vs Flue controller routing, orchestration plans, mission control policies, failure cooldowns, and CLAUDE_AUTO_ROUTE_DECISION events."
---

When the session model is `auto`, `claude.service` calls `autoRouterService.classifyAndRoute()` in the main process before streaming. That returns a `RoutingDecision` (tier, harness, model, orchestration plan, mission-control policy) and pushes it to the renderer on `claude:auto-route-decision` (`IPC_CHANNELS.CLAUDE_AUTO_ROUTE_DECISION`). Routing combines a deterministic heuristic classifier, an optional Cerebras-backed Flue meta-controller, per-session failure cooldowns, and multi-stage orchestration executed by `claude.service`.

## Routing pipeline

```mermaid
sequenceDiagram
  participant UI as Renderer (InputArea)
  participant CS as claude.service
  participant AR as autoRouterService
  participant FM as flueMetaRouterService
  participant Exec as Harness executors

  UI->>CS: sendMessage (model = auto)
  CS->>AR: classifyAndRoute(sessionId, message, options)
  AR->>AR: classifyHeuristic + workflow + permission
  opt Cerebras key present
    AR->>FM: route (structured JSON)
    FM-->>AR: FlueMetaRouteDecision or null
  end
  AR->>AR: chooseModel + buildOrchestrationPlan
  AR-->>CS: RoutingDecision
  CS->>UI: CLAUDE_AUTO_ROUTE_DECISION
  CS->>Exec: stream lead harness + optional delegate stages
```

<Steps>
<Step title="Classify intent">
Heuristic scoring assigns `requestedTier` from keywords, GStack mode, permission mode, attachments, and session phase.
</Step>
<Step title="Apply workflow rules">
`applyWorkflowAwareness` may shift tier (for example plan-before-build for complex work, build-after-plan for “go ahead”).
</Step>
<Step title="Run meta-controller (optional)">
If a Cerebras API key exists and confidence ≥ 0.55, Flue may override tier, model, and delegate stages (`method: 'controller'`).
</Step>
<Step title="Resolve execution">
Pick lead model/harness, merge `MetaHarnessPolicy`, build `OrchestrationPlan`, emit IPC event, stream lead then delegate stages.
</Step>
</Steps>

<ParamField body="RouteOptions" type="object">
Optional inputs to `classifyAndRoute`: `gstackMode`, `permissionMode`, `attachmentCount`, `attachmentTypes`, `recentMessages`, `isSSH`, `remoteCliCapabilities`, `goalObjective`, `goalSource`, `skipMetaController`.
</ParamField>

## Plan, build, verify, and refine tiers

| Tier | Typical intent | Default model (`DEFAULT_CONFIG`) |
|------|----------------|----------------------------------|
| `plan` | Design, risk, architecture, reviews before edits | `claude-sonnet-4-6` |
| `build` | Implementation, wiring, feature work | `codex:gpt-5.5` |
| `verify` | Tests, QA, debugging, root-cause investigation | `codex:gpt-5.5` |
| `refine` | Small UI/copy/docs tweaks | `cursor:composer-2.5` |

`TaskDomain` further scopes routing: `copy`, `frontend`, `backend`, `fullstack`, `debug`, `ops`, `docs`, `data`, `general`. Domain and attachment signals influence model candidates (for example frontend → Cursor Composer, backend → Codex).

Heuristic tiers use scored keyword lists (`PLAN_SIGNALS`, `BUILD_SIGNALS`, `VERIFY_SIGNALS`, `REFINE_SIGNALS`) plus special cases: capability-escalation phrases force `plan`, GStack modes (`plan-ceo-review`, `qa`, `investigate`, …) override, and `permissionMode === 'plan'` forces planning. `enforcePermissionMode` downgrades `build`/`refine` to `plan` when the session cannot mutate (`plan` or `dontAsk` permission modes).

## Heuristic classifier (fallback path)

The heuristic path always runs first. Its output is stored as `deterministicResult` and used when:

- No `cerebrasApiKey` / `EMBEDDED_KEYS.cerebras` / `CEREBRAS_API_KEY` is available
- Flue returns `null` (runtime failure, timeout, unsupported Node, auth cooldown)
- Controller confidence is below `META_MIN_CONFIDENCE` (0.55)
- `getMetaRouteRejectionReason` rejects a contradictory controller route (for example prompt-injection patterns forcing a different tier)

<Note>
`shouldUseDeterministicFastPath` is intentionally disabled: every request attempts the meta-controller when a key exists; heuristics are not skipped on “easy” messages.
</Note>

`RoutingDecision.method` is `'heuristic'` unless a valid controller decision is accepted, then `'controller'`. There is no separate `'llm'` method; legacy direct LLM classifiers were removed.

## Flue meta-controller

`flueMetaRouterService` loads `@flue/runtime` (optionally via `FLUE_RUNTIME_NODE_MODULES`), configures the Cerebras provider, and prompts `cerebras/gpt-oss-120b` for a single structured routing JSON object. The sandbox denies file I/O and shell execution so the controller only decides routing.

| Constant | Value | Role |
|----------|-------|------|
| `FLUE_TIMEOUT_MS` | `9000` (override with `FLUE_META_ROUTER_TIMEOUT_MS`) | Abort slow controller calls |
| `FLUE_FAILURE_COOLDOWN_MS` | `60000` | Back off after generic errors |
| `FLUE_AUTH_FAILURE_COOLDOWN_MS` | `600000` | Back off after 401/invalid API key |
| `FLUE_ROUTE_CACHE_TTL_MS` | `15000` | Dedupe identical prompts per session |

<Warning>
Flue requires Node **≥ 22.18**. Older Node versions skip the controller entirely.
</Warning>

Controller output fields map into `RoutingDecision`: `requestedTier`, `leadTier`, `leadModel`, optional `matchedCategoryId`, `confidence`, `reason`, and `stages[]` with triggers. Reasons are redacted for user-visible text (internal terms like “Flue meta-harness” become “workflow routing”). `sanitizeMetaLead` prefers the first configured candidate per tier unless the user asked for capability escalation.

## Orchestration plans

`OrchestrationPlan` describes how one user turn may span multiple harnesses:

<ResponseField name="mode" type="'single' | 'lead-with-delegates' | 'sequential'">
`single` when only the lead stage runs; `lead-with-delegates` when multiple harnesses participate; `sequential` when multiple stages share one harness.
</ResponseField>

<ResponseField name="stages" type="OrchestrationStage[]">
Each stage has `tier`, `harness`, `model`, `purpose`, `required`, `trigger`, optional `fallbackModels`, and per-stage `MetaHarnessPolicy` fields.
</ResponseField>

<ResponseField name="contextPolicy" type="object">
Controls handoff payload size: transcript references, plan file reference, `maxHandoffConversationChars: 24000`, project instructions, skills, agents, memories.
</ResponseField>

<ResponseField name="handoffPrompt" type="string">
Injected coordination text for the lead harness; tells the agent to avoid mentioning internal sequencing unless asked.
</ResponseField>

### Stage triggers

| Trigger | When used |
|---------|-----------|
| `now` | Lead stage (always first) |
| `after-plan` | Build or verify after planning completes |
| `after-build` | Verification after implementation |
| `on-failure` | Optional refine delegate when lead output mentions errors |
| `manual-follow-up` | Reserved; not auto-run |

Deterministic `buildOrchestrationPlan()` adds delegates when, for example, the user requested `build` but lead is `plan` and a Codex/Cursor/Gemini/OpenCode delegate is available. Controller plans merge Flue `stages` with required deterministic follow-ups so verification is not dropped.

After the lead stream finishes, `streamAutoBuildStages()` runs delegate stages (Codex, Cursor, Gemini, OpenCode only—not Claude/custom as helpers). Failures call `recordHarnessFailure`; successes call `recordHarnessSuccess` and `recordTierCompletion`.

## Mission control policies

`RoutingDecision.missionControl` is a `MetaMissionControlPolicy`:

- `controllerHarness: 'meta'` — routing brain is Flue, not the executing harness
- `requestedTier` — raw user intent tier from classifier/controller
- `leadTier` / `leadHarness` / `leadModel` — what runs first this turn
- Optional `categoryId` / `categoryLabel` when a custom settings category matched

Per-tier and per-category **MetaHarnessPolicy** fields ride on the decision and lead stage:

| Field | Values | Effect |
|-------|--------|--------|
| `effort` | string (e.g. thinking level) | Passed to Claude lead as `thinkingMode` when set |
| `speed` | `auto`, `standard`, `fast` | Harness speed hint |
| `workflow` | `auto`, `single`, `lead-with-delegates`, `sequential`, `dynamic` | Exposed on decision; orchestration `mode` may differ |
| `budgetUsd` | number | Budget cap hint |
| `verification` | `auto`, `none`, `optional`, `required` | Whether verify delegates are added/required |

Plan-tier routes also force `autoBuildLeadPermissionMode = 'plan'` in `claude.service` so the lead harness cannot mutate files until the user approves execution.

## Failure cooldowns

Per-session maps track helper failures:

- **Harness cooldown** (`sessionHarnessFailures`): codex, cursor, gemini, opencode — base 10 minutes, scales with repeat failures up to 60 minutes
- **Model cooldown** (`sessionModelFailures`): custom harness models and explicit model strings
- **Claude** harness failures are not cooled down (always available as fallback)

Cooldowns are restored from recent assistant messages (`Lead error:`, `Auto Build helper could not complete`) and from `message.metadata.workflowFailures`. `formatActiveHarnessCooldowns` appends avoidance text to the routing `reason`. Successful runs clear cooldowns; Cursor auth failures clear when `cursor-agent status` shows logged-in again.

Flue service failures use separate global cooldowns (60s / 10m auth) independent of per-harness session cooldowns.

## `CLAUDE_AUTO_ROUTE_DECISION` event

| Property | Value |
|----------|-------|
| Channel | `IPC_CHANNELS.CLAUDE_AUTO_ROUTE_DECISION` → `'claude:auto-route-decision'` |
| Direction | Main → renderer (push event, not invoke) |
| Payload | `{ sessionId: string; decision: RoutingDecision }` |

Emitted immediately after routing resolves, before the lead stream yields `system` info with `resolvedModel`.

**Preload:** `window.electronAPI.claude.onAutoRouteDecision(callback)` registers the listener; returns an unsubscribe function.

**Renderer store:** `session.store` writes `autoRouteDecision[sessionId]`, updates `activeStreamModel`, and seeds `activeMetaGoals` when `decision.goal` is present.

**UI:** When the model picker is `auto`, `InputArea` shows `AutoRouteBadge` with tier/domain colors and harness label (compact while sending).

```typescript
// Payload shape (simplified)
{
  sessionId: string;
  decision: {
    tier: 'plan' | 'build' | 'verify' | 'refine';
    domain?: TaskDomain;
    resolvedModel: string;
    resolvedHarness?: Harness;
    confidence: number;
    reason: string;
    method: 'heuristic' | 'controller';
    missionControl?: MetaMissionControlPolicy;
    orchestration?: OrchestrationPlan;
    goal?: { objective: string; source: 'slash-command' | 'ralph-loop' };
    // ...effort, speed, workflow, budgetUsd, verification
  };
}
```

<Info>
`enableGoals` is set for verify-tier routes or when a `/goal` objective is active; Codex goals may be auto-enabled in `~/.codex/config.toml` for delegated verification.
</Info>

## Cost-aware and learned routing

When `autoRouterConfig.costAware` is true (default), plan-tier Opus is downgraded to Sonnet unless the user explicitly configured plan Opus. `analyticsService.getHarnessInsightsForTier()` may reorder candidates when override/success stats justify it; analytics failures never block routing.

## Configuration and verification

Defaults and UI-editable fields live in `claudette-settings` → `settings.autoRouterConfig` (see **Auto Build configuration**). API keys gate harness availability: OpenAI/Codex, Google/Gemini, Cursor CLI, DeepSeek/OpenCode, custom proxy models, plus Cerebras for the controller.

Regression scripts (run individually or via `npm run verify:auto-router:meta-harness`):

<CodeGroup>
```bash title="Controller confidence gate"
npm run verify:auto-router:controller-confidence
```

```bash title="Plan permission mode wiring"
npm run verify:auto-router:plan-mode
```

```bash title="Flue fallback when controller fails"
npm run verify:auto-router:flue-fallback
```
</CodeGroup>

<Check>
After changing routing logic, run the meta-harness quality gate before release; live Flue eval needs `CEREBRAS_API_KEY` and `FLUE_RUNTIME_NODE_MODULES`.
</Check>

## Related pages

<CardGroup>
<Card title="Auto Build configuration" href="/auto-build-configuration">
`AutoRouterConfig` fields, category editor, and `verify:auto-router:*` scripts.
</Card>
<Card title="Shared types reference" href="/shared-types-reference">
`RoutingDecision`, `OrchestrationPlan`, `OrchestrationStage`, `SessionPhase`, `MetaHarnessPolicy`.
</Card>
<Card title="Harnesses and models" href="/harnesses-and-models">
Harness prefixes (`codex:`, `cursor:`, …) and permission modes that constrain routing.
</Card>
<Card title="IPC channels reference" href="/ipc-channels-reference">
Full `IPC_CHANNELS` catalog including `claude:auto-route-decision`.
</Card>
<Card title="Settings reference" href="/settings-reference">
`autoRouterConfig` defaults in `claudette-settings`.
</Card>
</CardGroup>

---

## 08. IPC and preload bridge

> How ipcMain handlers map to renderer window.electronAPI, typed channel constants, event vs invoke patterns, and secure contextBridge exposure in preload.ts.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/08-ipc-and-preload-bridge.md
- Generated: 2026-06-02T00:20:53.352Z

### Source Files

- `src/shared/constants/channels.ts`
- `src/main/preload.ts`
- `src/main/ipc/claude.ipc.ts`
- `src/main/ipc/session.ipc.ts`
- `src/main/ipc/settings.ipc.ts`
- `CLAUDE.md`

---
title: "IPC and preload bridge"
description: "How ipcMain handlers map to renderer window.electronAPI, typed channel constants, event vs invoke patterns, and secure contextBridge exposure in preload.ts."
---

Build routes all renderer-to-main communication through a single preload script that exposes `window.electronAPI`. Channel names live in `src/shared/constants/channels.ts` as `IPC_CHANNELS`; main-process handlers register in `src/main/ipc/*.ipc.ts` via `registerIPCHandlers()` in `src/main/index.ts`; the renderer never imports `ipcRenderer` directly.

## End-to-end flow

```mermaid
sequenceDiagram
  participant R as Renderer (React stores)
  participant P as preload.ts
  participant M as ipcMain handlers
  participant S as Main services

  R->>P: window.electronAPI.sessions.create(config)
  P->>M: ipcRenderer.invoke("session:create", config)
  M->>S: sessionService.createSession(config)
  S-->>M: Session
  M-->>P: resolved promise
  P-->>R: Session

  Note over M,R: Push path (no return value)
  S->>M: sessionService.emit("statusChanged")
  M->>R: broadcastToAll("session:status-changed", session)
  R->>P: onStatusChanged callback (ipcRenderer.on)
```

| Layer | File / symbol | Responsibility |
|-------|----------------|----------------|
| Channel catalog | `IPC_CHANNELS` in `channels.ts` | Stable string IDs (`domain:action`) shared by main and preload |
| Registration | `register*Handlers(ipcMain)` in `src/main/ipc/` | Bind `ipcMain.handle` / `ipcMain.on` to services |
| Bridge | `electronAPI` in `preload.ts` | Map domain methods to `invoke`, `send`, or `on` |
| Consumer | `window.electronAPI` in renderer | Zustand stores and components call typed API |
| Types | `ElectronAPI` exported from `preload.ts` | `Window` augmentation in `src/renderer/index.tsx` |

## Channel naming and typing

`IPC_CHANNELS` groups roughly thirty domains (auth, session, claude, git, terminal, settings, browser, ssh, mcp, qmd, analytics, and others). Values use a `namespace:verb` pattern, for example `session:create` and `claude:stream-chunk`.

```typescript
export const IPC_CHANNELS = {
  SESSION_CREATE: 'session:create',
  CLAUDE_STREAM_CHUNK: 'claude:stream-chunk',
  // ...
} as const;

export type IPCChannel = typeof IPC_CHANNELS[keyof typeof IPC_CHANNELS];
```

Preload and handlers import the same object so renames stay synchronized. A few legacy or auxiliary channels are still string literals in preload (for example `auth:oauth-callback`, `session-switcher`, and some `browser:*` push names); new surface area should prefer adding entries to `IPC_CHANNELS`.

<Info>
The full channel inventory, grouped by domain with invoke vs push semantics, is documented on the IPC channels reference page.
</Info>

## Preload security model

`BrowserWindow` is created with `contextIsolation: true`, `nodeIntegration: false`, and `sandbox: false` (required for `node-pty`). Webpack injects the preload bundle via `MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY`.

At the end of `preload.ts`:

```typescript
contextBridge.exposeInMainWorld('electronAPI', electronAPI);
export type ElectronAPI = typeof electronAPI;
```

Only the curated `electronAPI` object crosses the isolation boundary. The renderer cannot call Node APIs or `ipcRenderer` unless exposed here. Pop-out browser windows reuse the same preload path (`__preloadPath` on the parent window) with the same `webPreferences`.

`DEV_INSTANCE_NAME` from the dev script is exposed as `electronAPI.devInstanceName` for status-bar identification in development builds.

## Invoke vs send vs push

Build uses three IPC shapes. Choosing the right one keeps streaming responsive and avoids blocking the UI thread.

| Pattern | Preload API | Main registration | Typical use |
|---------|-------------|-------------------|-------------|
| **Invoke** (request/response) | `ipcRenderer.invoke(channel, ...args)` → `Promise` | `ipcMain.handle(channel, async handler)` | CRUD, settings, start/stop session, send chat message kickoff |
| **Send** (renderer → main, no reply) | `ipcRenderer.send(channel, ...args)` | `ipcMain.on(channel, handler)` | High-frequency terminal input, resize, browser webview registration |
| **Push** (main → renderer) | `ipcRenderer.on(channel, handler)` + unsubscribe fn | `webContents.send` or `broadcastToAll` | Stream chunks, session list updates, permission dialogs |

### Invoke handlers

Domain modules export `registerXHandlers(ipcMain: IpcMain)`. Handlers are thin wrappers around services.

**Sessions** (`session.ipc.ts`): every session operation is `ipcMain.handle` delegating to `SessionService`. Service events fan out to all windows:

```typescript
sessionService.on('statusChanged', (session) => {
  broadcastToAll(IPC_CHANNELS.SESSION_STATUS_CHANGED, session);
});
```

**Settings** (`settings.ipc.ts`): `SETTINGS_GET`, `SETTINGS_SET`, API key getters/setters, plus app utilities (`APP_GET_VERSION`, `APP_OPEN_EXTERNAL`, dialogs, Docker probe) on the same registrar.

Preload mirrors these as `window.electronAPI.sessions.create(...)`, `window.electronAPI.settings.get()`, and so on.

### Fire-and-forget send

Terminal I/O uses `send` because keystrokes and resize events do not need acknowledgements:

- Preload: `terminal.sendInput`, `terminal.resize`, `terminal.close` → `ipcRenderer.send`
- Main: `ipcMain.on(TERMINAL_INPUT | TERMINAL_RESIZE | TERMINAL_CLOSE, ...)`

Browser webview registration similarly uses `ipcRenderer.send('browser:register-webview', { sessionId, webContentsId })` handled in `browser.service.ts`.

### Main-to-renderer push

Push channels almost always return an **unsubscribe function** from preload `on*` methods:

```typescript
onStreamChunk: (callback) => {
  const handler = (_: IpcRendererEvent, chunk) => callback(chunk);
  ipcRenderer.on(IPC_CHANNELS.CLAUDE_STREAM_CHUNK, handler);
  return () => ipcRenderer.removeListener(IPC_CHANNELS.CLAUDE_STREAM_CHUNK, handler);
},
```

Stores subscribe in effects and call the returned cleanup on unmount.

**Broadcast vs targeted send**

- `broadcastToAll(channel, ...args)` in `index.ts` sends to every non-destroyed window in `allWindows`. Session list/status and wakeup events use this so multiple renderer surfaces stay in sync.
- **Claude streaming** uses `sendToSender` inside `claude.ipc.ts`: events go to `event.sender` (the window that invoked `CLAUDE_SEND_MESSAGE`). If that `webContents` is destroyed mid-stream, the handler falls back to any live window.

Terminal output uses a **dynamic channel**: `terminal:output:${terminalId}` batched every 16ms on the main side to limit IPC volume.

## Bidirectional dialogs (push + invoke)

Permission, question, and plan-approval flows combine push and invoke:

1. Main pushes `CLAUDE_PERMISSION_REQUEST` (or `CLAUDE_QUESTION_REQUEST`, `CLAUDE_PLAN_APPROVAL_REQUEST`) via `sendToSender`.
2. Renderer shows UI and calls `claude.respondToPermission(...)` (invoke on `CLAUDE_PERMISSION_RESPONSE`).

Same pattern applies to plan approval and harness questions. This avoids long-polling invoke from main while keeping user responses on the trusted invoke path.

## `window.electronAPI` shape

`electronAPI` is organized by product domain, not by IPC file:

| Namespace | Examples | Main registrar |
|-----------|----------|----------------|
| `auth` | `login`, `checkProviders`, `onOAuthCallback` | `auth.ipc.ts` |
| `sessions` | `create`, `list`, `onStatusChanged` | `session.ipc.ts` |
| `claude` | `sendMessage`, `onStreamChunk`, `respondToPermission` | `claude.ipc.ts` |
| `terminal` | `create`, `sendInput`, `onOutput` | `terminal.ipc.ts` |
| `git` | `getStatus`, `onBranchChanged` | `git.ipc.ts` |
| `settings` / `app` | `get`, `setApiKey`, `getVersion` | `settings.ipc.ts` |
| `browser` | `registerWebview`, `sendSnapshotData` | `browser.ipc.ts` + service listeners |
| `secureKeys` | `intercept`, `clearSession` | `secure-keys.ipc.ts` |
| `ssh`, `mcp`, `qmd`, `voice`, `analytics`, … | matching domains | respective `*.ipc.ts` |

Renderer code guards for non-Electron contexts with `!!window.electronAPI` (see `session.store.ts`) so tests or static previews do not throw.

TypeScript consumers use:

```typescript
declare global {
  interface Window {
    electronAPI: import('../main/preload').ElectronAPI;
  }
}
```

## Handler registration lifecycle

On `app.ready`, `registerIPCHandlers()` runs **before** `createWindow()`:

```typescript
function registerIPCHandlers(): void {
  registerAuthHandlers(ipcMain);
  registerSessionHandlers(ipcMain);
  registerGitHandlers(ipcMain);
  registerTerminalHandlers(ipcMain);
  registerClaudeHandlers(ipcMain);
  registerSettingsHandlers(ipcMain);
  // ... dev, fs, audio, realtime, voice, extension, browser, ssh,
  // memory, secure keys, qmd, mcp, plugin, codex, openclaw, analytics, queue
  registerSecureKeysIPC();
}
```

GStack mode helpers are registered inline in `index.ts` (not a separate ipc file). `registerQmdHandlers` receives `getMainWindow` for progress pushes tied to the primary window.

`getMainWindow()` resolves the focused tracked window, then `mainWindow`, then any surviving entry in `allWindows`—used when handlers need a target for `webContents.send`.

## Performance patterns on the bridge

<Warning>
High-frequency paths batch or namespace channels intentionally. Copying a invoke-based stream design for terminal or PTY output will flood IPC.
</Warning>

- **Claude text/thinking**: `ChunkBatcher` in `claude.ipc.ts` flushes every ~100ms before `CLAUDE_STREAM_CHUNK` / `CLAUDE_THINKING_CHUNK`.
- **Terminal output**: 16ms flush buffer per `terminalId`.
- **Codex second opinion**: mirrors Claude batching in `codex.ipc.ts`.

## Adding a new capability

<Steps>
<Step title="Add channel constants">
Add `MY_FEATURE_DO_THING: 'my-feature:do-thing'` to `IPC_CHANNELS` in `channels.ts`. Use push suffix channels only when main initiates (`my-feature:updated`).
</Step>
<Step title="Register main handler">
Create or extend `src/main/ipc/my-feature.ipc.ts` with `registerMyFeatureHandlers(ipcMain)`. Use `handle` for operations that return data; use `webContents.send` / `broadcastToAll` for subscriptions.
</Step>
<Step title="Wire registration">
Import and call your registrar inside `registerIPCHandlers()` in `index.ts`.
</Step>
<Step title="Expose preload API">
Add a namespace on `electronAPI` in `preload.ts`: `invoke` for calls, `on*` with cleanup for pushes, `send` only when no response is needed.
</Step>
<Step title="Consume in renderer">
Call `window.electronAPI.myFeature...` from a store or hook. Subscribe in `useEffect` and tear down with the returned unsubscribe function.
</Step>
</Steps>

## Common pitfalls

| Symptom | Likely cause |
|---------|----------------|
| Handler never runs | Channel string mismatch between preload and `ipcMain`; or handler registered after first invoke |
| Stream events missing after reload | Listener not re-subscribed; or events sent to destroyed `event.sender` without fallback |
| `electronAPI` undefined in test | Renderer running outside Electron; guard with `hasElectronAPI` |
| Permission UI stuck | `onPermissionRequest` not subscribed, or `respondToPermission` not invoked with matching `requestId` |
| Duplicate session updates | Multiple windows each receive `broadcastToAll`; filter by `sessionId` in the callback |

Secure API key capture uses invoke-only paths (`secure-keys:intercept`, `secure-keys:get`) so raw keys are stripped in main before transcript persistence.

## Related pages

<CardGroup>
<Card title="Electron process model" href="/electron-architecture">
Main vs renderer boundaries, service layout, webpack preload entry, and window lifecycle.
</Card>
<Card title="IPC channels reference" href="/ipc-channels-reference">
Full `IPC_CHANNELS` catalog with invoke vs push classification by domain.
</Card>
<Card title="Configure API keys and providers" href="/configure-providers">
Settings and secure-keys channels for BYOC/BYOK storage.
</Card>
<Card title="Manage coding sessions" href="/manage-sessions">
Session invoke handlers and status push events used by the session store.
</Card>
</CardGroup>

---

## 09. Configure API keys and providers

> Store Anthropic, OpenAI, Google, Foundry, Cursor, DeepSeek, and custom proxy models; provider CLI detection; secure key interception; and optional PostHog analytics keys.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/09-configure-api-keys-and-providers.md
- Generated: 2026-06-02T00:21:46.091Z

### Source Files

- `src/main/services/settings.service.ts`
- `src/main/services/secure-keys.service.ts`
- `src/main/ipc/secure-keys.ipc.ts`
- `src/renderer/components/settings/SettingsDialog.tsx`
- `src/shared/types/index.ts`
- `SECURITY.md`

---
title: "Configure API keys and providers"
description: "Store Anthropic, OpenAI, Google, Foundry, Cursor, DeepSeek, and custom proxy models; provider CLI detection; secure key interception; and optional PostHog analytics keys."
---

Build stores credentials locally in `electron-store` (`claudette-settings` on disk under the app userData path), exposes them through Settings → **API Keys** and dedicated IPC handlers on `window.electronAPI`, and never sends stored keys to Build servers—only to the provider APIs you configure. Harness CLIs can authenticate via OAuth/login on disk instead of an in-app key; onboarding and `auth:check-providers` report which paths are ready.

## Storage layout

All persistent secrets use the same store file name: `claudette-settings`. Keys are split between a nested `settings` object (`AppSettings`) and a few top-level keys for dedicated getters.

| Storage key / field | Consumer | How it is written |
| --- | --- | --- |
| `anthropicApiKey` (top-level) | Claude Agent SDK (`ClaudeService.getApiKey`) | `settings.setApiKey` → `SETTINGS_SET_API_KEY` |
| `settings` (`AppSettings`) | Model list, Foundry, Cursor, DeepSeek, Gemini CLI, custom proxies, optional PostHog | `settings.set` partial updates |
| `googleApiKey` (top-level) | Stagehand / browser AI (`settings.getGoogleApiKey`) | `settings.setGoogleApiKey` |
| `openaiApiKey` (top-level) | Whisper transcription (`AudioService`) | `audio.setOpenAiKey` |
| `elevenLabsApiKey` (top-level) | ElevenLabs TTS / voice (`AudioService`, `elevenlabs-voice.service`) | `audio.setElevenLabsKey` |
| `githubToken` (top-level) | GitHub integration (`SettingsService`) | `SettingsService.setGitHubToken` (typed on `AppSettings`, not shown in API Keys tab) |
| `audioSettings` | Voice mode toggles, agent ID | `audio.setSettings` |

<Note>
`SECURITY.md` states keys are only sent to providers and that no telemetry is collected. In code, **optional** PostHog export runs only when `posthogApiKey` (or build-time env vars) is set; without a key, `AnalyticsService` skips network capture.
</Note>

### `AppSettings` credential fields

Defined in `AppSettings` (`src/shared/types/index.ts`):

<ParamField body="anthropicApiKey" type="string">
Legacy duplicate on the type; the live Anthropic key is stored at top-level `anthropicApiKey` via `SettingsService`.
</ParamField>

<ParamField body="foundryEnabled" type="boolean">
Enables Azure Anthropic Foundry routing for Claude Code (`CLAUDE_CODE_USE_FOUNDRY=1`).
</ParamField>

<ParamField body="foundryBaseUrl" type="string">
Maps to `ANTHROPIC_FOUNDRY_BASE_URL` when Foundry is enabled.
</ParamField>

<ParamField body="foundryApiKey" type="string">
Maps to `ANTHROPIC_FOUNDRY_API_KEY`.
</ParamField>

<ParamField body="foundryDefaultSonnetModel" type="string">
Optional Foundry model IDs for Sonnet/Haiku/Opus tiers in the picker.
</ParamField>

<ParamField body="foundryDefaultHaikuModel" type="string">
</ParamField>

<ParamField body="foundryDefaultOpusModel" type="string">
</ParamField>

<ParamField body="cursorApiKey" type="string">
Cursor harness / SDK model listing (local sessions).
</ParamField>

<ParamField body="deepseekApiKey" type="string">
OpenCode harness (`DEEPSEEK_API_KEY` env).
</ParamField>

<ParamField body="geminiApiKey" type="string">
Gemini CLI harness (`GEMINI_API_KEY` / `GOOGLE_API_KEY`).
</ParamField>

<ParamField body="customModels" type="CustomModelConfig[]">
Anthropic-compatible proxy endpoints (`id`, `name`, `modelId`, `baseUrl`, `apiKey`, optional `description`).
</ParamField>

<ParamField body="posthogApiKey" type="string">
Optional project API key for analytics capture (no Settings UI field today).
</ParamField>

<ParamField body="posthogHost" type="string">
PostHog host; defaults to `https://us.i.posthog.com` when empty.
</ParamField>

```typescript
// CustomModelConfig shape
{
  id: string;       // picker id, e.g. "kimi-k26"
  name: string;     // display name
  modelId: string;  // API model id
  baseUrl: string;  // Anthropic-compatible base URL
  apiKey: string;
  description?: string;
}
```

## Settings UI workflow

Open **Settings** (gear) → **API Keys** tab (`SettingsDialog`, tab id `apiKeys`).

<Steps>
<Step title="Anthropic">
Enter `sk-ant-...` or skip and use `claude login` in a terminal (OAuth via Claude Code credentials).
</Step>
<Step title="Foundry (optional)">
Toggle **Anthropic Foundry**, set base URL, API key, and optional Sonnet/Haiku/Opus model name overrides.
</Step>
<Step title="OpenAI / ElevenLabs">
OpenAI key is labeled **Speech-to-Text** (Whisper). ElevenLabs key and agent ID power voice mode.
</Step>
<Step title="Cursor / DeepSeek / Gemini">
Cursor API key for the Cursor harness; DeepSeek for OpenCode; Gemini API key for the `gemini` CLI harness.
</Step>
<Step title="Google (browser AI)">
Separate **Google/Gemini API Key (Browser AI)** for Stagehand (`AIza...`), stored via `setGoogleApiKey`.
</Step>
<Step title="Custom models">
Use **+ Add Model** for Anthropic-compatible proxies (e.g. Kimi). Saves into `settings.customModels` and reloads the model picker.
</Step>
</Steps>

Text fields debounce 500ms; toggles and explicit saves call `autoSaveAppSettings` or `autoSaveApiKey` immediately. Model-affecting updates trigger `loadAvailableModels()` in the session store.

## IPC and preload surface

Renderer calls go through `preload.ts` → `ipcMain` handlers.

| Channel | Handler | Purpose |
| --- | --- | --- |
| `settings:get` | `settings.ipc` | Read full `AppSettings` |
| `settings:set` | `settings.ipc` | Merge partial `AppSettings` |
| `settings:get-api-key` / `settings:set-api-key` | `settings.ipc` | Anthropic top-level key |
| `settings:get-google-api-key` / `settings:set-google-api-key` | `settings.ipc` | Browser AI key |
| `audio:get-openai-key` / `audio:set-openai-key` | audio IPC | Whisper key |
| `audio:get-elevenlabs-key` / `audio:set-elevenlabs-key` | audio IPC | ElevenLabs key |
| `auth:check-providers` | `auth.ipc` | CLI + credential detection |
| `secure-keys:intercept` | `secure-keys.ipc` | Detect/replace keys in chat text |
| `secure-keys:get` | `secure-keys.ipc` | Resolve placeholder by id (agent use) |
| `secure-keys:list` | `secure-keys.ipc` | Metadata only per session |
| `secure-keys:clear-session` | `secure-keys.ipc` | Wipe session memory keys |

Preload grouping:

```typescript
window.electronAPI.settings.get / .set / .getApiKey / .setApiKey / .getGoogleApiKey / .setGoogleApiKey
window.electronAPI.audio.getOpenAiKey / .setOpenAiKey / .getElevenLabsKey / .setElevenLabsKey
window.electronAPI.auth.checkProviders()
window.electronAPI.secureKeys.interceptAndReplace / .getKey / .listKeys / .clearSession
```

## Provider CLI detection

`AUTH_CHECK_PROVIDERS` runs five checks in parallel (`auth.ipc.ts`) and returns `{ claude, codex, cursor, gemini, opencode }` with `ProviderStatus`: `installed`, `loggedIn`, `method` (`cli` | `apiKey` | `chatgpt`), `detail`, `path`, `version`, plus install/login hints.

```mermaid
flowchart TB
  subgraph UI["Renderer"]
    Onboarding["ApiKeyOnboarding"]
    Settings["SettingsDialog"]
  end
  subgraph Main["Main process auth.ipc"]
    Check["AUTH_CHECK_PROVIDERS"]
    Claude["checkClaudeCli"]
    Codex["checkCodexCli"]
    Cursor["checkCursorCli"]
    Gemini["checkGeminiCli"]
    OpenCode["checkOpenCodeCli"]
  end
  subgraph Disk["Local auth artifacts"]
    KC["macOS Keychain / ~/.claude/.credentials.json"]
    CodexAuth["~/.codex/auth.json"]
    CursorCLI["cursor-agent status"]
    Store["claudette-settings"]
  end
  Onboarding --> Check
  Settings --> Store
  Check --> Claude --> KC
  Check --> Codex --> CodexAuth
  Check --> Cursor --> CursorCLI
  Check --> Cursor --> Store
  Check --> Gemini --> Store
  Check --> OpenCode --> Store
```

| Provider | CLI binaries searched | Logged-in signal |
| --- | --- | --- |
| Claude | `claude` | macOS keychain `Claude Code-credentials` or `~/.claude/.credentials.json` |
| Codex | `codex` (+ bundled platform binary) | `~/.codex/auth.json` tokens or `OPENAI_API_KEY` |
| Cursor | `cursor-agent`, `agent` (common paths) | `cursor-agent status` shows logged in, or `cursorApiKey` / `CURSOR_API_KEY` |
| Gemini | `gemini` | CLI installed **and** `geminiApiKey`, `googleApiKey`, or `GEMINI_API_KEY` / `GOOGLE_API_KEY` |
| OpenCode | `opencode` or `npx` | Runner available **and** `deepseekApiKey` or `DEEPSEEK_API_KEY` |

Onboarding (`ApiKeyOnboarding.tsx`) animates scan phases while calling `checkProviders`, then polls until all five report `loggedIn` or the user continues with an Anthropic API key.

<Warning>
Provider detection uses `realUserHome()` on macOS so demo `HOME` overrides do not hide real keychain/credential state.
</Warning>

## How keys reach harnesses

```text
claudette-settings
├── anthropicApiKey ──────────► Claude Agent SDK (ANTHROPIC_API_KEY)
├── settings.foundry* ────────► getFoundryEnvVars() → Claude Code Foundry env
├── settings.customModels ────► getCustomModelEnvVars() → ANTHROPIC_BASE_URL / _API_KEY
├── settings.cursorApiKey ────► Cursor SDK + cursor harness
├── settings.deepseekApiKey ──► opencode.service → DEEPSEEK_API_KEY
├── settings.geminiApiKey ────► gemini.service (+ fallback googleApiKey)
├── googleApiKey ─────────────► stagehand.service (browser automation)
├── openaiApiKey ─────────────► audio.service Whisper
└── elevenLabsApiKey ─────────► voice / TTS services
```

- **Foundry**: When `foundryEnabled`, `ClaudeService.getAvailableModels()` lists configured Foundry model names instead of default Anthropic IDs.
- **Custom proxy**: Picker ids use prefix `custom:{id}`; env overrides `ANTHROPIC_BASE_URL`, `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, and tier model vars.
- **Codex**: Prefers `~/.codex/auth.json`; Build-stored `openaiApiKey` is also read for routing cost checks.
- **Audio fallbacks**: `AudioService` may use `EMBEDDED_KEYS` when the user has not set ElevenLabs/OpenAI keys (development convenience only).

## Secure key interception

`SecureKeysService` keeps detected secrets **in memory only** (singleton `Map`, cleared per session via `clearSessionKeys`). They are not written to `electron-store` or session transcripts as plaintext in the UI path.

**Detection order:**

1. `ENV_VAR=value` lines where the name matches secret patterns (`API_KEY`, `TOKEN`, `SECRET`, AWS keys, etc.) and value is not a placeholder.
2. Provider-specific regexes (`sk-ant-`, `sk-`, `ghp_`, `AIza`, Stripe, Slack, JWT, etc.).
3. Generic high-entropy tokens (Shannon entropy ≥ 3.5, ≥ 3 character classes).

**Send path** (`session.store.ts`):

1. Before enqueue/send, `secureKeys.interceptAndReplace(sessionId, message)` replaces raw values with `[SECURE_KEY:key_<hex>]`.
2. UI stores `keysDetected` metadata (descriptions only).
3. On session end, `secureKeys.clearSession(sessionId)`.

**Agent path** (`claude.service.ts`):

1. Before the model sees the user turn, placeholders in `streamMessage` are resolved back to real values via `secureKeysService.getKey`.
2. Env assignments detected in chat can be exported to a session-scoped env file (`prepareSecureEnvContext`) for local/SSH runs, mode `0600`.

<Info>
Logs record key **ids** and types, never values. `SECURE_KEYS_GET` returns the value to the main process for resolution—keep agent tooling on the IPC bridge, not renderer storage.
</Info>

```mermaid
sequenceDiagram
  participant R as Renderer session.store
  participant IPC as secure-keys IPC
  participant SK as SecureKeysService
  participant CS as ClaudeService
  participant M as Model / CLI
  R->>IPC: intercept(sessionId, text)
  IPC->>SK: interceptAndReplaceKeys
  SK-->>R: modifiedText + keysDetected
  R->>R: Persist chat with placeholders
  R->>CS: streamMessage(placeholder text)
  CS->>SK: getKey(keyId) per placeholder
  SK-->>CS: resolved secrets
  CS->>M: prompt with real values
```

Codex IPC applies the same intercept step on prompts before execution.

## Optional PostHog analytics

Routing and product analytics use `AnalyticsService` (`claudette-analytics` store for events; `posthog.distinctId` for anonymous id).

**Configuration precedence** (`getPostHogConfig`):

1. `settings.posthogApiKey` / `settings.posthogHost` in `claudette-settings`
2. Env: `BUILD_POSTHOG_API_KEY`, `POSTHOG_PROJECT_API_KEY`, `POSTHOG_API_KEY`, `POSTHOG_HOST`

If `apiKey` is empty, `capturePostHog` returns without network I/O. Payloads sanitize properties (drop raw `prompt`, `sessionName`, `error`; trim `routingDecision`). Capture sets `$process_person_profile: false`, disables geo IP, and uses `fetch` to `{host}/capture/`.

There is a separate `posthog.service.ts` wired to **build-time** `POSTHOG_API_KEY` env for `posthog-node`; user-facing optional keys in settings follow the `AnalyticsService` path above. No **API Keys** UI fields exist for PostHog today—set via `settings.set({ posthogApiKey, posthogHost })` or direct store edit.

## Verification

| Check | Expected signal |
| --- | --- |
| Anthropic key saved | `settings.getApiKey()` non-empty; Claude sessions start without “missing API key” |
| Provider scan | Onboarding shows green/ready for CLI you installed; `auth.checkProviders()` matches terminal auth |
| Custom model | Model picker shows `custom:{id}`; agent env includes your `baseUrl` |
| Secure intercept | Console: `Intercepted N API key(s)`; chat shows `[SECURE_KEY:...]` not raw `sk-` |
| PostHog (optional) | With key set, network POST to your PostHog host; without key, no capture attempts |

Report security issues per `SECURITY.md` to **security@parcha.ai** (do not file public issues for vulnerabilities).

## Related pages

<CardGroup>
<Card title="Quickstart" href="/quickstart">
First project, API key, harness pick, and first agent message.
</Card>
<Card title="Harnesses and models" href="/harnesses-and-models">
Harness ids, model strings, and permission modes per harness.
</Card>
<Card title="Settings reference" href="/settings-reference">
Full `AppSettings` and `claudette-settings` keys.
</Card>
<Card title="IPC and preload bridge" href="/ipc-bridge">
How `electronAPI` maps to `ipcMain` handlers.
</Card>
<Card title="Voice and audio" href="/voice-and-audio">
ElevenLabs, OpenAI realtime, and microphone setup.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
PATH, CLI detection, and credential recovery.
</Card>
</CardGroup>

---

## 10. Manage coding sessions

> Create, start, stop, fork, and rewind sessions; teleport/import flows; message queue coalescing; permission and plan approval dialogs; and session switcher UX.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/10-manage-coding-sessions.md
- Generated: 2026-06-02T00:23:07.527Z

### Source Files

- `src/main/ipc/session.ipc.ts`
- `src/main/services/session.service.ts`
- `src/main/services/message-queue.service.ts`
- `src/renderer/components/session/NewSessionDialog.tsx`
- `src/renderer/components/chat/MessageQueuePanel.tsx`
- `src/renderer/components/chat/ForkTabs.tsx`
- `src/main/ipc/queue.ipc.ts`

---
title: "Manage coding sessions"
description: "Create, start, stop, fork, and rewind sessions; teleport/import flows; message queue coalescing; permission and plan approval dialogs; and session switcher UX."
---

Session lifecycle in Build is driven by `SessionService` in the main process, exposed through typed IPC channels (`session:*`) and mirrored in the renderer `session.store`. Creating, activating, forking, rewinding, teleporting, and deleting sessions all persist to the `claudette-sessions` electron-store; chat backpressure uses a main-process `MessageQueueService` that drains into the renderer on `queue:send-next` after each agent turn completes.

## Architecture

```mermaid
flowchart TB
  subgraph renderer["Renderer"]
    NSD[NewSessionDialog]
    SL[SessionList]
    FT[ForkTabs]
    SS[SessionSwitcher]
    SSZ[session.store]
    MQP[MessageQueuePanel]
    PD[PermissionDialog]
    PP[PlanPanel]
  end

  subgraph main["Main process"]
    SIP[session.ipc.ts]
    SVC[SessionService]
    QIPC[queue.ipc.ts]
    MQS[MessageQueueService]
    CIPC[claude.ipc.ts]
  end

  subgraph storage["Persistence"]
    ES[(claudette-sessions)]
    CT[~/.claude/projects/*.jsonl]
  end

  NSD --> SSZ
  SL --> SSZ
  FT --> SSZ
  SS --> SSZ
  MQP --> SSZ
  PD --> SSZ
  PP --> SSZ
  SSZ -->|window.electronAPI.sessions| SIP
  SSZ -->|queue:*| QIPC
  SIP --> SVC
  QIPC --> MQS
  MQS -->|drain-ready| CIPC
  CIPC -->|queue:send-next| SSZ
  SVC --> ES
  SVC --> CT
```

| Layer | Responsibility |
|-------|----------------|
| `session.ipc.ts` | Registers `SESSION_*` handlers; broadcasts `SESSION_STATUS_CHANGED` and `SESSION_LIST_UPDATED` |
| `SessionService` | CRUD, discovery, fork/rewind transcript surgery, SDK `forkSession` for local conversations |
| `message-queue.service.ts` | Per-session FIFO queue, streaming gate, harness-aware drain delay |
| `session.store.ts` | UI state: active session, pending dialogs, local queue display, send/coalesce logic |

## Session statuses

<ParamField body="status" type="SessionStatus">
`creating` | `starting` | `setup` | `running` | `stopping` | `stopped` | `error`
</ParamField>

`startSession` and `stopSession` today flip `running` ↔ `stopped` without starting Docker containers for typical dev sessions. `setActiveSession` auto-invokes `startSession` when you focus a `stopped` session.

## Create sessions

`NewSessionDialog` is the primary entry for new work. It walks a multi-step wizard (`source` → repo/folder/config/teleport/ssh/openclaw) and calls different IPC surfaces depending on source.

<Steps>
<Step title="Pick a source">
GitHub repo (clone via `sessions.create`), local folder (`dev.createSession`), SSH (`ssh.createSession`), OpenClaw gateway (`openclaw.createSession`), or **Import from claude.ai** (teleport step).
</Step>
<Step title="Configure workspace">
For local folders: optional git worktree with setup script or instructions saved under `.grep/`. Branch picker loads when `dev.checkGitRepo` reports a repo.
</Step>
<Step title="Activate">
On success, `addSession` + `setActiveSession` run; setup progress streams via `dev.onSetupProgress` / `ssh.onSetupProgress`.
</Step>
</Steps>

| Source | IPC | Notes |
|--------|-----|-------|
| GitHub | `SESSION_CREATE` → `SessionService.createSession` | Clones into `userData/sessions/{id}/`, writes `.grep/setup.sh`, ends `stopped` |
| Local folder | `dev.createSession` | `isDevMode: true`; optional worktree under `~/.claudette/worktrees/` |
| SSH | `ssh.createSession` | `sshConfig` on session; may `resumeSessionId` |
| OpenClaw | `openclaw.createSession` | `openclawConfig` gateway URL + password |
| claude.ai import | `dev.createTeleportSession` | Requires Claude CLI; spawns with `--teleport` into chosen `cwd` |

<Note>
`NewSessionDialog` checks Claude CLI via `dev.checkClaudeCli` on open. Teleport/import and some resume paths depend on it being installed.
</Note>

## Start, stop, and delete

| Action | IPC channel | Behavior |
|--------|-------------|----------|
| Start | `session:start` | Sets status `running` |
| Stop | `session:stop` | Sets status `stopped` |
| Delete | `session:delete` | Stops Docker container if present; `claudeService.cleanupSession` + `browserService.cleanupSession`; purges renderer maps and `messageQueueService.cleanup` |

Deletion also clears `secureKeys` for the session and removes browser/plan/TTS side effects from other stores.

## Conversation forks

Build distinguishes **git worktree forks** (`isWorktree`, `parentRepoPath`, `forkName`) from **conversation forks** (`parentSessionId`, `childSessionIds`, `forkPoint`, `aiGeneratedName`).

### Create fork from chat

Sending while holding a fork intent (or explicit fork UI) calls `SESSION_CREATE_FORK`:

- **Local**: uses Claude Agent SDK `forkSession(parentSdkSessionId, { upToMessageId?, title, dir? })`.
- **SSH**: allocates a new UUID; first query resumes parent with `forkSession: true` server-side.
- Parent gains the child in `childSessionIds`; `sdkSessionMappings` wires transcript lookup.
- `createForkFromCurrent` in the store creates at `'end'`, switches to the fork, and sends the user message only to the child (parent stream continues).

### Fork tabs UI

`ForkTabs` renders siblings from `getForkSiblings` / `SESSION_GET_FORK_GROUP` (root + descendants sorted by `forkCreatedAt` or `createdAt`). Closing a tab sets `tabHidden` and moves the session to an overflow menu (`grep-overflow-forks-{rootId}` in `localStorage`). SSH roots auto-call `SESSION_SCAN_REMOTE` once to register orphan remote transcripts as hidden child sessions.

## Rewind and fork

Two rewind paths exist:

1. **Transcript rewind-fork** (`SESSION_REWIND_FORK` / `rewindAndFork`): Truncates `~/.claude/projects/{hash}/{sessionId}.jsonl` at a message UUID, writes a new `{forkedId}.jsonl`, clones Build supplemental transcript via `transcriptService`, creates a new session record, and switches the UI (`MessageBubble` rewind button on non-latest user messages).

2. **File rewind + fork** (`CLAUDE_REWIND_EXECUTE`): SDK `rewindFiles(messageId)` then `createForkFromInput` at that message.

<Warning>
Rewind-fork clears `sdkSessionId` on the fork so resume does not collide with the parent SDK session.
</Warning>

## Teleport and import flows

| Flow | Direction | Entry | Main behavior |
|------|-----------|-------|---------------|
| claude.ai import | External → local | `NewSessionDialog` teleport step | `dev.createTeleportSession({ sessionId, name, cwd })`; sets `isTeleported` when applicable |
| SSH teleport | Local → remote | `TeleportDialog` + `SessionList` | `ssh.teleportSession(sourceId, SSHConfig)` uploads `~/.claude/projects` transcripts and syncs settings |
| Download | Remote → local | `DownloadSessionDialog` | `ssh.downloadSession`; sets `downloadedFrom` on the new local session |

Session fields `teleportedFrom` and `downloadedFrom` link provenance across hosts. SSH teleport progress surfaces through the same setup-progress listeners as session creation.

## Message queue

While the agent streams (`isStreaming` or `isProcessingQueue`), new user input enqueues instead of starting a parallel turn.

```text
User send (streaming) --> renderer messageQueue[] + main MessageQueueService
Stream ends --> onStreamEnd --> scheduleDrain(minTurnGapMs)
drain-ready --> dequeue --> queue:send-next --> session.store sendMessage
```

| Surface | Role |
|---------|------|
| `queue:enqueue` / `remove` / `edit` / `moveToFront` / `clear` | Main-process queue mutations |
| `queue:state-changed` | Syncs renderer `messageQueue` display |
| `MessageQueuePanel` | Edit, reorder, clear, **interrupt** (lightning) via `interruptAndSend` |

`interruptAndSend` cancels the active stream, preserves partial assistant content, drains stale events (~300ms), then sends the queued message immediately.

### Coalescing

Two mechanisms reduce duplicate turns:

1. **Early follow-up coalescing** (renderer): If the user sends again while streaming but before visible assistant activity, `sendMessage` merges prompts via `combineUserPrompts`, cancels the in-flight turn, and resends once.

2. **Harness turn gap** (main): After `onStreamEnd`, `minTurnGapMs` delays drain (`0` for `claude`, `500` for codex/cursor/gemini/opencode/custom). `maxCoalesceWindowMs` is defined per harness in `harness-capabilities.ts` for future use; the active coalescing path today is the renderer early-follow-up branch.

Queued user bubbles render immediately with a queued indicator so sends are visible before drain.

## Permission and plan approval dialogs

### Permission requests

When the harness emits a tool permission request, `session.store` sets `pendingPermission[sessionId]`. `PermissionDialog` renders above the chat input (also in `CommandCenterCell` and `AgentView`) with:

| Action | Effect |
|--------|--------|
| Approve | `claude.respondToPermission` with optional modified input |
| Always approve | Bash pattern wildcard (e.g. `gh pr *`) |
| Deny | Rejects the tool call |
| BUILD IT! | Sets permission mode to `bypassPermissions` and approves |

`getSessionPriority` treats sessions with pending permission/question/plan as `needs-input`. SSH disconnect clears stale permission/question dialogs for that session.

### Plan approval

`ExitPlanMode` triggers `CLAUDE_PLAN_APPROVAL_REQUEST`. The store sets `pendingPlanApproval`, copies plan content, and opens `PlanPanel` for the active session (background sessions keep the request but do not auto-open the panel). Approve/reject call `claude.respondToPlanApproval` with `PlanApprovalResponse { requestId, approved, feedback? }`. Ultra Plan settings can auto-decompose tasks after approval (see settings reference).

## Session switcher UX

`SessionSwitcher` + `useSessionSwitcher` implement an Alt-Tab-style overlay for **running** sessions only (sorted by `updatedAt`).

| Input | Behavior |
|-------|----------|
| `Ctrl+Tab` | Open switcher; further tabs cycle forward |
| `Ctrl+Shift+Tab` | Cycle backward |
| Release `Ctrl` | Confirm selection → `setActiveSession` |
| `Esc` | Cancel without switching |
| IPC `app.onSessionSwitcher` | Same actions when a webview has focus (main forwards Ctrl+Tab from `before-input-event`) |

The overlay shows color blocks, status dots, branch labels, an **ACTIVE** badge, and a shortcut into Command Center.

## Sidebar session list

`SessionList` groups sessions by local project path or SSH host. Conversation forks with `parentSessionId` are hidden unless they are the active session (so the sidebar stays uncluttered while `ForkTabs` handles fork navigation). Starred sessions support drag reorder; teleport/download actions open their respective dialogs.

## IPC quick reference

| Channel | Pattern | Purpose |
|---------|---------|---------|
| `session:create` | invoke | GitHub clone session |
| `session:start` / `session:stop` | invoke | Status activation |
| `session:delete` | invoke | Remove session + service cleanup |
| `session:list` / `session:get` / `session:update` | invoke | Query/mutate records |
| `session:rewind-fork` | invoke | Transcript truncate + new session |
| `session:create-fork` | invoke | SDK conversation fork |
| `session:get-fork-group` | invoke | Root + children for tabs |
| `session:scan-remote` | invoke | SSH orphan transcript discovery |
| `session:status-changed` | push | Per-session status updates |
| `session:list-updated` | push | Full list refresh |
| `queue:*` | invoke + `queue:state-changed` push | Message queue |
| `queue:send-next` | push | Dequeued message ready to send |
| `claude:plan-approval-request` / `response` | push / invoke | Plan mode gate |

Preload exposes these as `window.electronAPI.sessions.*`, `window.electronAPI.claude.onQueueSendNext`, and related helpers.

## State diagram (activation)

```mermaid
stateDiagram-v2
  [*] --> stopped: create / discover
  stopped --> running: start OR setActiveSession
  running --> stopped: stop
  creating --> stopped: clone success
  creating --> error: clone failure
  error --> [*]: delete
  stopped --> [*]: delete
  running --> [*]: delete
```

<Tip>
Prefer `dev.createSession` for day-to-day local work; `session:create` is the Docker-era GitHub clone path. Both persist into the same `claudette-sessions` store namespace.
</Tip>

## Related pages

<CardGroup>
<Card title="Sessions and workspaces" href="/sessions-and-workspaces">
Session schema, worktrees, discovery, and electron-store layout.
</Card>
<Card title="IPC and preload bridge" href="/ipc-bridge">
How `session:*` channels map to `window.electronAPI`.
</Card>
<Card title="SSH remote sessions" href="/ssh-remote-sessions">
Teleport, download, resume candidates, and remote transcript scan.
</Card>
<Card title="Harnesses and models" href="/harnesses-and-models">
Permission modes and harness queue capabilities.
</Card>
</CardGroup>

---

## 11. SSH remote sessions

> SSHConfig fields, connection test, remote workdir setup scripts, resume candidates, download/teleport session flows, and detached bridge recovery.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/11-ssh-remote-sessions.md
- Generated: 2026-06-02T00:22:03.063Z

### Source Files

- `src/shared/types/index.ts`
- `src/main/services/ssh.service.ts`
- `src/main/ipc/ssh.ipc.ts`
- `src/renderer/components/session/SSHConfigForm.tsx`
- `src/renderer/components/session/DownloadSessionDialog.tsx`
- `scripts/verify-ssh-detached-bridge-resume.js`
- `.notes/ssh-download-implementation-summary.md`

---
title: "SSH remote sessions"
description: "SSHConfig fields, connection test, remote workdir setup scripts, resume candidates, download/teleport session flows, and detached bridge recovery."
---

Build runs agent harnesses on a remote host over SSH: the main process owns `ssh.service.ts` and `ssh.ipc.ts`, the renderer collects connection settings in `SSHConfigForm`, and each `Session` with `sshConfig` stores the remote workdir, SDK transcript mapping, and optional teleport/download lineage (`teleportedFrom`, `downloadedFrom`).

## Architecture

```mermaid
flowchart TB
  subgraph renderer["Renderer"]
    SSHConfigForm["SSHConfigForm"]
    DownloadDialog["DownloadSessionDialog"]
    SessionStore["session.store"]
  end
  subgraph preload["Preload bridge"]
    electronAPI_ssh["window.electronAPI.ssh"]
  end
  subgraph main["Main process"]
    ssh_ipc["ssh.ipc.ts"]
    ssh_svc["ssh.service.ts"]
    claude_svc["claude.service.ts"]
    session_store["claudette-sessions"]
  end
  subgraph remote["Remote host"]
    harness_cli["Harness CLIs"]
    claude_projects["~/.claude/projects/…"]
    detached_bridge["/tmp/claudette-ssh-bridge/{sessionId}/"]
  end
  SSHConfigForm --> electronAPI_ssh
  DownloadDialog --> electronAPI_ssh
  SessionStore --> electronAPI_ssh
  electronAPI_ssh --> ssh_ipc
  ssh_ipc --> ssh_svc
  ssh_ipc --> session_store
  claude_svc --> ssh_svc
  ssh_svc --> harness_cli
  ssh_svc --> claude_projects
  ssh_svc --> detached_bridge
```

| Layer | Responsibility |
| --- | --- |
| `SSHConfigForm` | Connection test, saved config, resume picker, teleport mode |
| `ssh.ipc.ts` | Session create, teleport, download, reconnect, progress events |
| `ssh.service.ts` | ssh2 client pool, setup scripts, SFTP, detached bridge spawn/attach |
| `claude.service.ts` | `createRemoteProcess` hook and `resumeRemoteTurn` for recovery |
| `claudette-settings` | `lastSSHConfig` (no passphrase) |
| `claudette-sessions` | `sessions.*`, `sdkSessionMappings.*` |

## SSHConfig and saved settings

Runtime sessions use `SSHConfig` on `Session.sshConfig`. The form persists a passphrase-free `SavedSSHConfig` under `claudette-settings` key `lastSSHConfig`.

<ParamField body="host" type="string" required>
Remote hostname or IP.
</ParamField>

<ParamField body="port" type="number">
SSH port. Default `22` when omitted at connect time.
</ParamField>

<ParamField body="username" type="string" required>
SSH user.
</ParamField>

<ParamField body="privateKeyPath" type="string" required>
Local path to the private key file (read on the main process).
</ParamField>

<ParamField body="remoteWorkdir" type="string" required>
Directory where harness tools run (`repoPath` / `worktreePath` on the session). May be rewritten after a setup script.
</ParamField>

<ParamField body="passphrase" type="string">
Optional key passphrase. Used only in memory for the connection; never stored in `lastSSHConfig`.
</ParamField>

<ParamField body="worktreeScript" type="string">
Shell command run from `$HOME` before the session starts (see setup scripts). Disables resume-candidate UI when non-empty.
</ParamField>

<ParamField body="syncSettings" type="boolean">
When not `false` (default **true**), syncs local `~/.claude` artifacts to the remote during pre-session setup.
</ParamField>

`SSHResumeCandidate` (from `ssh:list-resume-candidates`) includes `sessionId`, `backend: 'claude'`, `mtime`, and optional `localSessionId` / `localSessionName` when a local Build session already maps the same SDK id.

## Connection test

<Steps>
<Step title="Collect connection + workdir">
Fill host, username, private key, and remote working directory. `SSHConfigForm` debounces an automatic test (~800ms) when all required fields are present.
</Step>
<Step title="Invoke test">
Renderer calls `window.electronAPI.ssh.testConnection(config)` → `SSH_TEST_CONNECTION` → `sshService.testConnection`.
</Step>
<Step title="Verify remote harnesses">
On success, the service detects `claude`, `codex`, `cursor-agent`, `gemini`, and `opencode` on the remote PATH. At least one must exist or the test fails with `missingCliInstallCommands` install hints.
</Step>
<Step title="Optional resume probe">
After a successful test (non-teleport, no worktree script), the form calls `listResumeCandidates` to populate the existing-session picker.
</Step>
</Steps>

<ResponseField name="SSHConnectionTestResult" type="object">
<ParamField body="success" type="boolean" />
<ParamField body="error" type="string" />
<ParamField body="hostname" type="string" />
<ParamField body="claudeCodeVersion" type="string" />
<ParamField body="cliCapabilities" type="RemoteCliCapabilities" />
<ParamField body="setupWarning" type="string" />
<ParamField body="missingCliInstallCommands" type="RemoteCliSetupCommand[]" />
</ResponseField>

Connection uses a **30 second** ready/timeout window. Common errors are normalized (authentication, `ECONNREFUSED`, `ENOTFOUND`, `ETIMEDOUT`). `SSH_CHECK_CONNECTION` is a thin wrapper that returns `{ connected, error }` from the same test.

## Create session and pre-session setup

`SSH_CREATE_SESSION` returns a `Session` immediately with `status: 'creating'` while setup runs asynchronously. Progress is pushed on `SSH_SETUP_PROGRESS`; status updates use `SESSION_STATUS_CHANGED`.

```mermaid
sequenceDiagram
  participant UI as SSHConfigForm
  participant IPC as ssh.ipc
  participant SVC as ssh.service
  participant Remote as Remote host
  UI->>IPC: createSession(config, resumeSessionId?)
  IPC->>IPC: persist session + sdkSessionMappings
  IPC-->>UI: Session (creating)
  IPC->>SVC: connect(sessionId)
  alt worktreeScript or syncSettings
    IPC->>SVC: runPreSessionSetup
    SVC->>Remote: worktree script / SFTP sync
    SVC-->>IPC: workingDirectory?
  end
  IPC->>UI: SSH_SETUP_PROGRESS + status running/error
```

### Worktree setup script

When `worktreeScript` is set, `runWorktreeScript` runs:

```bash
source ~/.bash_profile 2>/dev/null; source ~/.profile 2>/dev/null; source ~/.bashrc 2>/dev/null; true
cd ~ && <worktreeScript> && echo "___WORKDIR_END___"
```

**Convention:** the last non-empty line of stdout **before** the marker is treated as the new `remoteWorkdir` / `worktreePath`. Setup output is stored on `session.setupOutput`. Resume selection is disabled in the UI because the final directory is unknown until the script finishes.

### Settings sync

When `syncSettings !== false`, `syncSettings` uploads (via SFTP/rsync) local `~/.claude/agents`, `commands`, `CLAUDE.md`, `settings.json`, skills, MCP config/auth, and optional GitHub `gh` hosts metadata. Sync failure logs a warning but does not fail the whole setup.

### SDK session mapping

| Case | `sdkSessionMappings[sessionId]` |
| --- | --- |
| Resume existing remote transcript | Remote SDK `sessionId` from picker |
| New session | Sentinel `'new'` until first agent turn assigns a real id |
| Local match on resume | Also sets `continuedFromSessionId` when host/user/workdir + mapping align |

Default session name: `{host}:{last-two-path-segments-of-workdir}`.

## Resume candidates

`SSH_LIST_RESUME_CANDIDATES` opens a short-lived probe connection (`ssh-resume-probe-{uuid}`), lists `~/.claude/projects/{escaped-remoteWorkdir}/*.jsonl` (excluding `agent-*`), sorts by mtime, returns the **top 10**, and disconnects the probe.

Transcript path escaping: `/` in the workdir becomes `-` (e.g. `/home/user/repo` → `-home-user-repo` under `~/.claude/projects/`).

## Teleport (local → remote)

Teleport moves a **local** session to SSH by copying transcripts after the final remote workdir is known.

<Steps>
<Step title="Open TeleportDialog">
Uses `SSHConfigForm` in teleport mode (`teleportSource` session, `onTeleport` handler).
</Step>
<Step title="Pre-session setup first">
`SSH_TELEPORT_SESSION` connects, runs `runPreSessionSetup` when `worktreeScript` or default sync applies, and captures `finalWorkdir`.
</Step>
<Step title="Copy transcripts">
`sshService.teleportSession` uploads `{sdkSessionId}.jsonl` from local `~/.claude/projects/-{escaped-local-worktree}/` into remote `~/.claude/projects/-{escaped-final-workdir}/` (SFTP uses absolute paths).
</Step>
<Step title="Create remote session record">
New session: `sshConfig`, `sdkSessionId` preserved, `teleportedFrom: sourceSessionId`, `status: 'stopped'`. Source must not already have `sshConfig`.
</Step>
</Steps>

<Warning>
Cannot teleport a session that is already SSH-backed (`sshConfig` present).
</Warning>

## Download (remote → local)

Reverse teleport: `DownloadSessionDialog` calls `downloadSession` with `DownloadSessionConfig`.

<ParamField body="localRepoPath" type="string" required>
Existing local git repository root (user picks via `dev.openLocalRepo`).
</ParamField>

<ParamField body="sessionName" type="string" required>
Name for the new local session.
</ParamField>

<ParamField body="branch" type="string">
Optional branch; defaults to remote `git branch --show-current` when omitted.
</ParamField>

Progress strings are broadcast on `SSH_DOWNLOAD_PROGRESS`.

| Step | Action |
| --- | --- |
| 1 | Require source `sshConfig`; read `sdkSessionId` from mapping or session |
| 2 | Connect (`download-{timestamp}`), read `git remote get-url origin`, branch, transcript path |
| 3 | Validate local repo exists; warn on origin URL mismatch but continue |
| 4 | Create worktree under `~/.claudette/worktrees/{repo-hash}/dl-{uuid}/` |
| 5 | SFTP transcript to `~/.claude/projects/{escaped-worktree}/{sdkSessionId}.jsonl` (non-fatal if missing) |
| 6 | New local session: `downloadedFrom`, `isWorktree: true`, `sdkSessionId` preserved, `status: 'stopped'` |

## Detached bridge and recovery

Remote harness turns do not use bare `ssh exec` for Claude. `createRemoteProcess` launches a **detached bridge** on the remote host so agent work survives app quit, sleep, and transport drops (`app.on('will-quit')` intentionally does not kill remote jobs).

```text
/tmp/claudette-ssh-bridge/{safeSessionId}/
  └── {jobId}/
        config.json
        stdout.log      # append-only stream log
        exit.json
        stdin.sock
        pid
        recovered.json  # after successful reattach
```

The bridge script is installed from `remote-bridge-script.ts` (`REMOTE_DETACHED_BRIDGE_SCRIPT`). Regression coverage: `node scripts/verify-ssh-detached-bridge-resume.js`.

### Recoverable job criteria

`getLatestRecoverableRemoteProcess` returns a job when:

- Not marked `recovered`
- Command is empty or `claude`
- **Active** (`kill -0` on pid), **or**
- **Completed** within the last **24 hours**, with `metadata.json`, `logBytes > 0`

`SSH_HAS_RECOVERABLE_REMOTE_PROCESS` / `SSH_HAS_ACTIVE_REMOTE_PROCESS` expose this to the renderer.

### Reattach flow

When you focus an SSH session, `startRemoteProcessMonitor` in `session.store.ts`:

1. Checks `hasRecoverableRemoteProcess` (fallback: `hasActiveRemoteProcess`)
2. Sets streaming UI state if recoverable
3. Loads transcript if messages are empty
4. If no backend query is active, calls `claude.resumeRemoteTurn` → `attachLatestDetachedCommandProcess` → tails `stdout.log` and replays JSONL stream events
5. Polls until the remote process finishes, then reloads messages (with delay so `onStreamEnd` can flush)

`SSH_CONNECTION_LOST` clears stale permission/question dialogs but **preserves** in-flight stream state; the backend reconnects and continues tailing the same log.

`SSH_RECONNECT` disconnects, waits 500ms, and calls `connect` again (invalidates transcript cache). Use from session card when the transport is dead but you want a fresh SSH session object.

Explicit cancel/delete still runs `killRemoteProcesses` (bridge job dirs and legacy tmux names).

## IPC surface (renderer)

| Channel | Pattern | Purpose |
| --- | --- | --- |
| `ssh:test-connection` | invoke | Full harness probe |
| `ssh:check-connection` | invoke | `{ connected }` shorthand |
| `ssh:create-session` | invoke | Create + async setup |
| `ssh:list-resume-candidates` | invoke | Remote transcript list |
| `ssh:get-saved-config` / `ssh:save-config` | invoke | `lastSSHConfig` |
| `ssh:sync-settings` | invoke | Manual settings push |
| `ssh:run-worktree-script` | invoke | Ad-hoc script run |
| `ssh:teleport-session` | invoke | Local → remote |
| `ssh:download-session` | invoke | Remote → local |
| `ssh:reconnect` | invoke | Transport reset |
| `ssh:browse-remote-files` | invoke | Directory listing for pickers |
| `ssh:has-active-remote-process` | invoke | Active bridge/tmux check |
| `ssh:has-recoverable-remote-process` | invoke | Reattach eligibility |
| `ssh:setup-progress` | event | Setup log + status |
| `ssh:download-progress` | event | Download step text |
| `ssh:connection-lost` | event | Transport dropped |

Preload namespace: `window.electronAPI.ssh` in `preload.ts`.

## Session fields (SSH-specific)

| Field | Meaning |
| --- | --- |
| `sshConfig` | Marks session as remote |
| `isDevMode` | `true` for SSH (no Docker) |
| `setupOutput` | Captured setup script stdout |
| `teleportedFrom` | Source local session id after teleport |
| `downloadedFrom` | Source SSH session id after download |
| `sdkSessionId` / `sdkSessionMappings` | Claude Agent SDK transcript id |
| `continuedFromSessionId` | Local session linked on SSH resume |

## Troubleshooting

<AccordionGroup>
<Accordion title="Connection test fails with no harness CLI">
Install at least one supported CLI on the remote host. The test result includes per-harness `npm`/`curl` install commands when capabilities are missing.
</Accordion>
<Accordion title="Setup script succeeded but wrong workdir">
Ensure the script prints the absolute workdir as its **last** stdout line before the internal `___WORKDIR_END___` marker.
</Accordion>
<Accordion title="Resume list empty">
Confirm transcripts exist under `~/.claude/projects/{escaped-remoteWorkdir}/` on the remote host and that you are not using a worktree script (resume is disabled until workdir is stable).
</Accordion>
<Accordion title="Stream stuck after laptop sleep">
Detached bridge should keep the remote turn alive. Focus the session to trigger recovery, or use reconnect on the session card. Check `/tmp/claudette-ssh-bridge/{sessionId}/` on the remote host for active jobs.
</Accordion>
<Accordion title="Download warns on git remote mismatch">
Download proceeds when local `origin` differs from remote; align remotes manually if you need strict parity.
</Accordion>
</AccordionGroup>

## Related pages

<CardGroup>
<Card title="Sessions and workspaces" href="/sessions-and-workspaces">
Session schema, statuses, worktrees, and `claudette-sessions` persistence.
</Card>
<Card title="IPC and preload bridge" href="/ipc-bridge">
How `electronAPI` maps to `ipcMain` handlers.
</Card>
<Card title="IPC channels reference" href="/ipc-channels-reference">
Full `SSH_*` channel catalog.
</Card>
<Card title="Manage coding sessions" href="/manage-sessions">
Teleport/import UX, session switcher, and lifecycle actions.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
SSH bridge recovery, logs, and common failures.
</Card>
</CardGroup>

---

## 12. Git workflows in Build

> Per-session worktree git operations: status, diff, commit, push/pull, branch watch events, and GitExplorer UI bindings to git IPC channels.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/12-git-workflows-in-build.md
- Generated: 2026-06-02T00:21:56.901Z

### Source Files

- `src/main/services/git.service.ts`
- `src/main/ipc/git.ipc.ts`
- `src/shared/constants/channels.ts`
- `src/renderer/components/git/GitExplorer.tsx`
- `src/shared/types/index.ts`

---
title: "Git workflows in Build"
description: "Per-session worktree git operations: status, diff, commit, push/pull, branch watch events, and GitExplorer UI bindings to git IPC channels."
---

Build runs Git against each session’s `worktreePath` (falling back to `repoPath`) through `GitService` in the main process, `registerGitHandlers` IPC in `git.ipc.ts`, and `window.electronAPI.git` exposed from `preload.ts`. The Git Explorer panel and status bar branch picker call those invoke channels; branch switches outside the UI are detected by `fs.watch` on `.git/HEAD` and pushed to renderers as `git:branch-changed`.

## Session path resolution

Every Git operation is keyed by `sessionId`. `GitService.getWorktreePath` loads the session from the session electron-store (`claudette-sessions`, via `getSessionStoreName()`), checking `sessions.{id}` then `discoveredSessions.{id}`:

| Field | Role |
| --- | --- |
| `worktreePath` | Primary directory passed to `simple-git` |
| `repoPath` | Fallback when `worktreePath` is empty |
| `branch` | Cached display name; updated by status bar watcher and `refreshSessionBranch` |

If neither path is set, handlers throw `Session {sessionId} not found`.

<Note>
Conversation forks (`parentSessionId`, `childSessionIds`) are separate from Git worktree forks (`isWorktree`, `parentRepoPath`). Git always targets the session’s filesystem checkout, not the chat fork graph.
</Note>

## Worktree layout at session creation

Optional isolated checkouts are created when starting a **dev session** with `createWorktree: true` (`DEV_CREATE_SESSION` in `dev.ipc.ts`), not through the Git IPC surface.

```text
~/.claudette/worktrees/{repo-hash}/wt-{sessionId-prefix}/
  └── linked from main repo via: git worktree add <path> <branch>
```

- `getMainRepoPath` resolves nested worktrees back to the main repository before `git worktree add`.
- `repoPath` on the session stays the user-selected folder; `worktreePath` points at the central worktree directory.
- Optional `.claudette/worktree-setup.sh` runs in the new worktree; `.claudette/worktree-setup.md` is stored on the session as `worktreeInstructions`.
- GitHub clone sessions (`session.service.ts`) set `worktreePath = repoPath` after clone—no separate worktree.

`GitService.createWorktree` / `removeWorktree` exist for programmatic worktree management but are **not** registered as IPC handlers; dev creation uses `simple-git` raw commands inline.

## Main-process GitService

`GitService` wraps [simple-git](https://github.com/steveukx/git-js) instances rooted at the session worktree.

| Method | Behavior |
| --- | --- |
| `getStatus` | `git.status()` → `current`, `tracking`, `ahead`/`behind`, `files[]` as `FileChange` |
| `getLog` | `git.log({ maxCount })` → `Commit[]` |
| `getBranches` | `git.branch(['-a', '-v'])` → local + remote `Branch[]` |
| `checkout` | `git.checkout(branch)` |
| `getDiff` | With `commitHash`: diff between parent and commit; else staged + unstaged working tree |
| `commit` | `git.add('.')` then `git.commit(message)` → returns commit hash |
| `push` | `git.push()` if tracking exists; else `git.push(['-u', 'origin', current])` |
| `pull` | `git.pull()` |
| `clone` | Used by session creation, not per-session |
| `stash` / `stashPop` | Implemented on service only—no IPC exposure |

### Worktree-aware HEAD watching

For linked worktrees, `.git` is a file pointing at the real `gitdir`. `getHeadPath` resolves that path and watches the resolved `HEAD` file. `readCurrentBranch` parses `ref: refs/heads/...` or returns an 8-character detached HEAD prefix.

## IPC channels

Invoke handlers are registered in `registerGitHandlers`; one-way events are sent from main to all `BrowserWindow` instances.

| Channel constant | Pattern | Payload / return |
| --- | --- | --- |
| `GIT_STATUS` | invoke | `sessionId` → status object |
| `GIT_LOG` | invoke | `sessionId`, optional `limit` → `Commit[]` |
| `GIT_BRANCHES` | invoke | `sessionId` → `Branch[]` |
| `GIT_CHECKOUT` | invoke | `sessionId`, `branch` |
| `GIT_DIFF` | invoke | `sessionId`, optional `commitHash` → unified diff string |
| `GIT_COMMIT` | invoke | `sessionId`, `message` → commit hash |
| `GIT_PUSH` | invoke | `sessionId` |
| `GIT_PULL` | invoke | `sessionId` |
| `GIT_CLONE` | invoke | `url`, `targetPath` |
| `GIT_REMOTE_BRANCH` | invoke | `sessionId` → branch name or `null` (SSH only) |
| `GIT_WATCH_BRANCH` | invoke | `sessionId` → `{ success, branch?, error? }` |
| `GIT_UNWATCH_BRANCH` | invoke | `sessionId` → `{ success: true }` |
| `GIT_BRANCH_CHANGED` | **event** | `{ sessionId, branch }` |

On branch file changes, `gitService.onBranchChange` broadcasts `GIT_BRANCH_CHANGED` to every window.

### SSH remote branch

When `session.sshConfig` is set, `GIT_REMOTE_BRANCH` runs `git -C "{remoteWorkdir}" rev-parse --abbrev-ref HEAD` over the SSH bridge (`ssh.service.ts`). Local `getStatus` is not used for SSH sessions in `refreshSessionBranch`.

## Preload bridge

`preload.ts` maps `window.electronAPI.git` to the channels above. `onBranchChanged` registers an `ipcRenderer.on` listener and returns an unsubscribe function.

<Warning>
`GIT_COMMIT` is exposed on the bridge but **GitExplorer does not call it** today. Commits from the UI would require a new control or agent tooling; agents typically commit via the terminal harness.
</Warning>

## Renderer: materialized session retry

Git calls from the renderer often wrap:

```typescript
withMaterializedSession(sessionId, () => window.electronAPI.git.getStatus(sessionId))
```

If the main process reports `Session {id} not found`, the helper issues `sessions.update(sessionId, {})` to materialize the record in the store, then retries once. Status bar branch watching uses the same pattern when `watchBranch` fails with a not-found error.

## GitExplorer panel

Mounted from `MainContent.tsx` when the git side panel is open (`isGitPanelOpen`). Requires `session.status === 'running'`; otherwise shows “Start the session to view git”.

```mermaid
sequenceDiagram
  participant GE as GitExplorer
  participant API as electronAPI.git
  participant IPC as git.ipc handlers
  participant GS as GitService

  GE->>API: getLog / getBranches / getStatus
  API->>IPC: invoke git:*
  IPC->>GS: simple-git at worktreePath
  GS-->>IPC: typed results
  IPC-->>GE: React Query cache

  GE->>API: push / pull
  API->>IPC: GIT_PUSH / GIT_PULL
  GE->>GE: refetch queries
```

| UI area | Query / action | IPC |
| --- | --- | --- |
| Header | Current branch, ahead/behind badges | `getStatus` (5s `refetchInterval`) |
| History tab | Commit timeline | `getLog(sessionId, 100)` |
| Branches tab | Local / remote lists, checkout | `getBranches`, `checkout` |
| Changes tab | File list + diff preview | `getStatus`, `getDiff` (optional commit hash from history selection) |
| Toolbar | Pull, push, refresh | `pull`, `push`, manual refetch |

Diff preview truncates at 2000 characters in the Changes tab.

## Status bar branch workflow

Separate from GitExplorer, the status bar keeps `session.branch` in sync:

<Steps>
  <Step title="Start watcher">
    On active session change, call `git.watchBranch(activeSessionId)`. Retry after `sessions.update` if the session was not materialized.
  </Step>
  <Step title="Listen for events">
    Subscribe to `git.onBranchChanged` and call `updateSession(sessionId, { branch })` for any session—not only the active one.
  </Step>
  <Step title="Initial sync">
    When watch succeeds, `refreshSessionBranch` loads branch via `getRemoteBranch` (SSH) or `getStatus` (local).
  </Step>
  <Step title="Switch branch">
    Branch menu loads `getBranches`, then `checkout` + `updateSession` with the new branch name.
  </Step>
  <Step title="Cleanup">
    On session switch or unmount, `unwatchBranch(activeSessionId)`.
  </Step>
</Steps>

External `git checkout` in a terminal updates `HEAD`, triggers `fs.watch`, and flows through `GIT_BRANCH_CHANGED` without polling.

## Shared types

Defined in `src/shared/types/index.ts`:

<ResponseField name="Commit" type="object">
  `hash`, `message`, `author`, `authorEmail`, `date`, `parents`
</ResponseField>

<ResponseField name="Branch" type="object">
  `name`, `current`, optional `remote`, `commit`
</ResponseField>

<ResponseField name="FileChange" type="object">
  `path`, `status` (`added` | `modified` | `deleted` | `renamed`), `additions`, `deletions` (line counts default to 0 in status mapping)
</ResponseField>

## Failure modes

| Symptom | Likely cause |
| --- | --- |
| `Session … not found` | Session missing from `claudette-sessions` or paths unset; try materialize via `sessions.update` |
| Git panel empty while session “running” | Panel gated on `status === 'running'`; start the session |
| Branch stuck on SSH | `GIT_REMOTE_BRANCH` failed—check SSH connection and `remoteWorkdir` |
| Watch returns `success: false` | No `.git/HEAD` at resolved path (non-repo folder) |
| Push sets upstream | First push uses `-u origin <current>` when no tracking branch |

<Tip>
For full channel semantics across domains, see the IPC channels reference. Session fields (`worktreePath`, `isWorktree`, SSH config) are documented on the sessions and workspaces page.
</Tip>

## Related pages

<CardGroup>
  <Card title="Sessions and workspaces" href="/sessions-and-workspaces">
    Session schema, worktree forks, and claudette-sessions persistence.
  </Card>
  <Card title="IPC and preload bridge" href="/ipc-bridge">
    invoke vs push patterns and contextBridge exposure.
  </Card>
  <Card title="IPC channels reference" href="/ipc-channels-reference">
    Complete GIT_* channel catalog.
  </Card>
  <Card title="SSH remote sessions" href="/ssh-remote-sessions">
    Remote workdir and `GIT_REMOTE_BRANCH` over SSH.
  </Card>
</CardGroup>

---

## 13. Browser preview and inspection

> In-window webview navigation, CDP attachment per session, DOM inspector injection, snapshots, console/network capture, and Stagehand integration for agent-driven browsing.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/13-browser-preview-and-inspection.md
- Generated: 2026-06-02T00:22:57.282Z

### Source Files

- `src/main/services/browser.service.ts`
- `src/main/services/cdp-proxy.service.ts`
- `src/main/services/stagehand.service.ts`
- `src/main/ipc/browser.ipc.ts`
- `src/renderer/components/preview/BrowserPreview.tsx`
- `.notes/cdp-proxy-hardening-summary.md`

---
title: "Browser preview and inspection"
description: "In-window webview navigation, CDP attachment per session, DOM inspector injection, snapshots, console/network capture, and Stagehand integration for agent-driven browsing."
---

Build embeds a per-session Electron `<webview>` in `BrowserPreview`, registers each instance with `browser.service` for CDP control, and exposes a localhost CDP proxy (`cdp-proxy.service`) so Playwright and Stagehand can attach over `connectOverCDP`. Agents reach the stack through the in-process `claudette-browser` MCP server in `claude.service`, which prefers Stagehand for natural-language automation while `browser.service` and `computer-use.service` provide direct CDP paths for snapshots, selectors, console/network capture, and coordinate-based interaction.

## Architecture

```mermaid
flowchart TB
  subgraph renderer["Renderer — BrowserPreview.tsx"]
    WV["webview partition persist:browser-{sessionId}"]
    INS["DOM inspector script GREP_INSPECTOR"]
  end

  subgraph main["Main process"]
    BS["browser.service"]
    CDP["cdp-proxy.service :9223"]
    SH["stagehand.service"]
    CU["computer-use.service"]
    CS["claude.service — claudette-browser MCP"]
  end

  WV -->|register-webview IPC| BS
  BS -->|notifyNewTarget / unregisterWebview| CDP
  SH -->|connectOverCDP ws://localhost:9223/devtools/browser| CDP
  CDP -->|debugger.attach 1.3| WV
  BS -->|sendCommand Runtime/Page/Input/Network| WV
  CS --> SH
  CS --> BS
  CS --> CU
  CU --> BS
```

| Layer | Module | Responsibility |
|-------|--------|----------------|
| UI | `src/renderer/components/preview/BrowserPreview.tsx` | Toolbar navigation, webview lifecycle, inspector injection, IPC fallbacks for snapshots/actions |
| Session CDP | `src/main/services/browser.service.ts` | `sessionId` ↔ `webContentsId` map, debugger attach, navigate/click/type, console/network buffers |
| External CDP | `src/main/services/cdp-proxy.service.ts` | HTTP `/json/*`, browser/page WebSockets, Target domain for Playwright |
| AI automation | `src/main/services/stagehand.service.ts` | Stagehand V3 LOCAL, webview CDP or headless fallback |
| Visual automation | `src/main/services/computer-use.service.ts` | 1024×768 virtual coordinates via CDP Input/Page |
| Agents | `src/main/services/claude.service.ts` | MCP tools, `ensureBrowserPanelOpen`, `BROWSER_UPDATE` events |
| IPC invoke | `src/main/ipc/browser.ipc.ts` | Snapshot capture, navigate, get snapshot, clear storage |

On app ready, main starts the CDP proxy before agents connect:

```typescript
await cdpProxyService.start(); // default port from CDP_PROXY_PORT or 9223, binds 127.0.0.1 only
```

## In-window webview

Each running session can show `BrowserPreview` when the browser panel is open (`ui.store` → `isBrowserPanelOpen`, toggled from the globe toolbar control).

<ParamField body="partition" type="string" required>
`persist:browser-${session.id}` — isolated cookies and storage per session.
</ParamField>

<ParamField body="initial URL" type="string">
`session.lastBrowserUrl`, else last `localhost` URL from chat transcript, else `http://localhost:${session.ports?.web || 3000}`.
</ParamField>

Navigation uses `webview.loadURL` when `url` state changes; `lastBrowserUrl` is persisted on `did-navigate`. Connection failures to local dev servers (`ERR_CONNECTION_REFUSED`, etc.) retry reload up to three times at 3s intervals.

The webview stays mounted when the panel is hidden (`isVisible` toggles CSS `display` only). Pop-out uses `window.electronAPI.app.openBrowserWindow()`.

## Webview registration and CDP attachment

When the webview fires `dom-ready`, the renderer calls `window.electronAPI.browser.registerWebview(sessionId, webContentsId)`. Main stores bidirectional maps and notifies the CDP proxy so late-connecting Playwright clients receive `Target.attachedToTarget`.

Unregister on unmount sends `browser:unregister-webview`; main detaches the debugger, calls `cdpProxyService.unregisterWebview`, and broadcasts `Target.targetDestroyed` to browser-level WebSocket clients.

<Warning>
Automation APIs return an error until at least one webview is registered: *"Browser panel is not open. Please open the browser panel (use the globe icon in the toolbar)…"*
</Warning>

`claude.service.ensureBrowserPanelOpen` sends `BROWSER_OPEN_PANEL` and polls up to 5s for registration when agents invoke browser tools before the user opens the panel.

## CDP proxy (Playwright / Stagehand bridge)

`CdpProxyService` implements Chrome discovery endpoints and WebSocket routing:

| Endpoint | Purpose |
|----------|---------|
| `GET /json/version` | Returns `webSocketDebuggerUrl: ws://localhost:{port}/devtools/browser` |
| `GET /json/list` | Page targets keyed by `sessionId` |
| `GET /json/cookies` | Cookies from `persist:browser-{firstSessionId}` |
| `ws://localhost:{port}/devtools/browser` | Browser-level connection; Target domain (`setAutoAttach`, `attachToTarget`, …) |
| `ws://localhost:{port}/devtools/page/{sessionId}` | Direct page CDP forwarding |

<ParamField body="CDP_PROXY_PORT" type="number">
Optional env override; default `9223`. If the port is in use, the service increments and retries.
</ParamField>

Security: the HTTP server listens on `127.0.0.1` only.

CDP proxy hardening:

- `sendCommandWithTimeout` — 30s cap on `debugger.sendCommand` to avoid hangs after detach
- `debugger.on('detach')` — emits `Target.detachedFromTarget` / `Target.targetDestroyed`
- `notifyNewTarget` — resolves race when Stagehand calls `setAutoAttach` before the webview exists
- `unregisterWebview` — cleans sessions and closes stale page WebSockets

Stagehand connects with `localBrowserLaunchOptions.cdpUrl = cdpProxyService.getBrowserWebSocketUrl()`. If no target exists after retries, it launches a headless browser (`headless: true`) that does not update the visible webview.

## Browser service (direct CDP)

`BrowserService` attaches Electron `webContents.debugger` at protocol `1.3` and routes commands per session.

| Method area | CDP / behavior |
|-------------|----------------|
| `navigate` | `Page.navigate`; IPC fallback `browser:navigate` to renderer |
| `click` / `type` | `Runtime.evaluate` + `Input.dispatchMouseEvent` / `Input.insertText` |
| `captureScreenshotCDP` | `Page.captureScreenshot` (PNG base64) |
| `getDOM` | `Runtime.evaluate` → `document.documentElement.outerHTML` |
| `enableConsoleCapture` | `Runtime.enable`; buffers last 500 `Runtime.consoleAPICalled` / `Runtime.exceptionThrown` |
| `enableNetworkCapture` | `Network.enable`; buffers last 200 requests with timing |
| `enableDebugging` | Both Runtime and Network domains |
| `captureSnapshot` | Parallel CDP screenshot + HTML + page info; 10s IPC fallback via `browser:capture-snapshot` |
| `saveSnapshotToDisk` | Writes PNG under `{tmpdir}/claudette-snapshots/` |

Automation visual feedback: `emitAutomationEvent` pushes `browser:automation-event` (`start` / `position` / `end`) for click ripples and status overlays in `BrowserPreview`.

Session deletion calls `cleanupSession` from `session.ipc` to drop maps, logs, and debugger state.

## DOM inspector

Inspector mode toggles from the target (crosshair) toolbar button. `injectInspector` runs a large `executeJavaScript` bundle that:

- Adds purple element overlay and yellow text-node highlights
- Resolves React component names via `__REACT_DEVTOOLS_GLOBAL_HOOK__` or fiber keys
- Builds CSS selectors walking up to `document.body`
- On element click: posts `GREP_INSPECTOR:{json}` through `console-message` → renderer captures element screenshot, dispatches `grep-insert-chat` with selector/HTML/React metadata
- On text click: inline editor; Enter sends `type: 'text_replacement'` to trigger `sendMessage` with an edit prompt
- Shift+drag region select: grid-samples `elementsFromPoint`, attaches region screenshot + element list to chat

Per-session inspector state lives in `ui.store` (`sessionInspectorActive`, `sessionSelectedElement`).

## Snapshots

| Path | Trigger | Output |
|------|---------|--------|
| CDP | `browserService.captureSnapshot` | `{ url, screenshot, html, timestamp }` |
| Renderer IPC | Main sends `browser:capture-snapshot` | `webview.capturePage()` + `outerHTML` → `browser:snapshot-captured` |
| Stagehand | `stagehandService.captureSnapshot` | Playwright page screenshot + HTML + title |
| User | Camera toolbar button | Viewport PNG attached to chat via `grep-insert-chat` |
| MCP | `BrowserSnapshot` tool | Stagehand navigate + snapshot; image in tool result |

Snapshots for agent tools are capped at 10 000 characters of HTML in the text portion of MCP responses.

## Console and network capture

Console and network data are collected only after explicit enable per session (`enableConsoleCapture`, `enableNetworkCapture`, or `enableDebugging`). Retrieval APIs:

- `getConsoleLogs(sessionId, { type?, limit?, since? })`
- `getNetworkRequests(sessionId, { urlPattern?, method?, status?, limit? })`
- `getResponseBody(sessionId, requestId)` via `Network.getResponseBody`

SSH/local chrome-devtools bridging in `claude.service.executeLocalChromeDevtool` exposes `get_console_logs` against `browserService.getConsoleLogs`. Cookie read/write uses the session partition or `/json/cookies` on the proxy.

## Stagehand integration

`StagehandService` wraps `@browserbasehq/stagehand` in `LOCAL` mode.

<ParamField body="model" type="string">
Default `google/gemini-2.5-flash` (requires Google API key via settings or `GOOGLE_API_KEY` / `GOOGLE_GENERATIVE_AI_API_KEY`).
</ParamField>

<ParamField body="anthropicApiKey" type="string">
Optional; copied to `process.env.ANTHROPIC_API_KEY` when set on the service.
</ParamField>

`syncCookiesFromWebview` copies Electron partition cookies into the Playwright context before navigation so agent and user sessions share auth.

Operations include `navigate`, `act`, `observe`, `agent`, `extract`, `captureSnapshot`, selector `click`/`type`, and `reconnectToWebview`. Timeouts: 30s for navigate/act/observe, 60s for agent. Destroyed-target errors trigger one re-init retry.

UI sync: successful tool runs call `emitBrowserUpdate` → `BROWSER_UPDATE` with screenshot and optional URL; `BrowserPreview` navigates the visible webview to match Stagehand when connected to the shared CDP target.

## Agent MCP tools (`claudette-browser`)

Fresh MCP server per query (`getBrowserMcpServer`). Tool groups:

<Tabs>
<Tab title="Stagehand (primary)">
`BrowserAct`, `BrowserObserve`, `BrowserAgent`, `BrowserExtractData`, `BrowserNavigate`, `BrowserSnapshot` — natural language and structured extraction.
</Tab>
<Tab title="Selector / CDP">
`BrowserClick`, `BrowserType`, `BrowserExtract`, `BrowserGetInfo`, `BrowserGetDOM` — CSS selectors via Stagehand page APIs.
</Tab>
<Tab title="Computer use">
`computer` — screenshot-driven 1024×768 coordinate actions via `computer-use.service`.
</Tab>
</Tabs>

Remote SSH sessions can execute browser tools locally on the machine running Build (`executeLocalBrowserTool`, `executeLocalChromeDevtool`) while the agent runs elsewhere.

Auto Build routing treats browser-related chat signals (`browser`, `screenshot`, `dom`, `playwright`, `stagehand`, `webview`, `localhost`, DOM attachments) as `needsBrowser` in `auto-router.service`.

## IPC and preload surface

**Invoke handlers** (`browser.ipc.ts` + `preload.ts`):

| Channel | Direction | Role |
|---------|-----------|------|
| `browser:capture-snapshot` | invoke | CDP/IPC snapshot |
| `browser:navigate-to` | invoke | CDP navigate |
| `browser:get-snapshot` | invoke | Last cached snapshot |
| `browser:clear-storage` | invoke | Clears `persist:browser` partition storage |

**Push channels** (main → renderer unless noted):

| Channel | Role |
|---------|------|
| `browser:register-webview` / `browser:unregister-webview` | Renderer → main registration |
| `browser:navigate` | Main → renderer URL load |
| `browser:capture-snapshot` | Main → renderer capture request |
| `browser:snapshot-captured` | Renderer → main snapshot payload |
| `browser:action` / `browser:action-result` | Renderer executes click/type/script fallbacks |
| `browser:automation-event` | CDP automation overlays |
| `browser:update` | Stagehand screenshot/URL sync |
| `browser:open-panel` | Open browser panel for session |

Full channel names are listed under browser domains in the IPC channels reference page.

## Operational constraints

<Steps>
<Step title="Open the browser panel">
Toggle the globe control or let an agent tool send `browser:open-panel`. The session must be `running`.
</Step>
<Step title="Verify CDP registration">
After load, logs should show webview registration; `/json/list` should return a target whose `id` equals `sessionId`.
</Step>
<Step title="Configure provider keys for Stagehand AI">
Set Google API key in settings for `BrowserAct` / `BrowserAgent`; Anthropic key is optional depending on model wiring.
</Step>
</Steps>

<AccordionGroup>
<Accordion title="CDP command timeouts or hangs">
Commands abort after 30s with a detach/timeout message. Close and reopen the browser panel to re-register the webview; restart the app if port 9223 is stuck.
</Accordion>
<Accordion title="Stagehand uses headless browser">
No webview was registered when Stagehand initialized. Reconnect via a browser tool after opening the panel, or call flows that invoke `reconnectToWebview`.
</Accordion>
<Accordion title="Visible webview out of sync with agent">
`browserService.navigate` is called after Stagehand navigation when possible; if Stagehand fell back to its own browser, only the headless page updates until CDP attaches to the webview.
</Accordion>
<Accordion title="Clear auth / storage">
Trash icon in toolbar: `clearStorage` IPC plus in-page `localStorage` / IndexedDB wipe, then hard reload.
</Accordion>
</AccordionGroup>

## Related pages

<CardGroup>
<Card title="Electron process model" href="/electron-architecture">
Main vs renderer boundaries, service layout, and CDP port startup in the main process.
</Card>
<Card title="IPC and preload bridge" href="/ipc-bridge">
How `electronAPI.browser` maps to `ipcMain` handlers and push events.
</Card>
<Card title="IPC channels reference" href="/ipc-channels-reference">
Complete `browser:*` channel catalog with invoke vs push semantics.
</Card>
<Card title="Configure API keys and providers" href="/configure-providers">
Anthropic, Google, and other keys used by Stagehand and harness tools.
</Card>
<Card title="MCP servers" href="/mcp-servers">
How `claudette-browser` is loaded into the Claude Agent SDK per session.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
CDP timeouts, MCP harness sync, and dev instance issues.
</Card>
</CardGroup>

---

## 14. MCP servers

> Install stdio/http/sse MCP servers from the registry marketplace, claudette-mcp-servers store schema, harness sync to Cursor/Gemini/Codex/OpenCode, and runtime loading into Claude Agent SDK.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/14-mcp-servers.md
- Generated: 2026-06-02T00:22:47.499Z

### Source Files

- `src/main/services/mcp.service.ts`
- `src/main/ipc/mcp.ipc.ts`
- `src/renderer/components/extensions/MCPMarketplace.tsx`
- `src/renderer/components/extensions/MCPInstallDialog.tsx`
- `scripts/verify-mcp-harness-sync.ts`
- `scripts/verify-mcp-runtime-matrix.js`

---
title: "MCP servers"
description: "Install stdio/http/sse MCP servers from the registry marketplace, claudette-mcp-servers store schema, harness sync to Cursor/Gemini/Codex/OpenCode, and runtime loading into Claude Agent SDK."
---

Build stores MCP server definitions in the `claudette-mcp-servers` electron-store, installs them from the official MCP Registry marketplace or raw JSON, normalizes remote HTTP/SSE endpoints through a pinned `mcp-remote@0.1.38` stdio wrapper, syncs merged configs into Cursor/Gemini/Codex/OpenCode harness files, and injects the normalized map into the Claude Agent SDK `mcpServers` option on every agent turn.

## Architecture

```mermaid
flowchart TB
  subgraph UI["Renderer"]
    MCPMarketplace["MCPMarketplace"]
    MCPInstallDialog["MCPInstallDialog"]
    ExtensionsExplorer["ExtensionsExplorer"]
  end

  subgraph Main["Main process"]
    McpIpc["mcp.ipc.ts"]
    McpService["mcp.service.ts"]
    ClaudeService["claude.service.ts"]
    HarnessServices["cursor / gemini / codex / opencode services"]
    SshService["ssh.service.ts"]
    StdioBridge["mcp-stdio-bridge.service.ts"]
  end

  subgraph Storage["electron-store"]
    McpStore["claudette-mcp-servers"]
    HarnessSyncStore["claudette-mcp-harness-sync"]
  end

  subgraph External["External"]
    Registry["registry.modelcontextprotocol.io"]
    McpRemote["npx mcp-remote@0.1.38"]
    McpAuth["~/.mcp-auth"]
  end

  subgraph HarnessFiles["Harness config files"]
    CursorJson["~/.cursor/mcp.json"]
    GeminiJson["~/.gemini/settings.json"]
    CodexToml["~/.codex/config.toml"]
    OpenCodeJson["~/.config/opencode/build-mcp.json"]
    ClaudeJson["~/.claude/config.json SSH only"]
  end

  MCPMarketplace --> McpIpc
  MCPInstallDialog --> McpIpc
  ExtensionsExplorer --> McpIpc
  McpIpc --> McpService
  McpService --> McpStore
  McpService --> HarnessSyncStore
  McpService --> Registry
  McpService --> HarnessFiles
  McpService --> McpRemote
  ClaudeService --> McpService
  HarnessServices --> McpService
  HarnessServices --> SshService
  SshService --> McpService
  SshService --> StdioBridge
  SshService --> McpAuth
  SshService --> ClaudeJson
  ClaudeService --> StdioBridge
```

<Note>
Build’s MCP catalog is separate from Claude Code’s `~/.claude/config.json`. Local Claude harness turns receive MCP servers directly through the Agent SDK; SSH sessions additionally merge into remote `~/.claude/config.json`.
</Note>

## Storage schema

### `claudette-mcp-servers`

Each key is a server ID (marketplace installs use the registry ID’s last path segment, e.g. `ai.exa/exa` → `exa`). Values follow `MCPServerConfig`:

| Field | Type | When used |
|-------|------|-----------|
| `type` | `stdio` \| `http` \| `sse` | Transport hint; remote installs set `http` or `sse` from registry `transport_type` |
| `command` | string | Stdio launcher (`npx`, `node`, …) |
| `args` | string[] | Stdio arguments; marketplace npm installs use `['-y', '<package>']` |
| `env` | `Record<string, string>` | Stdio environment variables (registry package env vars) |
| `url` | string | HTTP/SSE remote endpoint |
| `headers` | `Record<string, string>` | Remote auth headers (registry remote headers) |
| `alwaysLoad` | boolean | Optional; preserved through normalization |
| `tools` | `Record<string, unknown>[]` | Optional tool metadata; preserved through normalization |

On read, stored configs are normalized: any `mcp-remote` package arg is pinned to `mcp-remote@0.1.38`, and `http://` localhost URLs gain `--allow-http` when missing.

### `claudette-mcp-harness-sync`

Tracks harness merge state:

| Key | Purpose |
|-----|---------|
| `managedServerIds` | Server IDs last written by Build sync (sorted) |
| `removedServerIds` | IDs Build removed; used to delete stale entries from harness files on next sync |

Install marks a server as installed (removes it from `removedServerIds`). Uninstall appends the ID to `removedServerIds`.

## Transport types and normalization

### Stdio (native)

A local process with `command` + `args`. These are the servers that cannot run on a remote SSH host without bridging.

`isNativeStdioServer()` returns true when the config has a `command`, no `url`, and args do not include `mcp-remote` / `mcp-remote@…`.

### HTTP and SSE (remote)

Registry remotes install with `type: 'http'` or `type: 'sse'` and a `url`. Optional `headers` hold auth values from the install dialog.

### `mcp-remote` wrapper (default for Claude and harnesses)

For HTTP/SSE endpoints, Build normally wraps remotes as stdio:

```text
npx -y mcp-remote@0.1.38 <url> [--allow-http] [--header "Name: ${BUILD_MCP_...}"]
```

Header values become `BUILD_MCP_<SERVER>_<HEADER>` environment variables so OAuth tokens in `~/.mcp-auth` can be shared across harnesses.

`normalizeMcpServerForClaude()` applies this wrapping unless `preferNativeRemoteTransports: true` is passed (then SSE/HTTP types pass through with `url` and `headers` intact).

### Localhost reverse tunnels

Configs referencing `http://localhost` or `127.0.0.1` ports are detected via `getLocalhostMcpPorts()`. SSH setup creates reverse tunnels so remote harnesses reach the local service.

## Registry marketplace

| Constant | Value |
|----------|-------|
| API | `https://registry.modelcontextprotocol.io/v0/servers` |
| Cache TTL | 5 minutes |
| Max pages | 20 (pagination via `metadata.nextCursor`) |
| User-Agent | `Claudette/1.0` |

`fetchMarketplaceServers()` paginates, transforms entries to `MarketplaceMCPServer`, sorts by name, and falls back to stale cache on fetch errors.

`MarketplaceMCPServer` fields include `id`, `name`, `description`, `packages`, `remotes`, `authFields`, `requiresAuth`, `repositoryUrl`, `websiteUrl`, `icon`, `keywords`, `isLatest`, `publishedAt`.

Auth fields are extracted from required/secret package environment variables and remote headers, deduplicated by key.

### Install from marketplace

`installServer()` chooses:

1. **npm package** (`registry_name === 'npm'`): stdio `npx -y <package.name>` with `env` from auth dialog values.
2. **First remote**: `type` from `transport_type` (`sse` vs `http`), `url`, optional `headers`.
3. Otherwise: `{ success: false, error: 'No installation method available for this server' }`.

After store write, `prewarmMcpRemoteAuth()` may spawn `npx` with the wrapped args to complete OAuth; successful prewarm triggers harness + SSH resync via IPC.

## UI entry points

| Surface | Flow |
|---------|------|
| **MCP Marketplace** (`MCPMarketplace.tsx`) | `electronAPI.mcp.getMarketplace()` → install via `MCPInstallDialog` → `electronAPI.mcp.install(serverId, authValues)` |
| **Extensions explorer** (`ExtensionsExplorer.tsx`) | Manual add/edit: stdio (`command`, multiline `args`, `env`) or URL (`url`, `headers`); `electronAPI.mcp.installRaw(name, config)` |
| **Unified marketplace** | Also supports raw install for custom configs |

Installed state matches by full registry ID or trailing segment (`serverId.split('/').pop()`).

## IPC API

| Channel | Handler behavior |
|---------|------------------|
| `mcp:get-servers` | `getActiveServers()` — built-in `claudette-browser` plus installed servers |
| `mcp:get-raw-config` | Raw store entry for one server ID |
| `mcp:get-marketplace` | Registry fetch (cached) |
| `mcp:install-server` | Resolve marketplace entry, `installServer()`, then `syncHarnessesAndSshSessions()` |
| `mcp:install-server-raw` | `installServerRaw()` (requires `command` or `url`), then sync |
| `mcp:uninstall-server` | Delete from store, mark removed, sync |

Preload exposes these as `window.electronAPI.mcp.*`.

After install/uninstall/auth prewarm, IPC runs `syncLocalHarnessConfigs()` and queues SSH sync for connected sessions (30s timeout per session, coalesced if already in flight).

## Harness sync

`syncLocalHarnessConfigs()` writes **sanitized** configs (always `mcp-remote`-wrapped remotes) to:

| Harness | Path | Format |
|---------|------|--------|
| Cursor | `~/.cursor/mcp.json` | JSON `mcpServers` merge |
| Gemini | `~/.gemini/settings.json` | JSON `mcpServers` merge |
| Codex | `~/.codex/config.toml` | TOML `[mcp_servers.<name>]` blocks |
| OpenCode | `~/.config/opencode/build-mcp.json` | JSON merged over `~/.config/opencode/opencode.json` |

Merge behavior:

- **Upsert** all current Build servers into existing harness files.
- **Remove** IDs in `removeServerIds` (managed + removed + current set).
- **Preserve** unrelated harness-only servers (e.g. `customRemoteOnly` in verifier fixtures).

OpenCode launch sets `OPENCODE_CONFIG` to `build-mcp.json` (local and remote).

Cursor SDK path uses `toCursorSdkMcpServers()` to map sanitized configs to `@cursor/sdk` `McpServerConfig`. Cursor CLI adds `--approve-mcps` on launch after sync.

On app `ready`, main calls `mcpService.syncLocalHarnessConfigs()` once.

Each harness service (`cursor`, `cursor-cli`, `gemini`, `codex`, `opencode`) calls sync before streaming: local → `syncLocalHarnessConfigs()`; SSH → `sshService.syncMcpConfigsToRemote()`.

## Claude Agent SDK runtime

On **every** Claude message, `claude.service.ts` builds `mcpServersConfig`:

```text
claudette-browser (if browser history / local session)
+ user MCP from mcpService.getClaudeMcpServersConfig()  [or SSH bridge variant]
+ optional qmd stdio (local only, when enabled)
→ passed as mcpServers to Agent SDK query
```

| Session | User MCP source |
|---------|-----------------|
| Local | `getClaudeMcpServersConfig()` — wrapped remotes, pinned `mcp-remote` |
| SSH | `getClaudeMcpServersConfigForSSH(bridgePorts)` — native stdio replaced with `http://127.0.0.1:{port}/mcp` |

SSH also syncs `getClaudeMcpSyncDataForSSH()` into remote `~/.claude/config.json` before query. `chrome-devtools` stdio is stripped on remote and reattached as a local in-process MCP server.

Built-in **Claudette Browser** (`id: claudette-browser`, `type: sdk`) exposes `browser_snapshot`, `browser_navigate`, `browser_act` — not stored in `claudette-mcp-servers`.

## SSH remote MCP

`syncMcpConfigsToRemote()` sequence:

<Steps>
<Step title="Start stdio bridges">
`mcpStdioBridgeService.startBridgesForSession()` for `getNativeStdioServers()` — local HTTP on `/mcp` per server.
</Step>
<Step title="Sync Claude config">
Merge into `~/.claude/config.json` via `getClaudeMcpSyncDataForSSH()`.
</Step>
<Step title="Sync MCP auth">
SFTP `~/.mcp-auth` to remote; replicate token files across `mcp-remote-*` version dirs.
</Step>
<Step title="Sync harness files">
Remote `~/.cursor/mcp.json`, `~/.gemini/settings.json`, `~/.codex/config.toml`, `~/.config/opencode/build-mcp.json`.
</Step>
<Step title="Reverse tunnels">
Localhost MCP ports and bridge ports tunneled to remote `127.0.0.1:{port}`.
</Step>
</Steps>

Harness MCP sync is cached per host for 5 minutes. MCP auth sync is cached when local `~/.mcp-auth` mtime is unchanged.

## Remote auth prewarm

After install, if the normalized config is an `npx mcp-remote@0.1.38` wrapper, Build spawns a detached prewarm process (default `--auth-timeout 180`, overall timeout up to 190s). Listeners on stdout/stderr detect `Proxy established successfully`, `Local STDIO server running`, or `Authentication completed`. On success, IPC re-runs harness + SSH sync.

## Manual install example

Stdio server:

```json
{
  "type": "stdio",
  "command": "npx",
  "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"],
  "env": { "API_KEY": "..." }
}
```

HTTP remote with headers:

```json
{
  "type": "http",
  "url": "https://mcp.example.com/mcp",
  "headers": { "Authorization": "Bearer ..." }
}
```

Use **Extensions → MCP servers → Add** or `electronAPI.mcp.installRaw(id, config)`.

## Verification

| Script | What it checks |
|--------|----------------|
| `node scripts/verify-mcp-harness-sync.ts` | Normalization, merge helpers, Codex TOML, OpenCode merge, localhost port detection, Cursor SDK mapping (mocked electron-store) |
| `node scripts/verify-mcp-runtime-matrix.js [--skip-remote]` | Local (and optional SSH) harness config files contain pinned `mcp-remote@0.1.38`; CLI `mcp list` / tool listing where CLIs exist; Claude service injects `mcpService.getClaudeMcpServersConfig()` into SDK |

Runtime matrix options: `--remote-host`, `--remote-user`, `--remote-key`, `--fail-on-skips`.

## Troubleshooting

| Symptom | Likely cause |
|---------|----------------|
| Harness does not see new server | Run any harness turn (triggers sync) or restart app (startup sync); check `syncLocalHarnessConfigs()` errors in main log |
| Remote MCP auth fails | `~/.mcp-auth` not synced; complete OAuth via prewarm locally first |
| Localhost MCP unreachable on SSH | Reverse tunnel not established; verify URL uses `localhost` or `127.0.0.1` with explicit port |
| `mcp-remote` version drift | Store auto-migrates to `mcp-remote@0.1.38`; remote auth sync copies tokens across version folders |
| Native stdio missing on remote | Expected — use bridge (`http://127.0.0.1:{port}/mcp`) or remote-capable `mcp-remote` URL |
| Marketplace empty | Registry fetch failed; stale cache may still load; check network and registry status |

<Warning>
Secrets live in electron-store and synced harness files on disk. Treat `claudette-mcp-servers.json` and harness MCP configs as sensitive in backups and shared machines.
</Warning>

## Related pages

<CardGroup>
<Card title="Extensions and commands" href="/extensions-and-commands">
MCP marketplace UI, manual MCP editor, skills, and slash commands in the Extensions panel.
</Card>
<Card title="IPC channels reference" href="/ipc-channels-reference">
Full `mcp:*` channel list and invoke semantics.
</Card>
<Card title="SSH remote sessions" href="/ssh-remote-sessions">
Remote MCP sync, bridges, tunnels, and `~/.mcp-auth` replication.
</Card>
<Card title="Harnesses and models" href="/harnesses-and-models">
Per-harness launch, permission modes, and MCP capability limits.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
MCP harness sync errors, CDP timeouts, and production log locations.
</Card>
</CardGroup>

---

## 15. Semantic search (QMD)

> Opt-in QMD indexing via Bun bundle, collection creation, embedding generation, search IPC, project preferences, and setup-qmd dev/build integration.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/15-semantic-search-qmd.md
- Generated: 2026-06-02T00:23:25.996Z

### Source Files

- `src/main/services/qmd.service.ts`
- `src/main/ipc/qmd.ipc.ts`
- `scripts/setup-qmd.ts`
- `src/renderer/components/qmd/QMDPrompt.tsx`
- `src/main/services/settings.service.ts`
- `package.json`

---
title: "Semantic search (QMD)"
description: "Opt-in QMD indexing via Bun bundle, collection creation, embedding generation, search IPC, project preferences, and setup-qmd dev/build integration."
---

Build integrates [QMD](https://github.com/tobi/qmd) as an optional, local semantic codebase search layer. The main process resolves a Bun-wrapped QMD binary, indexes project folders into named collections, generates embeddings, and—when both global and per-project opt-in are satisfied—registers QMD as a stdio MCP server for local Claude Agent SDK sessions. Indexing, install, and search are exposed through typed IPC; the renderer can prompt per project and surface progress in settings or `QMDPrompt`.

## Opt-in model

QMD is off by default at every layer until the user enables it.

| Layer | Store / surface | Values | Default |
| --- | --- | --- | --- |
| Global | `claudette-settings` → `settings.qmdEnabled` (`AppSettings`) | `true` / `false` | `false` |
| Per project | `claudette-qmd` → `projectPreferences.<md5(projectPath)>` | `enabled` / `disabled` / unset (`unknown`) | unset |
| Runtime binary | Resolved by `QMDService.checkInstalled()` | bundled, user-local, or PATH | none until setup |

`QMDService.isEnabledForProject(projectPath, globalEnabled)` requires **both** `globalEnabled === true` and per-project preference `enabled`. SSH sessions skip QMD entirely because indexing runs on the local machine.

<Note>
`ClaudeService` reads the global flag with `store.get('qmdEnabled', false)` on the `claudette-settings` store root, while `SettingsService` persists `qmdEnabled` under the nested `settings` object. If semantic search does not attach after toggling **Enable QMD Search**, confirm the value ClaudeService reads matches what the settings UI wrote.
</Note>

## Resolution order for the QMD binary

`QMDService` caches `qmdPath` after the first successful resolve.

```mermaid
flowchart TD
  subgraph dev [Development - app not packaged]
    R1[resources/qmd/platformKey/qmd]
    R2[userData/qmd/platformKey/qmd]
    R1 -->|exists| USE[Use wrapper]
    R2 -->|fallback| USE
  end
  subgraph pkg [Packaged app]
    P1[App Resources/qmd/platformKey/qmd]
    P1 --> USE
  end
  subgraph fallback [Any mode]
    PATH[which qmd + common paths]
    PATH --> USE
  end
  USE --> CLI[Spawn qmd via wrapper or system binary]
```

Platform key format: `{platform}-{arch}` (for example `darwin-arm64`). Wrapper scripts invoke bundled Bun against `qmd-package/node_modules/qmd/src/qmd.ts`.

| Location | When used |
| --- | --- |
| `resources/qmd/<platformKey>/qmd` | Dev after `npm run setup-qmd`; packaged copy from forge hook |
| `<userData>/qmd/<platformKey>/qmd` | After `autoInstall` or manual user-local install |
| PATH / Homebrew / `~/.bun/bin/qmd` | Fallback when no bundle present |

Bun version pinned in repo: **1.1.38**. QMD package source: `https://github.com/tobi/qmd`.

## Bundle layout

:::files
resources/qmd/
└── <platform>-<arch>/
    ├── bun/                 # Bun runtime (extracted from GitHub release)
    ├── qmd-package/         # bun add https://github.com/tobi/qmd
    └── qmd or qmd.cmd       # Wrapper → bun --cwd=... src/qmd.ts
:::

Packaging (`forge.config.ts`) copies `resources/qmd/<platformKey>/` into the app `Resources/qmd/` tree and strips `.bun-cache` and `__MACOSX` before signing. Missing platform bundles log a warning and skip copy.

## Setup commands

| Command | Purpose |
| --- | --- |
| `npm run setup-qmd` | Bundle QMD for **current** platform into `resources/qmd/` |
| `npm run setup-qmd:all` | Run `scripts/setup-qmd.ts all` for every supported platform |
| `npx ts-node scripts/setup-qmd.ts <platformKey>` | Explicit platform (e.g. `darwin-arm64`) |

Supported `platformKey` values: `darwin-arm64`, `darwin-x64`, `linux-x64`, `win32-x64`.

`./scripts/dev.sh` runs `npm run setup-qmd` before starting the dev server so local Electron can find `resources/qmd/<platformKey>/qmd`. Production builds expect the same directory populated before `npm run make` (or accept runtime `autoInstall`).

## Indexing lifecycle

```mermaid
sequenceDiagram
  participant UI as Renderer
  participant IPC as qmd.ipc
  participant Svc as QMDService
  participant QMD as QMD CLI

  UI->>IPC: QMD_ENSURE_INDEXED(projectPath)
  IPC->>Svc: ensureProjectIndexed()
  Svc->>Svc: checkInstalled()
  alt no collection
    Svc->>QMD: collection add --name --mask
  end
  Svc->>QMD: embed --collection <name>
  QMD-->>Svc: stdout progress
  Svc-->>IPC: onProgress(message)
  IPC-->>UI: QMD_INDEXING_PROGRESS event
```

### Collection naming and file mask

- **Name:** `<sanitized-basename>-<6-char-md5-prefix>` from `path.basename(projectPath)` and path hash.
- **Default mask:** `**/*.{ts,tsx,js,jsx,py,go,rs,java,md,json,yaml,yml,toml}` (overridable via `QMD_CREATE_COLLECTION`).

`ensureProjectIndexed` is idempotent for an existing collection but still runs `embed` for that collection. A re-entrant guard (`isChecking`) drops overlapping calls.

### Embeddings and status

- `generateEmbeddings(collectionName?)` spawns `qmd embed [--collection name]` and streams stdout to progress callbacks.
- `getStatus()` runs `qmd status --json` and sets `embeddingsReady` from `embeddingsReady` or `hasEmbeddings`.

Collection metadata (last indexed timestamp) is also stored in `claudette-qmd` under `collections.<name>`.

## Agent integration (MCP)

For **local** sessions when global + project enablement pass and `getMcpServerConfig()` returns a path:

```text
mcpServersConfig['qmd'] = { type: 'stdio', command: <qmdPath>, args: ['mcp'] }
```

Background indexing: `ensureProjectIndexed(projectPath)` without blocking the agent turn.

When global QMD is on but per-project preference is still `unknown`, `shouldPromptForProject` may emit `QMD_PROMPT_RESPONSE` with `{ sessionId, projectPath }` so `QMDPrompt` can ask the user.

<Warning>
QMD does not run for SSH-backed sessions (`session.sshConfig` set). Remote workspaces are not indexed by this path.
</Warning>

## IPC surface

Handlers live in `registerQmdHandlers` (`src/main/ipc/qmd.ipc.ts`). Channel constants are in `IPC_CHANNELS` under the `qmd:*` prefix.

| Channel | Pattern | Role |
| --- | --- | --- |
| `qmd:get-status` | invoke | `QMDStatus`: installed, version, collections, embeddingsReady, bundled |
| `qmd:ensure-indexed` | invoke | Full index pipeline for `projectPath` |
| `qmd:create-collection` | invoke | `collection add` with optional `mask` |
| `qmd:generate-embeddings` | invoke | `embed` for optional collection name |
| `qmd:search` | invoke | Direct CLI search (`search` / `vsearch` / `query`, JSON results) |
| `qmd:get-project-preference` | invoke | `enabled` \| `disabled` \| `unknown` |
| `qmd:set-project-preference` | invoke | Persist per-project choice |
| `qmd:should-prompt` | invoke | Whether to show first-run prompt |
| `qmd:auto-install` | invoke | Download Bun + install QMD under `userData/qmd/` |
| `qmd:indexing-progress` | **event** (main → renderer) | Progress during index, embed, or install |
| `qmd:prompt-response` | **event** (main → renderer) | Open `QMDPrompt` modal |

### Preload API (`window.electronAPI.qmd`)

- **Invoke:** `getStatus`, `ensureIndexed`, `createCollection`, `generateEmbeddings`, `search`, `getProjectPreference`, `setProjectPreference`, `shouldPrompt`, `autoInstall`
- **Subscribe:** `onIndexingProgress`, `onPromptRequest`

### Search invoke parameters

<ParamField body="query" type="string" required>
Natural-language or keyword query passed to the QMD CLI.
</ParamField>

<ParamField body="options.collection" type="string">
Limit search to a collection name (typically the project-derived name).
</ParamField>

<ParamField body="options.mode" type="'search' | 'vsearch' | 'query'">
CLI mode; default `query`.
</ParamField>

<ParamField body="options.limit" type="number">
Result cap; default `10`.
</ParamField>

<ResponseField name="results" type="Array<{ file: string; score: number; content: string }>">
Parsed JSON from the QMD CLI stdout.
</ResponseField>

## Persistence

| File | Contents |
| --- | --- |
| `claudette-qmd.json` | `collections`, optional `qmdPath`, `projectPreferences` (MD5 keys) |
| `claudette-settings.json` | `settings.qmdEnabled` and other `AppSettings` |

Dev uses a separate user data root (`GREP_DEV_USER_DATA`, typically `/tmp/grep-build-dev`) so QMD indexes and preferences do not overwrite production data.

## User workflows

<Steps>
<Step title="Enable globally">
Open **Settings → General → Semantic Codebase Search**, turn on **Enable QMD Search**. Optionally use **Install** if status reports QMD missing; that calls `autoInstall` and listens for `QMD_INDEXING_PROGRESS`.
</Step>
<Step title="Accept per project">
On first local session with global QMD on, `QMDPrompt` offers **Enable** or **Not Now**. **Enable** may run `autoInstall`, sets preference `enabled`, then `ensureIndexed`. **Not Now** sets `disabled`.
</Step>
<Step title="Verify">
Settings should show `QMD (bundled) ready` or `(installed) ready`. Agent logs may include `QMD MCP server enabled for semantic search`. First embed can download models and take noticeable time on large repos.
</Step>
</Steps>

## Auto-install behavior

`QMDService.autoInstall` mirrors `setup-qmd.ts` for a single platform:

1. Download Bun zip from `oven-sh/bun` releases.
2. `bun add https://github.com/tobi/qmd` into `<userData>/qmd/<platformKey>/qmd-package`.
3. Write wrapper at `<userData>/qmd/<platformKey>/qmd` (or `qmd.cmd` on Windows).

Progress messages are forwarded on `QMD_INDEXING_PROGRESS` (install path omits `projectPath` in the payload).

## Troubleshooting

| Symptom | Likely cause | Action |
| --- | --- | --- |
| `QMD not installed` in settings | No bundle and no PATH binary | Run `npm run setup-qmd` (dev) or **Install** / `autoInstall` |
| Packaging warning for platform | `resources/qmd/<platformKey>` missing before `make` | `npm run setup-qmd` or `setup-qmd:all` for target arch |
| Prompt never appears | QMD not installed, global off, or preference already set | Enable global toggle; check `claudette-qmd` preferences |
| MCP not in session | SSH session, project disabled, or binary unresolved | Use local session; enable project; check `getStatus().installed` |
| Indexing stuck / silent | Concurrent `ensureProjectIndexed` (`isChecking`) | Wait and retry; inspect main process logs `[QMD Service]` |
| Dev vs prod data mixed | Wrong userData path | Confirm dev uses `GREP_DEV_USER_DATA` |

<Info>
First `embed` may download embedding models locally. No cloud API key is required for QMD itself; the feature is BYOC/BYOK-neutral at the search layer.
</Info>

## Related pages

<CardGroup>
<Card title="Settings reference" href="/settings-reference">
`qmdEnabled` and other `AppSettings` keys in `claudette-settings`.
</Card>
<Card title="IPC channels reference" href="/ipc-channels-reference">
Full `qmd:*` channel list with invoke vs push semantics.
</Card>
<Card title="MCP servers" href="/mcp-servers">
How stdio MCP servers are loaded alongside QMD in agent sessions.
</Card>
<Card title="npm scripts reference" href="/npm-scripts-reference">
`setup-qmd`, `setup-qmd:all`, and `./scripts/dev.sh` vs `npm run start`.
</Card>
<Card title="Build and release" href="/build-and-release">
Packaging QMD into distributables and forge resource copy.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Broader dev instance, PATH, and logging guidance.
</Card>
</CardGroup>

---

## 16. Voice and audio

> ElevenLabs conversational voice mode, OpenAI realtime transcription, TTS streaming IPC, microphone permissions on macOS, and optional API keys for audio providers.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/16-voice-and-audio.md
- Generated: 2026-06-02T00:24:00.318Z

### Source Files

- `src/main/services/audio.service.ts`
- `src/main/services/realtime.service.ts`
- `src/main/services/elevenlabs-voice.service.ts`
- `src/main/ipc/audio.ipc.ts`
- `src/main/ipc/voice.ipc.ts`
- `src/renderer/components/chat/VoiceMode.tsx`

---
title: "Voice and audio"
description: "ElevenLabs conversational voice mode, OpenAI realtime transcription, TTS streaming IPC, microphone permissions on macOS, and optional API keys for audio providers."
---

Build routes voice and audio through three main-process services (`AudioService`, `RealtimeService`, `ElevenLabsVoiceService`), registered IPC handlers in `audio.ipc.ts`, `realtime.ipc.ts`, and `voice.ipc.ts`, and renderer hooks/components that call `window.electronAPI.audio`, `.realtime`, and `.voice`. Chat voice mode today uses the ElevenLabs Conversational AI SDK over WebRTC; OpenAI powers batch Whisper transcription and a separate Realtime WebSocket path for streaming STT; ElevenLabs also powers message-level TTS streaming over IPC push events.

## Architecture

```mermaid
flowchart TB
  subgraph renderer["Renderer (React)"]
    MB[MicrophoneButton / VoiceMode]
    SDK[useVoiceConversationSDK @elevenlabs/client]
    SB[SpeakerButton]
    AS[audio.store Zustand]
    MB --> SDK
    SB --> AS
    AS --> SB
  end

  subgraph preload["preload.ts → electronAPI"]
    EA[audio.*]
    ER[realtime.*]
    EV[voice.*]
  end

  subgraph main["Main process"]
    AI[audio.ipc.ts]
    RI[realtime.ipc.ts]
    VI[voice.ipc.ts]
    Aud[AudioService]
    RT[RealtimeService]
    EL[ElevenLabsVoiceService]
    AI --> Aud
    RI --> RT
    VI --> EL
  end

  subgraph external["External APIs"]
    OAI_RT[OpenAI Realtime wss]
    OAI_WH[OpenAI Whisper]
    EL_CONV[ElevenLabs ConvAI]
    EL_TTS[ElevenLabs TTS]
  end

  SDK --> EV
  MB --> AS
  SB --> EA
  EA --> AI
  ER --> RI
  EV --> VI
  Aud --> OAI_WH
  Aud --> EL_TTS
  RT --> OAI_RT
  EL --> EL_CONV
  SDK -.WebRTC.-> EL_CONV
```

| Layer | Responsibility |
| --- | --- |
| `AudioService` | Whisper batch STT, ElevenLabs TTS streaming, voice catalog, `audioSettings` in `claudette-settings` |
| `RealtimeService` | OpenAI Realtime WebSocket (`gpt-4o-realtime-preview`), PCM16 input, server VAD, transcription events |
| `ElevenLabsVoiceService` | ElevenLabs ConvAI WebSocket (signed URL), agent prompt PATCH, tool results, status TTS via `[STATUS]` prefix |
| Renderer SDK path | `@elevenlabs/client` `Conversation.startSession` with `connectionType: 'webrtc'` and token from main |

<Note>
Neither Realtime nor ElevenLabs voice services auto-reconnect after disconnect. Reconnection requires an explicit user action (for example toggling the mic), which prevents voice mode from reactivating unintentionally.
</Note>

## API keys and BYOC

Keys live in the `claudette-settings` electron-store. User-provided keys override compile-time embedded keys from `EMBEDDED_KEYS` (injected via Webpack from `.env.production`).

| Store key | Used by | Purpose |
| --- | --- | --- |
| `openAiApiKey` | `AudioService`, `RealtimeService` | Whisper (`whisper-1`) and Realtime transcription |
| `elevenLabsApiKey` | `AudioService`, `ElevenLabsVoiceService` | TTS, ConvAI WebSocket, agent PATCH, conversation token |
| `audioSettings` | Renderer + `AudioService.getAudioSettings()` | Voice ID, agent ID, toggles, trigger word, language |

<ParamField body="elevenLabsAgentId" type="string">
ElevenLabs Conversational AI agent ID (`agent_...`). Required for voice mode; stored inside `audioSettings`.
</ParamField>

<ParamField body="voiceModeEnabled" type="boolean" default="true">
Feature toggle in Settings. Default in `DEFAULT_AUDIO_SETTINGS` is `true`.
</ParamField>

Settings UI (`SettingsDialog`) saves OpenAI and ElevenLabs keys through `window.electronAPI.audio.setOpenAiKey` / `setElevenLabsKey`, and merges other audio fields via `audio.setSettings` → `AUDIO_SETTINGS_SET`.

## ElevenLabs conversational voice mode

Primary chat integration: `MicrophoneButton` in `InputArea` (wrapped in `VoiceModeErrorBoundary`) uses `useVoiceConversationSDK`.

<Steps>
<Step title="Configure agent and keys">
Set **ElevenLabs API Key** and **ElevenLabs Agent ID** in Settings. Enable **Voice mode** if disabled.
</Step>
<Step title="Connect">
Click the phone/mic control. On macOS, Build checks `audio.checkMicrophonePermission` and may call `audio.requestMicrophonePermission` before connecting.
</Step>
<Step title="Run WebRTC session">
The hook calls `voice.getConversationToken({ agentId })`, then `Conversation.startSession({ conversationToken, connectionType: 'webrtc' })`. The SDK owns mic capture and hardware echo cancellation.
</Step>
<Step title="Finalize transcript into chat">
Final user transcripts invoke `onTranscriptionComplete` in `InputArea`, which can auto-submit when the utterance ends with the configured trigger word (default `please`).
</Step>
</Steps>

While connected, `MicrophoneButton` sets per-session `audioModeActive` so TTS auto-play does not compete with ConvAI audio (`triggerAutoPlayTTS` skips when voice mode is connected). It also sends `voice.sendContextUpdate`, `voice.sendUserActivity`, and `voice.sendText` for Build progress, thinking narration (`[THINKING]` in the system prompt), and permission announcements.

### Legacy main-process WebSocket path

`ElevenLabsVoiceService` + `voice.ipc.ts` still expose full ConvAI control: `VOICE_CONNECT`, `VOICE_SEND_AUDIO` (PCM16 `Int16Array` as number[]), `VOICE_USER_TRANSCRIPT`, `VOICE_AUDIO_CHUNK`, `VOICE_TOOL_CALL`, etc. The older `useVoiceConversation` hook uses this path with renderer-side mic streaming. The SDK/WebRTC path is what `MicrophoneButton` and `VoiceMode.tsx` use today.

`clearAudioBuffer()` on the ElevenLabs service is a documented no-op; echo prevention relies on client-side mic muting during agent playback.

## OpenAI Realtime transcription

`RealtimeService` opens `wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview` with headers `Authorization: Bearer <key>` and `OpenAI-Beta: realtime=v1`. After connect it sends `session.update` with:

- `modalities: ['text']` (no model audio output)
- `input_audio_format: 'pcm16'`
- `input_audio_transcription.model: 'whisper-1'`
- `turn_detection`: server VAD (`threshold: 0.3`, `silence_duration_ms: 800`)

Main relays events to the renderer:

| Push channel | Event |
| --- | --- |
| `realtime:transcription-delta` | Partial transcript text |
| `realtime:transcription-completed` | Final segment |
| `realtime:speech-started` / `realtime:speech-stopped` | VAD boundaries |
| `realtime:error` | API error message |
| `realtime:disconnected` | Socket closed |

Invoke handlers: `realtime:connect`, `realtime:disconnect`, `realtime:send-audio` (Int16 LE buffer), `realtime:commit-audio`, `realtime:clear-audio`.

`useAudioRecorder` implements the renderer side (24 kHz `AudioContext`, `ScriptProcessorNode`, resampling to PCM16). It is not wired into current chat UI components; it remains the reference integration for Realtime STT.

## AudioService: Whisper STT and ElevenLabs TTS

### Batch transcription (Whisper)

`AUDIO_TRANSCRIBE` accepts an `ArrayBuffer` of `audio/webm`, calls `openai.audio.transcriptions.create` with `model: 'whisper-1'` and optional `language` (defaults to `en`). Returns `{ success, result: { text, partial: false } }` or `{ success: false, error }`. No renderer caller is present in the current tree; the IPC surface is available for future or custom UI.

### TTS streaming

`AUDIO_TTS_STREAM` is invoked with a `TTSRequest`:

| Field | Type | Notes |
| --- | --- | --- |
| `text` | string | Source text |
| `messageId` | string | Stream and cancel key |
| `voiceId` | string | ElevenLabs voice; falls back to `audioSettings.selectedVoice` |
| `modelId` | string | Default `eleven_turbo_v2_5` in service |

The handler async-iterates `generateTTSStream`, pushing:

- `audio:tts-chunk` — `{ messageId, chunk: number[] }` (raw bytes as JSON array)
- `audio:tts-complete` — `{ messageId }`
- `audio:tts-error` — `{ messageId, error }`

`AUDIO_TTS_CANCEL` aborts via per-`messageId` `AbortController`. `SpeakerButton` decodes accumulated chunks in a shared `AudioContext` when `progress === 100`. `session.store` calls `triggerAutoPlayTTS` after assistant messages when `audioModeActive[sessionId]` is true and voice mode is not connected.

Default voice in `DEFAULT_AUDIO_SETTINGS`: `EXAVITQu4vr4xnSDxMaL` (Rachel).

## IPC and preload surface

Handlers register in `src/main/index.ts` via `registerAudioHandlers`, `registerRealtimeHandlers`, and `registerVoiceHandlers`.

### Audio (`window.electronAPI.audio`)

| Invoke | Channel | Returns / behavior |
| --- | --- | --- |
| `transcribe` | `audio:transcribe` | Whisper result |
| `streamTTS` | `audio:tts-stream` | Starts stream; chunks via `onTTSChunk` |
| `cancelTTS` | `audio:tts-cancel` | Aborts stream |
| `getVoices` | `audio:get-voices` | ElevenLabs voice list |
| `getSettings` / `setSettings` | `audio:settings-get/set` | `AudioSettings` object |
| `getElevenLabsKey` / `setElevenLabsKey` | `audio:get/set-elevenlabs-key` | Raw key string |
| `getOpenAiKey` / `setOpenAiKey` | `audio:get/set-openai-key` | Raw key string |
| `checkMicrophonePermission` | `audio:check-microphone-permission` | macOS `systemPreferences` status |
| `requestMicrophonePermission` | `audio:request-microphone-permission` | `askForMediaAccess('microphone')` |

Subscribe helpers: `onTTSChunk`, `onTTSComplete`, `onTTSError`.

### Realtime (`window.electronAPI.realtime`)

Invoke: `connect`, `disconnect`, `sendAudio`, `commitAudio`, `clearAudio`. Subscribe: `onConnected`, `onDisconnected`, `onTranscriptionDelta`, `onTranscriptionCompleted`, `onSpeechStarted`, `onSpeechStopped`, `onError`.

### Voice (`window.electronAPI.voice`)

Invoke includes `connect`, `disconnect`, `sendAudio`, `sendText`, `endInput`, `clearAudioBuffer`, `sendContextUpdate`, `sendToolResult`, `updateAgentPrompt`, `sendUserActivity`, `getSignedUrl`, `getConversationToken`. Subscribe: `onConnected`, `onDisconnected`, `onUserTranscript`, `onAgentResponse`, `onAudioChunk`, `onInterruption`, `onError`, `onToolCall`, `onReconnecting`.

For a full channel listing, see the IPC channels reference page.

## Renderer state (`audio.store`)

Zustand store tracks:

- `recordingStates` — per-session mic/recording (Realtime hook)
- `ttsStates` — per-message chunk playback
- `voiceModeStates` — per-session ConvAI UI state
- `audioModeActive` — enables auto TTS after assistant replies

`initializeTTSListeners()` registers global `onTTSChunk` / `onTTSComplete` / `onTTSError` once (HMR-safe via `window.__ttsListenerCleanups`).

## macOS microphone permissions

On `darwin`, `audio.ipc.ts` uses Electron `systemPreferences`:

| Status | `checkMicrophonePermission` | `requestMicrophonePermission` |
| --- | --- | --- |
| `granted` | `granted: true` | Returns success immediately |
| `not-determined` | `canRequest: true` | Calls `askForMediaAccess('microphone')` |
| `denied` / `restricted` | `canRequest: false` | Error directing user to System Settings → Privacy & Security → Microphone |

Non-macOS platforms return `{ status: 'granted', granted: true }`; renderer still uses `navigator.mediaDevices.getUserMedia` where applicable.

Electron `session.defaultSession` permission handlers allow `media` (microphone/camera) for the main window. Packaged macOS builds can include `com.apple.security.device.audio-input` in `entitlements.mac.plist`; distribution signing in `forge.config.ts` currently references `entitlements.plist` (network/JIT only). If mic access fails only in signed builds, verify the active entitlements file matches production needs.

## `AudioSettings` reference

Stored under `audioSettings` in `claudette-settings` (see `src/shared/types/audio.ts`):

| Field | Default | Role |
| --- | --- | --- |
| `selectedVoice` | Rachel voice ID | ElevenLabs TTS voice |
| `voiceSettings` | stability 0.5, similarity 0.75, etc. | Passed to `textToSpeech.convertAsStream` |
| `autoPlayResponses` | `false` | Setting exists; auto-play is gated by `audioModeActive` in practice |
| `transcriptionLanguage` | `en` | Whisper language |
| `voiceTriggerWord` | `please` | Auto-submit when final transcript matches trailing pattern |
| `elevenLabsAgentId` | `undefined` | ConvAI agent |
| `voiceModeEnabled` | `true` | Settings toggle |
| `ralphLoopEnabled` | `false` | Build It loop (orthogonal but stored with audio settings) |
| `computerUseEnabled` | `false` | Computer Use toggle |
| `maxComputerUseIterations` | `20` | Computer Use cap |

## Troubleshooting

<Warning>
**OpenAI API key not configured** — Realtime connect and Whisper transcribe throw if neither user nor `EMBEDDED_OPENAI_API_KEY` is set.
</Warning>

<Warning>
**ElevenLabs API key not configured** — TTS, voice list, ConvAI connect/token, and agent prompt update require `elevenLabsApiKey` or embedded key.
</Warning>

| Symptom | Likely cause | What to check |
| --- | --- | --- |
| Voice button disabled | Missing `elevenLabsAgentId` | Settings → ElevenLabs Agent ID |
| Mic works in dev, not production | macOS TCC / entitlements | System Settings microphone list; signing entitlements |
| No auto-read of replies | `audioModeActive` false or voice connected | Connect voice mode or enable audio mode path in chat |
| Duplicate TTS + agent speech | Both ConvAI and `streamTTS` active | Voice connected skips `triggerAutoPlayTTS` by design |
| Realtime never used in UI | Hook not mounted | `useAudioRecorder` exists; chat uses ElevenLabs SDK |
| Quota errors in voice mode | ElevenLabs billing | WebSocket close reason or SDK `handleErrorEvent` patch messages |

## Related pages

<CardGroup>
<Card title="Configure API keys and providers" href="/configure-providers">
Store OpenAI and ElevenLabs keys alongside other provider credentials.
</Card>
<Card title="IPC and preload bridge" href="/ipc-bridge">
How `electronAPI` maps invoke handlers and push events.
</Card>
<Card title="IPC channels reference" href="/ipc-channels-reference">
Full `audio:*`, `realtime:*`, and `voice:*` channel catalog.
</Card>
<Card title="Settings reference" href="/settings-reference">
`claudette-settings` keys including `audioSettings`.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
PATH, ports, and production log locations when audio features fail.
</Card>
</CardGroup>

---

## 17. Extensions, skills, and slash commands

> Scan project/user commands, skills, and agents; install skills from marketplace; plugin marketplaces; GStack workflow modes; and command autocomplete in chat.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/17-extensions-skills-and-slash-commands.md
- Generated: 2026-06-02T00:23:54.235Z

### Source Files

- `src/main/services/extension.service.ts`
- `src/main/ipc/extension.ipc.ts`
- `src/main/services/plugin.service.ts`
- `src/main/services/gstack.service.ts`
- `src/renderer/components/extensions/UnifiedMarketplace.tsx`
- `src/renderer/components/chat/CommandAutocomplete.tsx`

---
title: "Extensions, skills, and slash commands"
description: "Scan project/user commands, skills, and agents; install skills from marketplace; plugin marketplaces; GStack workflow modes; and command autocomplete in chat."
---

Build discovers Claude Code–compatible extensions on disk (commands, skills, agents), exposes them through main-process scanners and IPC, and surfaces them in the Extensions side panel and chat autocomplete. Plugin and MCP marketplaces install additional capabilities via the Claude CLI and `npx`; GStack adds a separate workflow skill pack under `~/.claude/skills/gstack` with per-session mode state and optional system-prompt routing.

## Architecture

```mermaid
flowchart TB
  subgraph disk [On-disk layout]
    UC["~/.claude/commands|skills|agents"]
    PC["{project}/.claude/..."]
    PM["~/.claude/plugins/marketplaces"]
    GS["~/.claude/skills/gstack"]
  end

  subgraph main [Main process]
    ES[extension.service.ts]
    PS[plugin.service.ts]
    GSvc[gstack.service.ts]
    EIPC[extension.ipc.ts]
    PIPC[plugin.ipc.ts]
    SSH[ssh.service.ts]
  end

  subgraph renderer [Renderer]
    EE[ExtensionsExplorer]
    UM[UnifiedMarketplace]
    IA[InputArea / CompactInputArea]
    CA[CommandAutocomplete]
  end

  UC --> ES
  PC --> ES
  PM --> PS
  GS --> GSvc
  ES --> EIPC
  PS --> PIPC
  GSvc --> index[index.ts GSTACK handlers]
  EIPC -->|contextBridge| EE
  EIPC --> IA
  PIPC --> UM
  SSH --> EIPC
  IA --> CA
```

| Layer | Module | Responsibility |
|-------|--------|----------------|
| Scan | `ExtensionService` | Walk `~/.claude` and project `.claude` trees for commands, skills, agents |
| Plugins | `PluginService` | Read `known_marketplaces.json`, parse manifests, shell out to `claude plugin` |
| GStack | `gstack.service.ts` | Discover gstack skill dirs, install/upgrade from GitHub, build routing prompt |
| IPC | `extension.ipc.ts`, `plugin.ipc.ts`, `index.ts` | Typed invoke handlers; SSH branches for remote sessions |
| UI | `ExtensionsExplorer`, `UnifiedMarketplace`, `CommandAutocomplete` | Browse, install, autocomplete in chat |

## On-disk extension layout

Build follows Claude Code conventions. User scope lives under the home directory; project scope under one or more `.claude` directories in the workspace.

| Kind | User path | Project path | File pattern |
|------|-----------|--------------|--------------|
| Commands | `~/.claude/commands/` | `{project}/.claude/commands/` (and nested `.claude` dirs, depth ≤ 3) | `*.md`; nested dirs become `namespace:command` names |
| Skills | `~/.claude/skills/{name}/` | Same under each discovered `.claude/skills/` | `SKILL.md` per skill directory |
| Agents | `~/.claude/agents/` | `{claudeDir}/agents/` | `*.md` with `#` description and `## System Prompt` / `## Prompt` sections |

**Command metadata:** The first line may be an HTML comment `<!-- description -->` parsed as `description`.

**Skill metadata:** The first heading line (`# Title`) becomes `description` when present.

**Agent parsing:** Agents without both a description paragraph and a system prompt section are skipped.

`ExtensionService.scanRootSkills()` scans only `~/.claude/skills` and `{projectPath}/.claude/skills` (no nested `.claude` walk). That method is not exposed over IPC; the UI and IPC use `scanSkills()`, which recursively discovers all skill directories under each `.claude/skills` tree.

## Scanning behavior

`ExtensionService` (`src/main/services/extension.service.ts`):

- **Commands:** `scanCommands(projectPath?)` merges user commands, then every `.claude/commands` found by `findClaudeDirs()` (skips `node_modules`, `.git`, `dist`, `build`, `.next`, `target`).
- **Skills:** `scanSkillsRec()` accepts directories and symlinks to directories; reads `SKILL.md` or recurses if missing.
- **Agents:** Recursive `.md` scan with `parseAgent()`.
- **Lookup:** `getCommandContent(cmdName, projPath?)` re-scans and matches by full namespaced name.

Returned shapes match shared types `Command`, `Skill`, and `AgentDefinition` (`scope`: `'user' | 'project'`).

## IPC and preload API

Handlers are registered in `registerExtensionHandlers()` and exposed as `window.electronAPI.extensions` in `preload.ts`.

| Channel constant | Invoke signature | Behavior |
|------------------|------------------|----------|
| `extension:scan-commands` | `{ sessionId?, projectPath? }` or legacy `projectPath` string | Local scan or `sshService.scanRemoteCommands` when session has `sshConfig` |
| `extension:scan-skills` | Same | Local or `scanRemoteSkills` |
| `extension:scan-agents` | Same | Local or `scanRemoteAgents` |
| `extension:get-command` | `commandName`, `projectPath?` | Returns markdown body or `null` |
| `extension:install-skill` | `source`, `{ global?, skills?, projectPath?, sessionId? }` | Runs `npx skills add <source> -y [-g] [--skill …] -a claude-code` locally, or remote install via SSH |
| `extension:list-available-skills` | `source` | Runs `npx skills list <source>` and parses stdout |

<Note>
SSH sessions route extension scans and skill installs through `ssh.service.ts` using remote `find`/`cat` over the SSH bridge, using `remoteWorkdir` from session config.
</Note>

## Extensions Explorer (installed tab)

`ExtensionsExplorer` loads in the session side panel (`MainContent.tsx`) with `sessionId` and `projectPath` (worktree path).

On mount it parallel-fetches:

- `extensions.scanCommands|scanSkills|scanAgents`
- `mcp.getServers`
- `plugins.getInstalled`

Sections: **Commands**, **Skills**, **Agents**, **MCP servers**, **Plugins**, plus a **Marketplace** tab hosting `UnifiedMarketplace`.

**Skill install (marketplace/catalog):**

- GitHub/catalog: `extensions.installSkill(source, { global, projectPath, sessionId })` → `npx skills add`.
- Local file upload: writes `{base}/.claude/skills/{name}/SKILL.md` via `fs.writeFile` (supports SSH `sessionId`).

**Create skill:** In-app dialog writes a new `SKILL.md` from a template.

Empty states point users at `.claude/commands/*.md` and skill directory layout.

## Plugin marketplaces

`PluginService` manages Claude Code plugins under `~/.claude/plugins/`:

- **Registry file:** `known_marketplaces.json` lists configured marketplaces and `installLocation`.
- **Discovery:** `getAvailablePlugins()` reads each marketplace’s `.claude-plugin/marketplace.json` and flags content types (`commands`, `skills`, `agents`, `hooks`, `mcp`).
- **CLI:** Requires `claude` on PATH (`resolveClaudeCli()`); uses `claude plugin list|install|uninstall|enable|disable` and `claude plugin marketplace add|remove|update`.

`POPULAR_MARKETPLACES` seeds the UI with curated GitHub repos (official Anthropic marketplaces and community catalogs).

| Preload API | IPC channel |
|-------------|-------------|
| `plugins.getPopularMarketplaces()` | `plugin:get-popular-marketplaces` |
| `plugins.getMarketplaces()` | `plugin:get-marketplaces` |
| `plugins.getInstalled()` | `plugin:get-installed` |
| `plugins.getAvailable()` | `plugin:get-available` |
| `plugins.install(id, marketplace, { scope? })` | `plugin:install` |
| `plugins.enable` / `disable` / `uninstall` | matching channels |
| `plugins.addMarketplace(source)` | `plugin:add-marketplace` |
| `plugins.updateMarketplaces(name?)` | `plugin:update-marketplace` |

Installed plugins show `id` as `{name}@{marketplace}`, version, scope, and enabled flag parsed from `claude plugin list` output.

## Unified Marketplace

`UnifiedMarketplace` combines two tabs:

1. **MCP Servers** — `mcp.getMarketplace()` registry browse; install via `MCPInstallDialog` or custom npm/remote dialog (`mcp.installRaw`).
2. **Plugins** — `plugins.getAvailable()`; install/enable/disable per card; **GitHub** button opens `GitHubPluginInstallDialog` → `plugins.addMarketplace(repo)`.

Refresh runs `plugins.updateMarketplaces()` then reloads both catalogs. Plugin cards show badges for commands, skills, agents, and MCP when manifest scanning detects those folders.

<Info>
MCP marketplace behavior is documented on the MCP servers page. This page only covers the shared marketplace shell and plugin tab.
</Info>

## GStack workflow modes

GStack is the upstream [garrytan/gstack](https://github.com/garrytan/gstack) skill pack, not a copy baked into app logic. Build discovers it from:

1. `~/.claude/skills/gstack` (with `setup/` present)
2. `{cwd}/.claude/skills/gstack`
3. Packaged `process.resourcesPath/gstack` when present

`discoverGStackSkills()` reads each subdirectory’s `SKILL.md` YAML frontmatter (`name`, `description`), applies `SKILL_CATEGORIES` for UI color/shortName, and hides internal utilities (`open-gstack-browser`, `gstack-upgrade`, etc.).

| IPC | Handler |
|-----|---------|
| `gstack:get-modes` | `getGStackModes()` |
| `gstack:is-installed` | `isGStackInstalled()` |
| `gstack:install` | `git clone` + `./setup` into `~/.claude/skills/gstack` |
| `gstack:upgrade` | `git pull` + `./setup` |

**Per-session mode:** `session.gstackMode` (string skill id) is stored in Zustand and persisted via `sessions.update`. `sendMessage` passes `gstackMode` to `CLAUDE_SEND_MESSAGE` for harness routing (e.g. Auto Build tier hints in `auto-router.service.ts`).

**Activation paths:**

- **GStack launcher** (`InputArea`): sends `/{skillId}` as a chat message (Claude Code invokes the skill).
- **Slash autocomplete:** GStack modes appear as pseudo-commands with `itemType: 'gstack'`; selecting sets `gstackMode` without leaving `/shortname` in the buffer when using inline `/` mode.
- **`/gstack-off`:** Clears active mode.

**System prompt routing:** When `AppSettings.gstackEnabled` is `true` (default `false` in `settings.service.ts`), `ClaudeService.buildSystemPromptAppend()` injects `getGStackRoutingPrompt()` — a dynamic list of `/{id}` routes and key mappings (office-hours, investigate, ship, qa, review, etc.).

<ParamField body="gstackEnabled" type="boolean">
Enables GStack routing text in Claude system prompt append. Does not install gstack; use `gstack:install` or the launcher UI.
</ParamField>

<ParamField body="gstackMode" type="string (per session)">
Active GStack skill id for UI accent (session card, thinking block) and Auto Build routing hints.
</ParamField>

## Slash commands and chat autocomplete

`CommandAutocomplete` is a presentational dropdown; parent components own keyboard handling via `CommandAutocompleteHandle` (`selectCurrent`, `moveSelection`, `dismiss`) — no global key listeners inside the dropdown.

**Trigger rules** (`InputArea`, `CompactInputArea`):

- `/` at start of input or after whitespace; query = text after `/` with no spaces.
- `@agent-` after `@` for subagent definitions.

**Filter behavior:**

| `type` prop | Matches |
|-------------|---------|
| `command` | Commands **and** skills (up to 10 combined) |
| `skill` | Skills only |
| `agent` | Agents only |

Display: `/{name}` for commands/skills, `@agent-{name}` for agents; scope shown in parentheses.

**Selection outcomes:**

| `itemType` | Effect |
|------------|--------|
| `command` | `extensions.getCommand` → replace `/name` with file body (HTML comment lines stripped) |
| `skill` / `claude-code` | Insert `/{name}` |
| `agent` | Insert `@agent-{name}` |
| `gstack` | `setGStackMode(sessionId, gstackId \| null)` |
| `codex` (builtin) | Switch model to a `codex:*` id |
| Builtins | `monitor`, `loop` (Claude Code); `codex` second opinion |

`InputArea` also loads GStack modes into the command list (shortName lowercase aliases) and supports a **popover** slash menu (button) vs **inline** `/` typing; popover mode inserts at cursor without requiring a leading `/` in the buffer.

## Provider-neutral skill sources

Skill packs are files, git repos, or catalog URLs — not tied to a single model vendor:

- **Project/user folders:** `.claude/skills` and `.claude/commands`
- **Catalog CLI:** `npx skills add` / `npx skills list` (BYOC: uses local Node/npm)
- **Plugin marketplaces:** any git/GitHub source accepted by `claude plugin marketplace add`
- **GStack:** optional third-party git checkout under `~/.claude/skills/gstack`

Build does not require hosted skill APIs; installation flows shell out to local tooling or SSH remotes.

## Verification

| Check | Expected signal |
|-------|-----------------|
| Local commands visible | Extensions → Commands lists `*.md` under `~/.claude/commands` or project `.claude/commands` |
| Skill install | `npx skills add` succeeds; skills list refreshes in Extensions Explorer |
| Plugin marketplace | `plugins.addMarketplace('owner/repo')` then Plugins tab shows new entries after refresh |
| GStack | `gstack:is-installed` true; launcher lists categorized skills; `/{id}` message invokes skill in harness |
| Autocomplete | Typing `/rev` filters commands/skills; Enter selects via parent `selectCurrent` |
| SSH session | Scans return remote `~/.claude` and `{remoteWorkdir}/.claude` content |

<Warning>
Plugin operations fail if the Claude Code CLI is missing or quarantined on macOS (`resolveClaudeCli()` error). Skill catalog install requires `npx` on PATH in the main process environment.
</Warning>

## Related pages

<CardGroup>
  <Card title="MCP servers" href="/mcp-servers">
    Registry marketplace, stdio/http install, and harness sync — complements the Unified Marketplace MCP tab.
  </Card>
  <Card title="IPC and preload bridge" href="/ipc-bridge">
    How `window.electronAPI` maps to `ipcMain` handlers for extensions, plugins, and GStack.
  </Card>
  <Card title="SSH remote sessions" href="/ssh-remote-sessions">
    Remote extension scans and skill installs over SSH.
  </Card>
  <Card title="Settings reference" href="/settings-reference">
    `gstackEnabled` and other `claudette-settings` keys.
  </Card>
  <Card title="Manage coding sessions" href="/manage-sessions">
    Session fields including `gstackMode` persistence and side panel layout.
  </Card>
</CardGroup>

---

## 18. Settings reference

> AppSettings and electron-store keys in claudette-settings: theme, fonts, QMD, ultra plan, reminders, GStack, Foundry, focus tasks, customModels, and autoRouterConfig defaults.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/18-settings-reference.md
- Generated: 2026-06-02T00:24:27.795Z

### Source Files

- `src/shared/types/index.ts`
- `src/main/services/settings.service.ts`
- `src/main/ipc/settings.ipc.ts`
- `src/renderer/components/settings/SettingsDialog.tsx`
- `SECURITY.md`

---
title: "Settings reference"
description: "AppSettings and electron-store keys in claudette-settings: theme, fonts, QMD, ultra plan, reminders, GStack, Foundry, focus tasks, customModels, and autoRouterConfig defaults."
---

Build persists user preferences in a single `electron-store` file named `claudette-settings.json` under Electron `userData`. The `AppSettings` object lives at the nested key `settings`; several API credentials and provider keys are stored at the top level of the same file. `SettingsService` merges partial updates, applies `DEFAULT_SETTINGS` on first launch, and exposes read/write through IPC (`settings:get`, `settings:set`, `settings:reset`).

## Storage location

| Environment | `userData` path | Settings file |
|-------------|-----------------|---------------|
| Production (macOS) | `~/Library/Application Support/Build` | `claudette-settings.json` |
| Dev (`./scripts/dev.sh`) | `/tmp/grep-build-dev` (via `GREP_DEV_USER_DATA`) | Same filename, isolated from production |

On first launch after a rename from older app bundles, `migrateFromGrepBuild()` copies `claudette-settings.json` (and related store files) from legacy `G-Build` or `Grep Build` directories into the current `userData` folder.

<Info>
Dev builds never write into production `userData` when started through `./scripts/dev.sh`. The script optionally seeds dev settings from production only when dev files are missing.
</Info>

## File layout

Most product toggles and structured config are nested under `settings`. Sensitive keys used by harness services are often stored at the root of the same JSON file for direct `store.get('anthropicApiKey')` access.

```text
claudette-settings.json
├── settings: AppSettings          ← SettingsService.getSettings() / setSettings()
├── anthropicApiKey                ← SETTINGS_GET/SET_API_KEY helpers
├── githubToken
├── googleApiKey
├── foundryApiKey                  ← duplicate of settings.foundryApiKey possible
├── openAiApiKey                   ← audio / realtime
├── elevenLabsApiKey               ← voice / TTS
└── audioSettings: AudioSettings   ← separate nested object (AudioService)
```

```mermaid
flowchart LR
  subgraph renderer [Renderer]
    SD[SettingsDialog]
    TS[task.store]
    AS[audio.store]
  end
  subgraph ipc [IPC preload]
    API["window.electronAPI.settings"]
  end
  subgraph main [Main process]
    SS[SettingsService]
    AR[auto-router.service]
    CS[claude.service]
  end
  subgraph disk [userData]
    FILE["claudette-settings.json"]
  end
  SD --> API
  TS --> API
  AS --> API
  API --> SS
  SS --> FILE
  AR --> FILE
  CS --> FILE
```

## AppSettings (`settings` object)

Type: `AppSettings` in `src/shared/types/index.ts`. Defaults: `DEFAULT_SETTINGS` in `src/main/services/settings.service.ts`. Updates are shallow-merged: `setSettings(partial)` reads current `settings`, spreads `partial`, and writes back.

### Appearance and editor

| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `theme` | `'dark' \| 'light' \| 'system'` | `'dark'` | Defined on `AppSettings`; not exposed in **Settings → General** today |
| `fontSize` | `number` | `14` | Same — no settings UI binding found |
| `fontFamily` | `string` | `'JetBrains Mono, Menlo, Monaco, monospace'` | Same — terminal/editor use hard-coded fonts in components |

### Session bootstrap

| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `defaultSetupScript` | `string` | Bash template (npm/pip install) | Docker session setup script |
| `autoStartContainer` | `boolean` | `true` | Auto-start container on session create |

### QMD semantic search

| Field | Type | Default | UI | Behavior |
|-------|------|---------|-----|----------|
| `qmdEnabled` | `boolean` | `false` | General tab toggle | Opt-in global flag; per-project prefs live in separate store `claudette-qmd.json` |

<Warning>
`SettingsService` persists `qmdEnabled` under `settings.qmdEnabled`. `claude.service` also reads a top-level `qmdEnabled` key when attaching the QMD MCP server. If QMD does not activate after enabling the toggle, verify both nested and root keys in `claudette-settings.json` or align reads to `settings.qmdEnabled`.
</Warning>

### Ultra Plan and plan approval

| Field | Type | Default | UI |
|-------|------|---------|-----|
| `ultraPlanMode` | `boolean` | `false` | General → Ultra Plan Mode |
| `showClearContextOnPlanAccept` | `boolean` | unset (UI treats as `false`) | General → Clear Context on Plan Accept |

When `ultraPlanMode` is true, Claude Agent SDK hooks after successful `ExitPlanMode` can decompose approved plans into structured tasks.

### Reminders and reviews

| Field | Type | Default | UI / runtime |
|-------|------|---------|--------------|
| `lunchReminderEnabled` | `boolean` | `false` | General; status bar countdown in `App.tsx` |
| `lunchReminderTime` | `string` (HH:MM) | `'12:00'` | Used only when lunch reminder enabled |
| `bedtimeReminderEnabled` | `boolean` | `false` | General; 5-minute snooze in status bar |
| `bedtimeReminderTime` | `string` (HH:MM) | `'23:00'` | Used only when bedtime reminder enabled |
| `dailyReviewEnabled` | `boolean` | `true` | General |
| `dailyReviewTime` | `string` (HH:MM) | `'09:00'` | Morning review prompt when enabled |
| `bedtimeTaskReviewEnabled` | `boolean` | `true` | General; runs ~30 minutes before configured bedtime |

### GStack

| Field | Type | Default | UI |
|-------|------|---------|-----|
| `gstackEnabled` | `boolean` | `false` | **No Settings dialog toggle** |

When `true`, `claude.service` appends GStack routing instructions from `gstack.service` into the system prompt. Session-level GStack modes (`Session.gstackMode`) and the chat **G** launcher work independently of this flag.

### Foundry (Azure-hosted Claude)

| Field | Type | Default | UI (API Keys tab) |
|-------|------|---------|-------------------|
| `foundryEnabled` | `boolean` | `false` | Toggle |
| `foundryBaseUrl` | `string?` | — | Base URL (debounced save) |
| `foundryApiKey` | `string?` | — | API key field |
| `foundryDefaultSonnetModel` | `string?` | — | Model ID fields |
| `foundryDefaultHaikuModel` | `string?` | — | |
| `foundryDefaultOpusModel` | `string?` | — | |

When enabled, Claude runs set `ANTHROPIC_FOUNDRY_*` environment variables and route model availability through Foundry defaults.

### Focus tasks

| Field | Type | Default | Persisted by |
|-------|------|---------|--------------|
| `focusTasks` | `FocusTask[]` | `[]` | `task.store` via `settings.set` |
| `focusModeEnabled` | `boolean` | `false` | Task list UI |
| `focusModeActiveTaskId` | `string?` | — | Active task in focus mode |

`FocusTask` shape: `id`, `title`, `order`, `status` (`pending` \| `active` \| `done`), optional `sessionId`, `createdAt`, `completedAt`, optional `subtasks[]`.

### Custom proxy models

| Field | Type | Default | UI |
|-------|------|---------|-----|
| `customModels` | `CustomModelConfig[]` | `[]` | API Keys → Custom models |

`CustomModelConfig`: `id`, `name`, `modelId`, `baseUrl`, `apiKey`, optional `description`. Entries need `id`, `apiKey`, `baseUrl`, and `modelId` for Auto Build credential checks.

### Harness API keys (nested in `settings`)

| Field | Type | UI tab |
|-------|------|--------|
| `cursorApiKey` | `string?` | API Keys |
| `deepseekApiKey` | `string?` | API Keys (OpenCode / DeepSeek) |
| `geminiApiKey` | `string?` | API Keys |

### Onboarding and analytics (optional)

| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `onboardingSkipped` | `boolean?` | — | Set when user skips API key onboarding |
| `posthogApiKey` | `string?` | — | Optional; `analytics.service` also reads env fallbacks |
| `posthogHost` | `string?` | — | Defaults to `https://us.i.posthog.com` when key present |

### Legacy / duplicate typed fields

`AppSettings` also declares `anthropicApiKey?` and `githubToken?`, but live reads for Anthropic/GitHub in main process prefer **top-level** store keys (see below).

## Top-level keys in `claudette-settings`

These sit beside `settings`, not inside it:

| Key | Access | Purpose |
|-----|--------|---------|
| `anthropicApiKey` | `SettingsService.getApiKey()` / `setApiKey()` | Primary Claude API key |
| `githubToken` | `getGitHubToken()` / `setGitHubToken()` | GitHub integration |
| `googleApiKey` | `getGoogleApiKey()` / `setGoogleApiKey()` | Gemini / Google harness |
| `foundryApiKey` | `getFoundryApiKey()` / `setFoundryApiKey()` | Foundry auth (may mirror `settings.foundryApiKey`) |
| `openAiApiKey` | `audio.service`, `realtime.service` | Transcription / Realtime |
| `elevenLabsApiKey` | `audio.service`, `elevenlabs-voice.service` | Voice mode / TTS |
| `audioSettings` | `AudioService` | Voice, Ralph Loop, Computer Use (see below) |

<Note>
Report security issues per [SECURITY.md](SECURITY.md): API keys stay on disk locally and are sent only to configured providers. Optional PostHog requires an explicit key in settings or environment.
</Note>

## `autoRouterConfig`

Optional field on `AppSettings`: `autoRouterConfig?: AutoRouterConfig`. Saved from **Settings → Auto Build** via `autoBuildConfigFromState()` in `SettingsDialog.tsx`. Runtime resolution merges saved values with `DEFAULT_CONFIG` in `auto-router.service.ts`.

### Runtime defaults (`DEFAULT_CONFIG`)

Used when no user overrides exist:

| Field | Default |
|-------|---------|
| `enabled` | `true` |
| `planModel` | `claude-sonnet-4-6` |
| `buildModel` | `codex:gpt-5.5` |
| `verifyModel` | `codex:gpt-5.5` |
| `refineModel` | `cursor:composer-2.5` |
| `fallbackModel` | `claude-sonnet-4-6` |
| `costAware` | `true` |
| `costThresholdPercent` | `80` (not exposed in Settings UI) |

Tier-specific `*Effort`, `*Speed`, `*Workflow`, `*BudgetUsd`, and `*Verification` fields are omitted until set in the UI (empty / `auto` in the dialog means “omit from saved JSON”).

### Settings UI defaults (before first save)

| Tier | Default model (UI) |
|------|-------------------|
| plan | `claude-sonnet-4-6` |
| build | `codex:gpt-5.5` |
| verify | `codex:gpt-5.5` |
| refine | `cursor:composer-2.5` |
| fallback | `claude-sonnet-4-6` |

Policy UI defaults: effort `''` (Default), speed/workflow/verification `'auto'`, budget `''`.

`costAware` defaults to `true` in the Auto Build tab. Custom categories use ids outside `plan`, `build`, `verify`, `refine`, `fallback` and match on `keywords` during routing.

Legacy configs may store tier models inside `categories[]` with fixed ids; `migrateAutoBuildModels()` promotes those into `planModel` / `buildModel` / etc. when loading the dialog.

For routing behavior, orchestration, and verification scripts, see the dedicated Auto Build configuration page.

## Audio settings (`audioSettings`)

Separate nested object, not part of `AppSettings`:

| Field | Default (high level) |
|-------|----------------------|
| `voiceModeEnabled` | `true` |
| `ralphLoopEnabled` | `false` |
| `computerUseEnabled` | `false` |
| `maxComputerUseIterations` | `20` |
| `selectedVoice` / `voiceSettings` | ElevenLabs Rachel preset |
| `voiceTriggerWord` | `'please'` |

Managed through `window.electronAPI.audio` IPC; Ralph Loop and Computer Use toggles appear under **Settings → General** (Just Build It Mode section).

## IPC and preload API

| Channel | Handler | Returns / effect |
|---------|---------|------------------|
| `settings:get` | `SettingsService.getSettings()` | Full `AppSettings` |
| `settings:set` | `setSettings(partial)` | Shallow merge into `settings` |
| `settings:reset` | `resetSettings()` | Restore `DEFAULT_SETTINGS` |
| `settings:get-api-key` | top-level `anthropicApiKey` | string (empty if missing) |
| `settings:set-api-key` | top-level write | |
| `settings:get-google-api-key` | top-level `googleApiKey` | |
| `settings:set-google-api-key` | top-level write | |

Renderer usage:

```typescript
await window.electronAPI.settings.get();
await window.electronAPI.settings.set({ qmdEnabled: true });
await window.electronAPI.settings.setApiKey('sk-...');
await window.electronAPI.settings.reset();
```

## Settings dialog tabs

| Tab | Persists to |
|-----|-------------|
| **General** | `qmdEnabled`, reminders, ultra plan, daily/bedtime reviews; audio flags via `audio` IPC |
| **Auto Build** | `autoRouterConfig` (models, costAware, tier policy, custom categories) |
| **API Keys** | Top-level Anthropic/OpenAI/Google/ElevenLabs keys; nested Foundry, Cursor, DeepSeek, Gemini, `customModels` |
| **Releases** | Read-only release notes |

Changes auto-save with debounced text fields (500 ms) for secrets and URLs.

## Related electron-store files

Not in `claudette-settings`, but commonly edited alongside settings:

| File | Purpose |
|------|---------|
| `claudette-sessions.json` | Session records |
| `claudette-qmd.json` | QMD collections and per-project enable/disable |
| `claudette-mcp-servers.json` | Installed MCP servers |
| `claudette-analytics.json` | Local usage events |
| `grep-auth` (separate store name) | GitHub OAuth tokens |

## Reset and inspection

- **In app:** no full reset button in the dialog; use `settings:reset` from devtools or custom tooling to restore `DEFAULT_SETTINGS` (does not clear top-level API keys unless you delete them separately).
- **On disk:** quit Build, edit or remove `claudette-settings.json` under the correct `userData` path, restart.

<Steps>
<Step title="Confirm which data directory is active">
Production uses `~/Library/Application Support/Build`. Dev uses `/tmp/grep-build-dev` when launched via `./scripts/dev.sh`.
</Step>
<Step title="Back up the file">
Copy `claudette-settings.json` before manual edits.
</Step>
<Step title="Verify nested vs top-level keys">
API keys for Claude should exist at `anthropicApiKey` (root). Product toggles belong under `settings`.
</Step>
</Steps>

## Related pages

<CardGroup>
<Card title="Configure API keys and providers" href="/configure-providers">
Anthropic, OpenAI, Google, Foundry, Cursor, DeepSeek, and custom proxy setup beyond the raw store keys.
</Card>
<Card title="Auto Build configuration" href="/auto-build-configuration">
Full `AutoRouterConfig` routing semantics, tiers, and `verify:auto-router:*` scripts.
</Card>
<Card title="Semantic search (QMD)" href="/semantic-search-qmd">
Project preferences in `claudette-qmd.json` and indexing workflow.
</Card>
<Card title="IPC and preload bridge" href="/ipc-bridge">
How `settings:*` channels map to `window.electronAPI.settings`.
</Card>
<Card title="Installation" href="/installation">
Production vs dev `userData` separation and first-run migration.
</Card>
</CardGroup>

---

## 19. IPC channels reference

> Complete IPC_CHANNELS catalog grouped by domain (auth, session, claude, browser, git, mcp, qmd, ssh, voice, analytics) with invoke vs push event semantics.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/19-ipc-channels-reference.md
- Generated: 2026-06-02T00:24:37.060Z

### Source Files

- `src/shared/constants/channels.ts`
- `src/main/preload.ts`
- `src/main/ipc/claude.ipc.ts`
- `src/main/ipc/browser.ipc.ts`
- `src/main/ipc/analytics.ipc.ts`
- `CLAUDE.md`

---
title: "IPC channels reference"
description: "Complete IPC_CHANNELS catalog grouped by domain (auth, session, claude, browser, git, mcp, qmd, ssh, voice, analytics) with invoke vs push event semantics."
---

Build names every main↔renderer boundary in `IPC_CHANNELS` (`src/shared/constants/channels.ts`). Handlers register from `src/main/index.ts` into `src/main/ipc/*.ts` (and a few inline handlers in `index.ts`). The renderer reaches main only through `contextBridge.exposeInMainWorld('electronAPI', …)` in `src/main/preload.ts`—no direct `ipcRenderer` access in React code.

## IPC patterns

| Pattern | Renderer | Main | Returns payload? | Typical use |
|--------|----------|------|------------------|-------------|
| **Invoke** | `ipcRenderer.invoke(channel, …args)` | `ipcMain.handle(channel, handler)` | Yes (`Promise`) | CRUD, settings, start/stop work |
| **Push (main → renderer)** | `ipcRenderer.on(channel, handler)` via preload `on*` helpers | `webContents.send(channel, payload)` or `broadcastToAll` | No | Streaming, status, dialogs |
| **Send (renderer → main)** | `ipcRenderer.send(channel, …args)` | `ipcMain.on(channel, handler)` | No | High-frequency input (terminal, webview registration) |

<Note>
Preload `on*` methods return an unsubscribe function. Always call it on unmount to avoid duplicate listeners.
</Note>

```mermaid
sequenceDiagram
  participant R as Renderer (React)
  participant P as preload.ts
  participant M as ipcMain handler
  participant S as Service layer

  R->>P: electronAPI.claude.sendMessage(...)
  P->>M: invoke claude:send-message
  M->>S: ClaudeService.stream...
  loop streaming
    S->>R: push claude:stream-chunk
  end
  S->>R: push claude:stream-end
  M-->>P: Promise resolved
  P-->>R: invoke completes
```

**Bidirectional flows** pair invoke + push on different channels—for example `claude:permission-request` (push) with `claude:permission-response` (invoke).

## Handler registration map

| Registrar | File |
|-----------|------|
| `registerAuthHandlers` | `auth.ipc.ts` |
| `registerSessionHandlers` | `session.ipc.ts` |
| `registerClaudeHandlers` | `claude.ipc.ts` |
| `registerGitHandlers` | `git.ipc.ts` |
| `registerBrowserHandlers` | `browser.ipc.ts` |
| `registerSSHHandlers` | `ssh.ipc.ts` |
| `registerMcpHandlers` | `mcp.ipc.ts` |
| `registerQmdHandlers` | `qmd.ipc.ts` |
| `registerVoiceHandlers` | `voice.ipc.ts` |
| `registerRealtimeHandlers` | `realtime.ipc.ts` |
| `registerAudioHandlers` | `audio.ipc.ts` |
| `registerAnalyticsHandlers` | `analytics.ipc.ts` |
| `registerQueueHandlers` | `queue.ipc.ts` |
| App, settings, docker | `settings.ipc.ts` |
| GStack | inline in `index.ts` |

---

## Auth

| Channel constant | String | Semantics | Preload API |
|------------------|--------|-----------|-------------|
| `AUTH_LOGIN` | `auth:login` | Invoke | `auth.login()` |
| `AUTH_LOGOUT` | `auth:logout` | Invoke | `auth.logout()` |
| `AUTH_GET_USER` | `auth:get-user` | Invoke | `auth.getUser()` |
| `AUTH_GET_REPOS` | `auth:get-repos` | Invoke | `auth.getRepos()` |
| `AUTH_CHECK_PROVIDERS` | `auth:check-providers` | Invoke | `auth.checkProviders()` |
| `AUTH_STATUS` | `auth:status` | — | *Declared in `IPC_CHANNELS`; no `ipcMain.handle` or preload wrapper found* |

**Push (not in `IPC_CHANNELS`):**

| Channel | Semantics | Preload API |
|---------|-----------|-------------|
| `auth:oauth-callback` | Main pushes OAuth code after `claudette://` callback | `auth.onOAuthCallback()` |

---

## Session

| Channel constant | String | Semantics | Preload API |
|------------------|--------|-----------|-------------|
| `SESSION_CREATE` | `session:create` | Invoke | `sessions.create(config)` |
| `SESSION_START` | `session:start` | Invoke | `sessions.start(sessionId)` |
| `SESSION_STOP` | `session:stop` | Invoke | `sessions.stop(sessionId)` |
| `SESSION_DELETE` | `session:delete` | Invoke | `sessions.delete(sessionId)` |
| `SESSION_LIST` | `session:list` | Invoke | `sessions.list()` |
| `SESSION_GET` | `session:get` | Invoke | `sessions.get(sessionId)` |
| `SESSION_UPDATE` | `session:update` | Invoke | `sessions.update(sessionId, updates)` |
| `SESSION_REWIND_FORK` | `session:rewind-fork` | Invoke | `sessions.rewindAndFork(...)` |
| `SESSION_CREATE_FORK` | `session:create-fork` | Invoke | `sessions.createFork(...)` |
| `SESSION_GET_FORK_GROUP` | `session:get-fork-group` | Invoke | `sessions.getForkGroup(sessionId)` |
| `SESSION_SCAN_REMOTE` | `session:scan-remote` | Invoke | `sessions.scanRemoteTranscripts(sessionId)` |
| `SESSION_STATUS_CHANGED` | `session:status-changed` | Push (`SessionService` → `broadcastToAll`) | `sessions.onStatusChanged()` |
| `SESSION_LIST_UPDATED` | `session:list-updated` | Push (full list refresh) | `sessions.onListUpdated()` |

---

## Claude (agent harness)

### Invoke (renderer → main)

| Channel constant | String | Preload API |
|------------------|--------|-------------|
| `CLAUDE_SEND_MESSAGE` | `claude:send-message` | `claude.sendMessage(...)` |
| `CLAUDE_RESUME_REMOTE_TURN` | `claude:resume-remote-turn` | `claude.resumeRemoteTurn(...)` |
| `CLAUDE_GET_MESSAGES` | `claude:get-messages` | `claude.getMessages(...)` |
| `CLAUDE_HAS_BUILD_TRANSCRIPT` | `claude:has-build-transcript` | `claude.hasBuildTranscript(...)` |
| `CLAUDE_GET_MODELS` | `claude:get-models` | `claude.getModels()` |
| `CLAUDE_CANCEL` | `claude:cancel` | `claude.cancel(sessionId)` |
| `CLAUDE_PERMISSION_RESPONSE` | `claude:permission-response` | `claude.respondToPermission(...)` |
| `CLAUDE_QUESTION_RESPONSE` | `claude:question-response` | `claude.respondToQuestion(...)` |
| `CLAUDE_PLAN_APPROVAL_RESPONSE` | `claude:plan-approval-response` | `claude.respondToPlanApproval(...)` |
| `CLAUDE_INJECT_MESSAGE` | `claude:inject-message` | `claude.injectMessage(...)` |
| `CLAUDE_HAS_ACTIVE_QUERY` | `claude:has-active-query` | `claude.hasActiveQuery(...)` |
| `CLAUDE_SET_PERMISSION_MODE` | `claude:set-permission-mode` | `claude.setPermissionMode(...)` |
| `CLAUDE_BTW_ASK` | `claude:btw-ask` | `claude.askBtw(...)` |
| `CLAUDE_RC_START` / `CLAUDE_RC_STOP` | `claude:rc-start` / `claude:rc-stop` | `claude.startRc` / `stopRc` |
| `CLAUDE_REWIND_PREVIEW` / `CLAUDE_REWIND_EXECUTE` | `claude:rewind-preview` / `claude:rewind-execute` | `claude.rewindPreview` / `rewindExecute` |
| `AUTO_RESUME_SAVE_STATE` | `auto-resume:save-state` | `claude.saveAutoResumeState(...)` |
| `AUTO_RESUME_GET_STATE` | `auto-resume:get-state` | `claude.getAutoResumeState()` |
| `AUTO_RESUME_CLEAR_STATE` | `auto-resume:clear-state` | `claude.clearAutoResumeState()` |

### Push (main → renderer)

| Channel constant | String | Preload listener | Emitted from |
|------------------|--------|------------------|--------------|
| `CLAUDE_STREAM_CHUNK` | `claude:stream-chunk` | `onStreamChunk` | `claude.ipc.ts` (`sendToSender`) |
| `CLAUDE_THINKING_CHUNK` | `claude:thinking-chunk` | `onThinkingChunk` | `claude.ipc.ts` |
| `CLAUDE_STREAM_END` | `claude:stream-end` | `onStreamEnd` | `claude.ipc.ts` |
| `CLAUDE_STREAM_ERROR` | `claude:stream-error` | `onStreamError` | `claude.ipc.ts` |
| `CLAUDE_TOOL_CALL` | `claude:tool-call` | `onToolCall` | `claude.ipc.ts` |
| `CLAUDE_TOOL_RESULT` | `claude:tool-result` | `onToolResult` | `claude.ipc.ts` |
| `CLAUDE_SYSTEM_INFO` | `claude:system-info` | `onSystemInfo` | `claude.ipc.ts` |
| `CLAUDE_PERMISSION_REQUEST` | `claude:permission-request` | `onPermissionRequest` | `claude.service.ts` |
| `CLAUDE_QUESTION_REQUEST` | `claude:question-request` | `onQuestionRequest` | `claude.service.ts` |
| `CLAUDE_PLAN_APPROVAL_REQUEST` | `claude:plan-approval-request` | `onPlanApprovalRequest` | `claude.service.ts` |
| `CLAUDE_PLAN_CONTENT` | `claude:plan-content` | `onPlanContent` | `claude.service.ts` |
| `CLAUDE_COMPACTION_STATUS` | `claude:compaction-status` | `onCompactionStatus` | `claude.service.ts` |
| `CLAUDE_COMPACTION_COMPLETE` | `claude:compaction-complete` | `onCompactionComplete` | `claude.service.ts` |
| `CLAUDE_CONTEXT_USAGE` | `claude:context-usage` | `onContextUsage` | `claude.ipc.ts` / service |
| `CLAUDE_PERMISSION_MODE_CHANGED` | `claude:permission-mode-changed` | `onPermissionModeChanged` | `claude.service.ts` |
| `CLAUDE_TASK_NOTIFICATION` | `claude:task-notification` | `onTaskNotification` | `claude.service.ts` |
| `CLAUDE_TASK_PROGRESS` | `claude:task-progress` | `onTaskProgress` | `claude.service.ts` |
| `CLAUDE_TASK_UPDATED` | `claude:task-updated` | `onTaskUpdated` | `claude.service.ts` |
| `CLAUDE_BTW_RESPONSE` | `claude:btw-response` | `onBtwResponse` | `claude.service.ts` |
| `CLAUDE_RC_STARTED` / `CLAUDE_RC_STOPPED` | `claude:rc-started` / `claude:rc-stopped` | `onRcStarted` / `onRcStopped` | `claude.service.ts` |
| `CLAUDE_WAKEUP_FIRED` | `claude:wakeup-fired` | `onWakeupFired` | `index.ts` (`broadcastToAll`) |
| `CLAUDE_AUTO_ROUTE_DECISION` | `claude:auto-route-decision` | `onAutoRouteDecision` | `claude.service.ts` (Auto Build routing) |
| `CLAUDE_BACKGROUND_TASK_OUTPUT` | `claude:background-task-output` | `onBackgroundTaskOutput` | *Preload wired; no main sender located* |

**Message queue (string literals, not in `IPC_CHANNELS`):**

| Channel | Semantics | Preload |
|---------|-----------|---------|
| `queue:enqueue` / `remove` / `edit` / `moveToFront` / `clear` / `getState` | Invoke | `electronAPI.queue.*` |
| `queue:send-next` | Push (main asks renderer to send next queued message) | `claude.onQueueSendNext()` |
| `queue:state-changed` | Push | `claude.onQueueStateChanged()` |

---

## Browser

| Channel constant | String | Semantics | Preload API |
|------------------|--------|-----------|-------------|
| `BROWSER_CAPTURE_SNAPSHOT` | `browser:capture-snapshot` | Invoke | `browser.captureSnapshot` |
| `BROWSER_NAVIGATE_TO` | `browser:navigate-to` | Invoke | `browser.navigateTo` |
| `BROWSER_GET_SNAPSHOT` | `browser:get-snapshot` | Invoke | `browser.getSnapshot` |
| `BROWSER_CLEAR_STORAGE` | `browser:clear-storage` | Invoke | `browser.clearStorage` |
| `BROWSER_INJECT_INSPECTOR` | `browser:inject-inspector` | Invoke (preload) | `browser.injectInspector` — *no `ipcMain.handle` found* |
| `BROWSER_NAVIGATE` | `browser:navigate` | Push | `browser.onNavigate()` ← `browser.service.ts` |
| `BROWSER_ELEMENT_SELECTED` | `browser:element-selected` | Push (preload listener) | `browser.onElementSelected()` — *no main sender found* |
| `BROWSER_UPDATE` | `browser:update` | Push (Stagehand screenshot/URL) | `browser.onBrowserUpdate()` |
| `BROWSER_OPEN_PANEL` | `browser:open-panel` | Push | `browser.onBrowserOpenPanel()` |

**Send / on pairs (webview bridge, not in `IPC_CHANNELS`):**

| Channel | Direction | Role |
|---------|-----------|------|
| `browser:register-webview` / `browser:unregister-webview` | Renderer → main (`send`) | CDP target registration (`browser.service.ts`) |
| `browser:capture-snapshot` | Main → renderer (`send`) | Async capture request |
| `browser:snapshot-captured` | Renderer → main (`send`) | Snapshot payload reply |
| `browser:action` / `browser:action-result` | Round-trip automation | Stagehand/CDP |
| `browser:automation-event` | Main → renderer | `browser.onAutomationEvent()` |

---

## Git

| Channel constant | String | Semantics | Preload API |
|------------------|--------|-----------|-------------|
| `GIT_STATUS` | `git:status` | Invoke | `git.getStatus` |
| `GIT_LOG` | `git:log` | Invoke | `git.getLog` |
| `GIT_BRANCHES` | `git:branches` | Invoke | `git.getBranches` |
| `GIT_CHECKOUT` | `git:checkout` | Invoke | `git.checkout` |
| `GIT_DIFF` | `git:diff` | Invoke | `git.getDiff` |
| `GIT_COMMIT` | `git:commit` | Invoke | `git.commit` |
| `GIT_PUSH` / `GIT_PULL` | `git:push` / `git:pull` | Invoke | `git.push` / `pull` |
| `GIT_CLONE` | `git:clone` | Invoke | `git.clone` |
| `GIT_REMOTE_BRANCH` | `git:remote-branch` | Invoke | `git.getRemoteBranch` (SSH) |
| `GIT_WATCH_BRANCH` / `GIT_UNWATCH_BRANCH` | `git:watch-branch` / `git:unwatch-branch` | Invoke | `git.watchBranch` / `unwatchBranch` |
| `GIT_BRANCH_CHANGED` | `git:branch-changed` | Push | `git.onBranchChanged()` |

---

## MCP

| Channel constant | String | Semantics | Preload API |
|------------------|--------|-----------|-------------|
| `MCP_GET_SERVERS` | `mcp:get-servers` | Invoke | `mcp.getServers(sessionId, projectPath?)` |
| `MCP_GET_RAW_CONFIG` | `mcp:get-raw-config` | Invoke | `mcp.getRawConfig(serverId)` |
| `MCP_GET_MARKETPLACE` | `mcp:get-marketplace` | Invoke | `mcp.getMarketplace()` |
| `MCP_INSTALL_SERVER` | `mcp:install-server` | Invoke | `mcp.install(serverId, authValues)` |
| `MCP_INSTALL_SERVER_RAW` | `mcp:install-server-raw` | Invoke | `mcp.installRaw(...)` |
| `MCP_UNINSTALL_SERVER` | `mcp:uninstall-server` | Invoke | `mcp.uninstall(serverId)` |

Installing or changing MCP config can trigger SSH remote sync inside `mcp.ipc.ts` (no separate push channel).

---

## QMD (semantic search)

| Channel constant | String | Semantics | Preload API |
|------------------|--------|-----------|-------------|
| `QMD_GET_STATUS` | `qmd:get-status` | Invoke | `qmd.getStatus()` |
| `QMD_ENSURE_INDEXED` | `qmd:ensure-indexed` | Invoke | `qmd.ensureIndexed(projectPath)` |
| `QMD_CREATE_COLLECTION` | `qmd:create-collection` | Invoke | `qmd.createCollection(...)` |
| `QMD_GENERATE_EMBEDDINGS` | `qmd:generate-embeddings` | Invoke | `qmd.generateEmbeddings(...)` |
| `QMD_SEARCH` | `qmd:search` | Invoke | `qmd.search(query, options?)` |
| `QMD_GET_PROJECT_PREFERENCE` | `qmd:get-project-preference` | Invoke | `qmd.getProjectPreference` |
| `QMD_SET_PROJECT_PREFERENCE` | `qmd:set-project-preference` | Invoke | `qmd.setProjectPreference` |
| `QMD_SHOULD_PROMPT` | `qmd:should-prompt` | Invoke | `qmd.shouldPrompt` |
| `QMD_AUTO_INSTALL` | `qmd:auto-install` | Invoke | `qmd.autoInstall()` |
| `QMD_INDEXING_PROGRESS` | `qmd:indexing-progress` | Push | `qmd.onIndexingProgress()` |
| `QMD_PROMPT_RESPONSE` | `qmd:prompt-response` | Push (opt-in indexing UI) | `qmd.onPromptRequest()` |

<Warning>
Despite the name `QMD_PROMPT_RESPONSE`, main uses this channel to **request** that the renderer show the QMD opt-in prompt (`claude.service.ts` → `webContents.send`). Renderer answers via invoke handlers, not by sending on this channel.
</Warning>

---

## SSH

| Channel constant | String | Semantics | Preload API |
|------------------|--------|-----------|-------------|
| `SSH_TEST_CONNECTION` | `ssh:test-connection` | Invoke | `ssh.testConnection` |
| `SSH_CREATE_SESSION` | `ssh:create-session` | Invoke | `ssh.createSession` |
| `SSH_LIST_RESUME_CANDIDATES` | `ssh:list-resume-candidates` | Invoke | `ssh.listResumeCandidates` |
| `SSH_SYNC_SETTINGS` | `ssh:sync-settings` | Invoke | *(no preload wrapper)* |
| `SSH_RUN_WORKTREE_SCRIPT` | `ssh:run-worktree-script` | Invoke | *(no preload wrapper)* |
| `SSH_GET_SAVED_CONFIG` / `SSH_SAVE_CONFIG` | `ssh:get-saved-config` / `ssh:save-config` | Invoke | `ssh.getSavedConfig` / `saveConfig` |
| `SSH_CHECK_CONNECTION` | `ssh:check-connection` | Invoke | `ssh.checkConnection` |
| `SSH_TELEPORT_SESSION` | `ssh:teleport-session` | Invoke | `ssh.teleportSession` |
| `SSH_DOWNLOAD_SESSION` | `ssh:download-session` | Invoke | `ssh.downloadSession` |
| `SSH_RECONNECT` | `ssh:reconnect` | Invoke | `ssh.reconnect` |
| `SSH_HAS_ACTIVE_REMOTE_PROCESS` | `ssh:has-active-remote-process` | Invoke | `ssh.hasActiveRemoteProcess` |
| `SSH_HAS_RECOVERABLE_REMOTE_PROCESS` | `ssh:has-recoverable-remote-process` | Invoke | `ssh.hasRecoverableRemoteProcess` |
| `SSH_BROWSE_REMOTE_FILES` | `ssh:browse-remote-files` | Invoke | `ssh.browseRemoteFiles` |
| `SSH_SETUP_PROGRESS` | `ssh:setup-progress` | Push | `ssh.onSetupProgress()` |
| `SSH_DOWNLOAD_PROGRESS` | `ssh:download-progress` | Push | `ssh.onDownloadProgress()` |
| `SSH_CONNECTION_LOST` | `ssh:connection-lost` | Push | `ssh.onConnectionLost()` |

---

## Voice and audio

Voice uses ElevenLabs conversational AI (`voice.ipc.ts`). Audio covers batch STT/TTS (`audio.ipc.ts`). Realtime OpenAI transcription lives in `realtime.ipc.ts`.

### Voice (`VOICE_*`)

| Type | Channels | Preload |
|------|----------|---------|
| Invoke | `VOICE_CONNECT`, `DISCONNECT`, `SEND_AUDIO`, `SEND_TEXT`, `END_INPUT`, `CLEAR_AUDIO_BUFFER`, `CONTEXT_UPDATE`, `TOOL_RESULT`, `UPDATE_AGENT_PROMPT`, `USER_ACTIVITY`, `GET_SIGNED_URL`, `GET_CONVERSATION_TOKEN` | `electronAPI.voice.*` |
| Push | `VOICE_CONNECTED`, `DISCONNECTED`, `RECONNECTING`, `USER_TRANSCRIPT`, `AGENT_RESPONSE`, `AUDIO_CHUNK`, `INTERRUPTION`, `ERROR`, `TOOL_CALL` | matching `on*` methods |

### Audio + realtime (related)

| Domain | Invoke examples | Push examples |
|--------|-----------------|---------------|
| `AUDIO_*` | `AUDIO_TRANSCRIBE`, `AUDIO_TTS_STREAM`, key/settings getters | `AUDIO_TTS_CHUNK`, `COMPLETE`, `ERROR` |
| `REALTIME_*` | `REALTIME_CONNECT`, `SEND_AUDIO`, `COMMIT_AUDIO`, … | `REALTIME_TRANSCRIPTION_DELTA`, `CONNECTED`, `SPEECH_STARTED`, … |

---

## Analytics

| Channel constant | String | Semantics | Preload API |
|------------------|--------|-----------|-------------|
| `ANALYTICS_GET_SUMMARY` | `analytics:get-summary` | Invoke | `analytics.getSummary()` |
| `ANALYTICS_GET_SESSION_COST` | `analytics:get-session-cost` | Invoke | `analytics.getSessionCost(sessionId)` |
| `ANALYTICS_GET_TIER_CONFIG` | `analytics:get-tier-config` | Invoke | `analytics.getTierConfig()` |
| `ANALYTICS_SET_TIER_CONFIG` | `analytics:set-tier-config` | Invoke | `analytics.setTierConfig(config)` |
| `ANALYTICS_GET_HARNESS_INSIGHTS` | `analytics:get-harness-insights` | Invoke | `analytics.getHarnessInsights()` |
| `ANALYTICS_REFRESH_HISTORICAL_USAGE` | `analytics:refresh-historical-usage` | Invoke | `analytics.refreshHistoricalUsage(options?)` |
| `ANALYTICS_RUN_ROUTER_EVAL` | `analytics:run-router-eval` | Invoke | `analytics.runRouterEval(options?)` |
| `ANALYTICS_RECORD_HARNESS_SELECTION` | `analytics:record-harness-selection` | Invoke | `analytics.recordHarnessSelection(event)` |
| `ANALYTICS_TOKEN_EVENT` | `analytics:token-event` | Push (per-token usage during streams) | `analytics.onTokenEvent()` |

Token events are also emitted from `claude.service.ts` during agent turns, not only from `analytics.ipc.ts`.

---

## Other `IPC_CHANNELS` domains (compact)

| Prefix | Invoke-heavy | Push-heavy | Notes |
|--------|--------------|------------|-------|
| `settings:` / `app:` | `SETTINGS_*`, most `APP_*` | `APP_CMD_R_PRESSED`, `APP_SHORTCUT_TRIGGERED` | Registered in `settings.ipc.ts` + `index.ts` |
| `dev:` | Most dev session/worktree ops | `DEV_SETUP_PROGRESS` (*no sender found*) | Extra literals: `dev:init-git`, `dev:get-active-session`, … |
| `terminal:` | `TERMINAL_CREATE` | `TERMINAL_OUTPUT:{terminalId}` | Input uses **send**: `TERMINAL_INPUT`, `RESIZE`, `CLOSE` |
| `docker:` | `DOCKER_STATUS` | — | `DOCKER_CONTAINER_STATS` / `LOGS` declared, no handlers found |
| `fs:` | All four | — | Project-scoped file search |
| `extension:` | Scan/install skills | — | |
| `secure-keys:` | All four | — | Key interception before agent send |
| `memory:` | remember/recall/forget/list/sync | — | |
| `plugin:` | Marketplace CRUD | — | |
| `codex:` | `CODEX_RUN`, `CODEX_CANCEL` | `CODEX_STREAM_CHUNK`, `THINKING`, `TOOL_CALL`, `COMPLETE`, `ERROR` | Second-opinion harness |
| `gstack:` | All four | — | Handlers inline in `index.ts` |
| `openclaw:` | `OPENCLAW_CREATE_SESSION` | — | |

---

## Constants without active handlers

These appear in `IPC_CHANNELS` and sometimes in preload, but no matching `ipcMain.handle` / `ipcMain.on` was found in the repo:

- `AUTH_STATUS`
- `DOCKER_CONTAINER_STATS`, `DOCKER_CONTAINER_LOGS`
- `BROWSER_INJECT_INSPECTOR`, `BROWSER_REGISTER` (registration uses `browser:register-webview` instead)
- `AUTO_RESUME_TRIGGER`
- `DEV_SETUP_PROGRESS`
- `CLAUDE_BACKGROUND_TASK_OUTPUT` (listener only)

Treat them as reserved or incomplete wiring until a handler lands.

---

## Related pages

<CardGroup>
  <Card title="IPC and preload bridge" href="/ipc-bridge">
    How handlers map to window.electronAPI and contextBridge security.
  </Card>
  <Card title="Electron process model" href="/electron-architecture">
    Main vs renderer boundaries and service layout.
  </Card>
  <Card title="Manage coding sessions" href="/manage-sessions">
    Session lifecycle channels in product workflow.
  </Card>
  <Card title="Auto Build routing" href="/auto-build-routing">
    claude:auto-route-decision and router analytics.
  </Card>
  <Card title="MCP servers" href="/mcp-servers">
    MCP install channels and remote SSH sync.
  </Card>
  <Card title="Semantic search (QMD)" href="/semantic-search-qmd">
    QMD invoke/push indexing flow.
  </Card>
</CardGroup>

---

## 20. Auto Build configuration reference

> AutoRouterConfig fields (planModel, buildModel, verifyModel, refineModel, categories, costAware), MetaHarnessPolicy options, and npm verify:auto-router:* regression scripts.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/20-auto-build-configuration-reference.md
- Generated: 2026-06-02T00:25:16.130Z

### Source Files

- `src/shared/types/index.ts`
- `src/main/services/auto-router.service.ts`
- `src/main/services/flue-meta-router.service.ts`
- `package.json`
- `scripts/verify-auto-router-meta-harness.ts`
- `scripts/verify-auto-router-goal-orchestration.ts`

---
title: "Auto Build configuration reference"
description: "AutoRouterConfig fields (planModel, buildModel, verifyModel, refineModel, categories, costAware), MetaHarnessPolicy options, and npm verify:auto-router:* regression scripts."
---

`autoRouterConfig` lives under `settings` in the `claudette-settings` electron-store. The main-process `autoRouterService` merges saved values with `DEFAULT_CONFIG`, applies tier and custom-category policies, and feeds routing when the session model picker is set to `auto`. Settings are edited in **Settings → Auto Build** and persisted through `autoBuildConfigFromState` in the renderer.

## Where configuration is stored

| Layer | Location | Behavior |
| --- | --- | --- |
| Persistence | `claudette-settings` → `settings.autoRouterConfig` | Survives restarts; merged with code defaults on read |
| UI | Settings dialog, **Auto Build** tab | Writes tier models, policies, custom categories, `costAware` |
| Runtime API | `autoRouterService.getConfig()` / `setConfig(partial)` | Main process; `setConfig` shallow-merges into current effective config |

Auto Build runs when `selectedModel === 'auto'` in `claude.service` (for example after `/goal`, which forces `auto`). The `AutoRouterConfig.enabled` field exists on the type and in `DEFAULT_CONFIG` (`true`), but `auto-router.service` does not gate routing on it today.

## Tier model defaults

`DEFAULT_CONFIG` in `auto-router.service` supplies runtime defaults when fields are missing:

| Field | Default |
| --- | --- |
| `planModel` | `claude-sonnet-4-6` |
| `buildModel` | `codex:gpt-5.5` |
| `verifyModel` | `codex:gpt-5.5` |
| `refineModel` | `cursor:composer-2.5` |
| `fallbackModel` | `claude-sonnet-4-6` |
| `costAware` | `true` |
| `costThresholdPercent` | `80` |

The Settings UI uses the same tier model defaults via `AUTO_BUILD_MODEL_DEFAULTS` (`plan`, `build`, `verify`, `refine`, `fallback`).

### Model string format

Model IDs use an optional harness prefix; bare `claude-*` IDs map to the Claude harness:

| Prefix | Harness |
| --- | --- |
| `codex:` | `codex` |
| `cursor:` | `cursor` |
| `gemini:` | `gemini` |
| `opencode:` | `opencode` |
| `custom:` | `custom` (requires matching `customModels` entry with API key and base URL) |
| (none, `claude-*`) | `claude` |

## AutoRouterConfig field reference

### Tier models (required strings)

<ParamField body="planModel" type="string" required>
Planning tier: architecture, reviews, tradeoffs, risk analysis.
</ParamField>

<ParamField body="buildModel" type="string" required>
Execution tier: implementation, scaffolding, integration.
</ParamField>

<ParamField body="verifyModel" type="string" required>
Verification tier: tests, QA, debugging, root-cause work.
</ParamField>

<ParamField body="refineModel" type="string" required>
Refinement tier: small UI/copy/style edits.
</ParamField>

<ParamField body="fallbackModel" type="string" required>
Used when preferred harness/model is unavailable or on cooldown.
</ParamField>

`getConfig()` also accepts legacy `categories` entries whose `id` is `plan`, `build`, `verify`, `refine`, or `fallback` and copies their `model` into the corresponding `*Model` field.

### Per-tier MetaHarnessPolicy fields

Each tier supports optional policy fields (`plan*`, `build*`, `verify*`, `refine*`, `fallback*`):

| Suffix | Type | UI / runtime notes |
| --- | --- | --- |
| `Effort` | `string` | UI: `''` (default), `low`, `medium`, `high`, `xhigh`, `max`. Normalized for Claude/Codex in `harness-policy.service` |
| `Speed` | `MetaHarnessSpeed` | `auto` \| `standard` \| `fast`; non-`auto` values are persisted |
| `Workflow` | `MetaWorkflowMode` | `auto` \| `single` \| `lead-with-delegates` \| `sequential` \| `dynamic` |
| `BudgetUsd` | `number` | Non-negative USD cap; emitted in mission-control preamble |
| `Verification` | `MetaVerificationMode` | `auto` \| `none` \| `optional` \| `required` |

`cleanPolicy()` strips `auto` speed/workflow/verification so they do not affect routing output. Tier policy is merged with any matched custom category via `resolveRoutePolicy()`.

### Categories

<ParamField body="categories" type="AutoRouterCategoryConfig[]">
Optional array of category overrides. Fixed tier IDs (`plan`, `build`, `verify`, `refine`, `fallback`) migrate to `*Model` fields. Custom categories (non-fixed `id`, or `custom-{timestamp}` IDs) participate in keyword/domain matching and Flue controller input when credentials exist.
</ParamField>

`AutoRouterCategoryConfig` extends `MetaHarnessPolicy` with:

| Field | Purpose |
| --- | --- |
| `id` | Unique key; fixed tier IDs are legacy aliases for `*Model` |
| `label`, `description` | Display and tier inference |
| `model` | Harness-prefixed model string |
| `tier` | Explicit `TaskTier` or `fallback` |
| `keywords` | Comma-separated or array; used for message matching |
| `domains` | Optional `TaskDomain[]` filter |

Custom categories are included in `config.categories` only when `isCurrentCustomAutoRouterCategory()` is true (has tier, description, keywords, or `custom-\d{10,}` id).

### Cost controls

<ParamField body="costAware" type="boolean" required>
When `true`, plan-tier routing downgrades `claude-opus*` to `claude-sonnet-4-6` unless the user explicitly set `planModel` (via `planModel` or `categories` id `plan`). Also affects candidate ordering in `metaCandidateModelsForTier`.
</ParamField>

<ParamField body="costThresholdPercent" type="number" required>
Default `80`. Loaded from store into `getConfig()` but not referenced elsewhere in `auto-router.service` today.
</ParamField>

<ParamField body="enabled" type="boolean" required>
Present on `AutoRouterConfig` and defaulted to `true`; not read by current routing code.
</ParamField>

## MetaHarnessPolicy

Shared policy shape used by tiers, categories, `OrchestrationStage`, and `RoutingDecision`:

```typescript
interface MetaHarnessPolicy {
  effort?: string;
  speed?: MetaHarnessSpeed;      // 'auto' | 'standard' | 'fast'
  workflow?: MetaWorkflowMode;   // 'auto' | 'single' | 'lead-with-delegates' | 'sequential' | 'dynamic'
  budgetUsd?: number;
  verification?: MetaVerificationMode; // 'auto' | 'none' | 'optional' | 'required'
}
```

### How policy reaches harnesses

`translateHarnessPolicy()` in `harness-policy.service` maps policy to harness-specific SDK options and environment variables (`BUILD_META_EFFORT`, `BUILD_META_SPEED`, `BUILD_META_WORKFLOW`, `BUILD_META_BUDGET_USD`, `BUILD_META_VERIFICATION`). It also builds a `<mission_control_policy>` preamble prepended to prompts (preserving `/goal` lines when present).

Effort aliases from legacy thinking modes are normalized: `off`→`low`, `thinking`→`medium`, `ultrathink`→`high`, `ultracode`→`max`.

### Mission control overlay

`MetaMissionControlPolicy` extends `MetaHarnessPolicy` with controller metadata (`controllerHarness: 'meta'`, `requestedTier`, `leadTier`, `leadHarness`, `leadModel`, optional `categoryId` / `categoryLabel`). This appears on `RoutingDecision.missionControl` when the Flue meta controller or heuristics produce a lead decision.

## Configuration flow

```mermaid
flowchart LR
  subgraph UI["Renderer"]
    SD["SettingsDialog Auto Build tab"]
    ABS["autoBuildConfigFromState"]
  end
  subgraph Store["Persistence"]
    ES["claudette-settings.settings.autoRouterConfig"]
  end
  subgraph Main["Main process"]
    ARS["autoRouterService.getConfig"]
    RT["classifyAndRoute"]
    HPS["harness-policy.service"]
    FLUE["flue-meta-router.service"]
  end
  SD --> ABS --> ES
  ES --> ARS
  ARS --> RT
  RT --> FLUE
  RT --> HPS
```

Flue controller routing requires a Cerebras API key: `settings.cerebrasApiKey`, embedded key, or `CEREBRAS_API_KEY`. Timeout defaults to 9s (`FLUE_META_ROUTER_TIMEOUT_MS` env override).

## Programmatic updates

```typescript
// Main process only
autoRouterService.setConfig({
  planModel: 'claude-opus-4-8',
  costAware: false,
  buildEffort: 'high',
});
```

`setConfig` merges partial updates, writes back to `claudette-settings`, and refreshes a short-lived settings cache (2s TTL).

## Regression scripts (`verify:auto-router:*`)

Scripts are offline TypeScript verifiers run with `npx ts-node` unless noted. They mock `electron-store` and often stub Flue/analytics modules.

### Individual npm scripts

| Script | Command | Focus |
| --- | --- | --- |
| `verify:auto-router:attachments` | `npm run verify:auto-router:attachments` | Attachment-aware routing with tier categories |
| `verify:auto-router:controller-confidence` | `npm run verify:auto-router:controller-confidence` | Controller confidence threshold (`META_MIN_CONFIDENCE` 0.55) |
| `verify:auto-router:flue-auth-cooldown` | `npm run verify:auto-router:flue-auth-cooldown` | Flue auth failure cooldown (10 min) |
| `verify:auto-router:flue-fallback` | `npm run verify:auto-router:flue-fallback` | Heuristic fallback when Flue fails |
| `verify:auto-router:flue-sandbox` | `npm run verify:auto-router:flue-sandbox` | Flue sandbox session wiring |
| `verify:auto-router:flue-timeout` | `npm run verify:auto-router:flue-timeout` | Flue prompt timeout / abort |
| `verify:auto-router:fixed-settings` | `npm run verify:auto-router:fixed-settings` | Legacy `categories` + tier `*Model` merge and custom category policy |
| `verify:auto-router:goal-orchestration` | `npm run verify:auto-router:goal-orchestration` | `/goal`, meta-goal continuations, `GoalOrchestration` types |
| `verify:auto-router:indistinguishable` | `npm run verify:auto-router:indistinguishable` | Streamed output must not expose Auto Build branding |
| `verify:auto-router:meta-harness` | `npm run verify:auto-router:meta-harness` | **Full quality gate** (orchestrates most scripts below) |
| `verify:auto-router:meta-eval` | `npm run verify:auto-router:meta-eval` | Tier/harness eval cases with mocked Flue |
| `verify:auto-router:meta-eval:live` | `npm run verify:auto-router:meta-eval:live` | Same eval with `--live --require-live` (needs Flue runtime + `CEREBRAS_API_KEY`) |
| `verify:auto-router:no-legacy-llm-after-controller` | `npm run verify:auto-router:no-legacy-llm-after-controller` | No direct fetch classifier after controller |
| `verify:auto-router:plan-mode` | `npm run verify:auto-router:plan-mode` | Plan-tier forces lead `plan` permission mode |
| `verify:auto-router:workflow-metadata` | `npm run verify:auto-router:workflow-metadata` | Workflow metadata on assistant messages |

<Note>
`npm run verify:auto-router:meta-harness` runs `scripts/verify-auto-router-meta-harness.ts`, the umbrella quality gate. It sequentially runs meta-eval, indistinguishable, Flue sandbox/timeout/auth/fallback, fixed-settings, goal-orchestration, handoff-context, attachments, plan-mode, harness-policy translation, transcript precedence, focused ESLint on router files, and `git diff --check`.
</Note>

### Recommended verification order

<Steps>
<Step title="Fast config merge check">
Run `npm run verify:auto-router:fixed-settings` after changing `getConfig()` or category migration.
</Step>
<Step title="Goal and permission paths">
Run `npm run verify:auto-router:goal-orchestration` and `npm run verify:auto-router:plan-mode` when touching `/goal` or permission modes.
</Step>
<Step title="Full gate before merge">
Run `npm run verify:auto-router:meta-harness` for the complete Auto Build regression suite.
</Step>
</Steps>

<Warning>
`verify:auto-router:meta-eval:live` calls the real Flue stack. Set `FLUE_RUNTIME_NODE_MODULES` to a directory containing `@flue/runtime` and provide `CEREBRAS_API_KEY` (or `CLAUDETTE_CEREBRAS_API_KEY`).
</Warning>

## Example saved config

```json
{
  "planModel": "claude-sonnet-4-6",
  "buildModel": "codex:gpt-5.5",
  "verifyModel": "codex:gpt-5.5",
  "refineModel": "cursor:composer-2.5",
  "fallbackModel": "claude-sonnet-4-6",
  "buildEffort": "high",
  "buildSpeed": "fast",
  "buildWorkflow": "lead-with-delegates",
  "buildBudgetUsd": 25,
  "buildVerification": "required",
  "costAware": true,
  "categories": [
    {
      "id": "custom-docs",
      "label": "Documentation",
      "model": "claude-sonnet-4-6",
      "tier": "refine",
      "keywords": ["readme", "changelog", "docs"],
      "verification": "optional"
    }
  ]
}
```

## Related pages

<CardGroup>
<Card title="Auto Build routing" href="/auto-build-routing">
Plan/build/verify/refine heuristics, Flue controller, orchestration, and `CLAUDE_AUTO_ROUTE_DECISION` events.
</Card>
<Card title="Settings reference" href="/settings-reference">
Full `AppSettings` and `claudette-settings` keys, including where `autoRouterConfig` fits.
</Card>
<Card title="npm scripts reference" href="/npm-scripts-reference">
All `package.json` scripts and when to use `./scripts/dev.sh` vs `npm run start`.
</Card>
<Card title="Shared types reference" href="/shared-types-reference">
`RoutingDecision`, `OrchestrationPlan`, `TaskTier`, and related interfaces.
</Card>
</CardGroup>

---

## 21. Shared types reference

> Core exported interfaces: Session, ChatMessage, RoutingDecision, OrchestrationPlan, Harness, PermissionRequest, MCP types, and message-queue HarnessCapabilities.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/21-shared-types-reference.md
- Generated: 2026-06-02T00:27:44.696Z

### Source Files

- `src/shared/types/index.ts`
- `src/shared/types/message-queue.ts`
- `src/shared/utils/message-recovery.ts`
- `src/shared/utils/prompt-truncation.ts`
- `src/main/services/harness-capabilities.ts`

---
title: "Shared types reference"
description: "Core exported interfaces: Session, ChatMessage, RoutingDecision, OrchestrationPlan, Harness, PermissionRequest, MCP types, and message-queue HarnessCapabilities."
---

Build’s Electron main and renderer processes share a single TypeScript contract under `src/shared/`. Domain models (`Session`, `ChatMessage`, Auto Build routing shapes, MCP registry types) live in `src/shared/types/index.ts`; per-harness queue behavior is typed in `src/shared/types/message-queue.ts` and enforced at runtime by `getHarnessCapabilities()` in `src/main/services/harness-capabilities.ts`. IPC payloads, Zustand stores, and services import these symbols directly so both sides agree on field names, unions, and optional metadata without duplicating schemas.

## Module layout

```text
src/shared/
├── types/
│   ├── index.ts          # Primary barrel: sessions, chat, routing, MCP, settings
│   ├── message-queue.ts  # QueuedMessage, QueueState, HarnessCapabilities
│   └── audio.ts          # Voice/TTS settings (re-exported from index)
├── utils/
│   ├── message-recovery.ts   # PersistedChatMessage, dedupe/merge helpers
│   └── prompt-truncation.ts  # Long-prompt middle truncation
└── constants/channels.ts     # IPC channel strings (separate reference page)

src/main/services/
└── harness-capabilities.ts   # Runtime HarnessCapabilities map per Harness
```

<Note>
`AppSettings`, `AutoRouterConfig`, and git/browser helpers are defined in `index.ts` but documented in depth on the settings and feature pages. This page focuses on session, chat, routing, agent dialogs, MCP, and the message queue.
</Note>

## Import pattern

Both processes import from the same paths:

```typescript
import type { Session, ChatMessage, RoutingDecision } from '../../shared/types';
import type { QueuedMessage, HarnessCapabilities } from '../../shared/types/message-queue';
```

Renderer stores (`session.store.ts`, `audio.store.ts`) and main services (`claude.service.ts`, `auto-router.service.ts`, `message-queue.service.ts`) depend on these types for IPC serialization and UI state.

```mermaid
classDiagram
  class Session {
    +string id
    +SessionStatus status
    +string repoPath
    +Harness model prefix via picker
  }
  class ChatMessage {
    +string id
    +role user|assistant|system
    +Harness harness
    +ToolCall[] toolCalls
  }
  class RoutingDecision {
    +TaskTier tier
    +string resolvedModel
    +OrchestrationPlan orchestration
  }
  class OrchestrationPlan {
    +mode single|lead-with-delegates|sequential
    +OrchestrationStage[] stages
  }
  class QueuedMessage {
    +string sessionId
    +string text
  }
  class HarnessCapabilities {
    +boolean supportsAsyncInjection
    +number minTurnGapMs
  }
  Session --> ChatMessage : messages per session
  RoutingDecision --> OrchestrationPlan : optional
  QueuedMessage --> HarnessCapabilities : drain timing via harness
```

## Harness identifier

<ParamField body="Harness" type="'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode' | 'custom'" required>
Union of supported agent backends. Stored on `ChatMessage.harness` and resolved from model picker strings.
</ParamField>

Model strings use prefixed forms for non-Claude harnesses. `harnessFromModel()` in `message-recovery.ts` maps:

| Model prefix | Harness |
|--------------|---------|
| (none / default) | `claude` |
| `codex:` | `codex` |
| `cursor:` | `cursor` |
| `gemini:` | `gemini` |
| `opencode:` | `opencode` |
| `custom:` | `custom` |
| `auto` | Resolved at runtime to `resolvedModel` from `RoutingDecision` |

When `model === 'auto'`, `withFallbackHarness()` may attach the resolved harness to assistant messages after routing completes.

## Session

`Session` is the persisted workspace record (electron-store `claudette-sessions`) and the in-memory object passed through session IPC.

### Core fields

| Field | Type | Role |
|-------|------|------|
| `id` | `string` | Stable session identifier |
| `name` | `string` | Display name |
| `repoPath` | `string` | Primary repository path |
| `worktreePath` | `string` | Active git worktree |
| `branch` | `string` | Checked-out branch |
| `status` | `SessionStatus` | Lifecycle gate for UI and services |
| `ports` | `PortAllocation` | `web`, `api`, `debug` port triple |
| `createdAt` / `updatedAt` | `Date` | Timestamps |
| `setupScript` | `string` | Worktree/bootstrap script |
| `model` | `string?` | Selected model for the session |

### SessionStatus

```text
creating → starting → setup → running → stopping → stopped
                                    ↘ error
```

`SetupProgressEvent` reports worktree/SSH setup progress with `status: 'running' | 'completed' | 'error'`.

### Execution modes (optional blocks)

<ParamField body="sshConfig" type="SSHConfig">
Remote execution: host, port (default 22), username, `privateKeyPath`, `remoteWorkdir`, optional `passphrase`, `worktreeScript`, `syncSettings`.
</ParamField>

<ParamField body="openclawConfig" type="OpenClawConfig">
OpenClaw Gateway: `gatewayUrl`, `gatewayPassword` (Bearer token).
</ParamField>

<ParamField body="isDevMode" type="boolean">
Local dev session without Docker container.
</ParamField>

<ParamField body="containerId" type="string">
Docker-backed session when not dev mode.
</ParamField>

### Transcript and fork metadata

| Field | Purpose |
|-------|---------|
| `sdkSessionId` | Claude Agent SDK session for transcript resume |
| `relatedSessionIds` | Alternate local/SDK IDs for same conversation |
| `continuedFromSessionId` | Prior Build session this one resumed |
| `isWorktree` / `parentRepoPath` / `forkName` | Git worktree fork from parent repo |
| `parentSessionId` / `childSessionIds` / `forkPoint` | Conversation fork tree (distinct from git worktree) |
| `isRoot` / `forkCreatedAt` / `aiGeneratedName` | Fork tab labeling |
| `teleportedFrom` / `downloadedFrom` / `isTeleported` | Teleport/import provenance |
| `isStarred` / `starredAt` | Favorites ordering |
| `gstackMode` | Active GStack skill id (`GStackMode` is `string`) |
| `tabHidden` | User-closed tab persistence |
| `htmlRenderMode` | `'md' \| 'html'` per-session rendering |

`SavedSSHConfig` is the electron-store-safe SSH preset (no passphrase). `SSHResumeCandidate` lists resumable remote transcripts. `DownloadSessionConfig` configures reverse teleport (SSH → local).

## ChatMessage and streaming artifacts

`ChatMessage` is the canonical chat timeline unit in the renderer and transcript merge logic.

<ParamField body="id" type="string" required>
Message id (UUID in queue; SDK/transcript ids when recovered).
</ParamField>

<ParamField body="role" type="'user' | 'assistant' | 'system'" required>
Speaker role.
</ParamField>

<ParamField body="content" type="string" required>
Combined text for search and backwards compatibility.
</ParamField>

<ParamField body="contentBlocks" type="ContentBlock[]">
Ordered `text` / `tool_use` blocks for interleaved UI; `toolCallId` references `toolCalls`.
</ParamField>

<ParamField body="toolCalls" type="ToolCall[]">
SDK tool invocations with `status: 'pending' | 'running' | 'completed' | 'error'`.
</ParamField>

<ParamField body="harness" type="Harness">
Which backend produced or received the message.
</ParamField>

<ParamField body="metadata" type="object">
Optional workflow hints, e.g. `workflowCompletedScope: TaskTier`, `workflowFailures[]` with harness/model/error.
</ParamField>

### ToolCall and multi-agent UI

`ToolCall` carries `input`, `result`, `error`, timestamps, and optional `agentId` (SDK `parent_tool_use_id` for teammates). `AgentInfo` pairs `id`, optional `name`, and a color from `AGENT_COLORS`. `ContentBlock.agentId` attributes blocks to lead vs delegate agents.

### Attachments and browser context

`Attachment` types: `file`, `image`, `dom_element` with optional `screenshot`. `DOMElementContext` captures inspector payloads (selector, HTML, computed styles, `boundingRect`). `BrowserSnapshot` is url + screenshot + html + timestamp.

### Persistence helper

`PersistedChatMessage` = `ChatMessage` with `timestamp: string` (ISO). Use `serializeCompletedStreamMessage()` / `normalizeCompletedStreamMessage()` when writing or reading stored transcripts.

<Warning>
Recovery utilities (`mergeRecoveredStreamMessages`, `isDuplicateStreamMessage`, `filterInternalPromptEchoes`) implement deduplication windows (60s default, 5s for close content duplicates). Do not change signatures without updating both transcript reload and live stream merge paths.
</Warning>

## Auto Build routing types

Auto Build (intelligent model routing) types cluster around `TaskTier`, `RoutingDecision`, and `OrchestrationPlan`. `auto-router.service.ts` builds decisions; `claude.service.ts` consumes `orchestration` for staged handoffs and emits `IPC_CHANNELS.CLAUDE_AUTO_ROUTE_DECISION` to the renderer.

### TaskTier and TaskDomain

| TaskTier | Typical use |
|----------|-------------|
| `plan` | Planning / spec work |
| `build` | Implementation |
| `verify` | Tests, lint, validation |
| `refine` | Fix-up after failure |

`TaskDomain` narrows routing: `copy`, `frontend`, `backend`, `fullstack`, `debug`, `ops`, `docs`, `data`, `general`.

### MetaHarnessPolicy and extensions

Shared optional knobs on tiers, categories, and stages:

| Field | Values |
|-------|--------|
| `effort` | Provider-specific effort string |
| `speed` | `MetaHarnessSpeed`: `auto`, `standard`, `fast` |
| `workflow` | `MetaWorkflowMode`: `auto`, `single`, `lead-with-delegates`, `sequential`, `dynamic` |
| `verification` | `MetaVerificationMode`: `auto`, `none`, `optional`, `required` |
| `budgetUsd` | Spend cap hint |

`MetaMissionControlPolicy` adds `controllerHarness: 'meta'`, `requestedTier`, `leadTier`, `leadHarness`, `leadModel`, and optional category labels for Flue/meta controller routes.

### RoutingDecision

<ResponseField name="RoutingDecision" type="object">
Auto Build output attached to a user turn and analytics.
</ResponseField>

| Field | Description |
|-------|-------------|
| `tier` | Resolved `TaskTier` for this turn |
| `domain` | Optional `TaskDomain` |
| `resolvedModel` | Model id sent to the harness |
| `resolvedHarness` | Backend when distinct from model prefix |
| `resolvedEffort` / `resolvedSpeed` / `workflow` / `budgetUsd` / `verification` | Applied policy |
| `confidence` | Router confidence score |
| `reason` | Human-readable routing explanation |
| `method` | `'heuristic' \| 'controller'` |
| `enableGoals` / `goal` | `GoalOrchestration` from `/goal` or Ralph loop |
| `missionControl` | `MetaMissionControlPolicy` when meta controller leads |
| `orchestration` | Multi-stage `OrchestrationPlan` when workflow ≠ single |

`SessionPhase` tracks `lastTierUsed`, `hasPlanContext`, `hasBuildContext`, and `recentTiers[]` for follow-up routing. `AutoRouterConfig` (in settings store) supplies per-tier models, efforts, workflows, verification modes, `categories[]`, and `costAware` / `costThresholdPercent`.

### OrchestrationPlan and OrchestrationStage

```typescript
interface OrchestrationPlan {
  mode: 'single' | 'lead-with-delegates' | 'sequential';
  leadHarness: Harness;
  leadModel: string;
  stages: OrchestrationStage[];
  contextPolicy: {
    includeTranscript: boolean;
    includeTranscriptReferences: boolean;
    includePlanFileReference: boolean;
    avoidBulkContextOnHandoff: boolean;
    maxHandoffConversationChars: number;
    includeProjectInstructions: boolean;
    includeSkills: boolean;
    includeAgents: boolean;
    includeMemories: boolean;
  };
  handoffPrompt: string;
}
```

Each `OrchestrationStage` includes `tier`, `harness`, `model`, optional `fallbackModels`, `purpose`, `required`, and `trigger`:

| trigger | Meaning |
|---------|---------|
| `now` | Run immediately with lead |
| `after-plan` | After plan stage completes |
| `after-build` | After build stage completes |
| `on-failure` | After a failed lead (often refine) |
| `manual-follow-up` | User-triggered follow-up |

Stages inherit `MetaHarnessPolicy` fields for per-stage effort/speed/verification.

## Agent interaction types

These structs cross the preload bridge when the Agent SDK blocks on user input.

### PermissionRequest / PermissionResponse

<ParamField body="PermissionRequest" type="object" required>
`sessionId`, `requestId`, `toolName`, `toolInput`, optional `message`.
</ParamField>

<ParamField body="PermissionResponse" type="object" required>
`requestId`, `approved`, optional `modifiedInput`, `alwaysApprove` (persist pattern to project settings).
</ParamField>

### QuestionRequest / QuestionResponse

`Question` contains `question`, `header`, `options: QuestionOption[]`, and `multiSelect`. `QuestionRequest` bundles `questions[]` with session/request ids; `QuestionResponse` returns `answers: Record<string, string>`.

### PlanApprovalRequest / PlanApprovalResponse

Used with ExitPlanMode: `planContent`, optional `planFilePath`, `allowedPrompts[]`. Response: `approved`, optional `feedback` on reject.

### CompactionStatus / CompactionComplete

Smart Compact events: `isCompacting`, optional model switch metadata, `preTokens` / `postTokens`, `trigger: 'manual' | 'auto'`.

## MCP and marketplace types

Runtime-installed servers use `MCPServerInfo`:

| Field | Notes |
|-------|-------|
| `id`, `name`, `description`, `version` | Identity |
| `status` | `active`, `inactive`, `error` |
| `type` | `sdk`, `stdio`, `http`, `sse` |
| `tools` | `MCPServerTool[]` (`name`, `description`) |
| `projectEnabled` | Per-project toggle |

Registry/marketplace browsing uses `MarketplaceMCPServer` with `packages[]` (`MCPRegistryPackage`), `remotes[]` (`MCPRegistryRemote`), and `authFields[]` (`MCPRegistryAuthField`). Plugin marketplaces add `PluginMarketplace`, `InstalledPlugin`, `MarketplacePlugin`, and `PopularMarketplace`.

## Message queue types

Defined in `message-queue.ts` and driven by `message-queue.service.ts` in the main process.

### QueuedMessage

| Field | Type | Notes |
|-------|------|-------|
| `id` | `string` | UUID at enqueue |
| `sessionId` | `string` | Owning session |
| `text` | `string` | Prompt body |
| `attachments` | `unknown[]?` | Opaque attachment payload |
| `timestamp` | `number` | `Date.now()` at enqueue |
| `model` | `string?` | Model active when queued |
| `suppressUserMessage` | `boolean?` | Hide user bubble (e.g. continue) |

### QueueState

`messages`, `isProcessing`, optional `activeHarness` — emitted on `state-changed` events for the session switcher UI.

### HarnessCapabilities

Interface consumed by `getHarnessCapabilities(harness?)`:

<ParamField body="supportsAsyncInjection" type="boolean">
`true` only for `claude` — can accept messages mid-stream.
</ParamField>

<ParamField body="supportsMultiTurn" type="boolean">
`true` for `claude` and `cursor` — conversation state across turns.
</ParamField>

<ParamField body="minTurnGapMs" type="number">
Delay after `onStreamEnd` before dequeuing next message (`0` for Claude, `500` for others).
</ParamField>

<ParamField body="maxCoalesceWindowMs" type="number">
Declared coalesce window (3000 ms for all harnesses); reserved for rapid-send batching.
</ParamField>

| Harness | Async injection | Multi-turn | minTurnGapMs |
|---------|-----------------|------------|--------------|
| `claude` | yes | yes | 0 |
| `cursor` | no | yes | 500 |
| `codex`, `gemini`, `opencode`, `custom` | no | no | 500 |

Unknown harness strings fall back to the default row (same as non-Claude backends). Today `message-queue.service.ts` applies `minTurnGapMs` on drain; other flags are part of the contract for UI and future coalescing.

## Prompt truncation utility

`truncateMiddlePreservingTail(value, maxChars, options?)` in `prompt-truncation.ts` shortens oversized prompts while keeping the tail (default 70% tail ratio). Used by `claude.service.ts` and `codex.service.ts` when context exceeds harness limits.

<ParamField body="marker" type="string">
Inserted middle marker; default `[... middle truncated due to length ...]`.
</ParamField>

<ParamField body="tailRatio" type="number">
Fraction of `maxChars` reserved for tail; clamped 0.1–0.9, default 0.7.
</ParamField>

## Audio types (re-export)

`index.ts` re-exports `src/shared/types/audio.ts`: `AudioState`, `TranscriptionStatus`, `TranscriptionResult`, `TTSState`, `TTSRequest`, `VoiceSettings`, `AudioSettings`, and `DEFAULT_AUDIO_SETTINGS`. Voice IPC uses these alongside `AppSettings` API key fields documented on the settings page.

## Quick reference: other index exports

| Group | Types |
|-------|-------|
| Git / GitHub | `Commit`, `Branch`, `FileChange`, `GitHubUser`, `GitHubRepo` |
| Focus mode | `FocusTask`, `FocusSubtask` |
| Settings | `AppSettings`, `CustomModelConfig` |
| Extensions | `Command`, `Skill`, `AgentDefinition` |
| Containers | `ContainerStats`, `PortAllocation` |
| Legacy GStack | `GSTACK_MODE_META` color/shortName map for known skill ids |

## Related pages

<CardGroup>
  <Card title="Sessions and workspaces" href="/sessions-and-workspaces">
    Session lifecycle, persistence keys, SSH/OpenClaw fields, and transcript resume.
  </Card>
  <Card title="Auto Build routing" href="/auto-build-routing">
    How RoutingDecision and OrchestrationPlan are produced and executed.
  </Card>
  <Card title="Harnesses and models" href="/harnesses-and-models">
    Harness identifiers, model prefixes, and capability limits in the UI.
  </Card>
  <Card title="IPC and preload bridge" href="/ipc-bridge">
    How typed payloads cross main/renderer via electronAPI.
  </Card>
  <Card title="Manage coding sessions" href="/manage-sessions">
    Message queue UX, permission dialogs, and session fork/rewind flows.
  </Card>
  <Card title="MCP servers" href="/mcp-servers">
    MCPServerInfo storage, marketplace install, and harness sync.
  </Card>
</CardGroup>

---

## 22. npm scripts reference

> package.json scripts: start, lint, make, setup-qmd, verify:auto-router:* suite, and when to use ./scripts/dev.sh vs npm run start directly.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/22-npm-scripts-reference.md
- Generated: 2026-06-02T00:25:24.561Z

### Source Files

- `package.json`
- `scripts/dev.sh`
- `scripts/build.sh`
- `scripts/setup-qmd.ts`
- `CLAUDE.md`
- `.claude/commands/dev.md`

---
title: "npm scripts reference"
description: "package.json scripts: start, lint, make, setup-qmd, verify:auto-router:* suite, and when to use ./scripts/dev.sh vs npm run start directly."
---

Build’s `package.json` defines Electron Forge entry points (`start`, `package`, `make`), housekeeping (`clean:build`, `lint`), QMD bundling (`setup-qmd`), and fifteen `verify:auto-router:*` regression scripts under `scripts/`. Day-to-day development should use `./scripts/dev.sh`, which sets dev-only environment variables and runs `npm run setup-qmd` before delegating to `npm run start`.

## Script inventory

| Script | Command | Purpose |
|--------|---------|---------|
| `start` | `electron-forge start` | Webpack dev server + Electron app (raw Forge entry) |
| `package` | `electron-forge package` | Pack app without full distributable makers |
| `clean:build` | Node one-liner | Deletes `.webpack` and `out/v{version}` |
| `make` | `clean:build` then `electron-forge make` | Production distributable (DMG/ZIP/etc.) |
| `publish` | `electron-forge publish` | Forge publish flow (not used in local README workflow) |
| `lint` | `eslint --ext .ts,.tsx .` | Full-repo TypeScript/TSX lint |
| `setup-qmd` | `npx ts-node scripts/setup-qmd.ts` | Bundle Bun + QMD for **current** OS/arch |
| `setup-qmd:all` | `npx ts-node scripts/setup-qmd.ts all` | Bundle QMD for all supported platforms |
| `verify:auto-router:*` | `npx ts-node scripts/verify-auto-router-*.ts` | Auto Build routing regression checks (see below) |

Current package version (used in `clean:build` and `outDir`): **0.5.27** (`package.json`).

## Development: `./scripts/dev.sh` vs `npm run start`

<Warning>
Use `./scripts/dev.sh` for normal development. Running `npm run start` alone skips QMD setup, dev instance naming, port cleanup, isolated `userData`, and production-settings sync.
</Warning>

### What `./scripts/dev.sh` does

```text
scripts/dev.sh
  ├─ DEV_INSTANCE_NAME  (random adjective-noun, e.g. snappy-koala)
  ├─ npm run setup-qmd
  ├─ kill dev Electron (node_modules/electron only; not out/ production)
  ├─ DEV_WEBPACK_PORT=9001  (+ free port 9001)
  ├─ GREP_DEV_USER_DATA=/tmp/grep-build-dev
  │    └─ one-time copy settings/MCP from prod; copy sessions when present
  └─ npm run start  →  electron-forge start
```

| Variable / path | Set by `dev.sh` | Effect |
|---------------|-----------------|--------|
| `DEV_INSTANCE_NAME` | Random name | Shown in terminal banner and status bar (`[{name}]`) via preload |
| `DEV_WEBPACK_PORT` | `9001` | Webpack dev server port in `forge.config.ts` (default otherwise `3000`) |
| `GREP_DEV_USER_DATA` | `/tmp/grep-build-dev` | `app.setPath('userData', …)` in main process; DevTools auto-open |
| Prod sync | `~/Library/Application Support/Build`, `G-Build`, or `Grep Build` | Copies `claudette-settings.json` / `claudette-mcp-servers.json` only if missing in dev; always attempts `claudette-sessions.json` |

Main process behavior tied to dev env (`src/main/index.ts`):

- **No** `main.log` file logging when `DEV_INSTANCE_NAME` is set (production writes to `userData/main.log`).
- CDP defaults to port **9223** in dev vs **9222** in production (overridable with `ELECTRON_CDP_PORT`).

<Info>
`forge.config.ts` also reads `DEV_WEBPACK_LOGGER_PORT` (default **9000**) for the Forge logger port. `dev.sh` frees **9001** (webpack), not 9000.
</Info>

### When `npm run start` alone is acceptable

- Debugging Electron Forge/webpack without dev isolation.
- CI or tooling that sets `DEV_INSTANCE_NAME`, `GREP_DEV_USER_DATA`, and runs `setup-qmd` itself.

Expect production `userData` paths, no status-bar dev instance label, and missing `resources/qmd` unless you ran `npm run setup-qmd` first.

### Hot reload vs full restart

| Layer | Change type | Action |
|-------|-------------|--------|
| Renderer | React, stores, styles | Hot reload (dev server) |
| Main | Services, IPC, `preload.ts` | Kill dev instance and re-run `./scripts/dev.sh` |

<Note>
Only one dev Electron from `node_modules/electron` should run at a time; `dev.sh` kills orphaned dev processes before launch. Do not kill production apps under `out/`.
</Note>

## Electron Forge scripts

### `npm run start`

Runs `electron-forge start` with the Webpack plugin (`forge.config.ts`): bundles main + renderer, serves renderer on `DEV_WEBPACK_PORT` (or 3000).

### `npm run package`

Packages the app without running all platform makers—useful for a faster pack smoke test.

### `npm run make`

```bash
npm run clean:build && electron-forge make
```

`clean:build` removes:

- `.webpack` (Forge webpack output)
- `out/v{npm_package_version}` (versioned distributable tree)

Forge `outDir` is `./out/v{version}`; macOS app bundle name is **Build** (`Build.app`, executable `build`). Typical arm64 path after build:

```text
out/v0.5.27/Build-darwin-arm64/Build.app
```

`postPackage` copies bundled QMD from `resources/qmd/{platform}-{arch}` into the app Resources folder; warns if missing. Optional signing/notarization when `APPLE_ID`, `APPLE_PASSWORD`, and `APPLE_TEAM_ID` are set.

### `npm run publish`

Standard Electron Forge publish—local release workflow in this repo centers on `make`, version bump, and git tags (see build-and-release page).

### Convenience wrapper: `./scripts/build.sh`

Not an npm script; shell wrapper that:

1. Reads version from `package.json`
2. `pkill` Build / electron-forge (aggressive—differs from `/build` guidance to avoid pkill before `make`)
3. `npm run make`
4. `git tag -f v{version}`
5. `open ./out/v{version}/Build-darwin-arm64/Build.app`

Prefer `npm run make` directly when other Electron instances must stay running.

## Lint

```bash
npm run lint
```

Runs ESLint on all `.ts` and `.tsx` files from the repo root. The Auto Build **meta-harness** gate also runs focused ESLint on routing-related paths (see below).

## QMD setup scripts

Semantic search bundles **Bun** (v1.1.38) and **QMD** (from `https://github.com/tobi/qmd`) into `resources/qmd/{platform}-{arch}/`.

| Script | Behavior |
|--------|----------|
| `npm run setup-qmd` | Current platform only (`darwin-arm64`, `darwin-x64`, `linux-x64`, `win32-x64`) |
| `npm run setup-qmd:all` | All platforms in `PLATFORMS` |

Outputs per platform:

- `bun/` — Bun runtime binary
- `qmd-package/` — QMD npm install via Bun
- `qmd` or `qmd.cmd` — wrapper invoking `bun … src/qmd.ts`

`dev.sh` runs `setup-qmd` on every dev start so local QMD matches the packaged layout. Before `make`, ensure the target platform slice exists or packaging logs a QMD warning.

## `verify:auto-router:*` regression suite

All scripts use `npx ts-node` and exit non-zero on assertion failure. They validate **Auto Build** routing (`auto-router.service`, Flue meta-router, Claude service integration)—not general app E2E.

### Orchestrator vs focused scripts

| Script | Role |
|--------|------|
| `verify:auto-router:meta-harness` | **Quality gate**: runs meta-eval, indistinguishable, flue-sandbox, workflow-metadata, controller-confidence, no-legacy-llm, flue-fallback, flue-auth-cooldown, flue-timeout, fixed-settings, goal-orchestration, handoff-context (script only—no separate npm entry), attachments, plan-mode, plus harness/transcript verifiers, focused ESLint, and `git diff --check` |
| Individual `verify:auto-router:*` | Run one concern in isolation for faster iteration |

### Per-script summary

| npm script | Verifier file | Check focus |
|------------|---------------|-------------|
| `verify:auto-router:attachments` | `verify-auto-router-attachments.ts` | Tier/model routing with DOM/image attachments (mocked store) |
| `verify:auto-router:controller-confidence` | `verify-auto-router-controller-confidence.ts` | Flue controller confidence / routing path |
| `verify:auto-router:flue-auth-cooldown` | `verify-auto-router-flue-auth-cooldown.ts` | Auth failure cooldown behavior |
| `verify:auto-router:flue-fallback` | `verify-auto-router-flue-fallback.ts` | Fallback when Flue controller fails |
| `verify:auto-router:flue-sandbox` | `verify-auto-router-flue-sandbox.ts` | Flue sandbox tool restrictions |
| `verify:auto-router:flue-timeout` | `verify-auto-router-flue-timeout.ts` | Flue timeout handling |
| `verify:auto-router:fixed-settings` | `verify-auto-router-fixed-settings.ts` | Legacy categories vs fixed `planModel`/`buildModel`/… settings |
| `verify:auto-router:goal-orchestration` | `verify-auto-router-goal-orchestration.ts` | `/goal` slash command → Auto routing (static source assertions) |
| `verify:auto-router:indistinguishable` | `verify-auto-router-indistinguishable.ts` | No leaked “Auto Build” branding in streamed UI text (static) |
| `verify:auto-router:meta-eval` | `verify-auto-router-meta-eval.ts` | Broad routing eval cases (mocked; optional live Flue paths) |
| `verify:auto-router:meta-eval:live` | same + `--live --require-live` | Requires live Flue runtime (`FLUE_RUNTIME_NODE_MODULES`, etc.) |
| `verify:auto-router:no-legacy-llm-after-controller` | `verify-auto-router-no-legacy-llm-after-controller.ts` | Legacy direct LLM classifier must not run after controller |
| `verify:auto-router:plan-mode` | `verify-auto-router-plan-mode.ts` | Plan-tier forces `plan` permission mode on lead harness (static) |
| `verify:auto-router:workflow-metadata` | `verify-auto-router-workflow-metadata.ts` | Workflow metadata in routing context (mocked messages) |

<Steps>
<Step title="Run full Auto Build gate">
```bash
npm run verify:auto-router:meta-harness
```
</Step>
<Step title="Run a single failing area">
```bash
npm run verify:auto-router:flue-timeout
```
</Step>
<Step title="Optional live Flue eval">
```bash
npm run verify:auto-router:meta-eval:live
```
Requires local Flue runtime; fails if `--require-live` cannot connect.
</Step>
</Steps>

There is **no** umbrella `verify:auto-router:all` npm script; use `meta-harness` for the bundled gate or run scripts individually.

## Optional Forge / build environment variables

| Variable | Used in | Purpose |
|----------|---------|---------|
| `DEV_WEBPACK_PORT` | `forge.config.ts`, `dev.sh` | Renderer webpack port |
| `DEV_WEBPACK_LOGGER_PORT` | `forge.config.ts` | Forge logger port (default 9000) |
| `DEV_INSTANCE_NAME` | `dev.sh`, main, preload, status bar | Dev build identifier |
| `GREP_DEV_USER_DATA` | `dev.sh`, `index.ts` | Isolated electron-store path |
| `ELECTRON_CDP_PORT` | `index.ts` | Override remote debugging port |
| `GREP_ELECTRON_ZIP_DIR` | `forge.config.ts` | Offline Electron zip for packaging |
| `GREP_ELECTRON_CACHE_ROOT` | `forge.config.ts` | Electron download cache root |
| `GREP_SKIP_ELECTRON_CHECKSUMS` | `forge.config.ts` | Set `1` to skip checksum verify |

## Recommended workflows

```text
Daily dev          →  ./scripts/dev.sh
Pre-commit         →  npm run lint
Auto Build change  →  npm run verify:auto-router:meta-harness
QMD / packaging    →  npm run setup-qmd  (or setup-qmd:all before release)
Ship binary        →  bump version → npm run make → git tag v{version}
```

<Check>
After `./scripts/dev.sh`, confirm the printed **DEV INSTANCE** name matches the status bar suffix (e.g. `[snappy-koala]`).
</Check>

## Related pages

<CardGroup>
<Card title="Build and release" href="/build-and-release">
Version bumping, `make` output layout, git tags, and release artifacts.
</Card>
<Card title="Semantic search (QMD)" href="/semantic-search-qmd">
Runtime QMD usage, IPC search, and how bundled `resources/qmd` is consumed.
</Card>
<Card title="Auto Build configuration" href="/auto-build-configuration">
`AutoRouterConfig` fields exercised by the verify scripts.
</Card>
<Card title="Auto Build routing" href="/auto-build-routing">
Plan/build/verify/refine tiers and controller routing behavior under test.
</Card>
<Card title="Installation" href="/installation">
Clone, `npm install`, and prerequisites before running scripts.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Port conflicts, dev instance locks, and packaged PATH issues.
</Card>
</CardGroup>

---

## 23. Build and release

> electron-forge make output paths, version bump rules, git tagging, dual remotes (origin/public), /build and /release slash-command workflows, and GitHub release artifacts.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/23-build-and-release.md
- Generated: 2026-06-02T00:25:33.759Z

### Source Files

- `forge.config.ts`
- `package.json`
- `scripts/build.sh`
- `.claude/commands/build.md`
- `.claude/commands/release.md`
- `CLAUDE.md`

---
title: "Build and release"
description: "electron-forge make output paths, version bump rules, git tagging, dual remotes (origin/public), /build and /release slash-command workflows, and GitHub release artifacts."
---

Production distributables are produced by **Electron Forge** via `npm run make`, which reads `package.json` `version`, wipes prior webpack and versioned output, and writes artifacts under `out/v{version}/`. The `forge.config.ts` `postPackage` hook copies native externals and the QMD bundle, optionally signs and notarizes on macOS, and can install `Build.app` into `/Applications`. Release tags follow `v{version}`; maintainer slash-command docs describe pushing `master` and tags to both `origin` and `public` remotes before publishing GitHub release assets.

## npm scripts and clean behavior

| Script | Command | Effect |
|--------|---------|--------|
| `start` | `electron-forge start` | Dev server (prefer `./scripts/dev.sh` wrapper) |
| `package` | `electron-forge package` | Packaged app without installers |
| `clean:build` | Node one-liner | Deletes `.webpack` and `out/v{npm_package_version}` |
| `make` | `npm run clean:build && electron-forge make` | Full production build + makers |
| `publish` | `electron-forge publish` | Forge publish flow (not the GitHub release skill) |

`clean:build` ties cleanup to the **current** `package.json` version. Bump the version before `make` if you intend to keep a prior `out/v*` tree; otherwise the matching directory is removed at the start of every `make`.

<Warning>
Agent-facing build docs say **never** `pkill` Electron before `npm run make` because other worktrees may be running. The repo also ships `scripts/build.sh`, which **does** stop running `Build.app` and forge processes. Use the script only when you intend that behavior.
</Warning>

## Forge output layout

`forge.config.ts` sets a version-scoped output root:

```text
out/v{version}/
├── Build-darwin-arm64/
│   └── Build.app                 # runnable macOS arm64 bundle
└── make/
    ├── zip/darwin/arm64/
    │   └── Build-darwin-arm64-{version}.zip
    └── Build-{version}-arm64.dmg # MakerDMG (ULFO format)
```

Packager metadata from the same config:

| Field | Value |
|-------|--------|
| `outDir` | `./out/v${version}` from `package.json` |
| `packagerConfig.name` | `Build` |
| `packagerConfig.executableName` | `build` |
| `packagerConfig.appBundleId` | `com.parcha.build` |
| macOS bundle path | `Build.app` inside `Build-darwin-arm64/` |

Configured makers: Squirrel (Windows), ZIP (darwin), DMG, RPM, Deb. Day-to-day release docs and the GitHub release skill target **macOS Apple Silicon** zip and dmg only.

`scripts/build.sh` mirrors the app path: `./out/v${VERSION}/Build-darwin-arm64/Build.app`, runs `npm run make`, force-creates `git tag -f v${VERSION}`, and opens the app.

<Note>
Older slash-command text and test notes refer to **Grep Build** paths (`Grep Build-darwin-arm64`, `Grep Build.app`). Current `productName` is **Build**; first-launch migration in `src/main/index.ts` moves user data from `Grep Build` / `G-Build` Application Support folders into **Build**.
</Note>

## postPackage hook (packaging pipeline)

After Electron Packager finishes, `postPackage` runs per output path:

```mermaid
flowchart TB
  subgraph forge [electron-forge make]
    WP[WebpackPlugin build]
    PKG[electron-packager]
    PP[postPackage hook]
  end
  subgraph pp [postPackage]
    NM[Copy webpack externals to Resources/node_modules]
    QMD[Copy resources/qmd platform bundle]
    SIG[Developer ID sign + notarize OR adhoc codesign]
    APP[Copy Build.app to /Applications on darwin]
  end
  WP --> PKG --> PP
  PP --> NM --> QMD --> SIG --> APP
```

**Externals copied** (must match `webpack.main.config.ts`): `node-pty`, Claude/Cursor SDK packages, `docx`, `monaco-editor`, and their dependency trees.

**QMD**: Copied from `resources/qmd/{platform}-{arch}` into `Resources/qmd`. Missing platform bundles log a warning to run `npm run setup-qmd` (or `setup-qmd:all`).

**macOS signing**:

| Condition | Behavior |
|-----------|----------|
| `APPLE_ID`, `APPLE_PASSWORD`, `APPLE_TEAM_ID` set | `@electron/osx-sign` with Developer ID + `@electron/notarize` |
| Credentials absent | Adhoc `codesign --force --sign -` on `Build.app` |

`packagerConfig.osxSign` is `false` during packager so post-copy signing stays valid.

**Install shortcut**: On darwin, signed or adhoc bundle is copied to `/Applications/Build.app` (failures are logged, not fatal).

### Offline / cache environment variables

<ParamField body="GREP_ELECTRON_ZIP_DIR" type="string">
Local Electron zip directory passed to `packagerConfig.electronZipDir` when set.
</ParamField>

<ParamField body="GREP_ELECTRON_CACHE_ROOT" type="string">
Download cache root when checksum skip is enabled. Default: `~/Library/Caches/electron`.
</ParamField>

<ParamField body="GREP_SKIP_ELECTRON_CHECKSUMS" type="string">
Set to `1` to pass `download.unsafelyDisableChecksums: true` for offline builds.
</ParamField>

## Version bump and verification

The shipped version is `package.json` → `"version"` (for example `0.5.27`). Electron exposes it through `app.getVersion()` (`APP_GET_VERSION` IPC). The status bar loads it on mount and renders **`G-BUILD v{appVersion}`**; dev mode also shows `[DEV_INSTANCE_NAME]` from `./scripts/dev.sh`.

<Steps>
<Step title="Bump patch version">
Increment the **patch** segment in `package.json` before each production release (slash-command docs use `0.0.68` → `0.0.69` style examples).
</Step>
<Step title="Validate in dev">
Run `./scripts/dev.sh` (not bare `npm run start`). Confirm behavior; standard release flow expects explicit QA approval before `make`.
</Step>
<Step title="Build on master">
Merge feature work to `master` first (see merge workflow below), checkout `master`, then `npm run make`.
</Step>
<Step title="Confirm version in UI">
Open the build and check the status bar shows the new `G-BUILD v*` string.
</Step>
</Steps>

## Git tagging and dual remotes

Release tags use the **`v` prefix** matching `package.json` without the prefix: version `0.5.27` → tag `v0.5.27`.

Maintainer slash-command documentation defines two remotes:

| Remote | Documented target | Role |
|--------|-------------------|------|
| `origin` | `Parcha-ai/claudette` | Private development repo |
| `public` | `Parcha-ai/grep-build` | Public mirror; GitHub **releases** attach here |

After build, push **both** remotes:

```bash
git push origin master && git push origin v{version}
git push public master && git push public v{version}
```

Create the tag locally if missing: `git tag v{version}`.

<Info>
`README.md` links end-user downloads to [github.com/Parcha-ai/build/releases](https://github.com/Parcha-ai/build/releases). `CONTRIBUTING.md` still references cloning `grep-build`. The **release** slash command explicitly uses `--repo Parcha-ai/grep-build`. Align the GitHub repo you publish to with your configured `public` remote and org naming at release time.
</Info>

### Merge-to-master before build

Force and standard build flows require **master** as the build branch:

1. Push the feature branch to `origin`.
2. If `master` is checked out in another worktree, open a PR with `gh pr create` / `gh pr merge --merge --delete-branch`.
3. Otherwise merge locally: `git checkout master`, `git merge {branch}`, `git push origin master`.
4. `git pull origin master` on `master`, then run `npm run make`.

## Production build workflows

### Standard release (QA-gated)

Intended when changes are not yet validated in dev:

1. Start `./scripts/dev.sh`.
2. Wait for explicit confirmation that dev behavior is correct.
3. Bump `package.json` version (patch).
4. Merge to `master` and checkout `master`.
5. `npm run make`.
6. `git tag v{version}`.
7. Push `master` and tag to `origin` and `public`.
8. Open `out/v{version}/Build-darwin-arm64/Build.app` (or rely on postPackage `/Applications` copy).

Do **not** kill unrelated Electron processes in other worktrees during agent-driven builds.

### Fast release (skip QA)

For changes already verified in dev:

1. Bump patch version in `package.json`.
2. Merge to `master` and checkout `master`.
3. `npm run make`.
4. Tag and dual-remote push as above.
5. Open the built app and note artifact paths under `out/v{version}/make/`.

### Helper script

`./scripts/build.sh` automates version read, process stop, `npm run make`, `git tag -f v{version}`, and `open` on `Build.app`. Prefer the QA-aware flow when using agent slash commands unless you explicitly want the script’s kill-and-build semantics.

## GitHub release artifacts

The **release** workflow does not build or bump versions. It assumes `npm run make` already produced installers.

<Steps>
<Step title="Sync git">
Push `master` and `v{version}` to `origin` and `public` (create tag locally if needed).
</Step>
<Step title="Verify artifacts">
Confirm both exist (legacy skill names **Grep Build**; current product name **Build**):

- `out/v{version}/make/zip/darwin/arm64/Build-darwin-arm64-{version}.zip`
- `out/v{version}/make/Build-{version}-arm64.dmg`

If missing, run a production build first and stop.
</Step>
<Step title="Generate notes">
`git log $(git tag --sort=-version:refname | sed -n '2p')..v{version} --oneline --no-decorate` → bullet list under `## Changes`.
</Step>
<Step title="Create release">
```bash
gh release create v{version} \
  "out/v{version}/make/zip/darwin/arm64/Build-darwin-arm64-{version}.zip" \
  "out/v{version}/make/Build-{version}-arm64.dmg" \
  --repo Parcha-ai/grep-build \
  --title "Grep Build v{version}" \
  --latest \
  --notes "..."
```
Upload is large (~500MB combined); use `--repo` explicitly. If the release exists: `gh release delete v{version} --repo Parcha-ai/grep-build --yes` after user confirmation.
</Step>
</Steps>

Release title in the skill template still says **Grep Build**; artifact file names follow **Build** from `productName`.

## Dev vs production separation

| Aspect | Dev (`./scripts/dev.sh`) | Production (`npm run make`) |
|--------|--------------------------|-----------------------------|
| User data | `GREP_DEV_USER_DATA=/tmp/grep-build-dev` | `~/Library/Application Support/Build` |
| Webpack logger port | `9001` (avoids prod `9000`) | N/A (packaged) |
| Instance label | Random adjective-noun in status bar | None |
| QMD setup | `npm run setup-qmd` before start | Bundled in `postPackage` |
| Output | `.webpack/` hot reload | `out/v{version}/` |

Production builds under `out/` should stay running when starting dev; `dev.sh` only kills Electron from `node_modules`, not `out/` bundles.

## Prerequisites for distributable quality

- **Node/npm**: Install deps with `npm install`; native modules (`node-pty`) rely on Electron rebuild via Forge.
- **QMD**: Run `npm run setup-qmd` (or `setup-qmd:all`) before `make` so `resources/qmd/darwin-arm64` exists for bundling.
- **Apple distribution**: Set `APPLE_ID`, `APPLE_PASSWORD`, `APPLE_TEAM_ID` for signed, notarized macOS builds (README claims signed/notarized releases for download).
- **Entitlements**: `entitlements.plist` referenced by Developer ID signing path.

## Related pages

<CardGroup>
<Card title="Installation" href="/installation">
macOS release download, from-source setup, and dev vs production user data paths.
</Card>
<Card title="npm scripts reference" href="/npm-scripts-reference">
When to use `./scripts/dev.sh` vs `npm run start`, lint, setup-qmd, and verify scripts.
</Card>
<Card title="Semantic search (QMD)" href="/semantic-search-qmd">
QMD bundling in dev and the postPackage copy step.
</Card>
<Card title="Electron process model" href="/electron-architecture">
Main/renderer split, webpack output, and packaged runtime layout.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Port conflicts, PATH in packaged apps, and production `main.log` location.
</Card>
</CardGroup>

---

## 24. Troubleshooting

> Dev instance port conflicts, PATH/node discovery in packaged apps, CDP timeouts, MCP harness sync errors, SSH bridge recovery, production main.log location, and security reporting.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/pages/24-troubleshooting.md
- Generated: 2026-06-02T00:26:48.564Z

### Source Files

- `scripts/dev.sh`
- `src/main/index.ts`
- `.notes/cdp-navigation-timeout-investigation.md`
- `scripts/verify-ssh-detached-bridge-resume.js`
- `SECURITY.md`
- `TEST_RESULTS_v0043.md`

---
title: "Troubleshooting"
description: "Dev instance port conflicts, PATH/node discovery in packaged apps, CDP timeouts, MCP harness sync errors, SSH bridge recovery, production main.log location, and security reporting."
---

Build surfaces operational failures through main-process logging (`main.log` in production), dev-only instance naming, and console prefixes such as `[CDP Proxy]`, `[MCP IPC]`, and `[SSH Service]`. Use this page to map symptoms to ports, `userData` paths, recovery APIs, and verification commands grounded in the current codebase.

## Symptom map

| Symptom | Likely layer | First checks |
| --- | --- | --- |
| Dev app never opens / instant exit | Single-instance lock + orphaned Electron | `./scripts/dev.sh` cleanup; only one `node_modules/electron` dev instance |
| Webpack / logger port bind failure | Dev vs production ports | `DEV_WEBPACK_PORT`, `DEV_WEBPACK_LOGGER_PORT`, `lsof` |
| `Claude Code executable not found` (packaged) | PATH + Node resolution | `main.log`; `CLAUDE_CODE_NODE_PATH`; launch from Terminal vs Finder |
| Stagehand / agent browse: `Navigation timed out after 30 seconds` | CDP proxy + Page domain | `[CDP Proxy]` logs; compare with in-app browser preview |
| MCP missing in Cursor/Gemini/Codex/OpenCode | Harness config sync | `[MCP Service]` / `[MCP IPC]` errors; `~/.cursor/mcp.json` etc. |
| SSH chat forks or queue bypass after reconnect | Detached bridge + streaming state | `hasActiveRemoteProcess`; `resumeRemoteTurn`; bridge job logs on remote |
| Need production diagnostics | `main.log` under `userData` | `~/Library/Application Support/Build/main.log` (macOS) |

```text
  Dev (./scripts/dev.sh)                Production (packaged Build.app)
  ------------------------              -------------------------------
  userData: /tmp/grep-build-dev         userData: ~/Library/Application Support/Build
  main.log: disabled (DEV_INSTANCE)    main.log: append-only console tee
  Webpack: DEV_WEBPACK_PORT (9001)      Logger: port 9000 (forge default)
  CDP: 9223 (default)                   CDP: 9222 (default)
  CDP proxy: 9223 (+ auto bump)         CDP proxy: 9223 (+ auto bump)
```

## Dev instance and port conflicts

Always start development with `./scripts/dev.sh`, not `npm run start` directly. The script assigns a random `DEV_INSTANCE_NAME` (for example `bouncy-penguin`), runs `npm run setup-qmd`, kills orphaned dev Electron processes, frees the dev webpack port, and isolates user data.

<Steps>
<Step title="Confirm the dev instance name">
After `./scripts/dev.sh` starts, note the banner line `DEV INSTANCE: <adjective>-<noun>`. The status bar uses this name to distinguish dev builds.
</Step>
<Step title="Resolve silent launch failures">
`scripts/dev.sh` kills processes matching `node_modules/electron/dist/Electron.app` and `electron-forge` before start. Orphaned dev instances hold the single-instance lock and can block a new launch without an obvious error.
</Step>
<Step title="Free the dev webpack port">
Dev sets `DEV_WEBPACK_PORT=9001` and runs `lsof -ti:$DEV_WEBPACK_PORT | xargs kill -9`. Production’s electron-forge logger defaults to port `9000` via `DEV_WEBPACK_LOGGER_PORT` in `forge.config.ts`, so dev deliberately avoids colliding with a running production app on the logger port.
</Step>
<Step title="Keep production user data separate">
`GREP_DEV_USER_DATA=/tmp/grep-build-dev` is applied via `app.setPath('userData', …)` in `src/main/index.ts`. Settings are copied from production once (never symlinked) so dev edits do not corrupt production `claudette-settings.json`.
</Step>
</Steps>

<Warning>
Do not run multiple dev Electron instances for the same worktree. `app.requestSingleInstanceLock()` exits secondary launches unless `GREP_DISABLE_SINGLE_INSTANCE=1`.
</Warning>

| Variable | Dev typical value | Role |
| --- | --- | --- |
| `DEV_INSTANCE_NAME` | `fuzzy-penguin` (random) | Enables dev CDP port `9223`; disables `main.log` tee |
| `GREP_DEV_USER_DATA` | `/tmp/grep-build-dev` | Isolated `electron-store` files |
| `DEV_WEBPACK_PORT` | `9001` | Webpack dev server (`forge.config.ts`) |
| `DEV_WEBPACK_LOGGER_PORT` | `9000` (default) | Forge logger port when unset |
| `GREP_DISABLE_SINGLE_INSTANCE` | unset | Set to `1` only when debugging multi-instance behavior |

## Production `main.log` location

When `DEV_INSTANCE_NAME` is unset (production / packaged runs), `src/main/index.ts` tees `console.log`, `console.warn`, and `console.error` into an append-only file:

```text
<userData>/main.log
```

On macOS with `productName: "Build"`, the default path is:

```text
~/Library/Application Support/Build/main.log
```

Legacy installs may still have data under `G-Build` or `Grep Build`; first launch migrates copies into `Build` when `.migrated-from-grep-build` is absent.

<Note>
Dev runs skip file logging entirely because `DEV_INSTANCE_NAME` is set. Use the terminal where `./scripts/dev.sh` is running, or open DevTools when `GREP_DEV_USER_DATA` is active (`mainWindow.webContents.openDevTools()`).
</Note>

## PATH and Node discovery (packaged macOS)

GUI-launched macOS apps do not inherit a login-shell `PATH`. Build applies two layers at startup in `src/main/index.ts`:

1. **`fix-path`** — restores shell-like PATH for the Electron main process.
2. **Explicit fallbacks** — prepends `/usr/local/bin`, `/opt/homebrew/bin`, `/opt/local/bin`, and common version-manager shim directories when missing.

Local Claude Code spawns (non-SSH) resolve Node through `ClaudeService.resolveLocalNodeExecutable()`:

- `CLAUDE_CODE_NODE_PATH`, `NODE`, `npm_node_execpath`
- Every entry on `process.env.PATH`
- Homebrew, nvm (newest version dir first), nodenv, asdf, volta paths

`createLocalClaudeCodeProcess` rewrites the SDK spawn when a Node binary is found (`node-override` / `node-script` modes). If resolution fails, logs show `Could not resolve a local Node executable explicitly; falling back to PATH lookup`.

<Tip>
For packaged apps launched from Finder, set `CLAUDE_CODE_NODE_PATH` to an absolute Node binary (for example `/opt/homebrew/bin/node`) in the environment before launch, or open Build from a Terminal where `node` is already on PATH.
</Tip>

`src/main/utils/local-executable.ts` additionally skips macOS-quarantined binaries when scanning PATH (relevant for user-installed CLIs).

Release notes document a related fix: misleading “Claude Code executable not found” when packaged builds could not find `node` on PATH; local spawns now route JavaScript entrypoints through an explicit Node candidate when resolved.

## CDP ports and navigation timeouts

Build exposes Chrome DevTools Protocol on two related surfaces:

| Surface | Default port | Override |
| --- | --- | --- |
| Electron remote debugging | `9222` production; `9223` when `DEV_INSTANCE_NAME` or `NODE_ENV=development` | `ELECTRON_CDP_PORT` |
| In-app CDP WebSocket proxy (`cdpProxyService`) | `9223` | `CDP_PROXY_PORT`; auto-increments on `EADDRINUSE` |

Stagehand connects Playwright over CDP to the proxy, which forwards debugger events from session webviews. `stagehand.service.ts` races `page.goto(url, { waitUntil: 'domcontentloaded' })` against a **30 second** timer and throws `Navigation timed out after 30 seconds` when the race loses.

### Known failure mode: Page domain not enabled on attach

Internal investigation (`.notes/cdp-navigation-timeout-investigation.md`) documents the root cause for hung Playwright navigation:

- After `Target.attachToTarget`, the proxy attaches `wc.debugger` and forwards messages.
- **`Page.enable` is not sent** in `cdp-proxy.service.ts` at attach time (verified: no `Page.enable` in that file).
- Electron suppresses `Page.domContentEventFired` / `Page.loadEventFired` until the Page domain is enabled, so Stagehand waits until the 30s timeout even when the webview visibly loaded.

By contrast, `browser.service.ts` in-app navigation calls `Page.navigate` then `Page.enable` (and falls back to `loadURL` on error)—preview navigation can succeed while Stagehand times out.

<Warning>
If agent-driven browsing times out but manual preview navigation works, treat it as a CDP proxy lifecycle issue, not a network block. Watch for `[CDP Proxy] Event[session-N] → Playwright: Page.*` lines after attach.
</Warning>

`cdp-proxy.service.ts` also enforces a **30s** default per CDP command (`COMMAND_TIMEOUT_MS`), emitting errors like `CDP command '…' timed out after 30000ms (debugger may be detached or target destroyed)` when the debugger detaches mid-command.

## MCP harness sync errors

Installed MCP servers live in `claudette-mcp-servers` (`electron-store`). Build mirrors selected entries into per-harness config files on disk:

| Harness | Target file |
| --- | --- |
| Cursor | `~/.cursor/mcp.json` |
| Gemini | `~/.gemini/settings.json` |
| Codex | `~/.codex/config.toml` |
| OpenCode | OpenCode config path from `mcp.service.ts` |

`mcpService.syncLocalHarnessConfigs()` returns `{ cursor?, gemini?, codex?, opencode?, errors: Record<string, string> }`. Any harness that throws records `result.errors.<harness>` with the exception message while other harnesses may still show `synced`.

Startup (`app.on('ready')`) calls `syncLocalHarnessConfigs()`; failures log `[Main] Failed to sync local MCP harness configs on startup`. The MCP IPC layer re-syncs after install/uninstall and queues SSH remote sync.

### SSH remote MCP sync

For connected SSH sessions, `mcp.ipc.ts`:

- Wraps each `sshService.syncMcpConfigsToRemote` in a **30s** timeout (`REMOTE_MCP_SYNC_TIMEOUT_MS`).
- Logs `[MCP IPC] Error syncing to SSH session:` on failure.
- Serializes overlapping syncs via `remoteMcpSyncInFlight` / `remoteMcpSyncQueued`.

Remote sync depends on **stdio-to-HTTP bridges** for native stdio MCP servers. Warnings such as `Could not start MCP stdio bridges` or `Could not sync harness MCP configs to remote` usually mean bridge startup failed before config upload. SSH harness sync is cached per host for **5 minutes** (`HARNESS_MCP_CACHE_TTL_MS`).

<Steps>
<Step title="Inspect local harness errors">
Trigger a sync from Extensions/MCP UI or restart the app; search `main.log` or the dev terminal for `[MCP Service] Local harness MCP sync complete` and non-empty `errors`.
</Step>
<Step title="Validate on-disk configs">
Confirm merged entries appear under the harness paths in the table above. Codex uses TOML merge logic; Cursor/Gemini use JSON merge.
</Step>
<Step title="Re-sync SSH after bridge fixes">
Reconnect the SSH session or install the MCP server again so `syncHarnessesAndSshSessions` runs with active `sshService.isConnected(session.id)`.
</Step>
</Steps>

## SSH detached bridge recovery

SSH Claude runs use a **detached bridge** (`REMOTE_DETACHED_BRIDGE_SCRIPT` uploaded to the remote) instead of fragile tmux/FIFO pipes. The bridge:

- Spawns `claude` (or configured command) with stdio captured to an append-only `stdout.log`
- Exposes a Unix socket for stdin
- Survives app quit, sleep, and transient SSH drops (`app.on('will-quit')` explicitly does **not** kill remote jobs)

### Recovery APIs

| API | Behavior |
| --- | --- |
| `ssh.hasActiveRemoteProcess(sessionId)` | True if a non-recovered detached bridge job is active (`command` empty or `claude`) or legacy tmux session exists |
| `claude.resumeRemoteTurn(sessionId, model?)` | Attaches latest recoverable bridge job and streams recovered stdout; errors if no job or stream already active |
| `sshService.getLatestRecoverableRemoteProcess` | Picks active jobs, or completed jobs within **24h** with metadata and log bytes |

Renderer `session.store.ts` starts `startRemoteProcessMonitor` when switching to an SSH session that is not locally marked streaming. If the backend has no active query, it calls `resumeRemoteTurn` and reloads the transcript.

Recoverable jobs must not have `recovered.json`. Completed jobs need `metadata.json` and non-zero log size.

### Bridge startup timeout

Spawning the bridge waits until `stdin.sock` and `stdout.log` exist (10s deadline). Failure throws `Timed out waiting for detached remote process bridge`.

### Verification script

```bash
node scripts/verify-ssh-detached-bridge-resume.js
```

This simulates mid-run disconnect and asserts recovered text `hello after disconnect` plus a `result` line in the bridge log (same JSONL shape Claude SDK uses).

### Queue / streaming after laptop sleep

Historical SSH queue bypass (documented in `TEST_RESULTS_v0043.md`) occurred when `isStreaming` stayed false after reconnect while remote Claude kept running. Mitigations in tree:

- Detached bridge jobs instead of blocking FIFOs
- `hasActiveRemoteProcess` driving remote process monitors
- Message queue injection via `streamInput` after tool completion

Good console signals: `[SessionStore] Reattaching to detached SSH turn`, `Session is streaming, queueing message`. Bad signal: `Session is NOT streaming, sending as new query` during an active remote turn.

## Security reporting

Per `SECURITY.md`:

<Steps>
<Step title="Do not file public GitHub issues for vulnerabilities">
Email **security@parcha.ai** instead.
</Step>
<Step title="Include reproduction detail">
Provide description, reproduction steps, impact, and optional fix suggestion.
</Step>
<Step title="Expect response targets">
Acknowledgement within **48 hours**; fix or mitigation plan within **7 days** for in-scope issues.
</Step>
</Steps>

**Scope:** Build application source and its build/distribution pipeline. npm dependencies are out of direct policy scope but reports are still welcome.

**API keys:** Stored locally via `electron-store`; transmitted only to configured providers (Anthropic, OpenAI, ElevenLabs, etc.), not to Build-operated analytics servers by default.

## Environment variables reference

<ParamField body="CLAUDE_CODE_NODE_PATH" type="string">
Absolute path to Node for local Claude Code spawns when PATH discovery fails in packaged apps.
</ParamField>

<ParamField body="ELECTRON_CDP_PORT" type="string">
Overrides Electron `remote-debugging-port` (default `9222` production, `9223` dev).
</ParamField>

<ParamField body="CDP_PROXY_PORT" type="number">
CDP WebSocket proxy listen port (default `9223`, auto-increment if busy).
</ParamField>

<ParamField body="GREP_DEV_USER_DATA" type="path">
Set by `scripts/dev.sh` to `/tmp/grep-build-dev` for isolated dev persistence.
</ParamField>

<ParamField body="GREP_DISABLE_SINGLE_INSTANCE" type="string">
Set to `1` to bypass `requestSingleInstanceLock` (debug only).
</ParamField>

<ParamField body="DEV_WEBPACK_PORT" type="number">
Webpack dev server port (dev script sets `9001`).
</ParamField>

## Related pages

<CardGroup>
<Card title="Electron architecture" href="/electron-architecture">
Main vs renderer, `userData`, CDP ports, and dev isolation.
</Card>
<Card title="npm scripts reference" href="/npm-scripts-reference">
When to use `./scripts/dev.sh` vs `npm run start`, verify scripts.
</Card>
<Card title="SSH remote sessions" href="/ssh-remote-sessions">
SSHConfig, resume candidates, teleport, and bridge behavior.
</Card>
<Card title="MCP servers" href="/mcp-servers">
Install flow, harness sync targets, and SDK loading.
</Card>
<Card title="Browser preview" href="/browser-preview">
Webview CDP, inspector, and Stagehand integration.
</Card>
<Card title="Build and release" href="/build-and-release">
Packaging paths, versioning, and production artifacts.
</Card>
</CardGroup>

---