Architecture Overview
OpenWork is a standalone desktop automation assistant built with Electron. The application hosts a local React UI (bundled via Vite) and communicates with the main process through contextBridge IPC. The main process spawns the OpenCode CLI to execute user tasks.
Six-Layer Architecture
graph TB
subgraph "Layer 6: User Interface"
UI[React Renderer<br/>HashRouter + Zustand]
end
subgraph "Layer 5: Preload Bridge"
PRELOAD[Preload Script<br/>contextBridge API]
end
subgraph "Layer 4: IPC Handlers"
IPC[IPC Handlers<br/>task/start, settings, etc.]
end
subgraph "Layer 3: Task Management"
TM[TaskManager<br/>Parallel Execution]
ADAPTER[OpenCodeAdapter<br/>PTY Process Wrapper]
end
subgraph "Layer 2: CLI Integration"
CLI[OpenCode CLI<br/>Bundled Binary]
MCP[MCP Servers<br/>file-permission, dev-browser]
end
subgraph "Layer 1: Storage & Security"
STORES[(electron-store<br/>secureStorage<br/>taskHistory<br/>appSettings)]
NODE[Bundled Node.js v20.18.1]
end
UI -->|window.accomplish| PRELOAD
PRELOAD -->|ipcRenderer.invoke| IPC
IPC --> TM
TM --> ADAPTER
ADAPTER -->|node-pty| CLI
CLI --> MCP
IPC --> STORES
ADAPTER --> NODE
style UI fill:#e1f5ff
style PRELOAD fill:#fff4e1
style IPC fill:#f0e1ff
style TM fill:#ffe1f0
style CLI fill:#e1ffe1
style STORES fill:#ffe1e1
Component Relationships
Data Flow Diagram
sequenceDiagram
participant UI as React Renderer
participant Pre as Preload Script
participant IPC as IPC Handlers
participant TM as TaskManager
participant OA as OpenCodeAdapter
participant CLI as OpenCode CLI
participant Store as electron-store
UI->>Pre: window.accomplish.startTask()
Pre->>IPC: ipcRenderer.invoke('task:start')
IPC->>TM: taskManager.startTask()
TM->>OA: new OpenCodeAdapter(taskId)
OA->>CLI: pty.spawn(opencode run)
CLI-->>OA: NDJSON stream
OA->>IPC: callbacks.onMessage()
IPC->>Pre: webContents.send('task:update')
Pre->>UI: onTaskUpdate callback
IPC->>Store: saveTask(), updateTaskStatus()
Process Architecture
graph LR
subgraph "Main Process"
MAIN[index.ts<br/>Electron Bootstrap]
HANDLERS[ipc/handlers.ts<br/>IPC Handler Registry]
TASKMGR[opencode/task-manager.ts<br/>Task Orchestration]
ADAPTER[opencode/adapter.ts<br/>PTY Wrapper]
STORE[store/<br/>Persistence Layer]
end
subgraph "Renderer Process"
RENDERER[main.tsx<br/>React Entry]
APP[App.tsx<br/>Router + Layout]
PAGES[pages/<br/>Home, Execution, History]
STORE_Z[stores/taskStore.ts<br/>Zustand State]
end
subgraph "Preload Script"
PRELOAD[preload/index.ts<br/>contextBridge API]
end
subgraph "External Processes"
PTY[node-pty<br/>Terminal Emulation]
OPENCODE[OpenCode CLI<br/>AI Agent Execution]
MCP[file-permission<br/>MCP Server]
BROWSER[dev-browser<br/>Playwright Server]
end
RENDERER --> APP
APP --> PAGES
PAGES --> STORE_Z
STORE_Z -.->|window.accomplish| PRELOAD
PRELOAD -.->|ipcRenderer| HANDLERS
HANDLERS --> TASKMGR
TASKMGR --> ADAPTER
ADAPTER --> PTY
PTY --> OPENCODE
OPENCODE --> MCP
OPENCODE --> BROWSER
HANDLERS --> STORE
style RENDERER fill:#e3f2fd
style PRELOAD fill:#fff3e0
style MAIN fill:#f3e5f5
style PTY fill:#e8f5e9
Key Architectural Decisions
1. Electron with Local React UI
Decision: Bundle React app locally instead of loading from remote URL.
Rationale:
- Eliminates network dependency for core UI
- Faster load times (no network latency)
- Works offline
- Simplifies deployment (single binary)
Source: apps/desktop/src/main/index.ts lines 112-119
// Load the local UI
if (VITE_DEV_SERVER_URL) {
mainWindow.loadURL(VITE_DEV_SERVER_URL);
} else {
const indexPath = path.join(RENDERER_DIST, 'index.html');
mainWindow.loadFile(indexPath);
}
2. contextBridge for Secure IPC
Decision: Use contextBridge to expose typed API to renderer.
Rationale:
- Prevents prototype pollution attacks
- Provides TypeScript types for all IPC methods
- Clear API boundary between main and renderer
- Enables safe remote code execution
Source: apps/desktop/src/preload/index.ts lines 11-139
3. PTY for CLI Process Spawning
Decision: Use node-pty instead of child_process.spawn.
Rationale:
- Proper terminal emulation (handles TTY prompts)
- Combines stdout/stderr into single stream
- Supports interactive CLI features
- Works with shebang scripts on all platforms
Source: apps/desktop/src/main/opencode/adapter.ts lines 171-181
4. Bundled Node.js Runtime
Decision: Package Node.js v20.18.1 with the application.
Rationale:
- MCP servers require Node.js
- Users may not have Node.js installed
- Ensures consistent Node version
- Enables
npxcommand execution
Source: apps/desktop/src/main/utils/bundled-node.ts
5. Parallel Task Execution
Decision: TaskManager supports up to 10 concurrent tasks.
Rationale:
- Users can work on multiple tasks simultaneously
- Task-scoped page names prevent browser collision
- Queue system for managing overload
- Each task gets isolated PTY process
Source: apps/desktop/src/main/opencode/task-manager.ts lines 243-288
6. Custom Secure Storage
Decision: Use AES-256-GCM encryption with machine-bound key instead of keytar.
Rationale:
- Avoids macOS Keychain prompts
- Keys encrypted at rest
- Machine-specific key derivation
- Suitable for rotatable API keys
Source: apps/desktop/src/main/store/secureStorage.ts lines 6-94
Communication Patterns
IPC Channel Naming
All IPC channels follow a consistent naming pattern:
'{domain}:{action}'
// Examples:
'task:start' // Start a new task
'task:cancel' // Cancel a task
'settings:api-keys' // Get API keys
'permission:respond' // Respond to permission request
'model:set' // Set selected model
Event Streaming
Task execution uses a streaming event model:
stateDiagram-v2
[*] --> step_start
step_start --> text: Assistant response
step_start --> tool_use: Tool invocation
text --> text: More content
tool_use --> tool_result: Tool output
tool_use --> text: Description
tool_result --> step_finish: Complete
text --> step_finish: Done
step_finish --> [*]
step_finish --> error: Failed
Message Batching
For performance optimization, multiple messages are batched:
// Per-task message batching state
interface MessageBatcher {
pendingMessages: TaskMessage[];
timeout: NodeJS.Timeout | null;
taskId: string;
flush: () => void;
}
Delay: 50ms (MESSAGE_BATCH_DELAY_MS)
Source: apps/desktop/src/main/ipc/handlers.ts lines 88-157
Technology Stack
| Layer | Technology | Purpose |
|---|---|---|
| UI Framework | React 18 + Vite | Fast development, HMR |
| Routing | React Router HashRouter | File:// protocol compatibility |
| State | Zustand | Lightweight state management |
| Styling | Tailwind CSS + shadcn/ui | Utility-first components |
| Desktop | Electron 28+ | Cross-platform desktop framework |
| IPC | contextBridge + ipcRenderer | Secure main-renderer communication |
| Storage | electron-store | Persistent settings and history |
| Encryption | Node.js crypto (AES-256-GCM) | API key encryption |
| Process | node-pty | Terminal emulation for CLI |
| CLI | OpenCode (opencode-ai) | Multi-provider AI agent |
| Runtime | Bundled Node.js v20.18.1 | MCP server execution |
File Structure
apps/desktop/
├── src/
│ ├── main/ # Main process (Node.js context)
│ │ ├── index.ts # Electron bootstrap
│ │ ├── ipc/ # IPC handlers
│ │ │ └── handlers.ts # All IPC channel handlers
│ │ ├── opencode/ # OpenCode CLI integration
│ │ │ ├── adapter.ts # PTY wrapper
│ │ │ ├── task-manager.ts # Parallel task orchestration
│ │ │ ├── cli-path.ts # Bundled CLI resolution
│ │ │ ├── config-generator.ts # OpenCode config
│ │ │ └── stream-parser.ts # NDJSON parser
│ │ ├── store/ # Persistence layer
│ │ │ ├── secureStorage.ts # Encrypted API keys
│ │ │ ├── appSettings.ts # App preferences
│ │ │ ├── taskHistory.ts # Task persistence
│ │ │ └── freshInstallCleanup.ts
│ │ └── utils/ # Utilities
│ │ ├── bundled-node.ts # Bundled Node.js paths
│ │ └── system-path.ts # PATH manipulation
│ ├── preload/ # Preload script (isolated context)
│ │ └── index.ts # contextBridge API exposure
│ └── renderer/ # Renderer process (browser context)
│ ├── main.tsx # React entry point
│ ├── App.tsx # Root component with routing
│ ├── pages/ # Page components
│ ├── components/ # Reusable UI components
│ ├── stores/ # Zustand state management
│ └── lib/ # Utilities and API wrappers
├── resources/ # Static assets (icons, etc.)
└── skills/ # OpenCode agent skills
├── dev-browser/ # Browser automation
└── file-permission/ # File permission MCP server
packages/shared/
└── src/
└── types/ # Shared TypeScript types
├── task.ts # Task-related types
├── opencode.ts # OpenCode message types
├── permission.ts # Permission request types
└── provider.ts # API provider types
Security Boundaries
graph TB
subgraph "Untrusted Context"
RENDERER[Renderer Process<br/>Web APIs Only]
end
subgraph "Isolated Bridge"
PRELOAD[Preload Script<br/>Limited Node APIs]
end
subgraph "Trusted Context"
MAIN[Main Process<br/>Full Node APIs]
end
subgraph "External"
CLI[OpenCode CLI<br/>Subprocess]
MCP[MCP Servers<br/>Separate Processes]
end
RENDERER -.->|contextBridge| PRELOAD
PRELOAD -.->|ipcRenderer| MAIN
MAIN -->|child_process| CLI
MAIN -->|HTTP| MCP
style RENDERER fill:#ffebee
style PRELOAD fill:#fff3e0
style MAIN fill:#e8f5e9
Security Measures:
- Context Isolation:
contextIsolation: truein webPreferences - Node Integration Disabled:
nodeIntegration: falsein renderer - Input Validation: All IPC inputs sanitized (max length, type checking)
- Window Focus Check:
assertTrustedWindow()verifies focused window - HTTPS Only:
shell:open-externalonly allows http/https URLs - Encrypted Storage: API keys encrypted with AES-256-GCM
Source: apps/desktop/src/main/ipc/handlers.ts lines 167-192