# CLUI Architecture

## Overview

CLUI is an Electron desktop application that provides a graphical interface for Claude Code CLI. It spawns `claude -p` subprocesses, parses their NDJSON output, and presents conversations in a floating overlay window.

```
┌──────────────────────────────────────────────────────────────┐
│                     Renderer Process                         │
│  React 19 + Zustand 5 + Tailwind CSS 4 + Framer Motion      │
│                                                              │
│  ┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌────────────┐  │
│  │ TabStrip  │ │Conversation  │ │ InputBar │ │ Marketplace│  │
│  │          │ │   View       │ │          │ │   Panel    │  │
│  └──────────┘ └──────────────┘ └──────────┘ └────────────┘  │
│                         │                                    │
│                    sessionStore (Zustand)                     │
│                         │                                    │
│              window.clui (preload bridge)                     │
├──────────────────────────────────────────────────────────────┤
│                     Preload Script                            │
│  Typed IPC bridge — contextBridge.exposeInMainWorld          │
├──────────────────────────────────────────────────────────────┤
│                     Main Process                             │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │                   ControlPlane                        │    │
│  │  Tab registry, session lifecycle, queue management    │    │
│  │                                                       │    │
│  │  ┌─────────────┐  ┌──────────────────┐               │    │
│  │  │ RunManager   │  │ EventNormalizer  │               │    │
│  │  │ Spawns       │  │ Raw stream-json  │               │    │
│  │  │ claude -p    │──│ → canonical      │               │    │
│  │  │ per prompt   │  │   events         │               │    │
│  │  └─────────────┘  └──────────────────┘               │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  ┌────────────────────┐  ┌────────────────────────────┐      │
│  │ PermissionServer   │  │ Marketplace Catalog        │      │
│  │ HTTP hooks on      │  │ GitHub raw fetch + cache   │      │
│  │ 127.0.0.1:19836    │  │ TTL: 5 minutes             │      │
│  └────────────────────┘  └────────────────────────────┘      │
└──────────────────────────────────────────────────────────────┘
         │                              │
    claude -p (NDJSON)          raw.githubusercontent.com
    (local subprocess)          (optional, cached)
```

## Main Process (`src/main/`)

### ControlPlane (`claude/control-plane.ts`)

Single authority for all tab and session lifecycle. Manages:

- **Tab registry** — maps tabId → session metadata, status, process PID.
- **State machine** — each tab transitions through: `connecting → idle → running → completed → failed → dead`.
- **Request routing** — maps requestIds to active RunManager instances.
- **Queue + backpressure** — max 32 pending requests, prompts queue behind running tasks.
- **Health reconciliation** — responds to renderer polls with tab status + process liveness.
- **Session ID tracking** — maps Claude session IDs to tabs for permission routing.

### RunManager (`claude/run-manager.ts`)

Spawns one `claude -p --output-format stream-json` process per prompt. Responsibilities:

- Constructs CLI arguments (`--resume`, `--permission-mode`, `--settings`, `--add-dir`, etc.)
- Reads NDJSON from stdout line-by-line via `StreamParser`.
- Passes raw events to `EventNormalizer` for canonicalization.
- Maintains stderr ring buffer (100 lines) for error diagnostics.
- Cleans up process on cancel, tab close, or unexpected exit.
- Removes `CLAUDECODE` from spawned environment to prevent credential leakage.

### EventNormalizer (`claude/event-normalizer.ts`)

Maps raw Claude Code stream-json events to canonical `NormalizedEvent` types:

| Raw Event | Normalized Event |
|-----------|-----------------|
| `system` (subtype: init) | `session_init` |
| `stream_event` (content_block_delta, text_delta) | `text_chunk` |
| `stream_event` (content_block_start, tool_use) | `tool_call` |
| `stream_event` (content_block_delta, input_json_delta) | `tool_call_update` |
| `stream_event` (content_block_stop) | `tool_call_complete` |
| `assistant` | `task_update` |
| `result` | `task_complete` |
| `rate_limit_event` | `rate_limit` |

### PermissionServer (`hooks/permission-server.ts`)

HTTP server that intercepts Claude Code tool calls via PreToolUse hooks:

1. ControlPlane starts PermissionServer on `127.0.0.1:19836`.
2. `generateSettingsFile()` creates a temp JSON file with hook config pointing at the server.
3. RunManager passes `--settings <path>` to each `claude -p` spawn.
4. When Claude wants to use a tool, the CLI POSTs to the hook URL.
5. PermissionServer emits a `permission-request` event to ControlPlane.
6. ControlPlane routes it to the correct tab via `_findTabBySessionId()`.
7. Renderer shows a `PermissionCard` with Allow/Deny buttons.
8. User decision flows back: IPC → ControlPlane → PermissionServer → HTTP response.
9. Claude Code proceeds or skips the tool based on the response.

Security: per-launch app secret, per-run tokens, sensitive field masking, 5-minute auto-deny timeout.

### Marketplace Catalog (`marketplace/catalog.ts`)

Fetches plugin metadata from three Anthropic GitHub repos:
- `anthropics/skills` (Agent Skills)
- `anthropics/knowledge-work-plugins` (Knowledge Work)
- `anthropics/financial-services-plugins` (Financial Services)

Uses Electron's `net.request()` with a 5-minute TTL cache. Individual fetch failures are isolated — one broken repo doesn't block others.

### Skill Installer (`skills/installer.ts`)

Auto-installs bundled skills on startup (currently: `skill-creator`). Uses pinned commit SHAs for deterministic downloads. Atomic install: validates in temp dir before swapping into `~/.claude/skills/`. Respects user-managed skills (skips if no `.clui-version` marker).

## Preload (`src/preload/`)

The preload script uses `contextBridge.exposeInMainWorld` to expose a typed `window.clui` API. This is the only communication surface between renderer and main process.

All methods map to `ipcRenderer.invoke()` (request-response) or `ipcRenderer.send()` (fire-and-forget). The full API surface is defined in `CluiAPI` interface.

## Renderer (`src/renderer/`)

### State Management

Single Zustand store (`stores/sessionStore.ts`) holds all application state:
- Tab list with full `TabState` objects (messages, status, attachments, permissions, etc.)
- Active tab selection
- Marketplace state (catalog, search, filter, install progress)
- UI state (expanded, marketplace open)

### Theme System (`theme.ts`)

Dual color palette (dark + light) defined as JS objects. `useColors()` hook returns the active palette reactively. All tokens are synced to CSS custom properties via `syncTokensToCss()` so CSS files can reference `var(--clui-*)`.

Theme mode state machine: `system | light | dark` with separate `_systemIsDark` tracking for OS value.

### Key Components

- **TabStrip** — tab bar with new tab, history picker, settings popover.
- **ConversationView** — scrollable message timeline with markdown rendering (react-markdown + remark-gfm), tool call cards, permission cards.
- **InputBar** — prompt input with attachment chips, voice recording, slash command menu, model picker.
- **MarketplacePanel** — plugin browser with search, semantic tag filters, install confirmation.

### Performance Patterns

- Narrow Zustand selectors with custom equality functions (field-level comparison) to prevent re-renders during streaming.
- RAF-throttled mousemove handler for click-through detection.
- Debounced marketplace search (200ms).
- Health reconciliation skips setState when no tabs changed.

## IPC Channel Map

All channels are defined in `src/shared/types.ts` under the `IPC` const. Events flow through a single `clui:normalized-event` channel for all Claude Code stream events, with separate channels for tab status changes and enriched errors.

## Data Flow: Prompt → Response

```
User types prompt
    → InputBar calls window.clui.prompt(tabId, requestId, options)
    → ipcRenderer.invoke('clui:prompt', ...)
    → Main: ControlPlane.prompt()
    → RunManager spawns: claude -p --output-format stream-json --resume <sid>
    → Claude CLI writes NDJSON to stdout
    → StreamParser emits lines
    → EventNormalizer maps to NormalizedEvent
    → ControlPlane updates tab state + broadcasts via IPC
    → Renderer: useClaudeEvents hook receives events
    → sessionStore.handleNormalizedEvent() updates messages
    → React re-renders ConversationView
```
