# CodeHydra Architecture

## Quick Navigation

| Section                                                 | Description                          |
| ------------------------------------------------------- | ------------------------------------ |
| [System Overview](#system-overview)                     | High-level architecture              |
| [Core Concepts](#core-concepts)                         | Project, Workspace, Views            |
| [Component Architecture](#component-architecture)       | Main components and their roles      |
| [Intent-Based Architecture](#intent-based-architecture) | Intent dispatcher, operations, hooks |
| [Theming System](#theming-system)                       | CSS variables and VS Code theming    |
| [Logging](#logging-system)                              | Log levels, files, and debugging     |

**Related Documentation:**

- [INTENTS.md](INTENTS.md) - Intent system implementation, platform abstractions, mock factories
- [AGENTS.md](AGENTS.md) - Agent provider interface, status tracking, MCP integration
- [PATTERNS.md](PATTERNS.md) - IPC, UI, and CSS implementation patterns

---

## System Overview

```
┌──────────────────────────────────────────────────────────────────────────┐
│                        CodeHydra Application                             │
├──────────────────────────────────────────────────────────────────────────┤
│  Main Process (Electron)                                                 │
│  ┌───────────────┐  ┌───────────────┐  ┌───────────────────────────────┐│
│  │Window Manager │  │ View Manager  │  │ App Services                  ││
│  │ BaseWindow    │  │WebContentsView│  │ ├─ Git Worktree Provider      ││
│  │ resize/bounds │  │ create/destroy│  │ ├─ Code-Server Manager        ││
│  │               │  │ bounds/z-order│  │ ├─ Project Store              ││
│  └───────────────┘  └───────────────┘  │ └─ Agent Server Managers      ││
│                                        └───────────────────────────────┘│
├──────────────────────────────────────────────────────────────────────────┤
│  UI Layer (WebContentsView with transparent background)                  │
│  Bounds change based on state: sidebar-only OR full-window              │
├──────────────────────────────────────────────────────────────────────────┤
│  Workspace Views (code-server WebContentsViews)                          │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐                        │
│  │ Workspace 1 │ │ Workspace 2 │ │ Workspace 3 │                        │
│  │ (visible)   │ │ (hidden)    │ │ (hidden)    │                        │
│  └─────────────┘ └─────────────┘ └─────────────┘                        │
└──────────────────────────────────────────────────────────────────────────┘
```

## Core Concepts

### Project vs Workspace

| Concept   | What it is                        | Viewable          | Actions              |
| --------- | --------------------------------- | ----------------- | -------------------- |
| Project   | Git repository (main directory)   | No                | Close, Add workspace |
| Workspace | Git worktree (NOT main directory) | Yes (code-server) | Select, Remove       |

**Key behavior:**

- Main git directory is the PROJECT (container, not a workspace)
- Only git worktrees are WORKSPACES (viewable in code-server)
- Fresh clone with no worktrees = 0 workspaces → create dialog auto-opens

### Worktree Discovery Logic

| Location                   | Type                                | Discovered as Workspace?    |
| -------------------------- | ----------------------------------- | --------------------------- |
| Main git directory         | Original clone                      | ❌ NO - this is the PROJECT |
| Manually created worktrees | User-created via `git worktree add` | ✅ YES                      |
| App-managed worktrees      | App-created in managed location     | ✅ YES                      |

Example - user opens `~/projects/myrepo`:

```
~/projects/myrepo/                              → PROJECT (not a workspace)
~/projects/myrepo-feature/                      → WORKSPACE (manual worktree)
~/.local/share/codehydra/.../workspaces/feat/   → WORKSPACE (app worktree)
```

### Worktree Storage (Platform-Specific)

New worktrees are created only in the managed location:

| Platform    | Path                                                                         |
| ----------- | ---------------------------------------------------------------------------- |
| Linux       | `~/.local/share/codehydra/projects/<name>-<hash>/workspaces/`                |
| macOS       | `~/Library/Application Support/codehydra/projects/<name>-<hash>/workspaces/` |
| Windows     | `%APPDATA%\codehydra\projects\<name>-<hash>\workspaces\`                     |
| Development | `./app-data/projects/<name>-<hash>/workspaces/`                              |

Discovery finds worktrees in ANY location; creation only in managed location.

### Remote Projects (Cloned from URL)

Projects can be created by cloning from a git URL. These "remote projects" have a special storage layout:

```
~/.local/share/codehydra/projects/<repo-name>-<url-hash>/
├── config.json        # Contains remoteUrl field
├── git/               # Bare clone of the repository
└── workspaces/        # Git worktrees created from the bare clone
```

**Key differences from local projects:**

- **remoteUrl field**: Projects cloned from URL have a `remoteUrl` field in their config
- **Bare clone**: The repository is cloned in bare mode to `git/` subdirectory
- **Duplicate detection**: Cloning the same URL returns the existing project (URL normalized for comparison)
- **Deletion option**: When closing a remote project, users can optionally delete the entire project directory

**URL normalization** for duplicate detection:

- Hostname and path converted to lowercase
- `.git` suffix removed
- Credentials stripped
- Trailing slashes removed
- Port numbers preserved

## WebContentsView Architecture

### View Management

- **Create**: When workspace added, create WebContentsView (detached, URL preloaded in parallel)
- **Activate (first)**: Attach to contentView, set bounds (URL already loaded)
- **Activate (subsequent)**: Attach to contentView, set bounds (URL already loaded)
- **Hide**: Detach from contentView (not attached, no bounds needed)
- **Destroy**: When workspace removed, destroy WebContentsView
- **Z-order**: Controlled by `contentView.addChildView(view)` (last added = front)

### View Lifecycle

```
[not created] ──createWorkspaceView()──► [created/detached/loading]
                                               │
                                               │ URL preloaded, not in contentView
                                               │ 10-second timeout started
                                               │
                                       setActiveWorkspace() [first time]
                                               │
                                               ▼
                                      ┌────────────────────┐
                                      │  [loading/attached]│
                                      │  URL already loaded│
                                      │  in contentView    │
                                      │  loading overlay   │
                                      └────────┬───────────┘
                                               │
                               first MCP request OR 10s timeout
                                               │
                                               ▼
                                      ┌────────────────────┐
                                      │  [active/attached] │
                                      │  URL loaded        │
                                      │  bounds: content   │
                                      │  unthrottled       │
                                      └────────┬───────────┘
                                               │
                                       setActiveWorkspace(other/null)
                                               │
                                               ▼
                                      ┌────────────────────┐
                                      │  [detached]        │
                                      │  URL loaded        │◄───────┐
                                      │  not in contentView│        │
                                      │  throttled (async) │        │
                                      └────────┬───────────┘        │
                                               │                    │
                        ┌──────────────────────┼────────────────────┘
                        │                      │
              setActiveWorkspace()    destroyWorkspaceView()
                        │                      │
                        │                      ▼
                        └──────────────► [destroyed]
```

- **Loading state**: New workspaces show a loading overlay until first MCP request is received (indicating TUI attachment) or 10-second timeout. The view URL is loaded but the view remains detached (not attached to contentView).
- **Detached views** retain their VS Code state (no reload when shown again)
- **Detachment** (vs zero-bounds) reduces GPU usage when many workspaces are open
- **URL preloading**: All workspace URLs are loaded in parallel when a project opens. This eliminates loading delays when switching between workspaces.

### UI Layer State Machine

The application uses a **detachment-based visibility approach**:

- **UI layer**: Always attached with full-window bounds. Visibility controlled by z-order.
- **Workspace views**: Only active view is attached with content bounds. Inactive views are detached from contentView entirely (not attached, no bounds, no GPU usage).

| State   | UI Z-Order                  | Focus    | Description                  |
| ------- | --------------------------- | -------- | ---------------------------- |
| Normal  | Behind workspace views      | VS Code  | User working in editor       |
| Overlay | In front of workspace views | UI layer | Shortcut mode or dialog open |

**State transitions:**

- Normal → Overlay: User activates shortcut mode (Alt+X) or opens dialog
- Overlay → Normal: User releases Alt, presses Escape, closes dialog, or window loses focus

**Implementation:**

- UI transparency: `setBackgroundColor('#00000000')`
- Z-order front: `contentView.addChildView(view)` (no index = add to end = top)
- Z-order back: `contentView.addChildView(view, 0)` (index 0 = bottom)

## Component Architecture

### Main Process Components

| Component       | Responsibility                                                      |
| --------------- | ------------------------------------------------------------------- |
| Window Manager  | BaseWindow lifecycle, resize handling, minimum size, overlay icons  |
| View Manager    | WebContentsView create/destroy, bounds calculation, z-order         |
| Badge Manager   | App icon badge showing count of idle workspaces (platform-specific) |
| IPC Handlers    | Bridge between renderer and services                                |
| Preload Scripts | Secure IPC exposure, keyboard capture                               |

### Preload Scripts

| Script           | Used By  | Purpose                                            |
| ---------------- | -------- | -------------------------------------------------- |
| preload/index.ts | UI layer | Expose IPC API for sidebar, dialogs, shortcut mode |

**Note**: Workspace views intentionally have NO preload script. Keyboard capture is handled via main-process `before-input-event` for simplicity and security.

### App Services (pure Node.js, no Electron deps)

Services are pure Node.js for testability without Electron:

| Service                  | Responsibility                                                  | Status      |
| ------------------------ | --------------------------------------------------------------- | ----------- |
| Git Worktree Provider    | Discover worktrees (not main dir), create, remove               | Implemented |
| Code-Server Manager      | Start/stop code-server, port management                         | Implemented |
| Project Store            | Persist open projects across sessions                           | Implemented |
| OpenCode Server Manager  | Spawn/manage one `opencode serve` per workspace                 | Implemented |
| OpenCode Status Provider | SSE connections, status aggregation                             | Implemented |
| VS Code Setup Service    | First-run extension and config installation                     | Implemented |
| KeepFiles Service        | Copy gitignored files from project root to new workspaces       | Implemented |
| NetworkLayer             | HTTP, SSE, port operations (HttpClient, SseClient, PortManager) | Implemented |
| PluginServer             | Socket.IO server for VS Code extension communication            | Implemented |
| McpServerManager         | MCP server for AI agent workspace API access                    | Implemented |
| PosthogModule            | PostHog analytics for DAU, version, platform, errors            | Implemented |
| AutoUpdater              | Check for updates daily, apply on quit (electron-updater)       | Implemented |

### Workspace Cleanup

The Git Worktree Provider includes resilient deletion and orphaned workspace cleanup:

**Workspace Deletion Sequence**: When a workspace is deleted, the following operations run in the "kill-terminals" step, followed by cleanup operations:

1. **Kill terminals and extension host** (best-effort, in "kill-terminals" step):
   - Sends `shutdown` event to the VS Code extension, which handles:
     1. Gets all terminals and disposes each one
     2. Waits for `onDidCloseTerminal` events (or 5s timeout)
     3. Removes workspace folders (releases file watchers)
     4. Terminates the extension host process
   - If the workspace is not connected or operations time out, the step is marked as done and deletion continues

2. **Close VS Code view**: Navigates to `about:blank`, clears session storage, and destroys the WebContentsView.

3. **Remove worktree**: Executes `git worktree remove --force` to remove the git worktree.

```
┌─────────────────────────────────────────────────────────────────────┐
│                    Workspace Deletion Flow                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  remove() ──► switchToNextWorkspace() ──► executeDeletion()        │
│                     │                           │                   │
│                     ▼                           ▼                   │
│              View detached          ┌───────────────────────┐       │
│              (still exists)         │ Op 1: kill-terminals  │       │
│                     │               │ "Terminating processes"│       │
│                     │               │ (PluginServer command) │       │
│                     │               └───────────┬───────────┘       │
│                     │                           │                   │
│                     │               ┌───────────────────────┐       │
│                     │               │ Op 2: cleanup-vscode  │       │
│                     │               │ "Closing VS Code view"│       │
│                     │               │ (ViewManager destroy)  │       │
│                     │               └───────────┬───────────┘       │
│                     │                           │                   │
│                     │               ┌───────────────────────┐       │
│                     │               │ Op 3: cleanup-workspace│      │
│                     │               │ "Removing workspace"  │       │
│                     │               │ (git worktree remove)  │       │
│                     │               └───────────────────────┘       │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
```

**Resilient Deletion**: When `git worktree remove --force` fails but the worktree was successfully unregistered (e.g., due to locked files in code-server), the deletion is considered successful. The orphaned directory will be cleaned up on next startup.

**Blocking Process Detection (Windows)**: When deletion fails due to locked files (EBUSY, EACCES, EPERM), the `WindowsFileLockModule` handles blocking processes using the Windows Restart Manager API via a PowerShell script. The module provides three hook-based operations:

| Operation | Hook Point | Description                                  |
| --------- | ---------- | -------------------------------------------- |
| Release   | `release`  | CWD-only scan + kill before deletion attempt |
| Detect    | `detect`   | Full handle detection after deletion failure |
| Flush     | `flush`    | Kill user-selected PIDs on retry             |

The UI shows a scrollable list of blocking processes (with files and CWD) and offers a split button with "Retry" as the main action and a dropdown menu with "Kill Processes" (terminates processes), "Close Handles" (closes handles with elevation), or "Ignore Blockers" (skips detection). A "Dismiss" button closes the dialog. On non-Windows platforms, the module is not registered (detection steps are skipped).

**Startup Cleanup**: On project open, `cleanupOrphanedWorkspaces()` runs non-blocking to remove directories in the workspaces folder that are not registered with git. This handles cases where previous deletions partially failed.

**Security Measures**:

- Skips symlinks (prevents symlink attacks targeting system directories)
- Validates paths stay within workspacesDir (prevents path traversal)
- Re-checks worktree registration before each deletion (TOCTOU protection)
- Concurrency guard prevents multiple cleanups running simultaneously

### Workspace Session Model

All workspaces share a single global Electron session to enable extension storage (globalState, secrets) to be shared across workspaces.

```
┌─────────────────────────────────────────────────────────────────────┐
│                    ALL WORKSPACES                                   │
│            partition: persist:codehydra-global                      │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │         IndexedDB, localStorage (globalState, secrets)          │ │
│ │                         SHARED                                  │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│                                                                     │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐              │
│  │ Workspace A  │  │ Workspace B  │  │ Workspace C  │              │
│  │ ?folder=/a   │  │ ?folder=/b   │  │ ?folder=/c   │              │
│  └──────────────┘  └──────────────┘  └──────────────┘              │
│  code-server uses folder path for workspace-specific state          │
└─────────────────────────────────────────────────────────────────────┘
```

**Key Points:**

- All WebContentsViews share the same Electron session (`persist:codehydra-global`)
- Session storage (IndexedDB, localStorage, cookies) is global
- code-server distinguishes workspaces via the `?folder=` URL parameter
- VS Code's workspace-specific state uses the folder path, not browser storage
- Extension `globalState` and `secretStorage` are shared across all workspaces

**View Destruction Cleanup:**

When a workspace is deleted, the ViewManager performs these cleanup steps:

1. **Navigate to about:blank**: Releases any resources held by the loaded page
2. **Close view**: Destroys the WebContentsView

```typescript
// Cleanup sequence in ViewManager.destroyWorkspaceView()
await view.webContents.loadURL("about:blank"); // Wait with timeout
view.webContents.close();
// NOTE: Session storage is NOT cleared - it's shared with other workspaces
```

**Note:** The `about:blank` navigation uses a timeout to prevent hanging if the view is unresponsive.

### Git Configuration Storage (Workspace Metadata)

CodeHydra stores workspace metadata in git config using the `branch.<name>.codehydra.<key>` pattern:

| Config Key                      | Purpose                                | Example                                   |
| ------------------------------- | -------------------------------------- | ----------------------------------------- |
| `branch.<name>.codehydra.base`  | Base branch workspace was created from | `branch.feature-x.codehydra.base = main`  |
| `branch.<name>.codehydra.note`  | User notes for the workspace           | `branch.feature-x.codehydra.note = WIP`   |
| `branch.<name>.codehydra.model` | AI model preference                    | `branch.feature-x.codehydra.model = gpt4` |

**Storage location**: Repository's `.git/config` file

**Why git config?**

- Portable: survives app reinstall, stored with the repository
- Standard mechanism: git provides CLI and library support
- Per-branch: each workspace/branch has isolated config

**Caveats**:

- Lost if branch is renamed (same as `branch.<name>.remote`)
- Not a standard git key, but git allows arbitrary branch config

#### Metadata Key Restrictions

Metadata keys are validated with `/^[A-Za-z][A-Za-z0-9-]*$/` and:

- Maximum length: 64 characters
- Cannot end with a hyphen

**Valid keys**: `base`, `note`, `model-name`, `AI-model`
**Invalid keys**: `_private` (leading underscore), `my_key` (underscore), `123note` (starts with digit), `note-` (trailing hyphen)

#### Base Branch Fallback Logic

The `base` key has special fallback logic for backwards compatibility. This fallback is applied ONLY to the `base` key, not other metadata:

```
metadata.base = config.base ?? branch ?? name
```

- First: git config value `codehydra.base` (if set)
- Second: current branch name (if not detached HEAD)
- Third: workspace directory name (fallback for detached HEAD)

Other metadata keys return their exact config value or `undefined` if not set.

### Shell and Platform Layers

Electron APIs are abstracted behind testable interfaces in two domains. This enables unit testing with behavioral mocks while boundary tests verify real Electron behavior.

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                             Main Process Components                         │
│                                                                             │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────────────────┐  │
│  │ WindowManager   │  │  ViewManager    │  │    BadgeManager             │  │
│  │ ShortcutCtrl    │  │                 │  │                             │  │
│  └────────┬────────┘  └────────┬────────┘  └─────────────┬───────────────┘  │
│           │                    │                         │                  │
│           │                    │                         │                  │
└───────────┼────────────────────┼─────────────────────────┼──────────────────┘
            │                    │                         │
            │                    │                         │
┌───────────▼────────────────────▼─────────────────────────▼──────────────────┐
│                          Abstraction Layers                                 │
│                                                                             │
│  ┌─────────────────────────────────┐  ┌───────────────────────────────────┐ │
│  │          Shell Layers           │  │         Platform Layers           │ │
│  │         (services/shell/)       │  │       (services/platform/)        │ │
│  │                                 │  │                                   │ │
│  │  WindowBoundary ───► ImageBoundary ───┼──┼─► ImageBoundary                      │ │
│  │       │                         │  │   IpcBoundary                        │ │
│  │       ▼                         │  │   DialogBoundary                     │ │
│  │  ViewBoundary ───► SessionBoundary    │  │   AppBoundary                        │ │
│  │                                 │  │   MenuBoundary                       │ │
│  └─────────────────────────────────┘  └───────────────────────────────────┘ │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
            │                    │                         │
            │                    │                         │
┌───────────▼────────────────────▼─────────────────────────▼──────────────────┐
│                            Electron APIs                                    │
│                                                                             │
│  BaseWindow    WebContentsView    session    ipcMain    dialog    app       │
│  nativeImage   Menu                                                         │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
```

**Layer Dependency Rules:**

| Rule                | Description                                                                                            |
| ------------------- | ------------------------------------------------------------------------------------------------------ |
| Shell → Platform    | Shell layers may depend on Platform layers (e.g., WindowBoundary uses ImageBoundary for overlay icons) |
| Platform → Platform | Platform layers are independent (no dependencies on each other)                                        |
| Shell → Shell       | Shell layers may depend on each other (e.g., ViewBoundary uses SessionBoundary)                        |
| Platform ↛ Shell    | Platform layers may NOT depend on Shell layers                                                         |

**Handle-Based Design:**

Layers return opaque handles instead of raw Electron objects:

| Layer             | Returns         | Instead of        |
| ----------------- | --------------- | ----------------- |
| `WindowBoundary`  | `WindowHandle`  | `BaseWindow`      |
| `ViewBoundary`    | `ViewHandle`    | `WebContentsView` |
| `SessionBoundary` | `SessionHandle` | `Session`         |
| `ImageBoundary`   | `ImageHandle`   | `NativeImage`     |

This pattern:

- Prevents Electron types from leaking into manager code
- Enables behavioral mocks that just return `{ id: "test-1", __brand: "ViewHandle" }`
- Centralizes all Electron access in layer implementations

**Boundary Tests:**

Each layer has boundary tests (`*.boundary.test.ts`) that verify behavior against real Electron APIs:

| Layer             | Boundary Test              |
| ----------------- | -------------------------- |
| `IpcBoundary`     | `ipc.boundary.test.ts`     |
| `DialogBoundary`  | `dialog.boundary.test.ts`  |
| `ImageBoundary`   | `image.boundary.test.ts`   |
| `AppBoundary`     | `app.boundary.test.ts`     |
| `MenuBoundary`    | `menu.boundary.test.ts`    |
| `WindowBoundary`  | `window.boundary.test.ts`  |
| `ViewBoundary`    | `view.boundary.test.ts`    |
| `SessionBoundary` | `session.boundary.test.ts` |

### Platform Abstractions Overview

All external system access goes through abstraction interfaces defined in `src/services/platform/`. This enables unit testing with mocks and boundary testing against real systems.

**CRITICAL RULE**: Services MUST use these interfaces, NOT direct library imports.

For detailed platform abstraction documentation including interface definitions, mock factories, and usage patterns, see [INTENTS.md](INTENTS.md#platform-abstractions).

| External System    | Interface               | Implementation               |
| ------------------ | ----------------------- | ---------------------------- |
| Filesystem         | `FileSystemBoundary`    | `DefaultFileSystemBoundary`  |
| HTTP requests      | `HttpClient`            | `DefaultNetworkLayer`        |
| Port operations    | `PortManager`           | `DefaultNetworkLayer`        |
| Process spawning   | `ProcessRunner`         | `ExecaProcessRunner`         |
| Build info         | `BuildInfo`             | `ElectronBuildInfo`          |
| Platform info      | `PlatformInfo`          | `NodePlatformInfo`           |
| Path resolution    | `PathProvider`          | `DefaultPathProvider`        |
| Path normalization | `Path` (class)          | Self-normalizing object      |
| Blocking processes | `WindowsFileLockModule` | Intent module (Windows only) |

### Frontend Components (Svelte 5)

| Component             | Purpose                                              |
| --------------------- | ---------------------------------------------------- |
| App                   | Mode router between setup and normal app modes       |
| MainView              | Normal app mode container, IPC initialization        |
| Sidebar               | Project list, workspace list, action buttons         |
| EmptyState            | Displayed when no projects are open                  |
| Dialog                | Base dialog component with focus trap, accessibility |
| CreateWorkspaceDialog | New workspace form with validation, branch selection |
| RemoveWorkspaceDialog | Confirmation with uncommitted changes warning        |
| CloseProjectDialog    | Confirmation when closing project with workspaces    |
| BranchDropdown        | Searchable combobox for branch selection             |
| ShortcutOverlay       | Keyboard shortcut hints (shown during shortcut mode) |
| SetupScreen           | Setup progress display with indeterminate bar        |
| SetupComplete         | Brief success message after setup completes          |
| SetupError            | Error display with Retry and Quit buttons            |
| Stores                | projects, dialogs, shortcuts, setup (Svelte 5 runes) |

## Intent-Based Architecture

All application behavior flows through an intent-based dispatcher. External triggers (IPC from the renderer, MCP commands, Electron lifecycle events) create typed intents that the dispatcher routes to operations. Operations orchestrate hook points where modules contribute behavior. Domain events propagate outcomes to subscribers.

### Architecture Overview

```
External Trigger (IPC / MCP / Electron lifecycle)
       │
       ▼
     Intent (typed, registered)
       │
       ▼
  Dispatcher
       │
       ├── Interceptors (may cancel — e.g., idempotency checks)
       │
       ▼
  Operation (1:1 with intent)
       │
       ├── Hook points (modules contribute behavior)
       ├── May dispatch child intents
       ├── Emits domain events
       │
       ▼
    Result
```

### Core Concepts

| Concept         | Responsibility                                               |
| --------------- | ------------------------------------------------------------ |
| **Intent**      | Declarative request describing _what should happen_          |
| **Operation**   | Orchestrates the workflow for one intent type                |
| **Hook**        | Module-provided behavior contributing data or side effects   |
| **Interceptor** | Pre-execution check or transformation (e.g., idempotency)    |
| **Event**       | Fire-and-forget signal emitted after something happened      |
| **Module**      | Declares hooks, interceptors, and event subscribers          |
| **Dispatcher**  | Routes intents to operations, delivers events to subscribers |

### Layer Ownership

| Component     | Owns                                                  | Does NOT Own                           |
| ------------- | ----------------------------------------------------- | -------------------------------------- |
| `Dispatcher`  | Intent routing, interceptor pipeline, event delivery  | Business logic (in modules)            |
| `Operations`  | Workflow orchestration, control flow decisions        | Side effects (delegated to hooks)      |
| `Modules`     | Hook handlers, event subscriptions, domain logic      | Workflow orchestration (in operations) |
| `UiIpcModule` | IPC-to-intent mapping, domain-event-to-IPC forwarding | Business logic or workflow control     |

### Core Principles

1. **All externally visible behavior starts with an Intent**
2. **1 Intent = 1 Operation** (enforced by registry)
3. **Operations orchestrate workflows, but do not implement business logic**
4. **Hooks, Events, and Interceptors are the only extension points**
5. **Hooks are unordered by default** -- any ordering must be declared explicitly
6. **Operations decide what happens next**, based on hook outcomes
7. **Modules never call each other**
8. **Composition happens only in the application shell** (`src/main/index.ts`)

### Key Rules

- **Hook execution**: Operations call `hooks.collect(hookPointId, ctx)` which runs all registered handlers. All handlers always run regardless of earlier errors. The operation inspects `HookResult.errors` and decides whether to continue, abort, or compensate.
- **Control flow**: Hooks do not decide control flow. Operations always decide. Examples: fail-fast (stop on first error), best-effort (collect errors, continue), compensate (run cleanup hooks).
- **Child intents**: Only operations dispatch child intents (via `ctx.dispatch()`). Hooks return data; operations decide whether to dispatch further.
- **Events**: Domain events are fire-and-forget signals emitted via `ctx.emit()`. They cannot affect control flow. Subscribers (UiIpcModule, BadgeModule, WindowTitleModule) react independently.

For concrete operations, hook points, domain events, IPC mappings, capability ordering, platform abstractions, and mock factories, see [INTENTS.md](INTENTS.md).

### Branded ID Types

The API uses branded types (`ProjectId`, `WorkspaceName`) for type safety:

```typescript
// Branded type prevents accidental string/ID confusion
declare const ProjectIdBrand: unique symbol;
export type ProjectId = string & { readonly [ProjectIdBrand]: true };

// Generated from path using deterministic algorithm
function generateProjectId(absolutePath: string): ProjectId {
  const normalizedPath = path.normalize(absolutePath);
  const basename = path.basename(normalizedPath);
  const safeName = basename.replace(/[^a-zA-Z0-9]/g, "-") || "root";
  const hash = crypto.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
  return `${safeName}-${hash}` as ProjectId;
}
```

**ID Format**: `<name>-<8-char-hash>` (e.g., `my-app-a1b2c3d4`)

**Test Vectors**:

| Input Path                    | Generated ID            |
| ----------------------------- | ----------------------- |
| `/home/user/projects/my-app`  | `my-app-<hash8>`        |
| `/home/user/projects/my-app/` | `my-app-<hash8>` (same) |
| `/home/user/Projects/My App`  | `My-App-<hash8>`        |

### IPC Channel Naming

The v2 API uses `api:` prefixed IPC channels:

| API Method                | IPC Channel            |
| ------------------------- | ---------------------- |
| `v2.projects.open(path)`  | `api:project:open`     |
| `v2.projects.close(id)`   | `api:project:close`    |
| `v2.projects.list()`      | `api:project:list`     |
| `v2.workspaces.create()`  | `api:workspace:create` |
| `v2.ui.switchWorkspace()` | `api:workspace:switch` |
| Event subscription        | `api:<event-name>`     |

### Main Process Startup Architecture

The main process uses a composition-root pattern in `src/main/index.ts`. All services are constructed (pure, no I/O), all operations and modules are registered, then `app:start` is dispatched. See [INTENTS.md — Composition Root](INTENTS.md#composition-root) for the full bootstrap diagram.

### First-Run Flow

On first startup (no `config.json` exists), the application follows this flow:

1. **Config Loading**: `Config.load()` reads `{dataRootDir}/config.json`. If missing, returns defaults with `agent: null`.

2. **Agent Selection**: When `agent` is null, UI shows `AgentSelectionDialog`. User selects Claude or OpenCode, which calls `lifecycle.setAgent()` to save the choice.

3. **Binary Resolution**: `BinaryResolutionService.resolve()` determines binary availability:
   - For code-server (pinned version): Check exact version in bundles directory
   - For agents with null version (Claude, OpenCode):
     1. Check system binary via `which`/`where`
     2. If not found, check bundles directory for any version
     3. Use latest available or mark for download

4. **Setup Screen**: Shows 3 progress rows (VSCode, Agent, Setup). Downloads run in parallel. Row statuses:
   - `pending`: Waiting to start
   - `running`: In progress (with percentage or indeterminate)
   - `done`: Complete (green checkmark)
   - `failed`: Error occurred (red X, shows Retry/Quit buttons)

5. **Service Startup**: After all binaries are available, the `app:start` operation's `start` hook point initializes servers (code-server, plugin server, agent servers, MCP server) and the `activate` hook point loads persisted projects and sets the active workspace. Earlier hook points (`register-config` through `check-deps`) handle configuration, Electron readiness, and dependency verification.

**Key invariant**: The renderer ALWAYS goes through "loading" before "ready". The multi-phase `app:start` design ensures config is loaded, dependencies are checked, and servers are running before data is loaded. This allows the UI to display a loading screen during service startup.

### Renderer Startup Flow

The renderer is event-driven — the main process controls mode transitions via IPC events:

```
App.svelte (mode router)
│
├── Starts in "initializing" mode (blank state)
│   └── Waits for IPC events from main process
│
├── lifecycle:show-agent-selection → "agent-selection" mode
│   ├── AgentSelectionDialog (user selects Claude/OpenCode)
│   ├── sendAgentSelected(agent) → main process continues
│   └── Transitions to "setup" mode
│
├── lifecycle:show-setup → "setup" mode
│   ├── SetupScreen.svelte (progress rows: vscode, agent, setup)
│   ├── lifecycle:setup-progress events → update row statuses
│   └── lifecycle:setup-error → SetupError (Retry sends sendRetry(), Quit calls lifecycle.quit())
│
├── lifecycle:show-starting → "loading" mode
│   └── SetupScreen with message="CodeHydra is starting..."
│
└── lifecycle:show-main-view → "ready" mode
    └── MainView.svelte
        │
        └── onMount:
            ├── lifecycle.ready() signals main process
            ├── listProjects()
            ├── Workspace status fetches
            └── Domain event subscriptions (project/workspace/agent)
```

**Key Design Decisions:**

1. **App.svelte owns global events**: Shortcut events and setup progress events work across modes
2. **MainView.svelte owns domain events**: IPC calls only happen when services are started
3. **Multi-phase startup**: The `app:start` operation runs five hook points in sequence (`before-ready` → `init` → `show-ui` → `check-deps` → `start`). The `init` hook uses capability-based ordering: ElectronLifecycleModule provides `"app-ready"` after `app.whenReady()`, and handlers needing Electron declare `requires: { "app-ready": ANY_VALUE }`. Errors in early hooks abort startup.
4. **Main-process-driven flow**: The main process sends IPC events to tell the renderer which mode to show. The renderer never polls or pulls state.
5. **Idempotent startup**: The `app:start` intent uses an idempotency interceptor to prevent duplicate execution
6. **IPC initialization timing**: `listProjects()` and workspace status fetches are called in MainView.onMount, not App.onMount
7. **Loading screen UX**: Services start AFTER UI loads so the loading screen can be displayed during service startup

See [VS Code Setup](#vs-code-setup) for the main process side of this flow.

### UI Mode System

The application uses a unified UI mode system with four modes:

| Mode        | UI Z-Order | Focus          | Description                              |
| ----------- | ---------- | -------------- | ---------------------------------------- |
| `workspace` | Behind     | Workspace view | Normal editing mode                      |
| `shortcut`  | On top     | UI layer       | Shortcut overlay visible                 |
| `dialog`    | On top     | Dialog (no-op) | Modal dialog open (blocks Alt+X)         |
| `hover`     | On top     | No change      | Sidebar expanded on hover (allows Alt+X) |

```
WORKSPACE MODE (normal):
┌─────────────────────────────────────────────────────────────────────┐
│ children[0]: UI Layer        │ children[N]: Workspace Views        │
│ z-order: BEHIND              │ z-order: ON TOP                     │
│ Sidebar visible              │ VS Code receives keyboard input     │
└──────────────────────────────┴─────────────────────────────────────┘

SHORTCUT/DIALOG MODE (overlay):
┌─────────────────────────────────────────────────────────────────────┐
│ children[0..N-1]: Workspace Views (z-order: BEHIND)                 │
├─────────────────────────────────────────────────────────────────────┤
│ children[N]: UI Layer (z-order: ON TOP)                             │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │                  Dialog or Shortcut Overlay                     │ │
│ │              (receives all keyboard/mouse events)               │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```

**Mode Transitions:**

| Trigger              | Mode Change          |
| -------------------- | -------------------- |
| Alt+X pressed        | workspace → shortcut |
| Alt+X pressed        | hover → shortcut     |
| Alt released         | shortcut → workspace |
| Escape pressed       | shortcut → workspace |
| Dialog opens         | any → dialog         |
| Dialog closes        | dialog → workspace   |
| Sidebar hover starts | workspace → hover    |
| Sidebar hover stops  | hover → workspace    |
| Dialog opens (hover) | hover → dialog       |

**Alt+X Blocking:**

Alt+X is blocked when `mode === "dialog"` but allowed for all other modes including `"hover"`. This allows:

- Activating shortcut mode while hovering over the expanded sidebar
- Blocking shortcut mode when a modal dialog is open (focus trap is active)

**API:** `api.ui.setMode(mode)` - unified method that handles z-order and focus:

- `setMode("workspace")`: UI behind workspaces, focus active workspace
- `setMode("shortcut")`: UI on top, focus UI layer
- `setMode("dialog")`: UI on top, no focus change (dialog manages its own)
- `setMode("hover")`: UI on top, no focus change (sidebar hover)

Mode transitions are idempotent - calling `setMode()` with the current mode is a no-op.

## Theming System

CodeHydra uses a CSS custom properties system for theming, with support for both VS Code integration and standalone operation.

### CSS Variable Architecture

```
┌─────────────────────────────────────────────────────────────────────────┐
│                         CSS THEMING ARCHITECTURE                        │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  variables.css                                                          │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │  :root {                                                          │  │
│  │    --ch-foreground: var(--vscode-foreground, #cccccc);            │  │
│  │    --ch-agent-idle: var(--ch-success); /* Reference semantic */   │  │
│  │  }                                                                │  │
│  │  @media (prefers-color-scheme: light) { ... light fallbacks ... } │  │
│  └───────────────────────────────────────────────────────────────────┘  │
│                              │                                          │
│                              ▼                                          │
│  Components use --ch-* variables exclusively                            │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │  .indicator--idle { background: var(--ch-agent-idle); }           │  │
│  │  .dialog-overlay { background: var(--ch-overlay-bg); }            │  │
│  └───────────────────────────────────────────────────────────────────┘  │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
```

### Variable Categories

| Category    | Variables                                                     | Purpose                  |
| ----------- | ------------------------------------------------------------- | ------------------------ |
| Core        | `--ch-foreground`, `--ch-background`                          | Base text and background |
| Border      | `--ch-border`, `--ch-input-border`, `--ch-input-hover-border` | Borders and dividers     |
| Interactive | `--ch-button-bg`, `--ch-button-fg`, `--ch-button-hover-bg`    | Buttons, inputs, forms   |
| Focus       | `--ch-focus-border`                                           | Focus indicators         |
| Semantic    | `--ch-success`, `--ch-danger`, `--ch-warning`                 | Status colors            |
| Agent       | `--ch-agent-idle`, `--ch-agent-busy`                          | Agent status (semantic)  |
| Overlay     | `--ch-overlay-bg`, `--ch-shadow-color`, `--ch-shadow`         | Modals, tooltips         |
| Layout      | `--ch-sidebar-width`, `--ch-dialog-max-width`                 | Sizing (theme-agnostic)  |

### VS Code Variable Fallback Pattern

Variables use `var(--vscode-*, fallback)` for dual-mode operation:

```css
--ch-foreground: var(--vscode-foreground, #cccccc);
```

- **In code-server context**: VS Code injects `--vscode-*` variables, which take precedence
- **In standalone mode**: Fallback values are used, controlled by `prefers-color-scheme`

### Light/Dark Theme Switching

Light and dark themes only change fallback values via `@media` query:

```css
:root {
  --ch-foreground: var(--vscode-foreground, #cccccc); /* Dark fallback */
}

@media (prefers-color-scheme: light) {
  :root {
    --ch-foreground: var(--vscode-foreground, #3c3c3c); /* Light fallback */
  }
}
```

This approach means:

- VS Code theme takes precedence when running in code-server
- System preference controls standalone appearance
- No JavaScript needed for theme switching
- Layout variables (widths, spacing) are NOT in the media query

## Logging System

The logging system provides comprehensive logging across both main and renderer processes using electron-log.

### Architecture Overview

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                           LOGGING ARCHITECTURE                               │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  MAIN PROCESS                                                                │
│  ┌─────────────────────────────────────────────────────────────────────────┐│
│  │                     Logging (interface)                           ││
│  │  - createLogger(name: LoggerName): Logger                               ││
│  │  - initialize(): void  (enables renderer logging via IPC)               ││
│  │                              │                                           ││
│  │                              ▼                                           ││
│  │              ElectronLog (boundary impl)                          ││
│  │  - Wraps electron-log/main                                              ││
│  │  - Configures file path: <app-data>/logs/<datetime>-<uuid>.log          ││
│  └─────────────────────────────────────────────────────────────────────────┘│
│                                                                              │
│  RENDERER PROCESS (via IPC)                                                  │
│  ┌─────────────────────────────────────────────────────────────────────────┐│
│  │  createLogger('ui') → Logger that calls window.api.log.*                ││
│  │                              │                                           ││
│  │                              │ IPC to main                               ││
│  │                              ▼                                           ││
│  │              Logging.createLogger(name).method(msg, context)     ││
│  └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘
```

### Configuration

| Variable        | Values                   | Description                                           |
| --------------- | ------------------------ | ----------------------------------------------------- |
| `CH_LOGLEVEL`   | debug\|info\|warn\|error | Override default log level                            |
| `CH_PRINT_LOGS` | any non-empty value      | Print logs to stdout/stderr                           |
| `CH_LOGGER`     | comma-separated names    | Only log from specified loggers (e.g., `git,process`) |

**Default Levels**:

- Development (isDevelopment=true): DEBUG
- Production (isDevelopment=false): WARN

### Logger Names/Scopes

| Logger          | Module                    | Description                           |
| --------------- | ------------------------- | ------------------------------------- |
| `[badge]`       | BadgeManager              | App icon badge updates                |
| `[process]`     | LoggingProcessRunner      | Spawned processes, stdout/stderr      |
| `[network]`     | DefaultNetworkLayer       | HTTP fetch, port operations           |
| `[fs]`          | DefaultFileSystemBoundary | File read/write operations            |
| `[git]`         | SimpleGitClient           | Git commands                          |
| `[opencode]`    | OpenCodeClient            | OpenCode SSE connections              |
| `[code-server]` | CodeServerManager         | code-server lifecycle                 |
| `[pidtree]`     | PidtreeProvider           | Process tree lookups                  |
| `[keepfiles]`   | KeepFilesService          | .keepfiles copy operations            |
| `[api]`         | IPC Handlers              | API request/response timing           |
| `[window]`      | WindowManager             | Window create/resize/close            |
| `[view]`        | ViewManager               | View lifecycle, mode changes          |
| `[app]`         | Application Lifecycle     | Bootstrap, startup, shutdown          |
| `[ui]`          | Renderer Components       | Dialog events, user actions           |
| `[extension]`   | PluginServer              | Extension-side logs forwarded to main |

### Log File Location

| Environment | Path                                                           |
| ----------- | -------------------------------------------------------------- |
| Development | `./app-data/logs/2025-12-16T10-30-00-abc123.log`               |
| Linux       | `~/.local/share/codehydra/logs/2025-12-16T10-30-00-abc123.log` |
| macOS       | `~/Library/Application Support/Codehydra/logs/...`             |
| Windows     | `%APPDATA%\Codehydra\logs\...`                                 |

### Usage in Services

Services receive a Logger via constructor injection (required parameter):

```typescript
class CodeServerManager {
  constructor(
    config: CodeServerConfig,
    processRunner: ProcessRunner,
    httpClient: HttpClient,
    portManager: PortManager,
    logger: Logger // Required
  ) {
    this.logger = logger;
  }

  async start(): Promise<void> {
    this.logger.info("Starting code-server");
    // ...
    this.logger.info("Started", { port, pid });
  }
}
```

### Usage in Renderer

Renderer components use `createLogger` from `$lib/logging`:

```svelte
<script lang="ts">
  import { createLogger } from "$lib/logging";

  const logger = createLogger("ui");

  function handleDialogOpen() {
    logger.debug("Dialog opened", { type: "create-workspace" });
  }

  function handleSubmit() {
    try {
      // ...
      logger.debug("Dialog submitted", { type: "create-workspace" });
    } catch (error) {
      logger.warn("UI error", { component: "Dialog", error: error.message });
    }
  }
</script>
```

**Svelte 5 Guidance**: Call logger methods in event handlers and lifecycle hooks (`onMount`, `onDestroy`), NOT inside `$effect()` or `$derived()` runes.

## Agent Integration

The agent integration layer provides real-time agent status monitoring for AI agents running in each workspace. Currently supports OpenCode and Claude Code with extensible architecture for future agent types.

### Agent Abstraction Layer

The agent abstraction layer (`src/agents/`) defines interfaces for pluggable agent implementations:

| Interface            | Purpose                                     | Scope                    |
| -------------------- | ------------------------------------------- | ------------------------ |
| `AgentSetupInfo`     | Binary distribution, config file generation | Singleton per type       |
| `AgentServerManager` | Server lifecycle (start, stop, restart)     | Shared across workspaces |
| `AgentProvider`      | Connection and status tracking              | One per workspace        |

**Agent status types:** `"none"` | `"idle"` | `"busy"`

For detailed agent system documentation including provider interface, status tracking, MCP integration, and implementation guide, see [AGENTS.md](AGENTS.md).

### IPC Channels

| Channel                  | Type    | Payload                             | Description                       |
| ------------------------ | ------- | ----------------------------------- | --------------------------------- |
| `agent:status-changed`   | Event   | `{ workspacePath, status, counts }` | Status update for workspace       |
| `agent:get-status`       | Command | `{ workspacePath: string }`         | Get status for specific workspace |
| `agent:get-all-statuses` | Command | `void`                              | Get all workspace statuses        |
| `agent:refresh`          | Command | `void`                              | Trigger immediate scan            |

## Plugin Interface

CodeHydra and VS Code extensions communicate via Socket.IO WebSocket connection. The protocol supports bidirectional communication:

- **Server → Client**: CodeHydra sends VS Code commands to extensions
- **Client → Server**: Extensions call CodeHydra API methods

### Architecture Overview

```
┌───────────────────────────────────────────────────────────────────────────┐
│                      CodeHydra (Electron Main)                            │
│                                                                           │
│  ┌─────────────────────────────────────────────────────────────────────┐  │
│  │  PluginServer                                                       │  │
│  │                                                                     │  │
│  │  connections: Map<workspacePath, Socket>                            │  │
│  │                                                                     │  │
│  │  Server → Client:                                                   │  │
│  │  ───► "command" (execute VS Code commands)                          │  │
│  │                                                                     │  │
│  │  Client → Server:                                                   │  │
│  │  ◄─── "api:workspace:getStatus" → PluginResult<WorkspaceStatus>     │  │
│  │  ◄─── "api:workspace:getMetadata" → PluginResult<Record<...>>       │  │
│  │  ◄─── "api:workspace:setMetadata" → PluginResult<void>              │  │
│  │  ◄─── "api:log" → (fire-and-forget, no response)                    │  │
│  │                                                                     │  │
│  │  API handlers registered via onApiCall() callback pattern           │  │
│  │  (PluginServer remains agnostic to API layer)                       │  │
│  └─────────────────────────────────────────────────────────────────────┘  │
│                                           ▲                               │
│                                           │ wirePluginApi() registers     │
│                                           │ handlers during app:start     │
│  ┌────────────────────────────────────────┴────────────────────────────┐  │
│  │  wirePluginApi() - src/main/index.ts                                │  │
│  │                                                                     │  │
│  │  Workspace path resolution:                                         │  │
│  │  1. appState.findProjectForWorkspace(workspacePath)                 │  │
│  │  2. generateProjectId(project.path)                                 │  │
│  │  3. path.basename(workspacePath) as WorkspaceName                   │  │
│  │  4. If not found → return { success: false, error: "..." }          │  │
│  │                                                                     │  │
│  │  Dispatches intents via Dispatcher after resolution                 │  │
│  └─────────────────────────────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────────────────────────────┘
                    │ WebSocket (localhost only)
                    ▼
┌───────────────────────────────────────────────────────────────────────────┐
│                    codehydra extension (code-server)                      │
│                                                                           │
│  ┌─────────────────────────────────────────────────────────────────────┐  │
│  │  extension.js                                                       │  │
│  │                                                                     │  │
│  │  // Socket.IO client                                                │  │
│  │  socket.on("command", handler)           // inbound: execute cmd    │  │
│  │  socket.emit("api:workspace:...", ack)   // outbound: API calls     │  │
│  │                                                                     │  │
│  │  // Connection state management                                     │  │
│  │  let connected = false;                                             │  │
│  │  let pendingReady = [];  // queue for whenReady()                   │  │
│  │                                                                     │  │
│  └─────────────────────────────────────────────────────────────────────┘  │
│                                                                           │
│  ┌─────────────────────────────────────────────────────────────────────┐  │
│  │  exports.codehydra = {                                              │  │
│  │    whenReady(): Promise<void>             // resolves when connected│  │
│  │    log: {                                                           │  │
│  │      silly/debug/info/warn/error(msg, ctx?) → fire-and-forget      │  │
│  │    }                                                                │  │
│  │    workspace: {                                                     │  │
│  │      getStatus(): Promise<WorkspaceStatus>                          │  │
│  │      getMetadata(): Promise<Record<string, string>>                 │  │
│  │      setMetadata(key, value): Promise<void>                         │  │
│  │    }                                                                │  │
│  │  }                                                                  │  │
│  │                                                                     │  │
│  │  Error handling: Returns rejected Promise with clear message        │  │
│  │  (matches PluginResult pattern - no throwing)                       │  │
│  │                                                                     │  │
│  │  Timeout: 10s (matches COMMAND_TIMEOUT_MS)                          │  │
│  └─────────────────────────────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────────────────────────────┘
                    │
                    │ vscode.extensions.getExtension()
                    ▼
┌───────────────────────────────────────────────────────────────────────────┐
│                    Third-party extension                                  │
│                                                                           │
│  const ext = vscode.extensions.getExtension('codehydra.sidekick');        │
│  const api = ext?.exports?.codehydra;                                     │
│  if (!api) throw new Error('codehydra extension not available');          │
│                                                                           │
│  await api.whenReady();  // wait for connection                           │
│  const status = await api.workspace.getStatus();                          │
│  const metadata = await api.workspace.getMetadata();                      │
│  await api.workspace.setMetadata('note', 'Working on feature X');         │
└───────────────────────────────────────────────────────────────────────────┘
```

### Protocol Messages

**Server → Client (Commands):**

| Event     | Payload          | Response                | Description             |
| --------- | ---------------- | ----------------------- | ----------------------- |
| `command` | `CommandRequest` | `PluginResult<unknown>` | Execute VS Code command |

**Client → Server (API Calls):**

| Event                          | Payload                  | Response                              | Description                       |
| ------------------------------ | ------------------------ | ------------------------------------- | --------------------------------- |
| `api:workspace:getStatus`      | (none)                   | `PluginResult<WorkspaceStatus>`       | Get workspace dirty/agent status  |
| `api:workspace:getMetadata`    | (none)                   | `PluginResult<Record<string,string>>` | Get all workspace metadata        |
| `api:workspace:setMetadata`    | `SetMetadataRequest`     | `PluginResult<void>`                  | Set or delete metadata key        |
| `api:workspace:executeCommand` | `ExecuteCommandRequest`  | `PluginResult<unknown>`               | Execute a VS Code command         |
| `api:workspace:create`         | `WorkspaceCreateRequest` | `PluginResult<Workspace>`             | Create a new workspace in project |

**Types:**

```typescript
interface CommandRequest {
  readonly command: string;
  readonly args?: readonly unknown[];
}

interface SetMetadataRequest {
  readonly key: string; // Must match /^[A-Za-z][A-Za-z0-9-]*$/
  readonly value: string | null; // null deletes the key
}

type PluginResult<T> = { success: true; data: T } | { success: false; error: string };
```

### Connection Lifecycle

1. **PluginServer starts** on dynamic port in main process
2. **code-server spawns** with `_CH_PLUGIN_PORT` env var
3. **Extension activates** and reads env var
4. **Extension connects** with `auth: { workspacePath }` (normalized path)
5. **Server validates** auth and stores connection by normalized path
6. **Bidirectional communication** begins with acknowledgment callbacks

### API Wiring

The Plugin API connects PluginServer to the intent dispatcher via MCP handlers that dispatch intents directly:

```typescript
// MCP handlers dispatch intents directly through the Dispatcher
function createMcpHandlers(dispatcher: Dispatcher, pluginServer: PluginServer) {
  return {
    getStatus: async (workspacePath) => {
      // 1. Resolve workspace path
      // 2. Dispatch workspace:getStatus intent
      // 3. Return result
    },
    getMetadata: async (workspacePath) => {
      /* similar — dispatches workspace:getMetadata intent */
    },
    setMetadata: async (workspacePath, key, value) => {
      /* similar — dispatches workspace:setMetadata intent */
    },
  };
}
```

### Error Handling

- **Unknown workspace**: Returns `{ success: false, error: "Workspace not found" }`
- **Invalid metadata key**: Returns `{ success: false, error: "Invalid key format" }`
- **API exceptions**: Caught and mapped to `{ success: false, error: message }`
- **Timeout**: 10 seconds per request (client-side)

### Type Declarations for Third-Party Extensions

TypeScript declarations for the API are in:
`extensions/sidekick/api.d.ts`

Third-party extension developers should copy this file into their project for type safety.

## External URL Handling

All URLs opened from code-server → external system browser:

- Implemented via `setWindowOpenHandler` returning `{ action: 'deny' }`
- Platform-specific: `xdg-open` (Linux), `open` (macOS), `start` (Windows)

## Binary Distribution

CodeHydra downloads code-server and opencode binaries from GitHub releases instead of bundling them or relying on devDependencies.

### Download Flow

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                         Binary Download Flow                                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  pnpm install                         App Setup (Production)                │
│       │                                      │                              │
│       v                                      v                              │
│  ┌─────────────────┐                 ┌─────────────────┐                    │
│  │ postinstall     │                 │ VscodeSetup     │                    │
│  │ script (tsx)    │                 │ Service         │                    │
│  └────────┬────────┘                 └────────┬────────┘                    │
│           │                                   │                             │
│           └───────────────┬───────────────────┘                             │
│                           │                                                 │
│                           v                                                 │
│              ┌────────────────────────┐                                     │
│              │ BinaryDownloadService  │  (shared download logic)            │
│              │  - isInstalled()       │                                     │
│              │  - download()          │                                     │
│              │  - getBinaryPath()     │                                     │
│              │  - createWrapperScripts()                                    │
│              └───────────┬────────────┘                                     │
│                          │                                                  │
│           ┌──────────────┼──────────────┐                                   │
│           │              │              │                                   │
│           v              v              v                                   │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐                            │
│  │ HttpClient  │ │ FileSystem  │ │ Archive     │                            │
│  │ (fetch)     │ │ Layer       │ │ Extractor   │                            │
│  └─────────────┘ └─────────────┘ └─────────────┘                            │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
```

### Version Management

Binary versions are defined in `src/services/binary-download/versions.ts`:

- `CODE_SERVER_VERSION` - e.g., "4.106.3"
- `OPENCODE_VERSION` - e.g., "0.1.47"

**Development**: `pnpm install` runs the postinstall script which downloads binaries to `./app-data/`.

**Production**: The VscodeSetupService downloads binaries to the user's app-data directory during first-run setup. If `CURRENT_SETUP_VERSION` is incremented (which happens when versions change), existing installations re-run setup on next launch.

### Platform-Specific Assets

| Platform      | code-server Source           | opencode Source       |
| ------------- | ---------------------------- | --------------------- |
| macOS (x64)   | coder/code-server (tar.gz)   | sst/opencode (tar.gz) |
| macOS (arm64) | coder/code-server (tar.gz)   | sst/opencode (tar.gz) |
| Linux (x64)   | coder/code-server (tar.gz)   | sst/opencode (tar.gz) |
| Linux (arm64) | coder/code-server (tar.gz)   | sst/opencode (tar.gz) |
| Windows (x64) | stefanhoelzl/codehydra (zip) | sst/opencode (tar.gz) |

**Windows Note**: code-server doesn't publish Windows binaries, so CodeHydra builds and publishes them via GitHub Actions (see `.github/workflows/build-code-server-windows.yaml`).

## VS Code Setup

### Startup Flow with Preflight

The application uses a preflight phase to detect what needs installation, enabling selective setup:

```
app.whenReady()
       │
       ▼
  regenerateWrapperScripts()   # Always regenerate (cheap, ~1ms)
       │
       ▼
  preflight()                  # Check what's installed
       │
       ├─► missingBinaries?    # code-server/<version>/ exists?
       ├─► missingExtensions?  # Extension directories exist?
       ├─► outdatedExtensions? # Bundled extension version matches?
       └─► markerValid?        # .setup-completed with schemaVersion 1?
       │
       ▼
  needsSetup?  ──NO──►  Normal startup (services start immediately)
       │ YES
       ▼
  Show SetupScreen     # Blocking UI with progress bar
       │
       ▼
  validateAssets()     # Check manifest.json exists
       │
       ▼
  Run SELECTIVE setup:
  1. downloadBinaries()      # Only download missing binaries
  2. cleanExtensions()       # Remove outdated extensions only
  3. installExtensions()     # Only install missing/outdated extensions
  4. setupBinDirectory()     # Create CLI wrapper scripts in bin/
  5. writeCompletionMarker() # .setup-completed (schemaVersion: 1)
       │
       ▼
  On success: Show "Setup complete!" (1.5s) → Continue to normal startup
  On failure: Show error with Retry/Quit buttons
```

**Key behaviors:**

- **Wrapper scripts regenerated on EVERY startup**: Cheap operation ensures scripts always match current binary versions
- **Preflight is read-only**: Only checks filesystem, no network calls
- **Selective setup**: Only installs what's missing/outdated based on preflight results
- **No full clean**: `cleanVscodeDir()` is NOT called; only specific outdated extensions are removed

### Asset Files

VS Code extension sources are stored in the `extensions/` directory at the project root:

```
extensions/
├── external.json              # External extension IDs and versions (downloaded at build time)
├── README.md                  # Documentation for adding extensions
└── sidekick/                  # Custom extension source
    ├── package.json
    ├── extension.js
    └── api.d.ts
```

### Build Process

1. `pnpm build:extensions` - auto-discovers extension folders, packages them to `dist/extensions/`, downloads external extensions from VS Code Marketplace, and generates `manifest.json` (flat array of all extensions)
2. `vite-plugin-static-copy` - copies `dist/extensions/*` to `out/main/assets/` during build
3. `pnpm build` - runs both steps sequentially

**Note:** External extensions are downloaded during the build process, not at runtime. This ensures reproducible builds and eliminates runtime network dependencies for extension installation.

### Distribution Build

The `dist/` directory contains both intermediate build artifacts and final distributables:

```
dist/
├── extensions/                # Extension builds (intermediate, copied to out/main/assets/)
│   ├── codehydra-sidekick-*.vsix
│   └── extensions.json
├── win-unpacked/              # Unpacked Windows app (for debugging)
├── linux-unpacked/            # Unpacked Linux app (for debugging)
├── CodeHydra-0.1.0.exe        # Windows portable executable
└── CodeHydra-0.1.0.AppImage   # Linux AppImage
```

**Distribution commands:**

| Command           | Platform   | Output                          |
| ----------------- | ---------- | ------------------------------- |
| `pnpm dist`       | Current OS | Platform-specific distributable |
| `pnpm dist:win`   | Windows    | `dist/CodeHydra-x.x.x.exe`      |
| `pnpm dist:linux` | Linux      | `dist/CodeHydra-x.x.x.AppImage` |

**Note:** Cross-platform builds have limitations - Windows portable can only be built on Windows, Linux AppImage can only be built on Linux.

### Runtime Asset Resolution

```
out/main/assets/ (ASAR in prod)
    │
    └─► *.vsix ──► <app-data>/vscode/ ──► code-server --install-extension
```

- `PathProvider.vscodeAssetsDir` resolves to `<appPath>/out/main/assets/`
- Node.js `fs` module reads transparently from ASAR in production
- Files are copied to app-data before use (external processes can't read ASAR)

### Directory Structure

```
<app-data>/
├── .setup-completed               # JSON: { schemaVersion: 1, completedAt: "ISO" }
├── bin/                           # CLI wrapper scripts (regenerated every startup)
│   ├── code (code.cmd)            # VS Code CLI wrapper
│   └── opencode (opencode.cmd)    # OpenCode wrapper (redirects to versioned binary)
├── code-server/
│   └── <version>/                 # e.g., 4.106.3/
│       ├── bin/code-server[.cmd]  # Actual code-server binary
│       ├── lib/                   # VS Code distribution
│       │   ├── node[.exe]         # Bundled Node.js (Windows only)
│       │   └── vscode/
│       └── out/node/entry.js      # Entry point
├── opencode/
│   └── <version>/                 # e.g., 0.1.47/
│       └── opencode[.exe]         # Actual opencode binary
├── vscode/
│   ├── sidekick-0.0.3.vsix     # Copied from assets for installation
│   ├── extensions/
│   │   ├── codehydra.sidekick-0.0.1/  # Installed by code-server
│   │   └── sst-dev.opencode-X.X.X/    # Installed by code-server
│   └── user-data/
│       └── User/
│           ├── settings.json      # Copied from assets
│           └── keybindings.json   # Copied from assets
├── runtime/                       # code-server runtime files
└── projects/                      # Git worktrees
```

### Setup Versioning

The setup system uses a **preflight-based approach** instead of a single version number. On every startup:

1. **Preflight checks** detect what's missing/outdated:
   - Binary versions (code-server, opencode directories exist?)
   - Extension versions (each extension in manifest.json array must match installed version)
   - Marker validity (has `schemaVersion: 1`?)

2. **Selective setup** runs only for components that need installation

The `.setup-completed` marker uses `schemaVersion` (not `version`) to track marker format changes:

```json
{
  "schemaVersion": 1,
  "completedAt": "2025-12-23T10:30:00.000Z"
}
```

**When `schemaVersion` changes**: Only for marker format/preflight architecture changes.
**When binary/extension versions change**: Preflight detects missing components automatically; no marker change needed.

### Codehydra Extension

The custom codehydra extension (packaged as `.vsix` at build time) runs on VS Code startup to:

1. Close sidebars to maximize editor space
2. Open OpenCode terminal automatically
3. Clean up empty editor groups

This provides an optimized layout for AI agent workflows.

### CLI Wrapper Scripts

During VS Code setup, CLI wrapper scripts are generated in `<app-data>/bin/`. These scripts enable command-line tools to work in the integrated terminal.

**Generated Scripts:**

| Script                      | Purpose                                                     |
| --------------------------- | ----------------------------------------------------------- |
| `code` / `code.cmd`         | VS Code CLI (code-server's remote-cli)                      |
| `opencode` / `opencode.cmd` | Redirects to `<app-data>/opencode/<version>/opencode[.exe]` |

**Note**: code-server is launched directly via `PathProvider.codeServerBinaryPath` (absolute path), not via a wrapper script.

**Environment Configuration (in CodeServerManager):**

When spawning code-server, the manager modifies the environment:

1. **PATH prepend**: `<app-data>/bin/` is prepended to PATH
2. **EDITOR**: Set to `<binDir>/code --wait --reuse-window`
3. **GIT_SEQUENCE_EDITOR**: Set to same value as EDITOR

**Script Generation (in VscodeSetupService):**

```
setupBinDirectory()
    │
    ├── mkdir bin/
    ├── resolveTargetPaths() → { codeRemoteCli, codeServerBinary, opencodeBinary }
    ├── generateScripts(platformInfo, targetPaths) → GeneratedScript[]
    └── for each script:
        ├── writeFile(binDir + filename, content)
        └── if needsExecutable: makeExecutable(path) [Unix only]
```

**Git Integration:**

With EDITOR configured, git operations open in code-server:

- `git commit` - Opens commit message editor
- `git rebase -i` - Opens interactive rebase editor
- Any tool respecting `$EDITOR`

## Keyboard Capture System

CodeHydra uses a **unified main-process keyboard capture system** where all shortcut detection happens in the main process.

### Architecture Overview

```
┌─────────────────────────────────────────────────────────────────────────┐
│ Main Process: ShortcutController                                        │
│  ├─ Registers before-input-event on ALL WebViews (workspace + UI)       │
│  ├─ Queries viewManager.getMode() for current state                     │
│  ├─ Alt+X when mode=workspace → setMode("shortcut")                     │
│  ├─ Action keys when mode=shortcut → emit shortcut:key event            │
│  └─ Alt release when mode=shortcut → setMode("workspace")               │
├─────────────────────────────────────────────────────────────────────────┤
│ Renderer: Receives events, executes actions                              │
│  ├─ ui:mode-changed → update shortcut overlay visibility                │
│  ├─ shortcut:key → execute workspace action (navigate, jump, dialog)    │
│  └─ Escape key → call api.ui.setMode("workspace")                       │
└─────────────────────────────────────────────────────────────────────────┘
```

### Key Detection Flow

| User Action            | Main Process                 | Renderer                  |
| ---------------------- | ---------------------------- | ------------------------- |
| Alt+X pressed          | setMode("shortcut")          | Show overlay              |
| Action key (↑↓0-9 etc) | Emit api:shortcut:key        | Execute action            |
| Escape pressed         | (passes through to renderer) | Call setMode("workspace") |
| Alt released           | setMode("workspace")         | Hide overlay              |

### ShortcutController State Machine

```
                              ┌──────────┐
              ┌───────────────│  NORMAL  │◄────────────────────────────────┐
              │               └────┬─────┘                                 │
              │                    │                                       │
              │ Alt up             │ Alt down                              │
              │ (suppress)         │ (preventDefault)                      │
              │                    ▼                                       │
              │            ┌─────────────┐                                 │
              │            │ ALT_WAITING │                                 │
              │            └──────┬──────┘                                 │
              │                   │                                        │
              │     ┌─────────────┼─────────────┐                          │
              │     │             │             │                          │
              │  Alt up      non-X key       X down                        │
              │  (suppress)  (let through)      │                          │
              │     │             │             ▼                          │
              │     │             │      • preventDefault                  │
              │     │             │      • setMode("shortcut")             │
              │     │             │      • focusUI()                       │
              │     │             │             │                          │
              └─────┴─────────────┴─────────────┘                          │
                                                                           │
              Main process returns to NORMAL, UI has focus ────────────────┘
```

**While in shortcut mode**, action keys (↑↓Enter Delete O 0-9) are captured and emitted as `api:shortcut:key` events. Unknown keys pass through to the focused view.

### Key Files

| File                                          | Purpose                                  |
| --------------------------------------------- | ---------------------------------------- |
| `src/main/shortcut-controller.ts`             | Main-process key detection and mode mgmt |
| `src/renderer/lib/stores/shortcuts.svelte.ts` | UI state (mode tracking)                 |
| `src/renderer/App.svelte`                     | Event subscriptions, action handlers     |

### Design Decisions

1. **Main process owns all detection**: Eliminates race conditions where renderer sees keys before focus switches
2. **ViewManager is single source of truth for mode**: ShortcutController queries `getMode()` instead of tracking its own state
3. **Escape handled in renderer**: Simplest key that doesn't require focus changes, just calls `setMode("workspace")`
4. **Mode transitions are idempotent**: Prevents spurious events during workspace switches

## Data Flow

### Opening a Project

```
User: Click "Open Project"
  → System folder picker
  → IPC: api:project:open → project:open intent dispatched
  → OpenProjectOperation runs "open" hook point:
      → LocalProjectModule: validate git repository, discover worktrees
      → (or RemoteProjectModule: clone from URL)
  → Operation dispatches workspace:open per discovered worktree
  → Sets first workspace as active
  → Emits project:opened domain event → UiIpcModule → sendToUI → Renderer
  → If 0 worktrees: auto-open create dialog
  → If 1+ worktrees: activate first workspace
```

### Switching Workspaces

```
User: Click workspace (or keyboard shortcut)
  → IPC: api:workspace:switch → workspace:switch intent dispatched
  → SwitchWorkspaceOperation runs "activate" hook:
      → ViewModule: attach target view, set bounds, detach previous view
  → Emits workspace:switched domain event → UiIpcModule → sendToUI → Renderer
```

### Creating a Workspace

```
User: Click [+], fill dialog, click OK
  → Validate name (frontend)
      → If invalid: show error, stay in dialog
      → If valid: continue
  → IPC: api:workspace:create → workspace:open intent dispatched
  → OpenWorkspaceOperation runs hook points:
      → "create": GitWorktreeWorkspaceModule creates git worktree
      → "setup": KeepFilesModule copies .keepfiles, AgentModule starts agent server
      → "finalize": CodeServerModule creates .code-workspace file
  → Operation dispatches workspace:switch to activate the new workspace
  → Emits workspace:created domain event → UiIpcModule → sendToUI → Renderer
```

### Closing a Project

```
User: Click [x] on project row
  → IPC: api:project:close → project:close intent dispatched
  → CloseProjectOperation:
      → Dispatches workspace:delete { removeWorktree: false } per workspace (runtime teardown)
      → Runs "close" hook point:
          → LocalProjectModule: dispose provider
          → (or RemoteProjectModule: delete cloned dir if removeLocalRepo)
      → Emits project:closed domain event → UiIpcModule → sendToUI → Renderer
  (NO files or git data deleted unless removeLocalRepo is true for remote projects)
```

## IPC Contract

All IPC channels are defined in `src/shared/ipc.ts` with TypeScript types for compile-time safety.

**Architecture Note**: IPC handlers in `UiIpcModule` create typed intents and dispatch them through the Dispatcher. They only perform input validation and intent construction -- all business logic lives in operations and hook modules. See [Intent-Based Architecture](#intent-based-architecture) for details.

### Commands (renderer → main)

| Channel                           | Payload                             | Response            | Description                                    |
| --------------------------------- | ----------------------------------- | ------------------- | ---------------------------------------------- |
| `api:project:open`                | `{ path: string }`                  | `Project`           | Open project, discover workspaces              |
| `api:project:close`               | `{ projectId, removeLocalRepo? }`   | `void`              | Close project, optionally delete cloned repo   |
| `api:project:clone`               | `{ url: string }`                   | `Project`           | Clone git repo from URL, create/return project |
| `project:list`                    | `void`                              | `Project[]`         | List all open projects                         |
| `project:select-folder`           | `void`                              | `string \| null`    | Show folder picker dialog                      |
| `workspace:create`                | `{ projectPath, name, baseBranch }` | `Workspace`         | Create workspace, create view                  |
| `workspace:remove`                | `{ workspacePath, deleteBranch }`   | `RemovalResult`     | Remove workspace, destroy view                 |
| `workspace:switch`                | `{ workspacePath }`                 | `void`              | Switch active workspace                        |
| `workspace:list-bases`            | `{ projectPath }`                   | `BaseInfo[]`        | List available branches                        |
| `workspace:update-bases`          | `{ projectPath }`                   | `UpdateBasesResult` | Fetch from remotes                             |
| `workspace:is-dirty`              | `{ workspacePath }`                 | `boolean`           | Check for uncommitted changes                  |
| `api:workspace:get-opencode-port` | `{ projectId, workspaceName }`      | `number \| null`    | Get OpenCode server port                       |
| `ui:set-dialog-mode`              | `{ isOpen: boolean }`               | `void`              | Swap UI layer z-order                          |
| `ui:focus-active-workspace`       | `void`                              | `void`              | Return focus to VS Code                        |

### Events (main → renderer)

| Channel              | Payload                                          | Description                               |
| -------------------- | ------------------------------------------------ | ----------------------------------------- |
| `project:opened`     | `{ project: Project }`                           | Project was opened                        |
| `project:closed`     | `{ path: string }`                               | Project was closed                        |
| `workspace:created`  | `{ projectPath: string, workspace: Workspace }`  | Workspace was created                     |
| `workspace:removed`  | `{ projectPath: string, workspacePath: string }` | Workspace was removed                     |
| `workspace:switched` | `{ workspacePath: string }`                      | Active workspace changed                  |
| `shortcut:enable`    | `void`                                           | Shortcut mode activated                   |
| `shortcut:disable`   | `void`                                           | Shortcut mode deactivated (race recovery) |

**Note**: `shortcut:enable` and `shortcut:disable` are defined as channel constants but are not typed in the `IpcEvents` interface (they use simple void payloads).

### IPC Data Flow

```
┌─────────────┐  IPC invoke   ┌────────────────┐  dispatch   ┌────────────┐  hooks   ┌──────────┐
│  Renderer   │ ────────────► │ UiIpcModule │ ─────────► │ Dispatcher │ ───────► │ Modules  │
│  (Svelte)   │               │ (intent+IPC)   │            │ +Operation │          │(services)│
│             │ ◄──────────── │                │ ◄────────── │            │ ◄─────── │          │
└─────────────┘  IPC events/   └────────────────┘  domain     └────────────┘  results └──────────┘
                 response       (sendToUI)         events
```
