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:

  1. Drag Region: Invisible top area for macOS window dragging (hiddenInset titlebar)
  2. Page Transitions: Framer Motion AnimatePresence with fadeUp variant
  3. Sidebar: Persistent navigation
  4. 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: true for running tasks
  • Sets isLoading: true for 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:

  1. Type Safety: Full TypeScript coverage
  2. Performance: Message batching, selective rendering
  3. User Experience: Streaming responses, progress indicators
  4. Error Handling: Graceful degradation, clear error messages
  5. Offline Capable: All UI bundled locally

Back to top

OpenWork Documentation - Community documentation for accomplish-ai/openwork