State Management
Relevant source files
The following files were used as context for generating this wiki page:
State management in Opcode is designed to bridge a React-based frontend with a Rust-based backend (Tauri or Axum). The system centralizes global state using Zustand to eliminate prop drilling and handle real-time updates from Claude Code processes. This architecture manages project metadata, interactive coding sessions, and background agent executions.
The system distinguishes between UI-driven state (managed in stores) and persistent data (managed via SQLite in the backend). Real-time state updates are pushed from the backend via WebSockets or Tauri events and consumed by specialized stores to update the interface reactively.
Sources: src/stores/README.md:3-8, src/stores/sessionStore.ts:1-10, web_server.design.md:7-14
Architecture and Data Flow
The architecture follows a unidirectional data flow where the frontend stores dispatch actions to the API, which interacts with the Rust backend. The backend, in turn, broadcasts events back to the frontend to update the stores.
Global Store Pattern
Opcode utilizes specialized Zustand stores to manage different domains of the application. These stores use the subscribeWithSelector middleware for optimized performance, allowing components to listen only to specific slices of state.
The diagram shows how state flows from user actions to backend processes and back via events.
Sources: src/stores/sessionStore.ts:39-160, src/stores/agentStore.ts:38-160, web_server.design.md:46-52
Session Management Store
The sessionStore is responsible for managing the lifecycle of interactive Claude Code sessions and project browsing. It keys sessions by their project ID and maintains a record of real-time outputs.
Store State Definition
| Field | Type | Description |
|---|---|---|
projects | Project[] | List of available project directories. |
sessions | Record<string, Session[]> | Sessions grouped by Project ID. |
currentSession | Session | null | The currently active interactive session. |
sessionOutputs | Record<string, string> | Real-time terminal output indexed by Session ID. |
isLoadingProjects | boolean | Global loading state for project fetching. |
Sources: src/stores/sessionStore.ts:7-25
Core Logic
The store handles the transition between the file system view and active session view. When a session is updated via a background event, handleSessionUpdate performs an immutable update on the session list and the current session if the IDs match.
handleSessionUpdate: (session: Session) => {
set(state => {
const projectId = session.project_id;
const projectSessions = state.sessions[projectId] || [];
const existingIndex = projectSessions.findIndex((s) => s.id === session.id);
let updatedSessions;
if (existingIndex >= 0) {
updatedSessions = [...projectSessions];
updatedSessions[existingIndex] = session;
} else {
updatedSessions = [session, ...projectSessions];
}
return {
sessions: { ...state.sessions, [projectId]: updatedSessions },
currentSession: state.currentSessionId === session.id ? session : state.currentSession
};
});
}
Sources: src/stores/sessionStore.ts:124-148
Agent Execution Store
The agentStore manages the state of autonomous agents. Unlike interactive sessions, agents are often long-running background processes, requiring polling and specialized metric tracking.
State and Actions
The store tracks a Set of runningAgents to facilitate UI indicators and manage polling logic. It includes an automated polling mechanism that fetches updates every 3000ms if active agents exist.
The sequence shows the transition from agent creation to the status polling loop.
Sources: src/stores/agentStore.ts:7-36, src/stores/agentStore.ts:143-165
Backend Persistence and Metrics
While the frontend manages volatile UI state, the Rust backend manages persistent state in SQLite and process-level metrics.
Database Schema
The backend uses SQLite to store agents and their execution history. The agent_runs table tracks the link between an agent configuration and a specific Claude session.
| Table | Column | Type | Constraints |
|---|---|---|---|
agents | id | INTEGER | PRIMARY KEY |
agents | system_prompt | TEXT | NOT NULL |
agent_runs | status | TEXT | DEFAULT 'pending' |
agent_runs | session_id | TEXT | UUID from Claude |
agent_runs | pid | INTEGER | Process ID |
Sources: src-tauri/src/commands/agents.rs:136-180
Metrics Calculation
The backend derives stateful metrics (tokens, cost, duration) by parsing the JSONL stream generated by the Claude process. This data is merged into the AgentRunWithMetrics structure before being sent to the frontend stores.
pub fn from_jsonl(jsonl_content: &str) -> Self {
let mut total_tokens = 0i64;
let mut cost_usd = 0.0f64;
// ... logic to iterate lines and aggregate usage objects
Self {
duration_ms,
total_tokens: if total_tokens > 0 { Some(total_tokens) } else { None },
cost_usd: if cost_usd > 0.0 { Some(cost_usd) } else { None },
// ...
}
}
Sources: src-tauri/src/commands/agents.rs:82-126
Event-Driven State Synchronization
Opcode uses an event-based system to synchronize state across the bridge. In Tauri mode, it uses the Tauri event bus; in Web mode, it uses standard DOM events.
- System Initialization: The backend emits a
system:initmessage containing thesession_idandcwd. - Output Streaming: As Claude produces output, the backend emits
claude-outputoragent-output. - UI Consumption:
ClaudeCodeSession.tsxlistens for these events and updates the local message array or dispatches to a Zustand store.
const listen = tauriListen || ((eventName: string, callback: (event: any) => void) => {
const domEventHandler = (event: any) => {
callback({ payload: event.detail });
};
window.addEventListener(eventName, domEventHandler);
return Promise.resolve(() => window.removeEventListener(eventName, domEventHandler));
});
Sources: web_server.design.md:25-33, src/components/ClaudeCodeSession.tsx:28-44, src-tauri/src/commands/agents.rs:518-540
Conclusion
State management in Opcode is a multi-layered system that combines the reactive capabilities of Zustand on the frontend with a robust, process-aware Rust backend. By separating UI state (stores), persistent state (SQLite), and transient process state (JSONL streams), the application maintains a consistent and performant experience even during complex, high-token AI interactions.
Sources: src/stores/README.md:10-15, web_server.design.md:155-170