Renderer Process Architecture
The renderer process is the user-facing layer of OpenWork, built with React and running in a browser-like context. It communicates with the main process exclusively through the preload script’s exposed API.
Renderer Overview
graph TB
subgraph "Entry Point"
MAIN[main.tsx<br/>React Bootstrap]
end
subgraph "Routing & Layout"
ROUTER[HashRouter<br/>File:// protocol compatible]
APP[App.tsx<br/>Root + Status Check]
SIDEBAR[Sidebar<br/>Navigation]
HEADER[Header<br/>Top Bar]
end
subgraph "Pages"
HOME[Home Page<br/>Task Input]
EXEC[Execution Page<br/>Task View]
HIST[History Page<br/>Task List]
end
subgraph "State Management"
STORE[taskStore.ts<br/>Zustand]
API[accomplish.ts<br/>IPC Wrapper]
end
subgraph "UI Components"
UI[shadcn/ui Components<br/>Reusables]
end
MAIN --> ROUTER
ROUTER --> APP
APP --> SIDEBAR
APP --> HEADER
SIDEBAR --> HOME
SIDEBAR --> EXEC
SIDEBAR --> HIST
HOME --> STORE
EXEC --> STORE
HIST --> STORE
STORE --> API
HOME --> UI
EXEC --> UI
HIST --> UI
style MAIN fill:#e3f2fd
style STORE fill:#f3e5f5
style API fill:#fff3e0
Entry Point
File: apps/desktop/src/renderer/main.tsx
React Bootstrap
sequenceDiagram
participant HTML as index.html
participant Main as main.tsx
participant Router as HashRouter
participant App as App.tsx
participant API as accomplish.ts
HTML->>Main: Load bundle
Main->>Main: createRoot()
Main->>Router: Wrap with HashRouter
Router->>App: Mount <App />
App->>API: isRunningInElectron()
API->>App: true/false
App->>API: getAccomplish()
App->>App: setOnboardingComplete(true)
App->>Router: Render routes
HashRouter Choice: Required for file:// protocol compatibility in packaged apps.
Source: Lines 1-19
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { HashRouter } from 'react-router-dom';
import App from './App';
const root = createRoot(container);
root.render(
<StrictMode>
<HashRouter>
<App />
</HashRouter>
</StrictMode>
);
Root Component
File: apps/desktop/src/renderer/App.tsx
Application Initialization Flow
stateDiagram-v2
[*] --> Loading: App mounts
Loading --> CheckEnv: useEffect
CheckEnv --> Error: Not in Electron
CheckEnv --> Ready: In Electron
Error --> [*]
Ready --> InitAPI: getAccomplish()
InitAPI --> SetOnboarding: setOnboardingComplete(true)
SetOnboarding --> Render: Render app with sidebar
Render --> [*]
note right of CheckEnv
isRunningInElectron() check
via window.accomplishShell
end note
Source: Lines 30-52
useEffect(() => {
const checkStatus = async () => {
if (!isRunningInElectron()) {
setErrorMessage('This application must be run inside the Openwork desktop app.');
setStatus('error');
return;
}
try {
const accomplish = getAccomplish();
await accomplish.setOnboardingComplete(true);
setStatus('ready');
} catch (error) {
console.error('Failed to initialize app:', error);
setStatus('ready');
}
};
checkStatus();
}, []);
Layout Structure
Source: Lines 81-124
graph TB
subgraph "Root Layout"
CONTAINER[div.flex.h-screen]
DRAG[drag-region<br/>Invisible titlebar drag area]
end
subgraph "Main Content"
SIDEBAR[Sidebar]
MAIN[main.flex-1]
ANIMATE[AnimatePresence]
end
subgraph "Routes"
HOME[/ - HomePage]
EXEC[/execution/:id - ExecutionPage]
WILD[* - Navigate to /]
end
CONTAINER --> DRAG
CONTAINER --> SIDEBAR
CONTAINER --> MAIN
MAIN --> ANIMATE
ANIMATE --> HOME
ANIMATE --> EXEC
ANIMATE --> WILD
style CONTAINER fill:#e3f2fd
style DRAG fill:#fff3e0
style ANIMATE fill:#f3e5f5
Key Features:
- Drag Region: Invisible top area for macOS window dragging (hiddenInset titlebar)
- Page Transitions: Framer Motion AnimatePresence with fadeUp variant
- Sidebar: Persistent navigation
- Hash Router: Routes work without server
State Management
File: apps/desktop/src/renderer/stores/taskStore.ts
Zustand Store Architecture
graph TB
subgraph "State Slice"
CURRENT[currentTask<br/>Active task]
LOADING[isLoading<br/>Loading state]
ERROR[error<br/>Error message]
TASKS[tasks<br/>Task history]
PERM[permissionRequest<br/>Pending permission]
SETUP[setupProgress<br/>Browser download]
end
subgraph "Actions"
START[startTask<br/>Launch new task]
FOLLOW[sendFollowUp<br/>Continue conversation]
CANCEL[cancelTask<br/>Stop execution]
INT[interruptTask<br/>Send Ctrl+C]
PERM_ACT[respondToPermission<br/>Handle permissions]
end
subgraph "Update Handlers"
UPDATE[addTaskUpdate<br/>Single message]
BATCH[addTaskUpdateBatch<br/>Batched messages]
STATUS[updateTaskStatus<br/>Status change]
end
subgraph "Persistence"
LOAD[loadTasks<br/>Fetch history]
LOAD_ID[loadTaskById<br/>Get specific task]
DELETE[deleteTask<br/>Remove task]
CLEAR[clearHistory<br/>Wipe history]
end
CURRENT --> START
LOADING --> START
START --> CURRENT
FOLLOW --> CURRENT
CANCEL --> CURRENT
INT --> CURRENT
PERM --> PERM_ACT
UPDATE --> CURRENT
UPDATE --> TASKS
BATCH --> CURRENT
STATUS --> TASKS
TASKS --> LOAD
TASKS --> DELETE
TASKS --> CLEAR
CURRENT --> LOAD_ID
style CURRENT fill:#e3f2fd
style UPDATE fill:#fff3e0
style BATCH fill:#e8f5e9
State Schema
Source: Lines 26-59
interface TaskState {
// Current task
currentTask: Task | null;
isLoading: boolean;
error: string | null;
// Task history
tasks: Task[];
// Permission handling
permissionRequest: PermissionRequest | null;
// Setup progress (e.g., browser download)
setupProgress: string | null;
setupProgressTaskId: string | null;
setupDownloadStep: number;
// Actions...
}
Task Start Action
Source: Lines 91-128
sequenceDiagram
participant UI as Component
participant Store as taskStore
participant API as accomplish
participant IPC as Main Process
UI->>Store: startTask({prompt})
Store->>Store: set({isLoading: true, error: null})
Store->>API: logEvent({level: info})
Store->>API: startTask(config)
API->>IPC: ipcRenderer.invoke('task:start')
IPC-->>API: Task object
API-->>Store: task
Store->>Store: Update currentTask
Store->>Store: Prepend to tasks list
Store->>Store: Set isLoading based on status
Store-->>UI: task or null
Store->>API: logEvent({status})
Key Behavior:
- Creates new task via IPC
- Adds to tasks list for immediate sidebar update
- Sets
isLoading: truefor running tasks - Sets
isLoading: truefor queued tasks (waiting for queue)
Batch Update Handler
Source: Lines 355-376
graph LR
subgraph "Batch Processing"
EVENT[TaskUpdateBatchEvent]
CHECK{Is current task?}
APPEND[Append all messages]
UPDATE[Single state update]
end
subgraph "Performance Benefit"
SINGLE[Single message<br/>N state updates]
BATCHED[Batch messages<br/>1 state update]
end
EVENT --> CHECK
CHECK -->|Yes| APPEND
CHECK -->|No| IGNORE((Ignore))
APPEND --> UPDATE
SINGLE -.->|N updates| STATE((Re-renders))
BATCHED -.->|1 update| STATE
style SINGLE fill:#ffebee
style BATCHED fill:#e8f5e9
Optimization: Multiple messages from the CLI are batched into a single state update to minimize re-renders.
Setup Progress Handler
Source: Lines 75-89
setSetupProgress: (taskId: string | null, message: string | null) => {
let step = useTaskStore.getState().setupDownloadStep;
if (message) {
const lowerMsg = message.toLowerCase();
if (lowerMsg.includes('downloading chromium headless')) {
step = 3;
} else if (lowerMsg.includes('downloading ffmpeg')) {
step = 2;
} else if (lowerMsg.includes('downloading chromium')) {
step = 1;
}
}
set({ setupProgress: message, setupProgressTaskId: taskId, setupDownloadStep: step });
}
Purpose: Detects which Playwright component is being downloaded for progress UI.
IPC API Wrapper
File: apps/desktop/src/renderer/lib/accomplish.ts
API Interface
classDiagram
class AccomplishAPI {
+getVersion() Promise~string~
+getPlatform() Promise~string~
+startTask(config) Promise~Task~
+cancelTask(taskId) Promise~void~
+interruptTask(taskId) Promise~void~
+getTask(taskId) Promise~Task~
+listTasks() Promise~Task[]~
+resumeSession(sessionId, prompt) Promise~Task~
+getApiKeys() Promise~ApiKeyConfig[]~
+addApiKey(provider, key) Promise~ApiKeyConfig~
+removeApiKey(id) Promise~void~
+onTaskUpdate(callback) () => void
+onPermissionRequest(callback) () => void
+onTaskProgress(callback) () => void
+logEvent(payload) Promise~unknown~
}
class Window {
+accomplish AccomplishAPI
+accomplishShell AccomplishShell
}
Window --> AccomplishAPI
Source: Lines 21-86
Safety Functions
Source: Lines 106-143
export function getAccomplish(): AccomplishAPI {
if (!window.accomplish) {
throw new Error('Accomplish API not available - not running in Electron');
}
return window.accomplish;
}
export function isRunningInElectron(): boolean {
return window.accomplishShell?.isElectron === true;
}
Purpose: Runtime checks prevent usage outside Electron context.
Page Components
Home Page
File: apps/desktop/src/renderer/pages/Home.tsx
graph TB
subgraph "Home Page Layout"
CENTER[Centered Container]
INPUT[TaskInputBar<br/>Main input]
RECENT[Recent Tasks<br/>Quick access]
end
subgraph "Interaction Flow"
SUBMIT[User submits prompt]
START[startTask action]
NAVIGATE[navigate to /execution/:id]
end
CENTER --> INPUT
CENTER --> RECENT
INPUT --> SUBMIT
SUBMIT --> START
START --> NAVIGATE
style CENTER fill:#e3f2fd
style INPUT fill:#fff3e0
Execution Page
File: apps/desktop/src/renderer/pages/Execution.tsx
graph TB
subgraph "Execution Page"
HEADER[Task Header<br/>Status, cancel, interrupt]
MESSAGES[Message List<br/>Chat history]
INPUT[Follow-up Input<br/>Continue conversation]
DEBUG[Debug Panel<br/>Optional, toggleable]
end
subgraph "Message Types"
USER[User messages]
ASST[Assistant responses]
TOOL[Tool use/results]
SCREEN[Screenshots]
end
subgraph "Event Subscriptions"
ON_UPDATE[onTaskUpdate]
ON_BATCH[onTaskUpdateBatch]
ON_PERM[onPermissionRequest]
ON_PROGRESS[onTaskProgress]
end
HEADER --> MESSAGES
MESSAGES --> INPUT
MESSAGES --> USER
MESSAGES --> ASST
MESSAGES --> TOOL
MESSAGES --> SCREEN
MESSAGES --> ON_UPDATE
MESSAGES --> ON_BATCH
ON_PERM --> HEADER
ON_PROGRESS --> HEADER
style HEADER fill:#e3f2fd
style MESSAGES fill:#f3e5f5
style DEBUG fill:#fff3e0
History Page
File: apps/desktop/src/renderer/pages/History.tsx
graph TB
subgraph "History Page"
LIST[Task List<br/>Chronological]
FILTER[Filters<br/>Status, date]
CLEAR[Clear History<br/>Button]
end
subgraph "List Item"
META[Prompt, status, date]
ACTIONS[View, Delete]
end
subgraph "Actions"
CLICK[Click to view]
DELETE[Delete task]
CONFIRM[Confirm dialog]
end
LIST --> FILTER
LIST --> CLEAR
LIST --> META
META --> ACTIONS
ACTIONS --> CLICK
ACTIONS --> DELETE
DELETE --> CONFIRM
style LIST fill:#e3f2fd
style META fill:#f3e5f5
Component Architecture
UI Component Library
graph TB
subgraph "shadcn/ui Components"
BUTTON[Button]
INPUT[Input, Textarea]
DIALOG[Dialog, AlertDialog]
DROP[DropdownMenu]
BADGE[Badge]
CARD[Card]
SCROLL[ScrollArea]
AVATAR[Avatar]
SEP[Separator]
LABEL[Label]
SKEL[Skeleton]
end
subgraph "Custom Components"
STREAM[StreamingText<br/>Typewriter effect]
WAIT[WaitingDetection<br/>Pause detection]
end
subgraph "Layout Components"
HEADER_L[Header<br/>Top bar]
SIDEBAR_L[Sidebar<br/>Navigation]
ITEM[ConversationListItem<br/>History item]
end
subgraph "Landing Components"
BAR[TaskInputBar<br/>Main input]
end
BUTTON --> HEADER_L
INPUT --> BAR
DIALOG --> HEADER_L
BADGE --> ITEM
CARD --> ITEM
SCROLL --> SIDEBAR_L
STREAM --> Execution
WAIT --> Execution
style BUTTON fill:#e3f2fd
style STREAM fill:#fff3e0
style HEADER_L fill:#f3e5f5
Streaming Text Component
File: apps/desktop/src/renderer/components/ui/streaming-text.tsx
Purpose: Implements typewriter effect for streaming AI responses.
Behavior:
- Buffers incoming text
- Reveals text character by character
- Handles markdown rendering
- Supports copy-to-clipboard
Waiting Detection
File: apps/desktop/src/renderer/lib/waiting-detection.ts
Purpose: Detects when AI is pausing during execution (e.g., waiting for browser load).
Algorithm: Monitors message timing patterns to detect intentional pauses vs. completion.
Event Flow: Task Execution
sequenceDiagram
participant User as User
participant Store as taskStore
participant API as accomplish.ts
participant Preload as Preload
participant Main as Main Process
User->>Store: startTask({prompt})
Store->>API: startTask(config)
API->>Preload: invoke('task:start')
Preload->>Main: IPC message
Main-->>Preload: Task object
Preload-->>API: Task
API-->>Store: Task
Store->>Store: set currentTask
loop Message stream
Main->>Preload: on('task:update')
Preload->>API: callback fires
API->>Store: addTaskUpdate(event)
Store->>Store: Update messages
end
loop Batched messages
Main->>Preload: on('task:update:batch')
Preload->>API: callback fires
API->>Store: addTaskUpdateBatch(event)
Store->>Store: Append all messages
end
Main->>Preload: on('task:progress')
Preload->>API: callback fires
API->>Store: Update progress UI
Performance Optimizations
1. Message Batching
Implementation: 50ms delay before sending batched messages
Benefit: Reduces state updates from N to 1 for rapid message streams
Source: taskStore.ts lines 355-376
2. Selective Rendering
Implementation: Only update currentTask if viewing the relevant task
Source: taskStore.ts lines 286-299
const isCurrentTask = state.currentTask?.id === event.taskId;
if (event.type === 'message' && event.message && isCurrentTask && state.currentTask) {
updatedCurrentTask = {
...state.currentTask,
messages: [...state.currentTask.messages, event.message],
};
}
3. Event Cleanup
Implementation: Cleanup functions for all event subscriptions
Pattern:
useEffect(() => {
const unsubscribe = window.accomplish.onTaskUpdate((event) => {
// handle event
});
return unsubscribe;
}, []);
Error Handling
Error Boundaries
Not explicitly implemented - errors propagate to component level
Error Display
In taskStore: Error state triggers error UI
Source: taskStore.ts lines 116-120
} catch (err) {
set({
error: err instanceof Error ? err.message : 'Failed to start task',
isLoading: false,
});
}
Error Recovery
User actions:
- Retry button on failed tasks
- Clear history to reset state
- Restart app for persistent issues
Routing Strategy
graph LR
subgraph "Hash Routes"
ROOT[/]
EXEC[/execution/:id]
end
subgraph "Route Handlers"
HOME_R[HomePage]
EXEC_R[ExecutionPage]
end
subgraph "Navigation"
SIDEBAR_CLICK[Sidebar item click]
PROG[Programmatic navigate]
end
ROOT --> HOME_R
EXEC --> EXEC_R
SIDEBAR_CLICK --> EXEC
PROG --> EXEC
style ROOT fill:#e3f2fd
style EXEC fill:#f3e5f5
Hash Router Benefits:
- Works with
file://protocol - No server configuration needed
- Deep linking supported
Summary
The renderer process is a modern React application with:
| Layer | Technology | Purpose |
|---|---|---|
| Routing | React Router HashRouter | File:// protocol compatibility |
| State | Zustand | Lightweight, simple state management |
| UI | shadcn/ui + Tailwind | Pre-built accessible components |
| IPC | accomplish.ts wrapper | Type-safe IPC communication |
| Animations | Framer Motion | Smooth page transitions |
Key Design Principles:
- Type Safety: Full TypeScript coverage
- Performance: Message batching, selective rendering
- User Experience: Streaming responses, progress indicators
- Error Handling: Graceful degradation, clear error messages
- Offline Capable: All UI bundled locally