# 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>

---
