-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Electron Main Process │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────────┐ │
│ │ SidecarManager │ │ SessionIndex │ │ StatsClient │ │
│ │ │ │ │ │ │ │
│ │ spawn(omp, │ │ chokidar watch │ │ HTTP GET polling │ │
│ │ ["--mode", │ │ ~/.omp/ │ │ localhost:3847 │ │
│ │ "rpc-ui", │ │ sessions/ │ │ 30s per active tab │ │
│ │ "--approval- │ │ │ │ │ │
│ │ mode",mode] │ │ Parse 256-byte │ │ 15 endpoints │ │
│ │ │ │ title slot + │ │ Range: 1h|24h|7d| │ │
│ │ env: { │ │ 4KB header + │ │ 30d|90d|all │ │
│ │ PI_RPC_EMIT_ │ │ 32KB tail │ │ │ │
│ │ TITLE: "1" │ │ │ │ │ │
│ │ } │ │ LRU cache 4096 │ │ │ │
│ └───────┬────────┘ └───────┬────────┘ └─────────┬──────────┘ │
│ │ │ │ │
│ ┌───────┴────────────────────┴──────────────────────┴──────────┐ │
│ │ IPC Bridge (contextBridge) │ │
│ │ invoke() → commands on() → event streams │ │
│ │ Serialized clone Batched at 16ms │ │
│ └────────────────────────────┬──────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────┴──────────────────────────────────┐ │
│ │ LogWatcher (tail ~/.omp/logs/) │ Tray │ Updater │ Menu │ │
│ └───────────────────────────────────────────────────────────────┘ │
└───────────────────────────────┼────────────────────────────────────┘
│ IPC (structured clone)
┌───────────────────────────────┼────────────────────────────────────┐
│ Electron Renderer (React 19) │
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ Zustand Stores │ │
│ │ session · messages · tools · subagents · model · todo │ │
│ │ settings · stats · ui · extensionUi │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ React Components │ │
│ │ ChatStream · WorkspaceDock · ToolRenderers(28+) · DiffViewer │ │
│ │ Todo/Plan/Agents/Queue cards · ModelPicker · CommandPalette │ │
│ │ SettingsForm · StatsDashboard · ExtensionDialogs │ │
│ └────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────────┐ ┌────────────┐ ┌──────────────┐
│ omp --mode │ │ omp stats │ │ ~/.omp/ │
│ rpc-ui │ │ (HTTP) │ │ (filesystem) │
│ (stdio) │ │ :3847 │ │ (read-only) │
└──────────────┘ └────────────┘ └──────────────┘
| Module | Responsibility | Key Implementation Detail |
|---|---|---|
sidecar.ts |
Spawn/kill omp, lifecycle, restart |
child_process.spawn; restart on exit!=0 (3 attempts, 1s/2s/4s backoff); no restart on exit=0 |
rpc-bridge.ts |
NDJSON framing, v2 chunk reassembly | readline on stdout; ChunkReassembler validates chunkId/index/count/byteLength; 64MB ceiling |
rpc-client.ts |
Typed command methods, id correlation |
Promise.withResolvers() per command; 30s timeout; commands serialized (only bash is concurrent) |
event-batcher.ts |
Batch events at 16ms for renderer | Accumulate in array; flush via setTimeout(16); never drop message_update or lifecycle events; drop intermediate tool_execution_update under backpressure (>1000 buffered) |
session-index.ts |
Watch sessions dir, parse metadata | chokidar + awaitWriteFinish: 300ms; parse 256-byte title slot + 4KB header; LRU 4096 entries keyed by (mtimeMs, size) |
stats-client.ts |
HTTP GET to stats server |
fetch with AbortSignal; 30s poll per active route; discovery via x-omp-stats-dashboard header probe |
log-watcher.ts |
Tail log files |
fs.createReadStream from last offset; 1000-line ring buffer; poll fallback where fs.watch unreliable |
broker-client.ts |
Daemon broker socket (optional) | Connect to ~/.omp/daemon/<hash>/broker.sock; read token from broker.token; JSON-line protocol with auth field |
tray.ts |
System tray | Status icon (idle/streaming/error); quick actions |
updater.ts |
Auto-update | electron-updater; GitHub Releases feed |
window.ts |
Window management | Multi-window; state persistence via electron-store; deep link omp://
|
ipc.ts |
IPC handler registration | Type-safe channel map; all handlers validated with zod |
contextBridge.exposeInMainWorld("omp", {
rpc: { /* 42+ typed command methods */ },
events: { /* subscribe/unsubscribe to event streams */ },
ui: { /* extension UI responses, host tool/URI results */ },
sessions: { /* list, delete, metadata */ },
stats: { /* fetch stats endpoints */ },
system: { /* openExternal, showSaveDialog, clipboard, notifications */ },
});| Directory | Content |
|---|---|
stores/ |
Zustand stores (session, messages, tools, subagents, model, todo, settings, stats, ui, extensionUi) |
components/chat/ |
MessageBubble, StreamingText, ThinkingBlock, MarkdownRenderer, CodeBlock |
components/chat/dock/ |
WorkspaceDock plus Todo, Plan, Agents, and Queue live cards mounted between transcript and composer |
components/tools/ |
28 named renderers + generic fallback |
components/panels/ |
Secondary workspace and management surfaces; the workspace drawer mounts Diff, Files, and Logs only |
components/dialogs/ |
ExtensionDialogs (select/confirm/input/editor/open_url), ApprovalDialog, CommandPalette, ModelPicker |
components/settings/ |
Tabbed settings form (read config.yml + RPC commands) |
components/stats/ |
Dashboard views (10 routes matching stats API) |
components/layout/ |
TitleBar, Sidebar, InputArea, PanelContainer |
hooks/ |
useRpcEvent, useSessionList, useStats, useExtensionUi, useVirtualScroll |
lib/ |
Event parser, markdown pipeline, diff renderer, theme tokens, keymap |
styles/ |
Tailwind config, 95-color theme token system |
ChatStream owns the primary vertical composition: virtualized transcript → WorkspaceDock → composer. WorkspaceDock mounts live Todo, Plan, Agents, and Queue cards; PanelContainer is a separate secondary drawer limited to Diff, Files, and Logs.
App Launch
|
+--> Create BrowserWindow (loads renderer bundle)
| \--> Renderer shows "Connecting..." skeleton
|
+--> Spawn: omp --mode rpc-ui --approval-mode <mode> [flags]
| env: { ...process.env, PI_RPC_EMIT_TITLE: "1", PI_NO_PTY: "1" }
| cwd: <project directory>
| stdio: ["pipe", "pipe", "pipe"]
| |
| +--> stdout readline -> parse NDJSON
| +--> Wait for { type: "ready" } frame
| +--> Send { id: "proto-1", type: "negotiate_protocol", protocolVersion: 2 }
| +--> Receive success -> enable v2 chunked output
| +--> Emit "sidecar:ready" to renderer
| |
| +--> [RUNNING: forward commands stdin<->stdout, batch events to renderer]
| |
| +--> On exit(0): normal shutdown, no restart
| +--> On exit(!=0): restart with backoff (1s, 2s, 4s), max 3 attempts
| \--> On 3 failures: show error dialog, stop
|
+--> Start session-index watcher (parallel with sidecar)
+--> Probe stats server (lazy, on first stats tab open)
\--> Create tray icon
| Interface | Transport | Direction | Purpose |
|---|---|---|---|
omp --mode rpc-ui |
stdio NDJSON (v1/v2, 64MB chunks) | Bidirectional | All agent operations |
omp stats HTTP server |
localhost:3847, GET JSON | Read-only | Usage/cost/behavior analytics |
~/.omp/sessions/*.jsonl |
File read | Read-only | Session listing, history metadata |
~/.omp/config.yml |
File read | Read-only | Display current configuration |
~/.omp/logs/omp.*.log |
File read (tail) | Read-only | Log viewer |
~/.omp/daemon/<hash>/broker.sock |
Unix socket + token auth | Bidirectional | Process management, PTY (optional) |
RPC commands are processed one-at-a-time by RpcInputDispatcher.
Exceptions that bypass the serial queue:
-
bash— dispatched in background (allows concurrentabort_bash) - Side-channel frames — dispatched immediately:
extension_ui_response,host_tool_result,host_tool_update,host_uri_result
The GUI must not assume parallel command execution. Rapid-fire commands (e.g., set_model + set_thinking_level) are queued and execute in order.
// BrowserWindow
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webSecurity: true,
}CSP (index.html meta):
default-src 'self'script-src 'self'style-src 'self' 'unsafe-inline'img-src 'self' data: blob:connect-src 'self' http://localhost:3847worker-src 'self' blob:
Key security properties:
- All filesystem access in main process only
- Preload exposes minimal typed API (no raw IPC channels leaked)
- Session files opened
O_RDONLY - Sidecar inherits env (API keys stay in main process, never serialized to renderer)
- Daemon broker requires token auth (read from
broker.tokenfile)
Most features available in the TUI are also available in the GUI via the RPC protocol. The following are confirmed as unavailable:
| Capability | Reason |
|---|---|
| Theme switching via RPC | No setTheme/getAllThemes RPC command exists in the wire protocol |
All other major features — plan mode, goal mode, vibe mode, loop mode, retry, dequeue, settings get/set, askDialog — are available via dedicated RPC commands (set_plan_mode, set_goal, guided_goal, set_vibe_mode, set_loop_mode, retry, dequeue, get_settings/set_setting, askDialog).