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 npx command 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:

  1. Context Isolation: contextIsolation: true in webPreferences
  2. Node Integration Disabled: nodeIntegration: false in renderer
  3. Input Validation: All IPC inputs sanitized (max length, type checking)
  4. Window Focus Check: assertTrustedWindow() verifies focused window
  5. HTTPS Only: shell:open-external only allows http/https URLs
  6. Encrypted Storage: API keys encrypted with AES-256-GCM

Source: apps/desktop/src/main/ipc/handlers.ts lines 167-192


Back to top

OpenWork Documentation - Community documentation for accomplish-ai/openwork