# CodeHydra API Reference

CodeHydra exposes APIs at two levels:

| Level       | Scope          | Consumers                            |
| ----------- | -------------- | ------------------------------------ |
| **Private** | Full API       | CodeHydra renderer UI only           |
| **Public**  | Workspace-only | VS Code extensions, external systems |

## Quick Links

- [Public API](#public-api) - Workspace-scoped API for external consumers
  - [VS Code Extension Access](#vs-code-extension-access)
  - [WebSocket Access](#websocket-access)
- [VS Code Object Serialization](#vs-code-object-serialization) - Format for passing VS Code objects through JSON
- [Private API](#private-api) - Full API for CodeHydra internals
- [Type Definitions](#type-definitions) - Shared types

For architectural details, see [docs/ARCHITECTURE.md](ARCHITECTURE.md).

---

## Public API

The public API provides a **workspace-scoped subset** of CodeHydra's functionality, designed for external consumers. Each connection operates on a single workspace.

### Access Methods

| Method                                         | Use Case                       | Connection                      |
| ---------------------------------------------- | ------------------------------ | ------------------------------- |
| [VS Code Extension](#vs-code-extension-access) | Third-party VS Code extensions | Via codehydra extension exports |
| [WebSocket](#websocket-access)                 | Other external systems         | Direct Socket.IO connection     |

Both methods provide the same API contract - only the transport differs.

### API Reference

#### Connection

| Method      | Signature             | Description                                                            |
| ----------- | --------------------- | ---------------------------------------------------------------------- |
| `whenReady` | `() => Promise<void>` | Wait for connection to CodeHydra. Call before using workspace methods. |

#### `workspace` Namespace

All methods operate on the **connected workspace**.

| Method                  | Signature                                                                              | Description                                                     |
| ----------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `getStatus`             | `() => Promise<WorkspaceStatus>`                                                       | Get workspace status (dirty flag, agent status)                 |
| `getOpenCodeSession`    | `() => Promise<OpenCodeSession \| null>`                                               | Get OpenCode session info (port + sessionId, null if not ready) |
| `restartOpencodeServer` | `() => Promise<number>`                                                                | Restart OpenCode server, preserving port, returns port          |
| `getMetadata`           | `() => Promise<Record<string, string>>`                                                | Get all metadata (always includes `base` key)                   |
| `setMetadata`           | `(key: string, value: string \| null) => Promise<void>`                                | Set or delete a metadata key                                    |
| `executeCommand`        | `(command: string, args?: unknown[]) => Promise<unknown>`                              | Execute a VS Code command (10-second timeout)                   |
| `delete`                | `(options?: { keepBranch?: boolean }) => Promise<{ started: true }>`                   | Delete the workspace (terminates OpenCode, async)               |
| `create`                | `(name: string, base: string, options?: WorkspaceCreateOptions) => Promise<Workspace>` | Create a new workspace in the same project                      |

#### `log` Namespace

Structured logging to CodeHydra's centralized logging system. All methods are fire-and-forget.

| Method  | Signature                                         | Description                                      |
| ------- | ------------------------------------------------- | ------------------------------------------------ |
| `silly` | `(message: string, context?: LogContext) => void` | Most verbose - per-iteration details             |
| `debug` | `(message: string, context?: LogContext) => void` | Development tracing information                  |
| `info`  | `(message: string, context?: LogContext) => void` | Significant operations (start/stop, completions) |
| `warn`  | `(message: string, context?: LogContext) => void` | Recoverable issues or deprecated behavior        |
| `error` | `(message: string, context?: LogContext) => void` | Failures that require attention                  |

**LogContext Type:**

```typescript
type LogContext = Record<string, string | number | boolean | null>;
```

**Note:** Logs appear in CodeHydra's log files with the `[extension]` scope. The workspace path is automatically appended to the context.

### Usage Examples

#### Check if Workspace Has Uncommitted Changes

```typescript
const status = await api.workspace.getStatus();
if (status.isDirty) {
  console.log("You have uncommitted changes");
}
```

#### Get Agent Status

```typescript
const status = await api.workspace.getStatus();
switch (status.agent.type) {
  case "none":
    console.log("No AI agents active");
    break;
  case "idle":
    console.log(`${status.agent.counts.total} agent(s), all idle`);
    break;
  case "busy":
    console.log(`${status.agent.counts.busy} agent(s) working`);
    break;
  case "mixed":
    console.log(`${status.agent.counts.busy} busy, ${status.agent.counts.idle} idle`);
    break;
}
```

#### Connect to OpenCode Server

```typescript
const session = await api.workspace.getOpenCodeSession();
if (session !== null) {
  // Connect to OpenCode API at http://localhost:${session.port}
  // Primary session ID is available as session.sessionId
  const response = await fetch(`http://localhost:${session.port}/api/sessions`);
  const sessions = await response.json();
}
```

#### Restart OpenCode Server

```typescript
// Restart the OpenCode server to reload configuration changes
try {
  const port = await api.workspace.restartOpencodeServer();
  console.log(`OpenCode server restarted on port ${port}`);
} catch (error) {
  console.error("Failed to restart:", error);
}
```

#### Store Custom Workspace Metadata

```typescript
// Set metadata
await api.workspace.setMetadata("note", "Working on feature X");
await api.workspace.setMetadata("model-name", "claude-3-5-sonnet");

// Read metadata
const metadata = await api.workspace.getMetadata();
console.log("Base branch:", metadata.base); // Always present
console.log("Note:", metadata.note);

// Delete metadata
await api.workspace.setMetadata("note", null);
```

#### Delete Current Workspace

```typescript
// Delete workspace (removes worktree and branch)
const result = await api.workspace.delete();
console.log("Deletion started:", result.started);

// Delete workspace but keep the git branch
const result = await api.workspace.delete({ keepBranch: true });
```

**Note:** Deletion is async - the Promise resolves immediately with `{ started: true }`. The actual cleanup happens in the background.

#### Create a New Workspace

```typescript
// Create a new workspace in the same project
const workspace = await api.workspace.create("feature-auth", "main");
console.log("Created workspace:", workspace.name, "at", workspace.path);

// Create workspace with an initial prompt for the AI agent
const workspace = await api.workspace.create("fix-bug-123", "main", {
  initialPrompt: "Fix the login validation bug described in issue #123",
});

// Create workspace with initial prompt and specific agent
const workspace = await api.workspace.create("refactor-api", "main", {
  initialPrompt: { prompt: "Refactor the API module for better testability", agent: "coder" },
  stealFocus: false, // Don't switch to the new workspace
});
```

**Notes:**

- The new workspace is created in the same project as the current workspace
- `initialPrompt` can be a string (uses default agent) or `{ prompt, agent }` object
- If `stealFocus` is `false`, the UI stays on the current workspace (unless no workspace is active); if `true` or omitted, it switches to the new one
- The initial prompt is sent asynchronously after the workspace is ready (fire-and-forget)

#### Execute VS Code Commands

```typescript
// Save all files
await api.workspace.executeCommand("workbench.action.files.saveAll");

// Open settings
await api.workspace.executeCommand("workbench.action.openSettings");

// Command with return value (some commands return data)
const text = await api.workspace.executeCommand("editor.action.getSelectedText");

// Command with arguments
await api.workspace.executeCommand("vscode.openFolder", ["/path/to/folder"]);
```

**Note:** Most VS Code commands return `undefined`. The return type is `unknown` because command return types are not statically typed. Commands have a 10-second timeout.

### Metadata Key Format

Metadata keys must follow this format:

- Start with a letter (a-z, A-Z)
- Contain only letters, digits, and hyphens
- Not end with a hyphen
- Maximum 64 characters

**Valid keys:** `base`, `note`, `model-name`, `AI-model`  
**Invalid keys:** `_private`, `my_key`, `123note`, `note-`

### Error Handling

All API methods return rejected Promises on failure. The rejection reason is a string error message:

```typescript
try {
  await api.workspace.setMetadata("key", "value");
} catch (error) {
  // error is a string describing what went wrong
  console.error("Failed to set metadata:", error);
}
```

### Timeout

All API calls have a **10-second timeout**. If CodeHydra doesn't respond within this time, the Promise is rejected.

---

## VS Code Extension Access

Third-party VS Code extensions running inside code-server can access the public API through the codehydra extension's exports.

### Getting the API

```typescript
import * as vscode from "vscode";

async function getCodehydraApi() {
  const ext = vscode.extensions.getExtension("codehydra.codehydra");
  const api = ext?.exports?.codehydra;

  if (!api) {
    throw new Error("CodeHydra extension not available");
  }

  // Wait for connection to CodeHydra
  await api.whenReady();

  return api;
}
```

### Complete Example

```typescript
import * as vscode from "vscode";

export async function activate(context: vscode.ExtensionContext) {
  // Get the CodeHydra API
  const ext = vscode.extensions.getExtension("codehydra.codehydra");
  const api = ext?.exports?.codehydra;

  if (!api) {
    vscode.window.showWarningMessage("CodeHydra extension not available");
    return;
  }

  await api.whenReady();

  // Register a command that uses the API
  const disposable = vscode.commands.registerCommand("myext.showStatus", async () => {
    try {
      const status = await api.workspace.getStatus();
      const dirty = status.isDirty ? "dirty" : "clean";
      const agents =
        status.agent.type === "none" ? "no agents" : `${status.agent.counts.total} agent(s)`;

      vscode.window.showInformationMessage(`Workspace: ${dirty}, ${agents}`);
    } catch (error) {
      vscode.window.showErrorMessage(`API error: ${error}`);
    }
  });

  context.subscriptions.push(disposable);
}
```

### Type Declarations

For TypeScript support, copy the type declarations from:  
`extensions/codehydra-sidekick/api.d.ts`

Or use these inline definitions:

```typescript
interface AgentStatusCounts {
  readonly idle: number;
  readonly busy: number;
  readonly total: number;
}

type AgentStatus =
  | { readonly type: "none" }
  | { readonly type: "idle"; readonly counts: AgentStatusCounts }
  | { readonly type: "busy"; readonly counts: AgentStatusCounts }
  | { readonly type: "mixed"; readonly counts: AgentStatusCounts };

interface WorkspaceStatus {
  readonly isDirty: boolean;
  readonly agent: AgentStatus;
}

type InitialPrompt = string | { prompt: string; agent?: string };

interface WorkspaceCreateOptions {
  initialPrompt?: InitialPrompt;
  stealFocus?: boolean;
}

interface Workspace {
  name: string;
  path: string;
  base: string;
}

interface OpenCodeSession {
  readonly port: number;
  readonly sessionId: string;
}

interface WorkspaceApi {
  getStatus(): Promise<WorkspaceStatus>;
  getOpenCodeSession(): Promise<OpenCodeSession | null>;
  restartOpencodeServer(): Promise<number>;
  getMetadata(): Promise<Readonly<Record<string, string>>>;
  setMetadata(key: string, value: string | null): Promise<void>;
  executeCommand(command: string, args?: readonly unknown[]): Promise<unknown>;
  delete(options?: { keepBranch?: boolean }): Promise<{ started: boolean }>;
  create(name: string, base: string, options?: WorkspaceCreateOptions): Promise<Workspace>;
}

interface CodehydraApi {
  whenReady(): Promise<void>;
  readonly workspace: WorkspaceApi;
}
```

---

## WebSocket Access

External systems can connect directly to CodeHydra's plugin server via Socket.IO WebSocket.

### Architecture

```
┌───────────────────────────────────────────────────────────────┐
│               CodeHydra (Electron Main Process)               │
│                                                               │
│   ┌─────────────────────────────────────────────────────┐     │
│   │              PluginServer (Socket.IO)               │     │
│   │                   :dynamic port                     │     │
│   │                                                     │     │
│   │   Handles: api:workspace:* events                   │     │
│   └─────────────────────────────────────────────────────┘     │
│                            ▲                                   │
└────────────────────────────┼───────────────────────────────────┘
                             │ localhost:port (WebSocket)
                             │
              ┌──────────────┴──────────────┐
              │                             │
    ┌─────────▼─────────┐       ┌───────────▼───────────┐
    │ codehydra ext     │       │ Your external system  │
    │ (built-in client) │       │ (custom client)       │
    └───────────────────┘       └───────────────────────┘
```

### Connection

1. Read port from `_CH_PLUGIN_PORT` environment variable
2. Connect via Socket.IO to `http://localhost:${port}`
3. Authenticate with workspace path

```typescript
import { io, Socket } from "socket.io-client";

const port = process.env._CH_PLUGIN_PORT;
if (!port) {
  throw new Error("Not running inside CodeHydra workspace");
}

const socket = io(`http://localhost:${port}`, {
  auth: {
    workspacePath: "/absolute/path/to/workspace",
  },
});

socket.on("connect", () => {
  console.log("Connected to CodeHydra");
});

socket.on("connect_error", (error) => {
  console.error("Connection failed:", error.message);
});
```

### Event Channels (Client → Server)

All events use acknowledgment callbacks for request/response pattern.

| Event                                 | Request Payload          | Response                                |
| ------------------------------------- | ------------------------ | --------------------------------------- |
| `api:workspace:getStatus`             | None                     | `PluginResult<WorkspaceStatus>`         |
| `api:workspace:getOpenCodeSession`    | None                     | `PluginResult<OpenCodeSession \| null>` |
| `api:workspace:restartOpencodeServer` | None                     | `PluginResult<number>`                  |
| `api:workspace:getMetadata`           | None                     | `PluginResult<Record<string, string>>`  |
| `api:workspace:setMetadata`           | `SetMetadataRequest`     | `PluginResult<void>`                    |
| `api:workspace:executeCommand`        | `ExecuteCommandRequest`  | `PluginResult<unknown>`                 |
| `api:workspace:delete`                | `DeleteWorkspaceRequest` | `PluginResult<DeleteWorkspaceResponse>` |
| `api:workspace:create`                | `WorkspaceCreateRequest` | `PluginResult<Workspace>`               |

### Event Channels (Server → Client)

| Event      | Request Payload  | Response                | Description                                     |
| ---------- | ---------------- | ----------------------- | ----------------------------------------------- |
| `config`   | `PluginConfig`   | (none)                  | Configuration sent after connection             |
| `command`  | `CommandRequest` | `PluginResult<unknown>` | Execute VS Code command                         |
| `shutdown` | None             | `PluginResult<void>`    | Terminate extension host for workspace deletion |

### Response Format

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

### Request Types

```typescript
interface SetMetadataRequest {
  key: string; // Must match /^[A-Za-z][A-Za-z0-9-]*$/ and not end with hyphen
  value: string | null; // null to delete
}

interface ExecuteCommandRequest {
  command: string; // VS Code command identifier (e.g., "workbench.action.files.save")
  args?: unknown[]; // Optional arguments to pass to the command
}

interface DeleteWorkspaceRequest {
  keepBranch?: boolean; // If true, keep the git branch after deletion. Default: false
}

interface DeleteWorkspaceResponse {
  started: boolean; // True if deletion was started (deletion is async)
}

type InitialPrompt = string | { prompt: string; agent?: string };

interface WorkspaceCreateRequest {
  name: string; // Name for the new workspace (becomes branch name)
  base: string; // Base branch to create the workspace from
  initialPrompt?: InitialPrompt; // Optional initial prompt to send to AI agent
  stealFocus?: boolean; // If true, switch to the new workspace. Default: false for API calls
}

interface WorkspaceCreateOptions {
  initialPrompt?: InitialPrompt;
  stealFocus?: boolean;
}

interface Workspace {
  name: string; // Workspace name (also the branch name)
  path: string; // Absolute path to the workspace directory
  base: string; // Base branch this workspace was created from
}
```

### Example Client

```typescript
import { io, Socket } from "socket.io-client";

class CodehydraClient {
  private socket: Socket;
  private connected = false;

  constructor(port: number, workspacePath: string) {
    this.socket = io(`http://localhost:${port}`, {
      auth: { workspacePath },
    });

    this.socket.on("connect", () => {
      this.connected = true;
    });

    this.socket.on("disconnect", () => {
      this.connected = false;
    });
  }

  async whenReady(): Promise<void> {
    if (this.connected) return;

    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        reject(new Error("Connection timeout"));
      }, 10000);

      this.socket.once("connect", () => {
        clearTimeout(timeout);
        resolve();
      });

      this.socket.once("connect_error", (err) => {
        clearTimeout(timeout);
        reject(err);
      });
    });
  }

  async getStatus(): Promise<WorkspaceStatus> {
    return this.emit("api:workspace:getStatus");
  }

  async getOpenCodeSession(): Promise<OpenCodeSession | null> {
    return this.emit("api:workspace:getOpenCodeSession");
  }

  async getMetadata(): Promise<Record<string, string>> {
    return this.emit("api:workspace:getMetadata");
  }

  async setMetadata(key: string, value: string | null): Promise<void> {
    return this.emit("api:workspace:setMetadata", { key, value });
  }

  private emit<T>(event: string, payload?: unknown): Promise<T> {
    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        reject(new Error("Request timeout"));
      }, 10000);

      const callback = (result: PluginResult<T>) => {
        clearTimeout(timeout);
        if (result.success) {
          resolve(result.data);
        } else {
          reject(new Error(result.error));
        }
      };

      if (payload !== undefined) {
        this.socket.emit(event, payload, callback);
      } else {
        this.socket.emit(event, callback);
      }
    });
  }

  disconnect(): void {
    this.socket.disconnect();
  }
}

// Usage
const port = process.env._CH_PLUGIN_PORT;
if (!port) {
  throw new Error("Not running inside CodeHydra workspace");
}

const client = new CodehydraClient(parseInt(port), "/path/to/workspace");

await client.whenReady();
const status = await client.getStatus();
console.log("Dirty:", status.isDirty);
```

### Server-to-Client Commands

CodeHydra can also send commands TO connected clients:

```typescript
interface CommandRequest {
  command: string; // VS Code command ID
  args?: unknown[]; // Optional arguments
}

// Handle incoming commands
socket.on("command", (request: CommandRequest, ack: (result: PluginResult<unknown>) => void) => {
  try {
    // Execute the command
    const result = executeCommand(request.command, request.args);
    ack({ success: true, data: result });
  } catch (error) {
    ack({ success: false, error: String(error) });
  }
});
```

This is used by CodeHydra to send startup commands (close sidebars, open terminal) when a workspace connects.

---

## VS Code Object Serialization

VS Code commands often require class instances (Uri, Position, Range, etc.) that cannot be serialized through JSON. The `executeCommand` method supports a `$vscode` wrapper format to pass these objects through MCP or WebSocket interfaces.

### Supported Types

| Type      | `$vscode` Value | Required Fields     | Field Types            | Reconstruction                  |
| --------- | --------------- | ------------------- | ---------------------- | ------------------------------- |
| Uri       | `"Uri"`         | `value`             | `string`               | `Uri.parse(value)`              |
| Position  | `"Position"`    | `line`, `character` | `number`, `number`     | `new Position(line, character)` |
| Range     | `"Range"`       | `start`, `end`      | `Position`, `Position` | `new Range(start, end)`         |
| Selection | `"Selection"`   | `anchor`, `active`  | `Position`, `Position` | `new Selection(anchor, active)` |
| Location  | `"Location"`    | `uri`, `range`      | `Uri`, `Range`         | `new Location(uri, range)`      |

### JSON Format Examples

#### Uri

```json
{ "$vscode": "Uri", "value": "file:///path/to/file.ts" }
```

#### Position

```json
{ "$vscode": "Position", "line": 10, "character": 5 }
```

#### Range

```json
{
  "$vscode": "Range",
  "start": { "$vscode": "Position", "line": 10, "character": 5 },
  "end": { "$vscode": "Position", "line": 10, "character": 20 }
}
```

#### Selection

```json
{
  "$vscode": "Selection",
  "anchor": { "$vscode": "Position", "line": 5, "character": 0 },
  "active": { "$vscode": "Position", "line": 10, "character": 15 }
}
```

#### Location (fully nested)

```json
{
  "$vscode": "Location",
  "uri": { "$vscode": "Uri", "value": "file:///path/to/file.ts" },
  "range": {
    "$vscode": "Range",
    "start": { "$vscode": "Position", "line": 10, "character": 5 },
    "end": { "$vscode": "Position", "line": 10, "character": 20 }
  }
}
```

### Usage Example

```typescript
// Open a file using vscode.open command with a Uri argument
await api.workspace.executeCommand("vscode.open", [
  { $vscode: "Uri", value: "file:///c:/path/to/file.ts" },
]);

// Go to a specific location
await api.workspace.executeCommand("editor.action.goToLocations", [
  { $vscode: "Uri", value: "file:///c:/path/to/file.ts" },
  { $vscode: "Position", line: 10, character: 0 },
  [
    {
      $vscode: "Location",
      uri: { $vscode: "Uri", value: "file:///c:/path/to/other.ts" },
      range: {
        $vscode: "Range",
        start: { $vscode: "Position", line: 5, character: 0 },
        end: { $vscode: "Position", line: 5, character: 10 },
      },
    },
  ],
]);
```

### Nested Object Handling

The reconstruction is recursive, processing:

- Arrays: each element is recursively processed
- Objects: each property value is recursively processed
- `$vscode` markers: validated and reconstructed using VS Code constructors

Plain objects and primitives pass through unchanged. Mixed objects work correctly:

```json
{
  "label": "Go to definition",
  "location": { "$vscode": "Location", "uri": {...}, "range": {...} }
}
```

Result: `{ label: "Go to definition", location: <Location instance> }`

### Error Messages

**Unknown type:**

```
Unknown VS Code object type: "Unknown". Supported types: Uri, Position, Range, Selection, Location
```

**Missing field:**

```
Invalid VS Code Position: missing required field "line"
```

**Invalid field type:**

```
Invalid VS Code Position: field "line" must be a number, got string
```

### Limitations

- **Circular references**: Not supported. Will cause stack overflow.
- **$vscode key collision**: If your data genuinely contains a `$vscode` key, wrap it in another object: `{ "data": { "$vscode": "literal" } }`.

---

## Private API

The private API is used exclusively by CodeHydra's renderer process (Svelte UI) to communicate with the main Electron process via IPC. **This API is not intended for external consumers.**

### Access Pattern

```typescript
// In renderer code, import from $lib/api for mockability
import { projects, workspaces, ui, lifecycle, on } from "$lib/api";

// Open a project
const project = await projects.open("/path/to/repo");

// Create a workspace
const workspace = await workspaces.create(project.id, "feature-x", "main");

// Subscribe to events
const unsubscribe = on("workspace:switched", (event) => {
  console.log(`Switched to ${event.workspaceName}`);
});
```

### API Namespaces

#### `projects` - Project Management

| Method       | Signature                                                                | Description                                           |
| ------------ | ------------------------------------------------------------------------ | ----------------------------------------------------- |
| `open`       | `(path: string) => Promise<Project>`                                     | Open a git repository as a project                    |
| `close`      | `(projectId: ProjectId, options?: ProjectCloseOptions) => Promise<void>` | Close a project and all its workspaces                |
| `clone`      | `(url: string) => Promise<Project>`                                      | Clone a git repository from URL and open as a project |
| `list`       | `() => Promise<readonly Project[]>`                                      | List all open projects                                |
| `get`        | `(projectId: ProjectId) => Promise<Project \| undefined>`                | Get a project by ID                                   |
| `fetchBases` | `(projectId: ProjectId) => Promise<{ bases: BaseInfo[] }>`               | Fetch available base branches                         |

**`ProjectCloseOptions`:**

```typescript
interface ProjectCloseOptions {
  /** If true and project has remoteUrl, delete the entire project directory including cloned repo */
  removeLocalRepo?: boolean;
}
```

**`clone()` behavior:**

- If the URL was already cloned, returns the existing project (no duplicate clones)
- Creates a bare clone in the app data directory
- The project will have a `remoteUrl` field indicating it was cloned from a URL
- URL formats supported: HTTPS (`https://github.com/org/repo.git`), SSH (`git@github.com:org/repo.git`)

#### `workspaces` - Workspace Management

| Method                  | Signature                                                                                                                                                                                                                      | Description                                                                                                                                                                                                                                                                                                                                                               |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `create`                | `(projectId: ProjectId, name: string, base: string) => Promise<Workspace>`                                                                                                                                                     | Create a new workspace from a base branch                                                                                                                                                                                                                                                                                                                                 |
| `remove`                | `(projectId: ProjectId, workspaceName: WorkspaceName, options?: { keepBranch?: boolean; skipSwitch?: boolean; force?: boolean; unblock?: "kill" \| "close" \| "ignore"; isRetry?: boolean }) => Promise<{ started: boolean }>` | Start workspace removal (fire-and-forget). Returns `{ started: false }` if blocked by idempotency. Options: `force: true` to bypass errors and idempotency, `unblock: "kill"` to kill blocking processes, `"close"` to close file handles (Windows only, requires UAC elevation), `"ignore"` to skip detection entirely. Set `isRetry: true` to skip proactive detection. |
| `get`                   | `(projectId: ProjectId, workspaceName: WorkspaceName) => Promise<Workspace \| undefined>`                                                                                                                                      | Get a workspace                                                                                                                                                                                                                                                                                                                                                           |
| `getStatus`             | `(projectId: ProjectId, workspaceName: WorkspaceName) => Promise<WorkspaceStatus>`                                                                                                                                             | Get workspace status                                                                                                                                                                                                                                                                                                                                                      |
| `getOpenCodeSession`    | `(projectId: ProjectId, workspaceName: WorkspaceName) => Promise<OpenCodeSession \| null>`                                                                                                                                     | Get OpenCode session info (port + sessionId)                                                                                                                                                                                                                                                                                                                              |
| `restartOpencodeServer` | `(projectId: ProjectId, workspaceName: WorkspaceName) => Promise<number>`                                                                                                                                                      | Restart OpenCode server, preserving port                                                                                                                                                                                                                                                                                                                                  |
| `setMetadata`           | `(projectId: ProjectId, workspaceName: WorkspaceName, key: string, value: string \| null) => Promise<void>`                                                                                                                    | Set/delete metadata                                                                                                                                                                                                                                                                                                                                                       |
| `getMetadata`           | `(projectId: ProjectId, workspaceName: WorkspaceName) => Promise<Record<string, string>>`                                                                                                                                      | Get all metadata                                                                                                                                                                                                                                                                                                                                                          |
| `executeCommand`        | `(projectId: ProjectId, workspaceName: WorkspaceName, command: string, args?: readonly unknown[]) => Promise<unknown>`                                                                                                         | Execute a VS Code command                                                                                                                                                                                                                                                                                                                                                 |

#### `ui` - UI State Management

| Method               | Signature                                                                                | Description                    |
| -------------------- | ---------------------------------------------------------------------------------------- | ------------------------------ |
| `selectFolder`       | `() => Promise<string \| null>`                                                          | Open native folder picker      |
| `getActiveWorkspace` | `() => Promise<WorkspaceRef \| null>`                                                    | Get currently active workspace |
| `switchWorkspace`    | `(projectId: ProjectId, workspaceName: WorkspaceName, focus?: boolean) => Promise<void>` | Switch to a workspace          |
| `setMode`            | `(mode: UIMode) => Promise<void>`                                                        | Set UI mode                    |

#### `lifecycle` - Application Lifecycle

| Method          | Signature                                   | Description                                             |
| --------------- | ------------------------------------------- | ------------------------------------------------------- |
| `getState`      | `() => Promise<AppStateResult>`             | Get app state and selected agent                        |
| `setAgent`      | `(agent: ConfigAgentType) => Promise<void>` | Save agent selection to config (called after UI choice) |
| `setup`         | `() => Promise<SetupResult>`                | Run first-time setup (does NOT start services)          |
| `startServices` | `() => Promise<SetupResult>`                | Start app services (idempotent, called after loading)   |
| `quit`          | `() => Promise<void>`                       | Quit the application                                    |

**`AppStateResult` return type:**

```typescript
interface AppStateResult {
  state: AppState; // "agent-selection" | "setup" | "loading" | "ready"
  agent: ConfigAgentType | null; // "claude" | "opencode" | null
}
```

**Note:** `getState()` never returns `state: "ready"` - the "ready" state is only reached after `startServices()` completes successfully.

### Events

| Event                         | Payload                                          | Description                                                                  |
| ----------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------- |
| `project:opened`              | `{ project: Project }`                           | Project was opened                                                           |
| `project:closed`              | `{ projectId: ProjectId }`                       | Project was closed                                                           |
| `project:bases-updated`       | `{ projectId: ProjectId, bases: BaseInfo[] }`    | Base branches refreshed                                                      |
| `workspace:created`           | `{ projectId: ProjectId, workspace: Workspace }` | Workspace created                                                            |
| `workspace:removed`           | `WorkspaceRef`                                   | Workspace removed                                                            |
| `workspace:switched`          | `WorkspaceRef \| null`                           | Active workspace changed                                                     |
| `workspace:status-changed`    | `WorkspaceRef & { status: WorkspaceStatus }`     | Status changed                                                               |
| `workspace:metadata-changed`  | `{ projectId, workspaceName, key, value }`       | Metadata updated                                                             |
| `workspace:loading-changed`   | `{ path: string, loading: boolean }`             | Workspace loading state changed                                              |
| `workspace:deletion-progress` | `DeletionProgress`                               | Workspace deletion progress update (includes `blockingProcesses` on Windows) |
| `ui:mode-changed`             | `{ mode: UIMode, previousMode: UIMode }`         | UI mode changed                                                              |
| `setup:progress`              | `{ step: SetupStep, message: string }`           | Setup progress (legacy)                                                      |
| `lifecycle:setup-progress`    | `SetupRowProgress`                               | Setup row progress update (3-row model)                                      |

**`SetupRowProgress` payload:**

```typescript
type SetupRowId = "vscode" | "agent" | "setup";
type SetupRowStatus = "pending" | "running" | "done" | "failed";

interface SetupRowProgress {
  id: SetupRowId; // Which row this update is for
  status: SetupRowStatus; // Current status
  progress?: number; // Progress percentage (0-100), only for "running"
  message?: string; // Status message to display
  error?: string; // Error message when status is "failed"
}
```

---

## Type Definitions

### Core Types

#### `ProjectId`

Branded string identifying a project. Format: `<name>-<8-hex-hash>`

```typescript
type ProjectId = string & { readonly [ProjectIdBrand]: true };
// Example: "my-app-a1b2c3d4"
```

#### `WorkspaceName`

Branded string identifying a workspace. Typically matches the git branch name.

```typescript
type WorkspaceName = string & { readonly [WorkspaceNameBrand]: true };
// Example: "feature-x"
```

#### `Project`

```typescript
interface Project {
  readonly id: ProjectId;
  readonly name: string; // Folder name
  readonly path: string; // Absolute path
  readonly workspaces: readonly Workspace[];
  readonly defaultBaseBranch?: string;
  readonly remoteUrl?: string; // Original git URL if project was cloned
}
```

**Note:** The `remoteUrl` field is present only for projects that were cloned using `projects.clone()`. For projects opened locally via `projects.open()`, this field is undefined.

#### `Workspace`

```typescript
interface Workspace {
  readonly projectId: ProjectId;
  readonly name: WorkspaceName;
  readonly branch: string | null; // null for detached HEAD
  readonly metadata: Readonly<Record<string, string>>;
  readonly path: string;
}
```

#### `WorkspaceRef`

```typescript
interface WorkspaceRef {
  readonly projectId: ProjectId;
  readonly workspaceName: WorkspaceName;
  readonly path: string;
}
```

#### `WorkspaceStatus`

```typescript
interface WorkspaceStatus {
  readonly isDirty: boolean;
  readonly agent: AgentStatus;
}
```

#### `AgentStatus`

```typescript
type AgentStatus =
  | { readonly type: "none" }
  | { readonly type: "idle"; readonly counts: AgentStatusCounts }
  | { readonly type: "busy"; readonly counts: AgentStatusCounts }
  | { readonly type: "mixed"; readonly counts: AgentStatusCounts };

interface AgentStatusCounts {
  readonly idle: number;
  readonly busy: number;
  readonly total: number;
}
```

#### `BaseInfo`

```typescript
interface BaseInfo {
  readonly name: string;
  readonly isRemote: boolean;
  readonly base?: string;
  readonly derives?: string;
}
```

| Field      | Type      | Description                                                                                                                                                         |
| ---------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`     | `string`  | Full branch reference (e.g., "main" or "origin/main")                                                                                                               |
| `isRemote` | `boolean` | Whether this is a remote-tracking branch                                                                                                                            |
| `base`     | `string?` | Suggested base branch for creating a workspace. For local branches: `codehydra.base` config value, or matching `origin/*` branch. For remote branches: the full ref |
| `derives`  | `string?` | Derivable workspace name if a workspace can be created from this branch. Set for local branches without worktrees or remote branches without local counterparts     |

#### `UIMode`

```typescript
type UIMode = "workspace" | "dialog" | "shortcut" | "hover";
```

#### `AppState`

```typescript
type AppState = "agent-selection" | "setup" | "loading" | "ready";
```

**States:**

- `agent-selection`: First run, no agent selected yet (shows AgentSelectionDialog)
- `setup`: Agent selected but binaries need to be downloaded (shows SetupScreen)
- `loading`: All binaries available, services starting (shows loading screen)
- `ready`: Services started, application fully operational

**Note:** `getState()` never returns "ready" - that state is only reached after `startServices()` completes successfully.

#### `ConfigAgentType`

```typescript
type ConfigAgentType = "claude" | "opencode";
```

User-selectable agent types. Stored in `config.json` after initial selection.

#### `SetupProgress` / `SetupResult`

```typescript
type SetupStep = "binary-download" | "extensions" | "settings";

interface SetupProgress {
  readonly step: SetupStep;
  readonly message: string;
}

type SetupResult =
  | { readonly success: true }
  | { readonly success: false; readonly message: string; readonly code: string };
```

#### `DeletionProgress`

```typescript
interface DeletionProgress {
  readonly workspacePath: WorkspacePath;
  readonly workspaceName: WorkspaceName;
  readonly projectId: ProjectId;
  readonly keepBranch: boolean;
  readonly operations: readonly DeletionOperation[];
  readonly completed: boolean;
  readonly hasErrors: boolean;
  readonly blockingProcesses?: readonly BlockingProcess[]; // Windows only
}

type DeletionOperationId =
  | "closing-handles"
  | "killing-blockers"
  | "kill-terminals"
  | "stop-server"
  | "cleanup-vscode"
  | "detecting-blockers"
  | "cleanup-workspace";

type DeletionOperationStatus = "pending" | "in-progress" | "done" | "error";

interface DeletionOperation {
  readonly id: DeletionOperationId;
  readonly label: string;
  readonly status: DeletionOperationStatus;
  readonly error?: string;
}
```

#### `BlockingProcess`

```typescript
interface BlockingProcess {
  readonly pid: number;
  readonly name: string; // Process name (e.g., "node.exe")
  readonly commandLine: string; // Full command line
  readonly files: readonly string[]; // Paths relative to workspace
  readonly cwd: string | null; // Working directory relative to workspace, or null
}
```

**Field details:**

- `files` - File handles held by this process, paths relative to workspace root
- `cwd` - Process working directory if within workspace (blocks deletion), null otherwise

**Note:** `blockingProcesses` is only populated on Windows when deletion fails due to locked files (EBUSY, EACCES, EPERM). On other platforms, it's always undefined.

---

## API Comparison

| Aspect              | Private API                       | Public API                        |
| ------------------- | --------------------------------- | --------------------------------- |
| **Access**          | `window.api` via Electron preload | Extension exports or WebSocket    |
| **Scope**           | Full API (all namespaces)         | Workspace-scoped only             |
| **Identifiers**     | `ProjectId` + `WorkspaceName`     | Auto-resolved from workspace path |
| **Events**          | Full event subscription           | Polling only (no events)          |
| **Cross-workspace** | Yes (switch, list, etc.)          | No (own workspace only)           |
| **UI Control**      | Yes (`ui.*` methods)              | No                                |
| **Lifecycle**       | Yes (`lifecycle.*` methods)       | No                                |
| **Intended Use**    | CodeHydra internals only          | External consumers                |

---

## Environment Variables

CodeHydra sets environment variables in workspace terminals for integration with extensions and tools.

### General Variables

| Variable          | Description                                            |
| ----------------- | ------------------------------------------------------ |
| `_CH_PLUGIN_PORT` | Socket.IO plugin server port for WebSocket connections |

### Claude Provider Variables

These variables are set when using the Claude agent provider.

| Variable                  | Description                                                                                                                                     |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `_CH_CLAUDE_SETTINGS`     | Path to hooks configuration file                                                                                                                |
| `_CH_CLAUDE_MCP_CONFIG`   | Path to MCP configuration file                                                                                                                  |
| `_CH_BRIDGE_PORT`         | HTTP bridge server port for hook notifications                                                                                                  |
| `_CH_MCP_PORT`            | Main MCP server port                                                                                                                            |
| `_CH_WORKSPACE_PATH`      | Absolute path to the workspace directory                                                                                                        |
| `_CH_INITIAL_PROMPT_FILE` | (Optional) Path to initial prompt JSON file. Contains `{ prompt, model?, agent? }`. The file is deleted after first read by the Claude wrapper. |

---

## Source Files

| Purpose              | File                                     |
| -------------------- | ---------------------------------------- |
| Core Interface       | `src/shared/api/interfaces.ts`           |
| Type Definitions     | `src/shared/api/types.ts`                |
| IPC Channels         | `src/shared/ipc.ts`                      |
| Preload (window.api) | `src/preload/index.ts`                   |
| Plugin Protocol      | `src/shared/plugin-protocol.ts`          |
| External API Types   | `extensions/codehydra-sidekick/api.d.ts` |
