Skip to content
Back to skills

Client Architecture

ASecurity

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.

  • 9 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 3, 2026
developmentgoshellbashreactgitapifrontend

Works with

  • terminal
  • cli
  • api

Security analysis

A100/100

Scanned October 7, 2026

npx -y skills add nikzlabs/shipit --skill client-architecture --agent claude-code

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.

Security grade badge for Client Architecture
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/nikzlabs-client-architecture/badge)](https://www.skillsdirectory.com/skills/nikzlabs-client-architecture)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.