IPC Communication Architecture
OpenWork uses Electron’s Inter-Process Communication (IPC) system to enable secure communication between the renderer process (React UI) and the main process (Node.js context). All communication flows through the preload script’s contextBridge API.
IPC Architecture Overview
graph TB
subgraph "Renderer Process"
UI[React Components]
STORE[taskStore<br/>Zustand]
API[accomplish.ts<br/>IPC Wrapper]
end
subgraph "Preload Script"
BRIDGE[contextBridge<br/>Security Boundary]
EXPOSED[window.accomplish<br/>Exposed API]
end
subgraph "Main Process"
IPC_MAIN[ipcMain<br/>Handler Registry]
HANDLERS[Handler Functions<br/>Business Logic]
SUBSYSTEMS[TaskManager, Stores,<br/>Permission API]
end
UI --> STORE
STORE --> API
API -->|invoke| EXPOSED
EXPOSED -->|ipcRenderer.invoke| IPC_MAIN
IPC_MAIN --> HANDLERS
HANDLERS --> SUBSYSTEMS
SUBSYSTEMS -->|webContents.send| IPC_MAIN
IPC_MAIN -->|ipcRenderer.on| EXPOSED
EXPOSED -->|callback| API
API --> STORE
STORE --> UI
style UI fill:#e3f2fd
style BRIDGE fill:#fff3e0
style IPC_MAIN fill:#f3e5f5
style SUBSYSTEMS fill:#e8f5e9
Communication Patterns
Bidirectional Communication Model
sequenceDiagram
participant R as Renderer
participant P as Preload
participant M as Main
Note over R,M: Request/Response Pattern (invoke)
R->>P: window.accomplish.method()
P->>M: ipcRenderer.invoke(channel, args)
M->>M: Execute handler
M-->>P: Promise<result>
P-->>R: Return value
Note over R,M: Event Pattern (on/send)
M->>P: webContents.send(channel, data)
P->>R: callback(event)
R->>R: Update state
Preload Script API
File: apps/desktop/src/preload/index.ts
API Structure
classDiagram
class accomplishAPI {
+getVersion() Promise~string~
+getPlatform() Promise~string~
+openExternal(url) Promise~void~
+startTask(config) Promise~Task~
+cancelTask(id) Promise~void~
+interruptTask(id) Promise~void~
+getTask(id) Promise~Task~
+listTasks() Promise~Task[]~
+deleteTask(id) Promise~void~
+clearTaskHistory() Promise~void~
+respondToPermission(resp) Promise~void~
+resumeSession(sid, prompt) Promise~Task~
+getApiKeys() Promise~ApiKeyConfig[]~
+addApiKey(provider, key) Promise~ApiKeyConfig~
+removeApiKey(id) Promise~void~
+getDebugMode() Promise~boolean~
+setDebugMode(enabled) Promise~void~
+getAppSettings() Promise~AppSettings~
+hasApiKey() Promise~boolean~
+setApiKey(key) Promise~void~
+getApiKey() Promise~string | null~
+validateApiKey(key) Promise~ValidateResult~
+validateApiKeyForProvider(p, k) Promise~ValidateResult~
+clearApiKey() Promise~void~
+getAllApiKeys() Promise~Record~
+hasAnyApiKey() Promise~boolean~
+getOnboardingComplete() Promise~boolean~
+setOnboardingComplete(bool) Promise~void~
+checkOpenCodeCli() Promise~CliStatus~
+getOpenCodeVersion() Promise~string | null~
+getSelectedModel() Promise~SelectedModel~
+setSelectedModel(m) Promise~void~
+onTaskUpdate(cb) Unsubscribe
+onTaskUpdateBatch(cb) Unsubscribe
+onPermissionRequest(cb) Unsubscribe
+onTaskProgress(cb) Unsubscribe
+onDebugLog(cb) Unsubscribe
+onTaskStatusChange(cb) Unsubscribe
+logEvent(payload) Promise~unknown~
}
contextBridge Exposure
Source: Lines 139-146
// Expose the API to the renderer
contextBridge.exposeInMainWorld('accomplish', accomplishAPI);
// Also expose shell info for compatibility checks
contextBridge.exposeInMainWorld('accomplishShell', {
version: process.env.npm_package_version || '1.0.0',
platform: process.platform,
isElectron: true,
});
Event Subscription Pattern
Source: Lines 101-132
// Event subscriptions
onTaskUpdate: (callback: (event: unknown) => void) => {
const listener = (_: unknown, event: unknown) => callback(event);
ipcRenderer.on('task:update', listener);
return () => ipcRenderer.removeListener('task:update', listener);
},
Pattern: Returns unsubscribe function for React cleanup
IPC Channel Registry
Invoke Channels (Request/Response)
graph TB
subgraph "Task Operations"
T_START[task:start]
T_CANCEL[task:cancel]
T_INT[task:interrupt]
T_GET[task:get]
T_LIST[task:list]
T_DEL[task:delete]
T_CLEAR[task:clear-history]
end
subgraph "Session Operations"
S_RESUME[session:resume]
end
subgraph "Permission Operations"
P_RESPOND[permission:respond]
end
subgraph "Settings Operations"
SG_KEYS[settings:api-keys]
SG_ADD[settings:add-api-key]
SG_RM[settings:remove-api-key]
SG_DBG[settings:debug-mode]
SG_SET_DBG[settings:set-debug-mode]
SG_APP[settings:app-settings]
end
subgraph "API Key Operations"
AK_EXISTS[api-key:exists]
AK_SET[api-key:set]
AK_GET[api-key:get]
AK_VAL[api-key:validate]
AK_VAL_P[api-key:validate-provider]
AK_CLR[api-key:clear]
AK_ALL[api-keys:all]
AK_ANY[api-keys:has-any]
end
subgraph "Onboarding Operations"
OB_GET[onboarding:complete]
OB_SET[onboarding:set-complete]
end
subgraph "OpenCode Operations"
OC_CHECK[opencode:check]
OC_VER[opencode:version]
end
subgraph "Model Operations"
M_GET[model:get]
M_SET[model:set]
end
subgraph "Shell Operations"
SH_OPEN[shell:open-external]
end
subgraph "Logging"
LOG[log:event]
end
subgraph "App Info"
APP_VER[app:version]
APP_PLAT[app:platform]
end
Send Channels (Events)
graph TB
subgraph "Task Events"
TU_UPDATE[task:update<br/>Single message]
TU_BATCH[task:update:batch<br/>Batched messages]
T_PROG[task:progress<br/>Progress update]
T_STATUS[task:status-change<br/>Status transition]
end
subgraph "Permission Events"
PERM_REQ[permission:request<br/>User approval needed]
end
subgraph "Debug Events"
DBG_LOG[debug:log<br/>Debug output]
end
subgraph "Auth Events"
AUTH_CB[auth:callback<br/>OAuth response]
end
Message Types
Task Update Event
interface TaskUpdateEvent {
taskId: string;
type: 'message' | 'progress' | 'complete' | 'error';
message?: TaskMessage;
progress?: TaskProgress;
result?: TaskResult;
error?: string;
}
Flow:
sequenceDiagram
participant CLI as OpenCode CLI
participant TM as TaskManager
participant IPC as IPC Handler
participant R as Renderer
CLI->>TM: NDJSON message
TM->>IPC: callbacks.onMessage()
IPC->>IPC: toTaskMessage() conversion
IPC->>IPC: queueMessage() batching
IPC->>R: webContents.send('task:update')
R->>R: addTaskUpdate()
Batch Update Event
interface TaskUpdateBatchEvent {
taskId: string;
messages: TaskMessage[];
}
Optimization:
graph LR
subgraph "Without Batching"
M1[Message 1] --> STATE1((State Update))
M2[Message 2] --> STATE2((State Update))
M3[Message 3] --> STATE3((State Update))
end
subgraph "With Batching"
M1 --> BUFFER[Buffer 50ms]
M2 --> BUFFER
M3 --> BUFFER
BUFFER --> STATE((Single State Update))
end
style STATE1 fill:#ffebee
style STATE2 fill:#ffebee
style STATE3 fill:#ffebee
style STATE fill:#e8f5e9
Permission Request Event
interface PermissionRequest {
id: string;
taskId: string;
type: 'file' | 'question';
fileOperation?: 'create' | 'delete' | 'rename' | 'move' | 'modify' | 'overwrite';
filePath?: string;
targetPath?: string;
contentPreview?: string;
question?: string;
options?: Array<{label: string; description: string}>;
multiSelect?: boolean;
createdAt: string;
}
Security Model
Security Boundaries
graph TB
subgraph "Renderer Context"
R_WEB[Web APIs Only]
R_NO[No Node.js Access]
R_NO2[No Direct fs, child_process]
end
subgraph "Preload Context"
P_LIMIT[Limited Node APIs]
P_IPC[ipcRenderer access]
P_BRIDGE[contextBridge]
end
subgraph "Main Context"
M_FULL[Full Node.js APIs]
M_FS[File System]
M_PROC[Child Processes]
M_CRYPTO[Cryptography]
end
R_WEB --> P_BRIDGE
P_BRIDGE --> P_IPC
P_IPC --> M_FULL
M_FULL --> M_FS
M_FULL --> M_PROC
M_FULL --> M_CRYPTO
style R_WEB fill:#ffebee
style P_LIMIT fill:#fff3e0
style M_FULL fill:#e8f5e9
Input Validation Chain
sequenceDiagram
participant R as Renderer
participant P as Preload
participant V as Validator
participant S as Sanitizer
participant H as Handler
R->>P: invoke(channel, data)
P->>V: Receive raw data
V->>V: Check schema
alt Invalid
V-->>R: Throw error
end
V->>S: Pass validated data
S->>S: sanitizeString()
S->>S: Check max length
S->>H: Call handler
H->>H: assertTrustedWindow()
H-->>R: Return result
Source: apps/desktop/src/main/ipc/handlers.ts
- Schema Validation (lines 239-251)
- String Sanitization (lines 180-192)
- Trusted Window Check (lines 167-178)
Window Focus Validation
Source: apps/desktop/src/main/ipc/handlers.ts lines 167-178
function assertTrustedWindow(window: BrowserWindow | null): BrowserWindow {
if (!window || window.isDestroyed()) {
throw new Error('Untrusted window');
}
const focused = BrowserWindow.getFocusedWindow();
if (BrowserWindow.getAllWindows().length > 1 &&
focused && focused.id !== window.id) {
throw new Error('IPC request must originate from the focused window');
}
return window;
}
Purpose: Prevents other windows from making privileged IPC calls
IPC Handler Implementation
Handler Wrapper
Source: apps/desktop/src/main/ipc/handlers.ts lines 239-251
function handle<Args extends unknown[], ReturnType = unknown>(
channel: string,
handler: (event: IpcMainInvokeEvent, ...args: Args) => ReturnType
): void {
ipcMain.handle(channel, async (event, ...args) => {
try {
return await handler(event, ...(args as Args));
} catch (error) {
console.error(`IPC handler ${channel} failed`, error);
throw normalizeIpcError(error);
}
});
}
Features:
- Type-safe generic wrapper
- Automatic error handling
- Async handler support
- Error normalization
Task Start Handler Flow
stateDiagram-v2
[*] --> Receive: task:start IPC
Receive --> Validate: assertTrustedWindow
Validate --> Sanitize: validateTaskConfig
Sanitize --> InitPerm: Initialize Permission API
InitPerm --> CreateID: Generate taskId
CreateID --> Callbacks: Create TaskCallbacks
Callbacks --> Start: taskManager.startTask()
Start --> Save: saveTask()
Save --> Return: Return Task object
Return --> [*]
note right of Sanitize
Sanitize prompt, taskId,
sessionId, workingDirectory
end note
note right of Callbacks
onMessage, onProgress,
onPermissionRequest,
onComplete, onError,
onDebug, onStatusChange
end note
Source: apps/desktop/src/main/ipc/handlers.ts lines 264-389
Message Batching Implementation
Batcher Architecture
Source: apps/desktop/src/main/ipc/handlers.ts lines 88-165
graph TB
subgraph "Per-Task Batcher"
STATE[pendingMessages: TaskMessage[]<br/>timeout: NodeJS.Timeout<br/>taskId: string<br/>flush: function]
end
subgraph "Queue Operation"
ADD[queueMessage]
RESET[Reset timer]
SET[Set timeout 50ms]
end
subgraph "Flush Operation"
FLUSH[flush()]
SEND[Send batch via IPC]
PERSIST[persist to history]
CLEAR[Clear pending messages]
CLR_TMR[Clear timeout]
end
ADD --> STATE
RESET --> STATE
SET --> STATE
STATE --> FLUSH
FLUSH --> SEND
FLUSH --> PERSIST
FLUSH --> CLEAR
FLUSH --> CLR_TMR
style STATE fill:#e3f2fd
style FLUSH fill:#e8f5e9
Batch Flow Diagram
sequenceDiagram
participant CLI as OpenCode CLI
participant Parser as StreamParser
participant Adapter as OpenCodeAdapter
participant Batcher as MessageBatcher
participant Renderer as Renderer
loop Rapid messages
CLI->>Parser: NDJSON line
Parser->>Adapter: Emit message
Adapter->>Batcher: queueMessage(msg)
Note over Batcher: Add to pendingMessages<br/>Reset 50ms timer
end
Note over Batcher: 50ms elapsed
Batcher->>Renderer: task:update:batch [msg1, msg2, msg3]
Batcher->>Store: addTaskMessage(msg1, msg2, msg3)
Batcher->>Batcher: Clear pendingMessages
Event Subscription Pattern
Renderer-Side Subscriptions
Source: apps/desktop/src/preload/index.ts lines 101-132
onTaskUpdate: (callback: (event: unknown) => void) => {
const listener = (_: unknown, event: unknown) => callback(event);
ipcRenderer.on('task:update', listener);
return () => ipcRenderer.removeListener('task:update', listener);
}
React Integration:
useEffect(() => {
const unsubscribe = window.accomplish.onTaskUpdate((event) => {
// Handle event
});
return unsubscribe;
}, []);
Multi-Event Subscription
Source: apps/desktop/src/renderer/stores/taskStore.ts lines 440-465
// Global subscription to setup progress events
if (typeof window !== 'undefined' && window.accomplish) {
window.accomplish.onTaskProgress((progress: unknown) => {
const event = progress as SetupProgressEvent;
if (event.message) {
useTaskStore.getState().setSetupProgress(event.taskId, event.message);
}
});
window.accomplish.onTaskUpdate((event: unknown) => {
const updateEvent = event as TaskUpdateEvent;
if (updateEvent.type === 'complete' || updateEvent.type === 'error') {
const state = useTaskStore.getState();
if (state.setupProgressTaskId === updateEvent.taskId) {
state.setSetupProgress(null, null);
}
}
});
}
Error Handling
Error Normalization
Source: apps/desktop/src/main/ipc/handlers.ts (referenced in handle wrapper)
function normalizeIpcError(error: unknown): Error {
if (error instanceof Error) {
return error;
}
if (typeof error === 'string') {
return new Error(error);
}
return new Error('An unknown error occurred');
}
Error Propagation
graph TB
subgraph "Main Process"
H[Handler Function]
TRY[try-catch block]
NORM[normalizeIpcError]
LOG[console.error]
end
subgraph "IPC Transport"
THROW[Throw error]
end
subgraph "Renderer Process"
CATCH[await catch]
SET_ERROR[set error in store]
SHOW[Display error UI]
end
H --> TRY
TRY -->|Error| NORM
NORM --> LOG
NORM --> THROW
THROW --> CATCH
CATCH --> SET_ERROR
SET_ERROR --> SHOW
style TRY fill:#fff3e0
style THROW fill:#ffebee
style SHOW fill:#e8f5e9
Performance Considerations
1. Message Batching
Problem: Rapid message streams cause excessive re-renders
Solution: 50ms batching window
Benefit: Reduces state updates from N to 1
2. Selective Event Handling
Problem: All messages sent to all listeners
Solution: Task-scoped batching
Implementation: queueMessage() only processes messages for the relevant task
3. Lazy Event Subscription
Problem: Events fire before components mount
Solution: Global subscriptions in taskStore.ts
Benefit: Events captured even if no component is listening
Complete Sequence: Task Execution
sequenceDiagram
participant UI as React UI
participant Store as taskStore
participant API as accomplish.ts
participant Preload as Preload
participant IPC as IPC Handlers
participant TM as TaskManager
participant OA as OpenCodeAdapter
participant CLI as OpenCode CLI
UI->>Store: startTask({prompt})
Store->>API: startTask(config)
API->>Preload: invoke('task:start')
Preload->>IPC: ipcRenderer.invoke
IPC->>IPC: Validate input
IPC->>TM: startTask(taskId, config, callbacks)
TM->>OA: new OpenCodeAdapter(taskId)
OA->>CLI: pty.spawn(opencode run)
loop Message Stream
CLI->>OA: NDJSON message
OA->>IPC: callbacks.onMessage()
IPC->>IPC: queueMessage() [batching]
alt 50ms elapsed
IPC->>Preload: send('task:update:batch')
Preload->>API: onTaskUpdateBatch callback
API->>Store: addTaskUpdateBatch()
Store->>UI: Re-render with new messages
end
end
CLI->>OA: step_finish
OA->>IPC: callbacks.onComplete()
IPC->>Preload: send('task:update', {complete})
Preload->>API: onTaskUpdate callback
API->>Store: addTaskUpdate()
Store->>UI: Update status to completed
Summary
OpenWork’s IPC architecture provides:
| Feature | Implementation | Benefit |
|---|---|---|
| Type Safety | TypeScript interfaces | Compile-time checking |
| Security | contextBridge + validation | Isolated renderer context |
| Performance | Message batching | Reduced re-renders |
| Error Handling | try-catch + normalization | Graceful failures |
| Cleanup | Unsubscribe functions | No memory leaks |
| Flexibility | Event + invoke patterns | Bidirectional communication |
Key Design Principles:
- Security First: Validate all inputs, check window focus
- Performance: Batch rapid messages, selective updates
- Type Safety: Full TypeScript coverage
- Error Handling: Normalize errors, provide clear messages
- Resource Management: Cleanup functions for all subscriptions