> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tempestai.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# State Management and Runtime Architecture

> Complete guide to Tempest's frontend and backend state management, persistence, and synchronization

## Overview

Tempest uses a hybrid state management system: the React frontend manages UI state through custom hooks and stores, while the Rust backend manages PTY sessions and git operations. The two layers synchronize through Tauri IPC commands and persist state to disk for recovery across app restarts.

## Frontend State Management

### Architecture

Tempest does not use Redux, Zustand, or Jotai. Instead, it uses a **custom store pattern** built on React's `useSyncExternalStore` hook combined with Node.js event emitter patterns. Each store is a self-contained module with:

* An internal data structure (Map, object, or class)
* Direct read/write functions (`get*`, `set*`)
* React hooks (`use*`) that subscribe to changes via `useSyncExternalStore`
* Optional localStorage fallback for persistence

This approach keeps store code simple and dependency-free while ensuring components re-render only when their subscribed state changes.

### Runtime State (Central Persistence)

**File:** `src/lib/runtimeState.ts`

The single source of truth for all persisted state. Stored in `<app-data-dir>/runtime-state.json` and loaded on app startup via `loadRuntimeState()`.

```typescript theme={null}
export interface RuntimeState {
  version: number;                          // Schema version for migrations
  sessions: Record<string, WorktreeSession>;   // All open worktree/root sessions
  openProjects: StoredProject[];            // Open projects in sidebar
  recents: RecentWorkspace[];               // Recently opened projects
  settings: Partial<AppSettings>;           // Terminal and app settings
  keybindings: Partial<Record<ActionId, Shortcut | null>>;  // Custom key bindings
  attribution: boolean;                     // Co-author hook enabled flag
  migrations: Record<string, boolean>;      // Migration tracking
  tabs: PersistedTab[];                     // Non-terminal tabs (diff, preview, editor)
  sessionOrder: string[];                   // instanceIds in tab-bar order
  activeInstanceId: string | null;          // Last focused session's instanceId
  prompts: Array<...>;                      // Custom and builtin prompts
  atlasProjects: Record<string, boolean>;   // Project codebase indexing decisions
}
```

**Key functions:**

| Function                 | Purpose                                                                      |
| ------------------------ | ---------------------------------------------------------------------------- |
| `loadRuntimeState()`     | Reads runtime-state.json from disk, falls back to localStorage for migration |
| `getRuntimeState()`      | Returns current in-memory state                                              |
| `setRuntimeState(patch)` | Merges patch into state and persists atomically to disk                      |
| `persist()`              | Calls `invoke("write_runtime_state", ...)` to write to disk                  |

All stores read from `getRuntimeState()` on access, so changes are immediately visible across the app.

## Store Modules

### 1. Work State (`src/store/workState.ts`)

Ephemeral per-session work status, never persisted. Detects when an agent finishes executing.

**State held:**

* Map of sessionId -> `"idle" | "working" | "done"`
* Separate listener sets per session (prevents O(N²) re-renders)

**Public API:**

| Function                         | Purpose                                                    |
| -------------------------------- | ---------------------------------------------------------- |
| `getWorkState(sessionId)`        | Returns current state (defaults to "idle")                 |
| `setWorkState(sessionId, state)` | Updates state and notifies all listeners                   |
| `clearWorkState(sessionId)`      | Deletes session from tracking                              |
| `useWorkState(sessionId)`        | React hook, subscribes to one session                      |
| `useWorkStateVersion()`          | React hook, incrementing version for cross-session sorting |

**Used by:** TerminalPane to update work badges, Sidebar to sort sessions by activity.

### 2. Worktree Sessions (`src/store/sessions.ts`)

Stores metadata for all terminal and agent sessions running in worktrees or project roots.

**Session schema:**

```typescript theme={null}
export interface WorktreeSession {
  name: string;                 // User-facing session name
  agent?: string;               // CLI command hint ("claude", "gemini", etc)
  conversationId?: string;      // Claude API conversation UUID for --resume
  instanceId?: string;          // Permanent PTY session ID (minted once)
  projectId: string;            // Project this session belongs to
  closed?: boolean;             // Soft-closed (tab closed but can re-open)
  isRootSession?: boolean;      // true = opened in project root (no worktree)
  noGit?: boolean;              // true = user chose to proceed without git
}
```

**Storage key structure:**

* Worktree sessions: keyed by worktree path (e.g., `/path/to/project/.tempest/my-workspace`)
* Root sessions: keyed by `<projectPath>::root::<sessionId>` (ROOT\_SESSION\_PREFIX) to avoid collisions
* Multiple root sessions can coexist in the same project

**Public API:**

| Function                                 | Purpose                                                    |
| ---------------------------------------- | ---------------------------------------------------------- |
| `getWorktreeSession(worktreePath)`       | Fetch session by path                                      |
| `rootSessionKey(projectPath, sessionId)` | Build a root-session store key                             |
| `isRootSessionKey(key)`                  | Check if a key is a root session                           |
| `rootSessionIdFromKey(key)`              | Extract sessionId from a root-session key                  |
| `getRootSessionsForProject(projectPath)` | Get all root sessions for a project                        |
| `saveWorktreeSession(path, session)`     | Persist a session                                          |
| `removeWorktreeSession(path)`            | Delete a session                                           |
| `markWorktreeSessionClosed(path)`        | Soft-close (set closed=true)                               |
| `markWorktreeSessionOpen(path)`          | Re-open (set closed=false)                                 |
| `dedupeRootSessions(projectPath)`        | Merge duplicate root sessions from the pre-fix restore bug |
| `pruneOrphanedSessions(validPaths)`      | Remove sessions for deleted projects                       |

### 3. App Settings (`src/store/appSettings.ts`)

Terminal appearance and behavior settings, merged with hardcoded defaults.

```typescript theme={null}
export interface AppSettings {
  terminalFontSize: number;            // Default: 13
  terminalFontFamily: string;          // Default: "Geist Mono"
  terminalCursorStyle: "block" | "bar" | "underline";  // Default: "block"
  terminalCursorBlink: boolean;        // Default: true
  terminalScrollback: number;          // Default: 1000
  sidebarFontSize: number;             // Default: 14
  branchPrefix: string;                // Auto-prefix for new branches
  commitMessageTemplate: string;       // Default: "Agent work"
  atlasEnabled: boolean;               // Enable codebase indexing
  atlasAutoIndex: boolean;             // Auto-index new projects
}
```

**Public API:**

| Function                    | Purpose                                             |
| --------------------------- | --------------------------------------------------- |
| `getSettings()`             | Fetch merged settings (defaults + runtime override) |
| `updateSetting(key, value)` | Update a single setting                             |
| `useSettings()`             | React hook, subscribes to changes                   |

### 4. Keybindings (`src/store/keybindings.ts`)

Customizable keyboard shortcuts, merged with ACTION\_DEFS defaults.

```typescript theme={null}
export type ActionId =
  | "toggleTheme" | "openSettings" | "toggleLeftSidebar"
  | "toggleRightSidebar" | "openProject" | "newWorkspace"
  | "closeTab" | "nextTab" | "prevTab" | "broadcast"
  | "splitPaneV" | "splitPaneH" | "openQueue";

export interface Shortcut {
  key: string;      // e.g., "T", "\\", "ArrowUp"
  ctrl: boolean;
  shift: boolean;
  alt: boolean;
}
```

Defaults are defined in `DEFAULTS` record and merged at access time. Custom bindings override defaults.

**Public API:**

| Function                                | Purpose                                    |
| --------------------------------------- | ------------------------------------------ |
| `getBindings()`                         | Return merged bindings (defaults + custom) |
| `setBinding(action, shortcut)`          | Override a keybinding                      |
| `resetBinding(action)`                  | Revert to default                          |
| `resetAllBindings()`                    | Clear all customizations                   |
| `useKeybindings()`                      | React hook, subscribes to all bindings     |
| `matchesEvent(shortcut, KeyboardEvent)` | Check if an event matches a shortcut       |
| `formatShortcut(shortcut)`              | Format for display (e.g., "Ctrl+Shift+T")  |
| `shortcutFromEvent(e)`                  | Extract shortcut from KeyboardEvent        |

### 5. Attribution (`src/store/attribution.ts`)

Simple flag for whether co-author attribution hook is enabled.

**Co-author line:**

```
Co-authored-by: Tempest <tempestai.dev@gmail.com>
```

**Public API:**

| Function                | Purpose        |
| ----------------------- | -------------- |
| `getAttribution()`      | Boolean flag   |
| `setAttribution(value)` | Enable/disable |
| `useAttribution()`      | React hook     |

### 6. Open Projects (`src/store/openProjects.ts`)

List of projects shown in the sidebar.

```typescript theme={null}
export interface StoredProject {
  id: string;         // UUID
  name: string;       // Display name
  path: string;       // File system path
  expanded: boolean;  // Sidebar tree expansion state
}
```

**Public API:**

| Function                     | Purpose             |
| ---------------------------- | ------------------- |
| `getOpenProjects()`          | Fetch list          |
| `saveOpenProjects(projects)` | Replace entire list |

### 7. Recent Workspaces (`src/store/recents.ts`)

Ordered list of recently opened projects.

```typescript theme={null}
export interface RecentWorkspace {
  id: string;
  name: string;
  path: string;
  lastOpened: string;  // ISO 8601 timestamp
}
```

**Public API:**

| Function             | Purpose                    |
| -------------------- | -------------------------- |
| `getRecents()`       | Fetch recent list (max 50) |
| `addRecent(ws)`      | Add or move to top         |
| `removeRecent(path)` | Remove a recent            |

### 8. Message Queue (`src/store/messageQueue.ts`)

Ephemeral queue for messages displayed in a UI popover, never persisted.

```typescript theme={null}
export interface QueueItem {
  id: string;
  text: string;
}
```

Per-session queues: each session can have its own queue independently.

**Public API:**

| Function                             | Purpose                         |
| ------------------------------------ | ------------------------------- |
| `enqueue(sessionId, text)`           | Add message                     |
| `dequeue(sessionId)`                 | Remove and return first message |
| `removeFromQueue(sessionId, itemId)` | Remove specific item            |
| `clearQueue(sessionId)`              | Empty queue                     |
| `getQueue(sessionId)`                | Fetch messages                  |
| `useQueue(sessionId)`                | React hook                      |

### 9. Prompts (`src/store/prompts.ts`)

Builtin and user-defined prompts for common tasks (Review, Tests, Security, Explain, Commit).

```typescript theme={null}
export interface PromptEntry {
  id: string;
  title: string;
  body: string;
  enabled: boolean;
  isBuiltin: boolean;
}
```

Builtin prompts are hardcoded and auto-inserted if missing (supports new builtins in app updates). Custom prompts have `id: "custom-<uuid>"`.

**Public API:**

| Function                             | Purpose                                      |
| ------------------------------------ | -------------------------------------------- |
| `getPrompts()`                       | Fetch all prompts (loaded from runtimeState) |
| `usePrompts()`                       | React hook                                   |
| `addPrompt(title, body)`             | Create custom prompt                         |
| `updatePrompt(id, patch)`            | Modify title/body/enabled                    |
| `deletePrompt(id)`                   | Delete custom prompt or disable builtin      |
| `resetPrompt(id)`                    | Restore builtin to defaults                  |
| `clonePrompt(id)`                    | Create copy of a prompt                      |
| `reorderPrompts(fromId, toId, side)` | Drag-and-drop reordering                     |
| `isBuiltinModified(prompt)`          | Check if builtin differs from default        |

### 10. Session Manager (`src/store/sessionManager.ts`)

Singleton that detects when agent sessions finish by monitoring PTY output, escape sequences, and terminal title changes. Ephemeral, never persisted.

**Detection signals monitored:**

| Signal                                        | Meaning                                                     |
| --------------------------------------------- | ----------------------------------------------------------- |
| OSC 9 (iTerm notification)                    | Agent turn complete                                         |
| `OSC 9;4;<state>` (Windows Terminal progress) | State 0 = done, 1-4 = working                               |
| OSC 133 B,D (FinalTerm shell-integration)     | Prompt ready, command finished                              |
| OSC 777 (rxvt-unicode)                        | Attention/turn complete                                     |
| DEC mode 2004 (bracketed paste)               | Enabled = prompt ready, disabled = busy                     |
| DEC mode 1049 (alt screen buffer)             | Leaving alt screen = app exited                             |
| Title regex (agent-specific)                  | Claude: "✳" idle, spinner busy; Gemini: "◇" idle, "✦✋" busy |

**Heuristic timers:**

* **QUIET\_MS (5000ms):** If no output received for 5s, mark done (unless title says busy or insufficient bytes received)
* **TURN\_STARTED\_BYTES (200):** Minimum bytes required before heuristic timers fire (gates false positives during thinking window)
* **DEAD\_ZONE\_MS (300ms):** After user presses Enter, suppress done signals to avoid echo false-positives
* **CEIL\_MS (12000ms):** Hard ceiling; if still working after 12s, force done (safety net for keepalive loops)

**Architecture:**

```typescript theme={null}
class SessionManager {
  register(sessionId, channel, isAgent, onDone?, onChunk?, agentHint?)
  unregister(sessionId)
  attach(sessionId, onData)        // Called by TerminalPane on mount
  detach(sessionId, onData)        // Called by TerminalPane on unmount
  markUserInput(sessionId)         // Called when user presses Enter
  updateTitle(sessionId, title)    // Called by term.onTitleChange()
  setOnChunk(sessionId, onChunk)   // Update per-chunk callback
}

export const sessionManager = new SessionManager();
```

### 11. Runtime State (Lib) (`src/lib/runtimeState.ts`)

Central reader/writer for all persisted state. Handles localStorage migration on first load.

**Initialization:**

```typescript theme={null}
export async function loadRuntimeState(): Promise<void>
```

On first app launch, tries to read from `write_runtime_state` (Rust), then falls back to localStorage keys:

| localStorage key            | RuntimeState field |
| --------------------------- | ------------------ |
| `tempest-worktree-sessions` | `sessions`         |
| `tempest-open-projects`     | `openProjects`     |
| `tempest-recents`           | `recents`          |
| `tempest-app-settings`      | `settings`         |
| `tempest-keybindings`       | `keybindings`      |
| `tempest-attribution`       | `attribution`      |

After migration, all state moves to disk and localStorage is no longer used.

**Persistence:**

Every call to `setRuntimeState(patch)` immediately:

1. Merges patch into in-memory `_state`
2. Calls `persist()` which invokes `write_runtime_state` over Tauri IPC
3. Waits for Rust to write atomically (temp file, then rename)

This ensures state survives app crashes.

## Backend State (Rust)

### Architecture

The Rust backend (`src-tauri/src/lib.rs`) uses Tauri's state management system with two main persistent structures:

| Structure                                            | Purpose                                |
| ---------------------------------------------------- | -------------------------------------- |
| `PtyState(Arc<DashMap<String, Arc<PtySession>>>)`    | Active PTY sessions (shell processes)  |
| `ZenState(Mutex<HashMap<String, (String, String)>>)` | Zen window (secondary window) metadata |

No other state is kept in memory; all config flows through Frontend -> Tauri command -> Rust subprocess.

### PtyState (Concurrent PTY Registry)

```rust theme={null}
pub struct PtyState(pub(crate) Arc<DashMap<String, Arc<PtySession>>>);

struct PtySession {
  writer: Mutex<Box<dyn Write + Send>>,           // stdin to PTY
  master: Mutex<Box<dyn portable_pty::MasterPty + Send>>,  // master side of PTY
  _slave: Mutex<Box<dyn portable_pty::SlavePty + Send>>,   // slave side (slave end of PTY)
  child:  Mutex<Box<dyn portable_pty::Child + Send + Sync>>, // shell process
  cwd: String,                                    // working directory
}
```

**DashMap:** Lock-free concurrent HashMap from the `dashmap` crate. Allows multiple threads to read/write simultaneously without global locks. Each PTY session is wrapped in Arc for cheap cloning across threads.

**Lifecycle:**

1. Frontend calls `create_pty_session(...)` with a unique `session_id`
2. Rust spawns PTY, starts background reader thread
3. PTY is inserted into DashMap under `session_id`
4. Frontend can then call `write_to_pty(session_id, data)` or `resize_pty(session_id, ...)`
5. When tab closes, frontend calls `close_pty_session(session_id)` or `close_and_remove_worktree(session_id, ...)`
6. Rust removes entry from DashMap, kills child process, waits for exit, cleans up

**Process tree termination:**

On Windows, `portable-pty` creates a job object that includes the shell and all descendants (agent CLI process, npm, git, etc). Killing the job kills everything atomically.

On Unix, killing the shell process sends SIGTERM to its children. Any orphaned agent processes are force-killed via `kill -9` if `.tempest-pid` persists across app restart.

### ZenState (Secondary Windows)

```rust theme={null}
pub struct ZenState(pub Mutex<std::collections::HashMap<String, (String, String)>>);
```

Maps window label (e.g., `"zen-1234567890"`) to `(project_path, project_name)`. Used to initialize the React component in a secondary zen window.

Populated by `open_zen_window()`, read by `get_zen_config()`, cleared when the window closes.

## State Synchronization

### Frontend to Backend

**Pattern:** Frontend -> `invoke(command, params)` -> Rust -> returns result

All backend operations are stateless: Rust doesn't cache anything. The frontend sends complete information (paths, settings) on each command.

**Examples:**

| Frontend                           | Rust Backend               | State Modified           |
| ---------------------------------- | -------------------------- | ------------------------ |
| `createWorktree(path, name)`       | `create_terminal_worktree` | Worktree dir created     |
| `createPtySession(...)`            | `create_pty_session`       | PTY added to DashMap     |
| `writeToTerminal(sessionId, data)` | `write_to_pty`             | Data sent to PTY stdin   |
| `closePtySession(sessionId)`       | `close_pty_session`        | PTY removed from DashMap |

### Backend to Frontend

**Pattern 1: Poll-based (Tauri commands)**

Frontend calls Rust command, waits for response:

```typescript theme={null}
const branch = await invoke<string>("get_git_branch", { path });
```

**Pattern 2: Push-based (Event channels)**

PTY output flows via a dedicated `Channel<PtyOutputPayload>` passed at PTY creation:

```typescript theme={null}
const channel = new Channel<{ session_id: string; data: string }>();
channel.onmessage = (event) => {
  const { session_id, data } = event.payload;
  // Update terminal state
};
await invoke("create_pty_session", {
  session_id,
  cwd,
  rows, cols,
  command, args,
  on_event: channel,
});
```

Rust thread reads from PTY master and sends each chunk as `PtyOutputPayload`, O(1) directly to this session's channel (no broadcast, no filtering).

### Storage Layer

**Frontend persistence:**

* All frontend state lives in `RuntimeState` (typed, centralized)
* Persisted to `<app-data-dir>/runtime-state.json` atomically via `write_runtime_state`
* Loaded on app startup via `loadRuntimeState`
* Falls back to localStorage for migration from older app versions

**Backend persistence:**

* No persistent backend state (stateless design)
* Git state is stored in `.git/` directories (managed by git CLI)
* PTY session state exists only while the process runs
* Process IDs persisted to `.tempest-pid` sidecar files inside worktrees for crash recovery

## localStorage Keys (Deprecated)

For reference, these are no longer used (migrated to RuntimeState):

| Key                         | Type              | Purpose               |
| --------------------------- | ----------------- | --------------------- |
| `tempest-worktree-sessions` | JSON Record       | All worktree sessions |
| `tempest-open-projects`     | JSON array        | Sidebar projects      |
| `tempest-recents`           | JSON array        | Recent workspaces     |
| `tempest-app-settings`      | JSON object       | Terminal settings     |
| `tempest-keybindings`       | JSON object       | Custom key bindings   |
| `tempest-attribution`       | "true" \| "false" | Co-author enabled     |

On first app load with the new version, `loadRuntimeState` attempts to read from disk. If that fails, it reads from localStorage, merges everything into `RuntimeState`, and persists atomically to disk. Subsequently, localStorage is never read again.

## Session State Deep Dive

### Complete Session Lifecycle

1. **Creation:** User clicks "New Workspace" or opens a project, frontend calls `createWorktree(projectPath, name)`
   * Rust creates `.tempest/<name>` git worktree
   * Returns worktree path

2. **Storage:** Frontend saves session to RuntimeState:
   ```typescript theme={null}
   const session: WorktreeSession = {
     name: "feature-x",
     projectId: project.id,
     isRootSession: false,
     // No conversationId or agent yet
   };
   saveWorktreeSession(worktreePath, session);
   ```

3. **PTY Spawn:** Frontend calls `createPtySession(sessionId, cwd, ...)`
   * Rust opens PTY, saves PID to `.tempest-pid`
   * Starts background reader thread streaming output

4. **Agent Launch:** If user runs `claude` in shell, sessionManager detects OSC title changes
   * Sets `workState` to "working"
   * Stores conversationId (extracted from agent output)

5. **Completion:** Agent finishes, signals OSC/DEC sequence
   * sessionManager detects signal or timeout
   * Sets `workState` to "done"
   * Optional: frontend auto-saves session with timestamp

6. **Closure:** User closes tab
   * Frontend calls `closeWorktreeSession(worktreePath)`
   * Sets `closed: true` in RuntimeState (soft close)
   * PTY is still alive (can re-open)

7. **Deletion:** User clicks "Delete" in sidebar
   * Frontend calls `closeAndRemoveWorktree(sessionId, repoPath, worktreePath)`
   * Rust kills PTY, waits for process exit, removes worktree dir
   * Frontend removes session from RuntimeState

### Restore on App Restart

At app startup:

1. `loadRuntimeState()` reads disk, populating sessions
2. For each open (non-closed) worktree session:
   * Check if `.tempest-pid` exists
   * If yes, process was orphaned; kill it via `taskkill /F /T` (Windows) or `kill -9` (Unix)
3. For each root session:
   * Call `dedupeRootSessions(projectPath)` to merge duplicates from old restore bug
   * Restore ghost tabs for root sessions that were closed but are being restored

### Project State Tracking

**Current project path:** Stored implicitly through:

* `activeInstanceId` in RuntimeState (which session tab is focused)
* That session's `WorktreeSession.projectId`
* That project's `StoredProject.path`

**Project detection:**

* Sidebar lists all `StoredProject` entries
* Frontend tracks open projects via `getOpenProjects()` / `saveOpenProjects(...)`
* Projects are added via folder picker in welcome screen, persisted to RuntimeState

**Project cleanup:**

* On app startup, `pruneOrphanedSessions(validPaths)` removes sessions for deleted projects
* Prevents stale entries accumulating

## Key Design Decisions

1. **No Redux/Zustand:** Custom stores are simpler, smaller, and need no external dependencies for this use case.

2. **useSyncExternalStore:** Ensures React re-renders only when subscribed state changes, avoiding O(N²) updates across hundreds of sessions.

3. **RuntimeState centralization:** Single source of truth makes state debugging and persistence easy.

4. **Atomic disk writes:** Every state change persists atomically (temp file + rename), preventing corruption on crash.

5. **Stateless Rust backend:** No memory-resident app state reduces crashes and restart costs.

6. **DashMap for PTY sessions:** Lock-free concurrent HashMap allows many PTY read/write operations to proceed in parallel without global locks.

7. **Lazy initialization of merged defaults:** Keybindings and settings merge defaults on access, so app updates can add new defaults without migration code.

8. **Ephemeral vs persistent:** Work state, queue, WebGL pool are ephemeral (lost on restart). Everything else lives in RuntimeState and survives.
