ShipIt React client architecture: Zustand stores, communication hooks (useApi, useSessionWebSocket, useServerEvents, useMessageHandler), component inventory, data flow patterns. Load when working on frontend components, stores, hooks, or client state.
Installs into .claude/skills of the current project.
Are you the author of Client Architecture?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/nikzlabs-client-architecture)
---
name: client-architecture
description: "ShipIt React client architecture: Zustand stores, communication hooks (useApi, useSessionWebSocket, useServerEvents, useMessageHandler), component inventory, data flow patterns. Load when working on frontend components, stores, hooks, or client state."
user-invocable: true
---
# Client Architecture
The client is a React 19 SPA built with Vite and Tailwind CSS v4. State management uses Zustand stores. Communication with the server uses three channels: HTTP for reads/mutations, per-session WebSocket for streaming and real-time interaction, and SSE for global broadcasts.
## Entry Point
`src/client/main.tsx` renders the app inside a `BrowserRouter` with an `ErrorBoundary`. Two route patterns:
- `/session/:sessionId` — session view
- `*` — catch-all (home screen, `/{slug}/new` for new repo sessions)
`src/client/App.tsx` is the main component. It wires together all hooks, stores, and UI components. Layout is a three-panel design: sidebar (sessions), center (chat), right (preview/files/git/terminal).
## State Management (Zustand)
The main stores (partial — `ls src/client/stores/` for the full set):
| Store | File | Key State |
|-------|------|-----------|
| Session | `session-store.ts` | `sessionId`, `messages[]`, `isLoading`, `activity`, `sessions[]`, `authUrl`, `queuedMessages[]` |
| Git | `git-store.ts` | `commits[]`, `identity`, `turnDiff`, `lastCommitPair` |
| File | `file-store.ts` | `tree[]`, `viewingFile`, `viewingFileContent`, `docFiles[]` |
| Preview | `preview-store.ts` | `status`, `selectedPort`, `installStatus`, `crashInfo` |
| Terminal | `terminal-store.ts` | `entries[]`, `mode`, `shellStarted` |
| PR | `pr-store.ts` | `result`, `status`, `descGenerating` |
| Settings | `settings-store.ts` | `permissionMode`, `systemPromptContent`, `githubStatus`, `pendingFiles[]` |
| UI | `ui-store.ts` | `rightTab`, `templates[]`, `agentList`, `modelInfo`, `toast`, `features[]` |
| Repo | `repo-store.ts` | `repos[]`, `addRepoDialogOpen` |
### Patterns
- Stores are created with `create<StateType>((set, get) => ({...}))`.
- Each store has a `reset()` method for clearing session-specific state during session switching.
- Components subscribe to individual fields via selectors: `useSessionStore((s) => s.messages)`.
- Stores are updated directly — no centralized dispatcher. Handlers call `store.getState().setX(value)`.
- **`App.tsx` selects single rows, never a list that changes for other sessions** (`sessions`, the PR store's per-session maps): each such change would re-render the whole app. `setSessions` keeps unchanged row objects (`utils/session-rows.ts`), so a one-row selector is stable. A reader of the whole list goes in a leaf component (`AttentionNotifications`, `AllSessionsDialogWithRows`) or in `AppLayout`, which gets the panels as finished elements. Guard: `App.render-count.test.tsx`.
### Session Actions
`src/client/stores/actions/session-actions.ts` contains cross-store coordination:
- `resetSessionState()` — clears session-specific stores (messages, git, files, preview, terminal, threads)
- `resumeSessionInternal(sessionId)` — reset + fetch history
- `handleSessionResume(sessionId, navigate)` — resume + navigate
- `newSession(navigate)` — reset all + navigate home
- `fullResetAllStores()` — nuclear reset (used after server full_reset_complete)
## Communication Hooks
### `useWebSocket` (base)
`src/client/hooks/useWebSocket.ts` — generic WebSocket hook.
- Connects when URL is provided, disconnects when URL is null
- Auto-reconnect with exponential backoff: 2s -> 4s -> 8s -> 16s -> 30s cap
- Returns `{ send, lastMessage, status, reconnectAttempt, reconnect }`
- `status`: `"connecting"` | `"open"` | `"closed"`
### `useForegroundSignal`
`src/client/hooks/useForegroundSignal.ts` — decides what counts as a
background-to-foreground transition. Used by **all three** hooks that react to
one: `useWebSocket`, `useServerEvents`, `useConnectionSync`.
- Exists because the two long-lived connections force a fresh socket on resume
(a backgrounded mobile socket reads OPEN while being dead), so the trigger
cannot be "the connection looks unhealthy" — it has to be page lifecycle.
- **Window `focus` is not a foreground signal on its own.** It also fires when
focus returns from an iframe to the top-level document, which the preview
iframe does on every load — that produced one forced reconnect per second on
both channels, seen as preview flicker and a composer toggling disabled.
- `visibilitychange`→visible / `pageshow` / `online` always reconnect;
`visibilitychange`→hidden / `pagehide` / `freeze` record a transition;
`focus` is classified against the preceding `blur` (`document.hasFocus()` is
true when an iframe took focus, false when the window lost system focus) and
only the provably-internal case is suppressed.
- Coalesces one reactivation's event burst into a single reconnect (1s).
- **Never add a bare `focus` listener that treats focus as a resume** — use
this hook, so the surfaces cannot drift apart on the question again.
### `useSessionWebSocket`
`src/client/hooks/useSessionWebSocket.ts` — wraps `useWebSocket` for per-session connections.
- Connects to `/ws/sessions/{sessionId}?agent={savedAgent}` when sessionId is defined
- Returns null URL (disconnects) when sessionId is undefined
- Session switching triggers URL change -> old socket closes, new one opens
### `useApi`
`src/client/hooks/useApi.ts` — HTTP client.
- Methods: `get()`, `post()`, `patch()`, `put()`, `del()`
- Returns typed responses: `Promise<T>`
- Throws `ApiError(status, message)` on failure
- Used in App.tsx callbacks and store async actions
### `useServerEvents`
`src/client/hooks/useServerEvents.ts` — SSE connection for global broadcasts.
- Connects to `/api/events`, always active
- Handles: `session_list`, `session_started`, `repo_list`, `repo_status`, `repo_warm_ready`, `auth_required`, `agent_list`, `active_runners`, `full_reset_complete`
- Updates session, repo, UI, and settings stores
### `useMessageHandler`
`src/client/hooks/useMessageHandler.ts` — listens to `lastMessage` and hands each `WsServerMessage` to
`dispatchMessage` in `hooks/message-handlers/index.ts`, which routes to one handler file per type and drops
foreign-session messages (`TRANSCRIPT_SCOPED_MESSAGES`). It holds agent events back until `historyLoaded`.
### `useConnectionSync`
`src/client/hooks/useConnectionSync.ts` — initialization on mount and WS connect.
- On mount: `GET /api/bootstrap` -> populates session, repo, UI, settings stores
- On WS open: `GET /api/sessions/{id}/history` -> loads messages, commits, threads
- HTTP fallback: `GET /api/sessions/{id}/preview-status` (retries once after 3s if unknown)
- Sends pending WS message if stored
## Other Hooks
| Hook | File | Purpose |
|------|------|---------|
| `useAutoFix` | `useAutoFix.ts` | Monitors preview errors, auto-sends fix requests to Claude (3 retries, 5s cooldown) |
| `useKeyboardShortcuts` | `useKeyboardShortcuts.ts` | Ctrl+F (search), ? (shortcuts overlay), Esc (interrupt) |
| `useNotification` | `useNotification.ts` | Tab visibility tracking, browser notifications on agent finish |
| `useTheme` | `useTheme.ts` | Theme selection from `THEME_DEFS` (localStorage) |
| `useSearch` | `useSearch.ts` | Case-insensitive message search |
| `useResizablePanel` | `useResizablePanel.ts` | Drag-to-resize split panels |
| `usePreviewErrors` | `usePreviewErrors.ts` | Captures errors from preview iframe |
| `useMediaQuery` | `useMediaQuery.ts` | Mobile detection |
## Components
Far more components live in `src/client/components/` than are listed here. This is a partial orientation map, not an inventory; `ls src/client/components/` for the real set.
### Layout
- **`SessionSidebar`** — session list with rename/archive, repo grouping
- **`MobileTabBar`** — bottom tab navigation on mobile
- **`ResizeHandle`** — drag handle between panels
### Chat
- **`MessageList`** — renders messages, tool calls, tool results, checkpoint dividers
- **`MessageInput`** — text input + image upload + file autocomplete + permission mode selector
- **`StreamingIndicator`** — activity label during Claude turns (thinking, writing, running)
- **`QueueIndicator`** — shows queued message count
- **`ToolResult`** — renders individual tool results (file diffs, bash output)
- **`DiffBlock`** — syntax-highlighted diff display
- **`TodoPanel`** — the agent's to-do list, folded from its task calls by `task-list.ts` (docs/045)
- **`AskUserQuestion`** — renders permission/question prompts from Claude
### Right Panel
- **`PreviewFrame`** — iframe for dev server preview + port selector + error display
- **`FileTree`** — workspace file browser with expand/collapse
- **`GitHistory`** — commit timeline with diff viewer
- **`DiffPanel`** — file-by-file diff with accept/reject actions
- **`TerminalPanel`** — build logs display
- **`InteractiveTerminal`** — xterm.js shell (lazy loaded)
- **`DocsViewer`** — markdown file viewer
### Modals & Overlays
- **`AuthOverlay`** — Claude/GitHub authentication flows
- **`DeploymentStatusRow`** (in `PrLifecycleCard.tsx`) — shows deploy status from GitHub Deployments API
- **`Settings`** — git identity, system prompt, agent config
- **`UsageModal`** — cost/token breakdown
- **`KeyboardShortcutsOverlay`** — shortcut reference
- **`OnboardingWizard`** — first-time setup
### Home
- **`HomeScreen`** — session list, import repo, templates
- **`AddRepoDialog`** — import GitHub repo dialog
## Data Flow: Sending a Message
```
User types in MessageInput -> handleSend() callback in App.tsx
|
+- If session exists:
| Add user message to session store
| Set isLoading
| WS send: { type: "send_message", text, sessionId, images?, files?, permissionMode? }
|
+- If no session (home page):
POST /api/sessions { title }
Store pending WS message
Navigate to /session/{id}
-> WS auto-connects -> useConnectionSync sends pending message
|
v
useMessageHandler receives WS responses:
agent_event (assistant) -> append text to messages, update activity
agent_event (tool_use) -> show tool activity label
agent_event (result) -> mark complete, clear loading
git_committed -> update git store
files_changed -> refresh file tree
preview_status -> update preview store
```
## Local Storage
| Key | Purpose |
|-----|---------|
| `shipit-theme` | Selected theme |
| `vibe-permission-mode` | Auto/plan/normal permission mode |
| `vibe-sidebar-collapsed` | Sidebar collapsed state |
| `vibe-agent-id` | Preferred agent (claude/codex) |
| `vibe-panel-split` | Right panel split ratio |
## Styling
Tailwind CSS v4 over **semantic color tokens** — ShipIt is multi-theme, not dark-mode-only. Concrete values live in per-theme CSS under `src/client/themes/`; never hardcode a palette value. See the `design-language` skill. Shared animations are in `src/client/index.css`. All components use utility classes directly — no CSS modules or styled-components.