# Renderer UI & Zustand State Management

> The React renderer process: App entry point, Zustand store slices (worktrees, tabs, terminals, repos, agent-status), tab-group layout, terminal shell integration via xterm.js, sidebar, and the IPC bridge through which renderer slices subscribe to main-process events.

- Repository: stablyai/orca
- GitHub: https://github.com/stablyai/orca
- Human wiki: https://grok-wiki.com/public/wiki/stablyai-orca-47ffb1f68457
- Complete Markdown: https://grok-wiki.com/public/wiki/stablyai-orca-47ffb1f68457/llms-full.txt

## Source Files

- `src/renderer/src/App.tsx`
- `src/renderer/src/store/index.ts`
- `src/renderer/src/store/slices/worktrees.ts`
- `src/renderer/src/store/slices/tabs.ts`
- `src/renderer/src/store/slices/terminals.ts`
- `src/renderer/src/store/slices/tab-group-state.ts`
- `src/renderer/src/components/Sidebar.tsx`
- `src/renderer/src/components/terminal/TerminalShell.tsx`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [src/renderer/src/App.tsx](src/renderer/src/App.tsx)
- [src/renderer/src/store/index.ts](src/renderer/src/store/index.ts)
- [src/renderer/src/store/types.ts](src/renderer/src/store/types.ts)
- [src/renderer/src/store/slices/worktrees.ts](src/renderer/src/store/slices/worktrees.ts)
- [src/renderer/src/store/slices/tabs.ts](src/renderer/src/store/slices/tabs.ts)
- [src/renderer/src/store/slices/terminals.ts](src/renderer/src/store/slices/terminals.ts)
- [src/renderer/src/store/slices/tab-group-state.ts](src/renderer/src/store/slices/tab-group-state.ts)
- [src/renderer/src/store/slices/agent-status.ts](src/renderer/src/store/slices/agent-status.ts)
- [src/renderer/src/store/slices/repos.ts](src/renderer/src/store/slices/repos.ts)
- [src/renderer/src/hooks/useIpcEvents.ts](src/renderer/src/hooks/useIpcEvents.ts)
- [src/renderer/src/components/Sidebar.tsx](src/renderer/src/components/Sidebar.tsx)
- [src/renderer/src/components/sidebar/index.tsx](src/renderer/src/components/sidebar/index.tsx)
- [src/renderer/src/components/terminal/TerminalShell.tsx](src/renderer/src/components/terminal/TerminalShell.tsx)
- [src/renderer/src/components/terminal-pane/TerminalPane.tsx](src/renderer/src/components/terminal-pane/TerminalPane.tsx)
</details>

# Renderer UI & Zustand State Management

Orca's renderer process is a React application running inside Electron's `BrowserWindow`. It owns all user-visible UI: the left sidebar, the workspace area with its terminal/editor/browser tabs, the right sidebar, the status bar, and every modal. All application state—worktrees, tabs, terminals, agents, settings, and UI chrome—is managed in a single **Zustand store** composed from around 29 slice creators. This page documents the App entry point, how the store is constructed, what each major slice owns, how the tab-group layout model works, how the terminal shell integrates xterm.js, what the sidebar renders, and how the renderer subscribes to main-process events through the IPC bridge.

The renderer is the only Electron surface a user directly interacts with. Understanding its state model is the key to understanding data flow, feature boundaries, and startup/shutdown sequencing across the entire application.

---

## App Entry Point (`App.tsx`)

`App.tsx` is the root React component. It is responsible for startup hydration, keybinding dispatch, session persistence, theme application, and hosting all top-level layout regions.

### Startup Hydration Sequence

On mount, a single `useEffect` runs an async initialization chain:

```
fetchSettings()
  → fetchRepos()
  → fetchAllWorktrees()
  → fetchWorktreeLineage()
  → window.api.ui.get()          // load persisted sidebar/filter state
  → window.api.session.get()     // load persisted tabs/terminals
  → fetchKeybindings()
  → SSH reconnect (eager + deferred)
  → reconnectPersistedTerminals()
  → setHydrationSucceeded(true)  // unlock session writer
```

Settings are fetched first because `activeRuntimeEnvironmentId` controls whether subsequent calls go to the local IPC or a remote runtime server. If any step throws, the renderer stays in a "no-save" degraded mode (`hydrationSucceeded` stays `false`) so session persistence does not overwrite on-disk data with a partially-hydrated in-memory state.

Sources: [src/renderer/src/App.tsx:319-480]()

### Action Subscriptions

Rather than registering one `useAppStore` subscription per action, `App` consolidates all stable action references into a single `useShallow` subscription. This prevents React from registering a separate subscription for each action selector, avoiding re-renders on every unrelated store mutation.

```tsx
// src/renderer/src/App.tsx
const actions = useAppStore(
  useShallow((s) => ({
    toggleSidebar: s.toggleSidebar,
    fetchRepos: s.fetchRepos,
    // ...~25 more action refs
  }))
)
```

Sources: [src/renderer/src/App.tsx:219-260]()

### Session Persistence

A `createSessionWriteSubscriber` subscription (outside React's render cycle) writes the workspace session to disk via a debounced `window.api.session.set()` call whenever tab, terminal, or editor state changes. On `beforeunload`, a synchronous `window.api.session.setSync()` captures terminal scrollback buffers before the renderer tears down.

Sources: [src/renderer/src/App.tsx:580-610, 630-680]()

### Layout Regions

```text
┌─────────────────────────────────────────────────────────┐
│  Titlebar (drag region + nav buttons + WindowControls)   │
├──────────┬──────────────────────────────────┬───────────┤
│          │         Workspace Area           │           │
│ Sidebar  │  TabBar (≥2 tabs) + TerminalPane │  Right    │
│          │  or: Landing / Settings / etc.   │  Sidebar  │
├──────────┴──────────────────────────────────┴───────────┤
│  StatusBar                                               │
└─────────────────────────────────────────────────────────┘
```

`activeView` selects the workspace content (`'terminal'`, `'settings'`, `'activity'`, `'space'`, `'skills'`). The sidebar is hidden for `settings`, `activity`, `space`, and `skills` views. The right sidebar is guarded by `canShowRightSidebarForView(activeView)`.

Sources: [src/renderer/src/App.tsx:540-556]()

### Windows-Specific Chrome

On Windows, `titleBarStyle: 'hidden'` removes the native OS title bar, so `App.tsx` renders custom `<WindowControls>` with SVG Fluent/Win11-style minimize/maximize/close buttons. Close routes through `window.api.ui.requestClose()` so the Electron `'close'` event fires and the terminal-running guard remains active.

Sources: [src/renderer/src/App.tsx:107-170]()

---

## Zustand Store Architecture

### Single Store, Slice Composition

The entire renderer state lives in one Zustand store exported as `useAppStore`:

```ts
// src/renderer/src/store/index.ts
export const useAppStore = create<AppState>()((...a) => ({
  ...createRepoSlice(...a),
  ...createWorktreeSlice(...a),
  ...createTerminalSlice(...a),
  ...createTabsSlice(...a),
  ...createUISlice(...a),
  ...createAgentStatusSlice(...a),
  // ... 23 more slices
}))
```

`AppState` is a TypeScript intersection type of all slice interfaces:

```ts
// src/renderer/src/store/types.ts
export type AppState = RepoSlice & WorktreeSlice & TerminalSlice & TabsSlice
  & UISlice & SettingsSlice & KeybindingsSlice & GitHubSlice & AgentStatusSlice
  & /* ...20 more */ WorkspaceCleanupSlice
```

Sources: [src/renderer/src/store/index.ts:1-60](), [src/renderer/src/store/types.ts:1-48]()

### Complete Slice Inventory

| Slice | Key state | Purpose |
|---|---|---|
| `repos` | `repos[]` | Git repository list; add/remove/update |
| `worktrees` | `worktreesByRepo`, `activeWorktreeId` | Worktree lifecycle, creation, deletion, lineage |
| `terminals` | `tabsByWorktree`, `ptyIdsByTabId`, `terminalLayoutsByTabId` | PTY sessions, xterm split-pane layouts |
| `tabs` (unified) | `unifiedTabsByWorktree`, `groupsByWorktree`, `layoutByWorktree` | Unified tab model with split-group layout tree |
| `ui` | `sidebarOpen`, `sidebarWidth`, `activeView`, `activeModal` | UI chrome, sidebar geometry, modal routing |
| `settings` | `settings` | User settings; theme, font, keybindings |
| `keybindings` | `keybindings` | Custom keybinding map |
| `github` | PR/issue caches | GitHub PR/issue data by repo |
| `hosted-review` | Hosted review cache | GitLab/GitHub review state |
| `agent-status` | `agentStatusByPaneKey`, `retainedAgentsByPaneKey` | Real-time agent status per pane |
| `editor` | `openFiles`, `activeFileId`, `editorDrafts` | Open files, draft content, view modes |
| `browser` | `browserTabsByWorktree` | In-app browser tabs |
| `ssh` | `sshConnectionStates` | SSH connection registry |
| `workspace-cleanup` | Cleanup job state | Worktree cleanup wizard |
| `worktree-nav-history` | Navigation stack | Worktree history back/forward |
| … | … | Rate limits, dictation, stats, memory, etc. |

### Dev / E2E Store Exposure

In development and E2E mode (guarded by `import.meta.env.DEV || e2eConfig.exposeStore`), the store is attached to `window.__store` so Playwright tests can read state directly without DOM scraping.

Sources: [src/renderer/src/store/index.ts:62-67]()

---

## Slice Deep Dives

### Worktrees Slice

**State fields:** `worktreesByRepo`, `detectedWorktreesByRepo`, `worktreeLineageById`, `activeWorktreeId`, `deleteStateByWorktreeId`, `sortEpoch`, `everActivatedWorktreeIds`, `lastVisitedAtByWorktreeId`.

`worktreesByRepo` holds visible worktrees per repo; `detectedWorktreesByRepo` holds the richer `DetectedWorktreeListResult` including hidden and foreign-ownership worktrees.

**Runtime routing:** All IPC calls are routed through `getActiveRuntimeTarget(settings)`. If `activeRuntimeEnvironmentId` is set, calls go to a remote runtime server via `callRuntimeRpc`; otherwise they call `window.api.worktrees.*` locally.

```ts
// Simplified from worktrees.ts
const target = getActiveRuntimeTarget(get().settings)
const result = target.kind === 'local'
  ? await window.api.worktrees.create(createArgs)
  : await callRuntimeRpc(target, 'worktree.create', { ... })
```

**Hydration-time purge:** On `fetchAllWorktrees`, after all repos report authoritative results, the slice diffs `tabsByWorktree` keys against the live worktree id set and purges stale entries—worktrees deleted in a previous session that left orphaned tab state behind.

**Atomic teardown on delete:** `removeWorktree` calls `buildWorktreePurgeState`, which removes ~25 per-worktree keys from state in one atomic `set()`. This prevents any intermediate state where, for example, a tab references a worktree that no longer exists.

Sources: [src/renderer/src/store/slices/worktrees.ts:1-50, 430-480, 580-640]()

---

### Tabs Slice (Unified Tab Model)

The unified tab model is the central abstraction for all workspace content (terminals, editors, browser tabs). A `Tab` has a `contentType` (`'terminal' | 'editor' | 'diff' | 'browser' | 'conflict-review'`), an `entityId` (the PTY tab id, file id, or browser tab id it wraps), and a `groupId`.

**State fields:** `unifiedTabsByWorktree`, `groupsByWorktree`, `activeGroupIdByWorktree`, `layoutByWorktree`.

#### Tab Group Layout Tree

`layoutByWorktree` maps each worktree id to a `TabGroupLayoutNode`—a binary tree of `leaf` and `split` nodes:

```text
TabGroupLayoutNode =
  | { type: 'leaf'; groupId: string }
  | { type: 'split'; direction: 'horizontal' | 'vertical';
      first: TabGroupLayoutNode; second: TabGroupLayoutNode; ratio: number }
```

This mirrors VS Code's editor grid. `createEmptySplitGroup` inserts a new sibling leaf via `replaceLeaf`; `closeEmptyGroup` prunes a leaf via `removeLeaf` and collapses the parent split. Ratios are adjusted with `setTabGroupSplitRatio`.

#### MRU-Aware Tab Selection

Each `TabGroup` carries `recentTabIds`—a most-recently-used stack of tab ids within the group. When the active tab is closed, `pickNextActiveTab` walks the MRU stack to restore the previously focused tab rather than selecting the visual neighbor.

#### Active Surface Derivation

`deriveActiveSurfaceForWorktree` computes `activeFileId`, `activeBrowserTabId`, and `activeTabId` from group-and-tab state. It examines the active group's `activeTabId`, checks whether its `entityId` still exists in `openFiles` / `browserTabsByWorktree` / `tabsByWorktree`, and falls back gracefully. This keeps the four top-level active-surface fields (`activeTabId`, `activeFileId`, `activeBrowserTabId`, `activeTabType`) consistent with the unified model.

Sources: [src/renderer/src/store/slices/tabs.ts:1-100, 200-280, 350-400]()

---

### Terminals Slice

**Key state fields:** `tabsByWorktree` (legacy `TerminalTab[]` per worktree), `ptyIdsByTabId` (list of active PTY ids per tab), `terminalLayoutsByTabId` (xterm split-pane layout snapshot per tab), `unreadTerminalTabs`, `expandedPaneByTabId`, `canExpandPaneByTabId`.

The legacy `tabsByWorktree` record predates the unified tab model. `reconcileWorktreeTabModel` (in the tabs slice) migrates entries from `tabsByWorktree` that are not yet in `unifiedTabsByWorktree` into the unified model on demand.

`reconnectPersistedTerminals` is called at startup. It iterates the persisted tab list, spawns or attaches PTYs for each tab (local `window.api.pty.create` or remote `callRuntimeRpc`), and populates `ptyIdsByTabId`. It flips `workspaceSessionReady` once complete, which is the gate that allows the session write subscriber to begin persisting state.

Sources: [src/renderer/src/store/slices/terminals.ts:155-230]()

---

### Agent Status Slice

The agent status slice tracks real-time AI agent progress per xterm pane. State is keyed by `paneKey` (`${tabId}:${leafId}`).

**Key state fields:**
- `agentStatusByPaneKey` — live entries; updated at PTY event frequency
- `retainedAgentsByPaneKey` — snapshots of finished/vanished agents kept for the dashboard and sidebar hover
- `retentionSuppressedPaneKeys` — pane keys torn down by user action (tab close, X button); suppresses re-retention

**Lifecycle:** `setAgentStatus` upserts a live entry. `removeAgentStatus` removes a live entry and, unless suppressed, promotes it to `retainedAgentsByPaneKey`. `dropAgentStatus` (user-initiated) removes the entry and marks it suppressed so it will not reappear. `removeAgentStatusByTabPrefix` sweeps all entries for a closed tab atomically.

`RetainedAgentEntry` captures the full `TerminalTab` and `AgentType` at retention time so the dashboard can render completed rows even after the tab is gone.

Sources: [src/renderer/src/store/slices/agent-status.ts:1-80]()

---

### Tab-Group State Helpers (`tab-group-state.ts`)

This module contains pure, stateless functions used by both the tabs slice and the terminals slice:

| Helper | Role |
|---|---|
| `findTabAndWorktree` | Linear search across `unifiedTabsByWorktree` to find a tab by id |
| `findGroupForTab` | Look up a `TabGroup` by id within a worktree |
| `findGroupAndWorktree` | Cross-worktree group lookup |
| `findTabByEntityInGroup` | Find a tab by `entityId` + optional `contentType` within a specific group |
| `ensureGroup` | Create a root group if none exists for a worktree |
| `updateGroup` | Replace a group by id in a group array |
| `dedupeTabOrder` | Remove duplicate ids from a tab order list |
| `pushRecentTabId` | Prepend to the MRU stack, capping length |
| `sanitizeRecentTabIds` | Remove MRU entries that are no longer in the tab order |
| `pickNextActiveTab` | Walk MRU stack to select the next active tab after a close |

Sources: [src/renderer/src/store/slices/tab-group-state.ts:1-145]()

---

## IPC Bridge: `useIpcEvents`

`useIpcEvents` is the single hook that wires all main-process-to-renderer push events. It lives in `src/renderer/src/hooks/useIpcEvents.ts` and is called once at the top of `App`. It registers all subscriptions inside one `useEffect` and cleans them all up on unmount.

### Main-Process Events Handled

| IPC Channel | Store Action Called |
|---|---|
| `repos.onChanged` | `fetchRepos()` (skipped if runtime env is active) |
| `worktrees.onChanged` | `fetchWorktrees(repoId)` + diff-purge of removed worktree ids |
| `worktrees.onBaseStatus` | `updateWorktreeBaseStatus(event)` |
| `agent:status` | `setAgentStatus(paneKey, payload)` with pending-retry for unregistered pane keys |
| `ssh:state-changed` | `setSshConnectionState(targetId, state)` |
| `remote-workspace:snapshot` | `applyRemoteWorkspaceSnapshot(targetId, snapshot)` — merges remote session |
| `window:close-requested` | Confirm-close flow; dispatches `beforeunload` to trigger buffer captures |
| `ui:zoom-in/out` | `applyUIZoom()` + CSS var update |
| `tab:switch`, `tab:switch-recent` | `handleSwitchTab`, `handleSwitchRecentTab` |
| `terminal:split-pane` | Insert new leaf into `terminalLayoutsByTabId` via `addSplitLeafToLayout` |
| `worktrees.onRemoteChanged` | Refetch worktrees for remote runtime repos |
| `runtime:browser-driver-state` | `setDriverForBrowserPage` |
| `runtime:terminal-driver-state` | `setDriverForPty` |

The agent-status events include a retry mechanism: if the pane key is not yet registered (the PTY is still starting), the event is queued and retried every 100 ms for up to 15 seconds.

Sources: [src/renderer/src/hooks/useIpcEvents.ts:1-100, 580-680]()

### Architecture Diagram

```mermaid
sequenceDiagram
    participant Main as Electron Main Process
    participant API as window.api (preload IPC bridge)
    participant Hook as useIpcEvents (renderer)
    participant Store as useAppStore (Zustand)
    participant UI as React Components

    Main->>API: worktrees.onChanged({repoId})
    API->>Hook: callback fires
    Hook->>Store: fetchWorktrees(repoId)
    Store->>API: window.api.worktrees.listDetected({repoId})
    API->>Store: DetectedWorktreeListResult
    Store->>UI: state update → re-render Sidebar/WorktreeList

    Main->>API: agent:status payload
    API->>Hook: callback fires
    Hook->>Store: setAgentStatus(paneKey, payload)
    Store->>UI: agentStatusByPaneKey → WorktreeCardAgents re-render
```

---

## Sidebar

The left sidebar is implemented in `src/renderer/src/components/sidebar/index.tsx` (re-exported from `components/Sidebar.tsx`).

### Structure

```text
<Sidebar>
  ├── <SidebarNav />          — top navigation icons (workspaces, settings, skills…)
  ├── <SidebarHeader />       — repo filter chip + add-repo button
  ├── <WorktreeList />        — virtualized list of WorktreeCard rows
  ├── <SetupScriptPromptCard /> — setup-script nag if pending
  └── <SidebarToolbar />      — sort, group, filter controls + resize handle
```

`WorktreeList` is virtualized and maintains scroll position across worktree remounts via `worktreeScrollOffsetRef` and `worktreeScrollAnchorRef` refs that are owned by `App` and passed down, so the scroll state survives sidebar visibility toggles.

The sidebar has a resizable width (`MIN_WIDTH=220`, `MAX_WIDTH=500`) managed by `useSidebarResize`. The live draft width is applied as a CSS custom property `--workspace-sidebar-live-width` during drag for zero-jank resizing.

```tsx
// sidebar/index.tsx (simplified)
<div ref={containerRef} className="... bg-sidebar ...">
  <SidebarNav />
  <SidebarHeader />
  <WorktreeList scrollOffsetRef={...} scrollAnchorRef={...} />
  <SetupScriptPromptCard />
  <SidebarToolbar onResizeStart={onResizeStart} />
</div>
```

`WorktreeCard` renders an individual workspace row with inline agent-status indicators from `agentStatusByPaneKey` and `retainedAgentsByPaneKey`.

Sources: [src/renderer/src/components/sidebar/index.tsx:1-110]()

---

## Terminal Shell Integration

### `TerminalShell` Component

`src/renderer/src/components/terminal/TerminalShell.tsx` is a thin presentational wrapper. It receives all state and callbacks as props (no direct store access) and renders:

1. A `<TabBar>` (only if `tabs.length >= 2`)
2. One `<TerminalPane>` per visible terminal tab
3. An `editorPanel` slot for the editor/browser surface

```tsx
// TerminalShell.tsx (condensed)
<div className="flex flex-col flex-1 min-w-0 min-h-0 overflow-hidden">
  {activeWorktreeId && (
    <TabBar tabs={tabs} activeTabId={activeTabId} ... />
  )}
  {tabs.map(tab => (
    <TerminalPane key={tab.id} tab={tab} ... />
  ))}
  {editorPanel}
</div>
```

Sources: [src/renderer/src/components/terminal/TerminalShell.tsx:60-160]()

### `TerminalPane` and xterm.js

`TerminalPane` (`src/renderer/src/components/terminal-pane/TerminalPane.tsx`) owns the xterm.js integration:

- **PTY connection:** `connectPanePty` (via `pty-transport.ts`) attaches xterm.js to a local or remote PTY. PTY data handlers are registered/unregistered via `ensurePtyDispatcher` / `unregisterPtyDataHandlers`.
- **Split-pane layout:** `terminalLayoutsByTabId[tabId]` holds a `TerminalLayoutSnapshot`—a binary tree of `TerminalPaneLayoutNode` (`leaf | split`), mapping `leafId → ptyId`. The pane serializes its current layout into this tree on resize events and shutdown.
- **Fit/resize:** `fitPanes` and `SYNC_FIT_PANES_EVENT` ensure xterm columns/rows match the DOM element size. `useLayoutEffect` in `App` dispatches this event whenever the sidebar opens or closes so there is no transient mis-sized terminal frame.
- **Scrollback capture:** On `beforeunload`, registered `shutdownBufferCaptures` callbacks serialize xterm scrollback into the store for `window.api.session.setSync`.
- **Mobile/remote driver overlay:** `MobileDriverOverlay` is conditionally mounted when a remote runtime driver (mobile session, relay) is active for a pane's PTY.

Terminal tab titles update through `runtimePaneTitlesByTabId` (per-leaf title tracking), which feeds into the unified tab label via `updateUnifiedTerminalLabel` in the terminals slice.

Sources: [src/renderer/src/components/terminal-pane/TerminalPane.tsx:1-80]()

---

## Startup State Flow Summary

```mermaid
stateDiagram-v2
    [*] --> Mounting: App mounts
    Mounting --> FetchingSettings: fetchSettings()
    FetchingSettings --> FetchingRepos: fetchRepos()
    FetchingRepos --> FetchingWorktrees: fetchAllWorktrees()
    FetchingWorktrees --> HydratingUI: window.api.ui.get()
    HydratingUI --> HydratingSession: window.api.session.get()
    HydratingSession --> SSHReconnect: reconnect SSH targets
    SSHReconnect --> TerminalReconnect: reconnectPersistedTerminals()
    TerminalReconnect --> Ready: setHydrationSucceeded(true)
    Ready --> [*]: workspaceSessionReady = true; session writer unlocked

    HydratingSession --> DegradedMode: any step throws
    DegradedMode --> [*]: workspaceSessionReady = true; hydrationSucceeded = false
```

In `DegradedMode`, the UI mounts with whatever state was hydrated before the failure. The session write subscriber stays gated (`hydrationSucceeded = false`) to avoid overwriting on-disk data with partial in-memory state. A toast offers a one-click "Restart now" relaunch action.

Sources: [src/renderer/src/App.tsx:319-480]()

---

## Summary

The renderer process is organized around a single Zustand store composed from ~29 slices. `App.tsx` is the only component that drives startup hydration, keybinding dispatch, theme application, and session persistence. The IPC bridge (`useIpcEvents`) funnels all main-process push events into store actions through `window.api` preload bindings. The unified tab model (tabs slice) models all workspace content—terminal, editor, browser—as `Tab` records organized in a binary split-group layout tree, with MRU-aware close and drag-reorder semantics. The sidebar virtualizes the worktree list and displays live agent status drawn from the agent-status slice. Terminal shell integration via xterm.js lives in `TerminalPane`, which manages PTY connections, split-pane layout trees, scrollback capture, and fit synchronization.
