State Management Architecture
OpenWork uses a layered state management architecture with Zustand for runtime state and electron-store for persistence. The design prioritizes simplicity, type safety, and performance through targeted updates and message batching.
State Architecture Overview
graph TB
subgraph "Renderer State (Zustand)"
STORE[taskStore<br/>Runtime State]
end
subgraph "Persistence Layer (electron-store)"
SECURE[secure-storage<br/>Encrypted API Keys]
SETTINGS[app-settings<br/>Preferences]
HISTORY[task-history<br/>Task Records]
end
subgraph "Data Flow"
GETTER[Getters<br/>Read state]
SETTER[Setters<br/>Update state]
ACTION[Actions<br/>Async operations]
end
subgraph "IPC Bridge"
API[accomplish.ts<br/>IPC Wrapper]
end
STORE --> GETTER
STORE --> SETTER
STORE --> ACTION
ACTION --> API
API --> SECURE
API --> SETTINGS
API --> HISTORY
style STORE fill:#e3f2fd
style SECURE fill:#fff3e0
style SETTINGS fill:#f3e5f5
style HISTORY fill:#e8f5e9
Zustand Store Structure
File: apps/desktop/src/renderer/stores/taskStore.ts
State Schema
classDiagram
class TaskState {
+currentTask: Task | null
+isLoading: boolean
+error: string | null
+tasks: Task[]
+permissionRequest: PermissionRequest | null
+setupProgress: string | null
+setupProgressTaskId: string | null
+setupDownloadStep: number
+startTask(config)
+setSetupProgress(taskId, message)
+sendFollowUp(message)
+cancelTask()
+interruptTask()
+setPermissionRequest(request)
+respondToPermission(response)
+addTaskUpdate(event)
+addTaskUpdateBatch(event)
+updateTaskStatus(taskId, status)
+loadTasks()
+loadTaskById(taskId)
+deleteTask(taskId)
+clearHistory()
+reset()
}
class Task {
+id: string
+prompt: string
+status: TaskStatus
+sessionId?: string
+messages: TaskMessage[]
+createdAt: string
+startedAt?: string
+completedAt?: string
+result?: TaskResult
}
class TaskStatus {
<<enumeration>>
pending
queued
running
waiting_permission
completed
failed
cancelled
interrupted
}
TaskState --> Task
Task --> TaskStatus
Source: Lines 26-59
State Categories
graph TB
subgraph "Task State"
CURRENT[currentTask<br/>Currently viewing]
LIST[tasks<br/>Sidebar list]
STATUS[isLoading<br/>Operation state]
ERR[error<br/>Error message]
end
subgraph "Permission State"
PERM[permissionRequest<br/>Pending dialog]
end
subgraph "Setup State"
PROG[setupProgress<br/>Download message]
PROG_ID[setupProgressTaskId<br/>Task ID]
STEP[setupDownloadStep<br/>1=Chromium, 2=FFMPEG, 3=Shell]
end
subgraph "Actions"
TASK_OPS[startTask, sendFollowUp,<br/>cancelTask, interruptTask]
PERM_OPS[setPermissionRequest,<br/>respondToPermission]
UPDATE_OPS[addTaskUpdate,<br/>addTaskUpdateBatch,<br/>updateTaskStatus]
PERSIST[loadTasks, loadTaskById,<br/>deleteTask, clearHistory]
end
CURRENT --> TASK_OPS
LIST --> TASK_OPS
PERM --> PERM_OPS
PROG --> UPDATE_OPS
UPDATE_OPS --> TASK_OPS
PERSIST --> LIST
style CURRENT fill:#e3f2fd
style PERM fill:#fff3e0
style PROG fill:#f3e5f5
Persistence Layer
electron-store Instances
graph TB
subgraph "Store Files"
SECURE_FILE[secure-storage{-dev}.json<br/>userData/]
SETTINGS_FILE[app-settings.json<br/>userData/]
HISTORY_FILE[task-history.json<br/>userData/]
end
subgraph "Security Wrapper"
SECURE_WRAP[secureStorage.ts<br/>AES-256-GCM encryption]
end
subgraph "Direct Access"
SETTINGS_WRAP[appSettings.ts<br/>Plain JSON]
HISTORY_WRAP[taskHistory.ts<br/>Debounced writes]
end
SECURE_FILE --> SECURE_WRAP
SETTINGS_FILE --> SETTINGS_WRAP
HISTORY_FILE --> HISTORY_WRAP
SECURE_WRAP -->|getApiKey,<br/>storeApiKey| API_KEYS[API Keys]
SETTINGS_WRAP -->|get/setDebugMode,<br/>get/setOnboardingComplete| PREFS[Preferences]
HISTORY_WRAP -->|getTasks,<br/>saveTask,<br/>updateTaskStatus| TASKS[Tasks]
style SECURE_WRAP fill:#fff3e0
style SETTINGS_WRAP fill:#f3e5f5
style HISTORY_WRAP fill:#e8f5e9
Secure Storage (API Keys)
File: apps/desktop/src/main/store/secureStorage.ts
sequenceDiagram
participant Renderer as Renderer
participant IPC as IPC Handler
participant Secure as secureStorage
participant Crypto as Node.js crypto
participant Store as electron-store
Renderer->>IPC: api-key:set
IPC->>Secure: storeApiKey('anthropic', key)
Secure->>Crypto: getDerivedKey()
Crypto-->>Secure: 256-bit key
Secure->>Crypto: encryptValue(key)
Crypto->>Crypto: AES-256-GCM encrypt
Crypto-->>Secure: iv:authTag:ciphertext
Secure->>Store: Set 'apiKey:anthropic'
Store->>Store: Write to disk
Storage Format:
// In secure-storage.json
{
"values": {
"apiKey:anthropic": "base64iv:base64tag:base64ciphertext",
"apiKey:openai": "base64iv:base64tag:base64ciphertext"
},
"salt": "base64salt"
}
Source: Lines 148-154
App Settings (Preferences)
File: apps/desktop/src/main/store/appSettings.ts
Schema:
interface AppSettingsSchema {
debugMode: boolean;
onboardingComplete: boolean;
selectedModel: SelectedModel | null;
}
Source: Lines 7-14
Defaults (lines 16-26):
const appSettingsStore = new Store<AppSettingsSchema>({
name: 'app-settings',
defaults: {
debugMode: false,
onboardingComplete: false,
selectedModel: {
provider: 'anthropic',
model: 'anthropic/claude-opus-4-5',
},
},
});
Settings Operations:
| Operation | Handler | Purpose |
|---|---|---|
settings:debug-mode |
getDebugMode() |
Check if debug panel enabled |
settings:set-debug-mode |
setDebugMode(bool) |
Toggle debug mode |
settings:app-settings |
getAppSettings() |
Get all settings |
onboarding:complete |
getOnboardingComplete() |
Check onboarding status |
onboarding:set-complete |
setOnboardingComplete(bool) |
Set onboarding complete |
model:get |
getSelectedModel() |
Get AI model choice |
model:set |
setSelectedModel(model) |
Set AI model choice |
Task History (Tasks)
File: apps/desktop/src/main/store/taskHistory.ts
Schema:
interface StoredTask {
id: string;
prompt: string;
status: TaskStatus;
messages: TaskMessage[];
sessionId?: string;
createdAt: string;
startedAt?: string;
completedAt?: string;
}
interface TaskHistorySchema {
tasks: StoredTask[];
maxHistoryItems: number;
}
Source: Lines 7-21
Debounced Writes
Source: Lines 31-51
graph TB
subgraph "Write Operation"
MODIFY[Modify tasks array]
PENDING[pendingTasks = newTasks]
SCHEDULE[schedulePersist]
CHECK{timeout exists?}
WAIT[Wait 250ms]
FLUSH[flushPendingTasks]
WRITE[Write to electron-store]
CLEAR[Clear pending state]
end
MODIFY --> PENDING
PENDING --> SCHEDULE
SCHEDULE --> CHECK
CHECK -->|Yes| WAIT
CHECK -->|No| CREATE[Create timeout]
CREATE --> WAIT
WAIT --> FLUSH
FLUSH --> WRITE
WRITE --> CLEAR
style WAIT fill:#fff3e0
style WRITE fill:#e8f5e9
Purpose: Reduces disk I/O by batching rapid updates
Implementation (lines 39-51):
function schedulePersist(tasks: StoredTask[]): void {
pendingTasks = tasks;
if (persistTimeout) {
return; // Already scheduled
}
persistTimeout = setTimeout(() => {
if (pendingTasks) {
taskHistoryStore.set('tasks', pendingTasks);
pendingTasks = null;
}
persistTimeout = null;
}, PERSIST_DEBOUNCE_MS); // 250ms
}
Flush on Quit
Source: Lines 57-66
export function flushPendingTasks(): void {
if (persistTimeout) {
clearTimeout(persistTimeout);
persistTimeout = null;
}
if (pendingTasks) {
taskHistoryStore.set('tasks', pendingTasks);
pendingTasks = null;
}
}
Called from: apps/desktop/src/main/index.ts line 187
app.on('before-quit', () => {
flushPendingTasks();
disposeTaskManager();
});
State Update Patterns
Action Categories
graph TB
subgraph "User Actions"
START[startTask]
FOLLOW[sendFollowUp]
CANCEL[cancelTask]
INT[interruptTask]
end
subgraph "System Actions"
UPDATE[addTaskUpdate]
BATCH[addTaskUpdateBatch]
STATUS[updateTaskStatus]
PROG[setSetupProgress]
end
subgraph "Persistence Actions"
LOAD[loadTasks]
LOAD_ID[loadTaskById]
DELETE[deleteTask]
CLEAR[clearHistory]
end
subgraph "Permission Actions"
SET_PERM[setPermissionRequest]
RESP_PERM[respondToPermission]
end
START --> SYSTEM
FOLLOW --> SYSTEM
CANCEL --> SYSTEM
INT --> SYSTEM
style START fill:#e3f2fd
style BATCH fill:#fff3e0
style LOAD fill:#f3e5f5
Task Start Flow
Source: Lines 91-128
sequenceDiagram
participant UI as Component
participant Store as taskStore
participant API as accomplish.ts
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
Note over Store: Update state
Store->>Store: currentTask = task
Store->>Store: tasks = [task, ...oldTasks]
Store->>Store: isLoading = (status === 'queued')
Store->>API: logEvent({status})
Store-->>UI: task | null
Key Behaviors:
- Optimistic UI: Updates state immediately
- Queue Handling: Keeps loading state if queued
- List Update: Adds to sidebar immediately
- Error Handling: Sets error message on failure
Message Update Flow
Source: Lines 277-353
stateDiagram-v2
[*] --> Receive: addTaskUpdate(event)
Receive --> CheckTask: Is current task?
CheckTask -->|No| Skip: No update needed
CheckTask -->|Yes| CheckType: Check event type
CheckType --> Message: type === 'message'
CheckType --> Complete: type === 'complete'
CheckType --> Error: type === 'error'
Message --> Append: Append message
Append --> [*]
Complete --> MapStatus: Map result status
MapStatus --> UpdateTask: Update currentTask
MapStatus --> UpdateList: Update tasks list
UpdateTask --> [*]
UpdateList --> [*]
Error --> SetFailed: Set status = 'failed'
SetFailed --> UpdateCurrent: Update currentTask
UpdateCurrent --> [*]
Skip --> [*]
Selective Updates (lines 286-299):
// Determine if this event is for the currently viewed task
const isCurrentTask = state.currentTask?.id === event.taskId;
// Handle message events - only if viewing this task
if (event.type === 'message' && event.message && isCurrentTask && state.currentTask) {
updatedCurrentTask = {
...state.currentTask,
messages: [...state.currentTask.messages, event.message],
};
}
Benefit: Prevents unnecessary re-renders when viewing other tasks
Batch Update Flow
Source: Lines 355-376
graph LR
subgraph "Single Message Flow"
M1[Message 1] --> STATE1[State Update]
M2[Message 2] --> STATE2[State Update]
M3[Message 3] --> STATE3[State Update]
STATE1 --> RENDER1[Re-render]
STATE2 --> RENDER2[Re-render]
STATE3 --> RENDER3[Re-render]
end
subgraph "Batched Flow"
M4[Message 1] --> BUFFER[Buffer 50ms]
M5[Message 2] --> BUFFER
M6[Message 3] --> BUFFER
BUFFER --> STATE[Single State Update]
STATE --> RENDER[One Re-render]
end
style STATE1 fill:#ffebee
style STATE2 fill:#ffebee
style STATE3 fill:#ffebee
style STATE fill:#e8f5e9
Implementation (lines 356-375):
addTaskUpdateBatch: (event: TaskUpdateBatchEvent) => {
set((state) => {
if (!state.currentTask || state.currentTask.id !== event.taskId) {
return state;
}
// Add all messages in a single state update
const updatedTask = {
...state.currentTask,
messages: [...state.currentTask.messages, ...event.messages],
};
return { currentTask: updatedTask, isLoading: false };
});
}
Event Subscriptions
Global Subscriptions
Source: Lines 440-465
graph TB
subgraph "Module Load"
INIT[Module initializes]
CHECK{window.accomplish?}
end
subgraph "Subscriptions"
PROG_SUB[onTaskProgress]
UPDATE_SUB[onTaskUpdate]
end
subgraph "Handlers"
PROG_H[Handle progress]
UPDATE_H[Handle completion]
end
INIT --> CHECK
CHECK -->|Yes| PROG_SUB
CHECK -->|Yes| UPDATE_SUB
PROG_SUB --> PROG_H
UPDATE_SUB --> UPDATE_H
PROG_H --> CLEAR[Clear progress on complete]
UPDATE_H --> CLEAR
style INIT fill:#e3f2fd
style PROG_SUB fill:#fff3e0
style UPDATE_SUB fill:#f3e5f5
Purpose: Capture events even before components mount
Setup Progress Handler (lines 443-453):
window.accomplish.onTaskProgress((progress: unknown) => {
const event = progress as SetupProgressEvent;
if (event.message) {
// Clear progress if installation completed
if (event.message.toLowerCase().includes('installed successfully')) {
useTaskStore.getState().setSetupProgress(null, null);
} else {
useTaskStore.getState().setSetupProgress(event.taskId, event.message);
}
}
});
Completion Handler (lines 456-464):
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);
}
}
});
Status Management
Task Status States
stateDiagram-v2
[*] --> Pending: Task created
Pending --> Queued: At max concurrent tasks
Pending --> Running: Task starts
Queued --> Running: Slot available
Running --> WaitingPermission: Permission needed
WaitingPermission --> Running: Permission granted
Running --> Completed: Success
Running --> Failed: Error
Running --> Interrupted: User interrupted
Running --> Cancelled: User cancelled
Queued --> Cancelled: User cancelled
Interrupted --> Running: User sends follow-up
Completed --> [*]
Failed --> [*]
Cancelled --> [*]
Interrupted --> [*]
Status Transitions (source: packages/shared/src/types/task.ts lines 5-13):
| Status | Description | Can Transition To |
|---|---|---|
| pending | Initial state | queued, running |
| queued | Waiting for slot | running, cancelled |
| running | Executing | completed, failed, interrupted, cancelled |
| waiting_permission | Awaiting user input | running, cancelled |
| completed | Success | - |
| failed | Error | - |
| cancelled | User cancelled | - |
| interrupted | Paused (Ctrl+C) | running |
Status Update Handler
Source: Lines 379-399
updateTaskStatus: (taskId: string, status: TaskStatus) => {
set((state) => {
// Update in tasks list
const updatedTasks = state.tasks.map((task) =>
task.id === taskId
? { ...task, status, updatedAt: new Date().toISOString() }
: task
);
// Update currentTask if it matches
const updatedCurrentTask =
state.currentTask?.id === taskId
? { ...state.currentTask, status, updatedAt: new Date().toISOString() }
: state.currentTask;
return {
tasks: updatedTasks,
currentTask: updatedCurrentTask,
};
});
}
Permission State
Permission Request Flow
sequenceDiagram
participant Main as Main Process
participant Store as taskStore
participant UI as Permission Dialog
participant User as User
Main->>Store: setPermissionRequest(request)
Store->>Store: set({permissionRequest: request})
Store->>UI: Re-render with dialog
UI->>User: Show permission request
User->>UI: Select option
UI->>Store: respondToPermission({decision, options})
Store->>Main: permission:respond IPC
Main->>Store: set({permissionRequest: null})
Source: Lines 262-275
setPermissionRequest: (request) => {
set({ permissionRequest: request });
},
respondToPermission: async (response: PermissionResponse) => {
const accomplish = getAccomplish();
await accomplish.respondToPermission(response);
set({ permissionRequest: null });
}
Performance Optimizations
1. Message Batching
Problem: Rapid message streams cause excessive re-renders
Solution: Batch messages within 50ms window
Benefit: Reduces state updates from N to 1
Source: Lines 355-376
2. Selective Updates
Problem: All messages update state regardless of visibility
Solution: Check if message is for current task
Benefit: Avoids unnecessary re-renders
Source: Lines 286-299
3. Debounced Persistence
Problem: Every message writes to disk
Solution: Batch writes with 250ms debounce
Benefit: Reduces disk I/O significantly
Source: apps/desktop/src/main/store/taskHistory.ts lines 31-51
4. Global Event Capture
Problem: Events lost before component mount
Solution: Subscribe at module load time
Benefit: No events missed
Source: Lines 440-465
State Hydration
Initial Load
sequenceDiagram
participant App as App Component
participant Store as taskStore
participant API as accomplish.ts
participant IPC as Main Process
App->>App: useEffect(() => {})
App->>Store: loadTasks()
Store->>API: listTasks()
API->>IPC: ipcRenderer.invoke('task:list')
IPC-->>API: Task[] from history
API-->>Store: tasks
Store->>Store: set({tasks})
Store-->>App: Tasks loaded
Source: Lines 401-405
loadTasks: async () => {
const accomplish = getAccomplish();
const tasks = await accomplish.listTasks();
set({ tasks });
}
Task by ID
Source: Lines 407-411
loadTaskById: async (taskId: string) => {
const accomplish = getAccomplish();
const task = await accomplish.getTask(taskId);
set({ currentTask: task, error: task ? null : 'Task not found' });
}
Error State Management
Error Handling Pattern
Source: Lines 116-127, 209-224
graph TB
subgraph "Error Flow"
OP[Async Operation]
TRY[try-catch]
SUCCESS[Success path]
FAIL[Error path]
SET_ERR[Set error state]
LOG[Log error]
CLEAR_ERR[Clear on retry]
end
OP --> TRY
TRY --> SUCCESS
TRY -->|throw| FAIL
FAIL --> SET_ERR
SET_ERR --> LOG
SET_ERR --> UI[Display error]
UI --> CLEAR_ERR
CLEAR_ERR --> OP
style FAIL fill:#ffebee
style SET_ERR fill:#fff3e0
Example (startTask error handling):
} catch (err) {
set({
error: err instanceof Error ? err.message : 'Failed to start task',
isLoading: false,
});
void accomplish.logEvent({
level: 'error',
message: 'UI task start failed',
context: { error: err instanceof Error ? err.message : String(err) },
});
return null;
}
Summary
OpenWork’s state management provides:
| Layer | Technology | Purpose |
|---|---|---|
| Runtime State | Zustand | Fast, simple state management |
| API Keys | AES-256-GCM encrypted store | Secure credential storage |
| Settings | electron-store (plain) | User preferences |
| Task History | electron-store (debounced) | Persistent task records |
| IPC Bridge | accomplish.ts | Type-safe IPC wrapper |
Key Design Principles:
- Simplicity: Zustand over Redux for minimal boilerplate
- Type Safety: Full TypeScript coverage
- Performance: Message batching, selective updates
- Reliability: Debounced persistence, flush on quit
- User Experience: Optimistic UI, error boundaries