The web client is the Next.js 16 / React 19 application in apps/web that renders the dashboard, the chat surface, the in-app docs site and the Next.js API routes that front provisioning and filesystem work. A reader can use this page to find which module owns a piece of browser behaviour (message cache, sync stream, telemetry, modals, team templates, pixel world) and where its tests live. Source: ../../apps/web.
The dashboard opens a task workbench. components/chat/task-start.tsx provisions a Claude Code or Codex binding through the existing agent route and hands the first instruction to AgentSessionView. Tasks stay in the existing direct-conversation store; there is no separate task runtime. Project files and background collaboration are explicit sidebar entries, and the Actions menu retains device, account, import and group management. Saved group selection does not reopen collaboration on startup; an explicit conversation link still opens its target.
lib/ is grouped by domain; a new module goes in the folder whose name matches the concept it serves, and a React hook goes in hooks/:
| Folder | Holds |
|---|---|
lib/api/ |
gateway client, wire types, route auth guard, request origin, telemetry (choruz-api.ts, choruz-types.ts, api-auth.ts, choruz-trace.ts, telemetry-sanitize.ts, principals.ts, request-origin.ts) |
lib/messages/ |
message cache and IndexedDB store, threads and thread unreads, mentions, quotes, conversation flags, sidebar ordering |
lib/api/transport.ts |
the two ways out of the browser: transportFetch for same-origin /api/* calls and transportSocket for the gateway sockets; the local transport is the default, a relay transport can be installed for a remote dashboard |
lib/remote/relay-session.ts |
The paired browser's Remote Control session: rendezvous socket, transport offer, device.hello, AES-GCM envelopes in send and receive order, RelayStatus; RelayLink is the narrow surface the transport needs. |
lib/remote/relay-pairing.ts |
Device side of the committed ECDH pairing handshake (pairWithHost) and per-gateway credential storage in localStorage. |
lib/remote/relay-transport.ts |
createRelayTransport(link): a ChoruzTransport that turns fetches into http.* frames and gateway sockets into stream.* frames (RelaySocket), chunked at CHUNK_BYTES; the frame contract is remote_control_executor.rs. |
lib/agents/ |
agent provisioning, instruction templates, tokens, AI-manager instructions, harness accounts |
lib/drivers/ |
driver registry, availability, model catalogue and validation |
lib/groups/ |
team templates and rendering, group provisioning jobs (contract, runner, store, db, issue display) |
lib/channel-tasks/ |
board task assignees, creation, reconciliation and shared task helpers |
lib/terminal/ |
terminal bindings, PTY write buffer, ANSI stripping |
lib/remote/ |
remote-control hosts, pairing crypto, remote server install |
lib/workspace/ |
workspace path guard, git-graph repository path resolution |
lib/ (root) |
cross-cutting helpers only: utils.ts, format-bytes.ts, format-chat-time.ts, audio-utils.ts, avatar.ts |
hooks/ |
every use-*.ts React hook, including use-chat-web-socket.ts |
components/ follows the same rule, one folder per feature surface:
| Folder | Holds |
|---|---|
components/chat/ |
chat-app.tsx (the dashboard orchestrator), header, input, modals, sidebar, conversation rows, detail panel, message list and bubble, thread panel, channel conversation tabs |
components/channel-tasks/ |
the board and the create-task modal |
components/agents/ |
agent config editor, instruction form, skills list, create-agent modal, driver pickers, harness account picker, summary and modal, workspace session import |
components/groups/ |
create-group and create-company modals, step tabs and setup input fields they share |
components/workspace/ |
file tree, file editor, path and folder pickers, git graph |
components/runtime/ |
server, runtime host and remote-control managers, runtime status panel, terminal view |
components/ui/ |
primitives with no product knowledge: modal.tsx, spinner.tsx, avatar.tsx, empty-state.tsx, resize-handle.tsx, theme-provider.tsx |
components/pixel-world/ |
the canvas renderer, its game scenes and unit tests |
Styles are plain CSS: app/globals.css is an ordered @import manifest and each section lives in app/styles/<section>.css (theme tokens first, then the chat shell, header, message list, composer, detail panel, modals, responsive rules, and one file per feature surface). A new rule goes in the file for its surface; a new surface gets a file and an @import line at the position its rules must win or lose from.
Unit tests sit next to the module they pin (foo.test.ts beside foo.ts).
| Area | Path |
|---|---|
| App Router entry | app/page.tsx (LocalEntryPage, redirects to /dashboard when the session cookie is present), app/dashboard/page.tsx (DashboardPage, calls fetchDashboardBootstrap(sessionToken, { limit: 100 }) and maps it with lib/api/dashboard-snapshot.ts), app/remote/page.tsx (RemotePage, renders components/remote/remote-dashboard.tsx: pairs through the Cloud Gateway, installs the relay transport, fetches the bootstrap through it and mounts the same ChatApp), app/layout.tsx |
| Auth routes | app/auth/logout/route.ts, app/auth/session-invalid/route.ts |
| Export route | app/export/[conversationId]/route.ts (GET, wraps exportConversation) |
| In-app docs | app/docs/layout.tsx (sidebar nav, docs.css) and one page.tsx per topic under app/docs/{getting-started,concepts,features,agents,api,operations,troubleshooting,reference} |
| Next.js API routes | app/api/agent-config, agent-skills, agents/provision, agents/batch-disable, attachments/[id], companies, drivers/availability, drivers/models, filesystem, git-graph, group-provisioning-jobs (plus [jobId]/{run,retry,cancel}), harness-accounts (plus [id]/probe, [id]/login), skills/scan under app/api |
| Route auth guard | lib/api/api-auth.ts requireAuth verifies the choruz_session cookie against GET /v1/me on the gateway |
| Chat shell | components/chat/chat-app.tsx plus sidebar.tsx, message-list.tsx, message-bubble.tsx, chat-input.tsx, chat-header.tsx, chat-modals.tsx, detail-panel.tsx, terminal-view.tsx, thread-panel.tsx |
| Gateway client | lib/api/choruz-api.ts (apiBaseUrl, apiJson, apiFetch, fetchDashboardBootstrap, sendMessage, fetchThread, markThreadViewed, fetchChannelTasks, patchChannelTask, …), types in lib/api/choruz-types.ts |
| Sync stream | hooks/use-chat-web-socket.ts connects to /v1/ws/sync, handles sync_ready, sync_changes, sync_acked, sync_error, sends sync_ack |
| Message cache | lib/messages/messages.ts (pure merge helpers), lib/messages/message-db.ts (Dexie/IndexedDB) |
| Threads | lib/messages/threads.ts, lib/messages/thread-unreads.ts; see threads.md |
| Hooks delegated from chat-app | hooks/use-conversation-flags.ts, hooks/use-company-management.ts, hooks/use-panel-resize.ts, hooks/use-message-search.ts, use-edge-swipe.ts, use-thinking-agents.ts, use-modal-a11y.ts |
| Helpers | lib/terminal/terminal-bindings.ts, lib/messages/mentions.ts, lib/api/principals.ts, lib/messages/conversation-flags.ts |
| Telemetry | lib/api/choruz-trace.ts, lib/api/telemetry-sanitize.ts, lib/api/activity-outbox.ts |
| Modal shell and shared fields | components/ui/modal.tsx (Modal), components/groups/setup-input-field.tsx (SetupInputField), components/workspace/path-picker.tsx |
| Client plugins | plugins/registry.ts (resolveClientPluginIds), plugins/client-plugin.ts, one client.tsx each under plugins/{kanban,pixel-world,workspace-git,remote-ssh,remote-control,agent-skills} |
| Pixel world | components/pixel-world (pixel-world.tsx, pixel-world-store.ts, game/, docs/) behind plugins/pixel-world/client.tsx |
| Team templates | lib/groups/team-templates.ts, lib/groups/team-template-renderer.ts, lib/groups/team-template-validation.ts, create-agent-template-flow.ts, create-group-template-flow.ts, group-provisioning-{runner,store,contract}.ts, and the instruction fragments in crates/choruz-host-runtime/assets/agent-templates/ composed by lib/agents/agent-instruction-template.ts |
| Build and test config | next.config.ts, vitest.config.ts, playwright.config.ts, infra/host/web_e2e.sh, scripts/prepare-next-types.mjs |
FileEditor retains the loaded text as its save precondition. A conflict keeps the draft editable and offers Reload (adopt the returned disk snapshot) or Overwrite (save against that snapshot, detecting another intervening edit). Both local and remote saves use the existing transport and gateway file owner.
ChatMessage (lib/api/choruz-types.ts) is the per-message row the client keeps: id, conversation_id, sender_id, content, content_type, metadata, server_seq, idempotency_key, created_at. MessagesByConv in lib/messages/messages.ts is Record<string, ChatMessage[]>, the in-memory cache keyed by conversation.
OPTIMISTIC_SERVER_SEQ (Number.MAX_SAFE_INTEGER, defined once in lib/messages/messages.ts) marks a locally inserted row that the server has not confirmed; upsertConfirmedMessage, mergeFetchedMessages, appendIncrementalMessages, mergePreviewIntoMessages, messagesMissingFromPrevious and maxCachedSeq all key on it.
MessageDatabase in lib/messages/message-db.ts is the Dexie database choruz_messages with two tables: messages (primary key [conversation_id+server_seq], index conversation_id) and syncState (primary key &principal_id, rows of DashboardSyncState { principal_id, device_id, ack_cursor }). Exports: persistMessages, loadConversationMessages, loadAllCachedMessages, maxPersistedSeq, loadDashboardSyncState, persistDashboardSyncCursor, resetMessageDb.
DashboardBootstrap and DashboardSyncChange (lib/api/choruz-types.ts) are the bootstrap page and the sync-feed change rows; chat-app.tsx applies changes by event_type / entity_type (message rows, conversation.deleted, pin/archive/hidden flags, conversation.read_state_changed, thread.read_state_changed, channel_task) and falls back to a bootstrap refresh for anything else.
HostPluginManifest rows from bootstrap.plugins are matched against CLIENT_PLUGINS in plugins/registry.ts; chat-app.tsx derives kanbanEnabled, pixelWorldEnabled, workspaceGitEnabled, remoteSshEnabled, remoteControlEnabled and agentSkillsEnabled from the resulting id set.
RoleTemplate, GroupTemplate, SetupInput (type: "text" | "textarea" | "path" | "select"), OutputContract and the constants ROLE_TEMPLATES, GROUP_TEMPLATES, BOARD_TASKS_CREATED_SECTION live in lib/groups/team-templates.ts; renderRoleInstructions and renderGroupKickoff in lib/groups/team-template-renderer.ts turn them into agent instructions and kickoff text.
TraceEntry and Span in lib/api/choruz-trace.ts are the telemetry records; trace.start(name, data) returns a span, trace.event(name, data) is one-shot, traceRing() exposes the in-memory ring, and every payload passes through sanitizeTelemetryData before leaving the browser.
Spans persist separate started and finished events with one span ID. The
finished event includes duration and outcome. transportFetch adds correlated
HTTP lifecycle events for both transports without consuming response bodies.
Context carries the selected Company/conversation and, for a terminal binding,
its device/account IDs. These observations are not authoritative audit records.
Dashboard activity includes semantic clicks, input snapshots, selected values, toggle changes, selected file counts, submissions, dialog open/close, command keys, copy/paste intent, settled scroll and page visibility. Each changed input snapshot includes at most 2048 characters, its length and a truncation flag; native change after input does not duplicate the snapshot. Clipboard contents, file bytes and raw terminal input are excluded. Password, authentication, invitation and explicitly private values emit only an omission marker. Filesystem values are omitted. Recognizable secret markers are redacted, but arbitrary secrets in ordinary free text cannot be reliably identified; stored drafts require private-data access and retention controls.
Use data-activity for a stable control identity when the accessible name is absent or dynamic. The shared modal supplies data-activity-surface; controls otherwise derive identity from their name, stable ID or accessible label, never CSS classes or coordinates. data-activity-private excludes a region entirely; data-activity-value="private" retains actions while omitting values. Dynamic message, attachment and folder labels are omitted. Custom canvas actions require explicit semantic events. Missing completion does not establish abandonment, and HTTP success does not imply AI turn success.
PixelWorldState in components/pixel-world/pixel-world-store.ts is a zustand store (usePixelWorldStore) holding PixelAgentState, HouseInfo, PlayerState and WalkabilityMask; emitPixelWorldEvent is the bridge chat-app uses to animate agents.
-
Online: the sidebar Actions menu opens
components/online/online-modal.tsx. Local use remains login-free. The modal calls the authenticated local API for registration, sign-in, account state and sign-out; it never receives the cloud token.signed_inmeans authenticated, not connected to other people. Password fields clear after each submission and are not stored in browser persistence.online-groups.tsxcreates single-person group invitations and accepts them. Owners open the canonical group; guests read and send shared text in the main chat pane using the shared message renderer. Connection state is separate from login state. Queued messages survive a closed browser through server storage. Leaving or removal disables sending; saved history remains readable. Files, terminal output and private chats are not included in the shared projection. -
Browser:
/→/dashboard; the authenticated client bootstrap mountsChatApp, then opens/v1/ws/sync?device_id=…&cursor=…throughuseChatWebSocketand re-fetches/v1/unreadson demand. Failed bootstrap requests retry in a connection screen before the chat shell mounts. -
Gateway traffic:
next.config.tsrewrites/api/v1/:path*to the gateway resolved fromCHORUZ_API_BASE_URL,CHORUZ_API_URLorCHORUZ_API_PORT(defaulthttp://127.0.0.1:3000);apiBaseUrl()inlib/api/choruz-api.tsuses the same precedence server-side, andNEXT_PUBLIC_CHORUZ_API_PORTis exposed to the browser. -
Telemetry:
choruz-trace.tsstores sanitized entries in the IndexedDB outbox before POSTing to/api/v1/telemetry. The gateway commits each batch atomically and deduplicates event IDs within the authenticated actor and workspace. HTTP 204 removes only the acknowledged IDs; failed delivery retries, including after reload. Client occurrence time is separate from server receipt time. These are client-reported observations, not authoritative audit evidence. -
Next.js API routes call
requireAuthfirst, then useCHORUZ_RUNTIME_DIR,CHORUZ_GIT_REPO_PATH,CHORUZ_INTERNAL_PROVISION_TOKEN,CHORUZ_DATABASE_URL/CHORUZ_PG_*and theCHORUZ_{CLAUDE,CODEX,PI,GROK,OPENCODE}_BINARYvariables for provisioning and filesystem work. -
Docs:
/docsand the nested topic pages are static App Router pages with no gateway dependency. -
Tests:
pnpm --dir apps/web test(vitest),pnpm --dir apps/web e2e(Playwright),bash infra/host/web_e2e.sh [spec…](full stack),pnpm --dir apps/web check(prepare-next-types.mjsthentsc --noEmit).
-
Group-template job creation, continuation and recovery use the active browser transport. Remote template provisioning requires the target's Next.js provisioning routes; an unavailable target route fails on that target, without falling back to the controller's API.
-
IndexedDB history merges through
mergeFetchedMessages, retaining newer bootstrap and sync messages. Pin, archive and hide reconciliation receives the bootstrap request's start time, so a delayed response cannot supersede a mutation acknowledged during that request. -
Search results load in bounded pages through
useMessageSearch; one lookahead result controls the Load more action. A failed continuation retains loaded matches and offers Retry. Query or identity changes invalidate the prior continuation without replacing the selected-message navigation owner. -
Message search selection retains its message ID.
MessageListuses the existing older-history loader until the target is present, then scrolls and highlights it; Cancel and conversation/view changes stop continuation. History errors use the same Retry action as scrolling. Quiet replies open their existing thread panel after history reaches the root. Search does not merge isolated messages across pagination gaps. -
Remote attachment DOM resources use
hooks/use-transport-url.ts: media previews fetch through the active transport, ordinary files fetch on click, and unmount revokes blob URLs and discards late responses. Local resources retain native URLs.remote-file-transport.spec.tschecks B's bytes while hosted A refuses API calls. -
A sync page is applied to state and persisted before its cursor is acknowledged (
useChatWebSocketsendssync_ackwithframe.next_cursorafteronChangesresolves), so a crash during apply replays the page; pinned bytests/e2e/websocket.spec.ts("converges on one message in two tabs", "deduplicates optimistic messages with sync confirmations"). -
Optimistic rows never reach IndexedDB:
persistMessagesfiltersserver_seq < OPTIMISTIC_SERVER_SEQ; pinned bylib/messages/message-db.test.tsandlib/messages/messages.test.ts. -
mentionedAgentIdsmirrors the router's@all/@namerules so the thinking indicator matches which agents wake; pinned bylib/messages/mentions.test.ts. -
A terminal binding stays mounted for every open tab (
openTerminalBindings), so switching tabs never drops the PTY WebSocket; pinned bylib/terminal/terminal-bindings.test.ts. -
Every Next.js API route that acts on a principal goes through
requireAuth, which round-trips toGET /v1/merather than trustingdecodeSessionClaims; pinned by theroute.test.tsfiles beside each route. -
A client plugin renders only when the host manifest satisfies
requiredHostCapabilities(hostSupportsClientPlugin); pinned byplugins/registry.test.tsandtests/e2e/plugins.spec.ts. -
Telemetry payloads are redacted before local persistence and again at the gateway.
lib/api/telemetry-sanitize.test.tsand the gateway observability tests check sensitive fields.tests/e2e/telemetry.spec.tsverifies replay after a lost commit acknowledgement without duplicate database rows. -
Role templates never require a prose "Assignments" section; coordinator roles require
BOARD_TASKS_CREATED_SECTION; pinned bylib/groups/team-templates.test.tsandlib/groups/team-template-renderer.test.ts.
- Agent provisioning preserves gateway HTTP errors and
Retry-Afterthrough its step wrapper. Local failures remain HTTP 500; step records and raw error causes are not serialized to the caller. - IndexedDB unavailable (quota, private mode, schema upgrade): every
message-db.tsoperation catches and emitstrace.event("indexeddb_fallback", { op, error }); the chat path re-fetches over HTTP. - Sync WebSocket drop:
useChatWebSocketreportsstatus: "reconnecting"and retries with backoff fromRECONNECT_BASE_MS(500 ms) toRECONNECT_MAX_MS(16 s), resuming from the persistedack_cursor. - Initial bootstrap failure on
/dashboard:components/chat/dashboard.tsxdisplays the connection error and retries automatically or through Retry connection. It does not initialize the chat shell with an empty identity. - Gateway unreachable from
requireAuth: the route answers503 Auth service unavailable(3 s timeout) rather than401. - Unknown sync change types trigger a full bootstrap refresh (
refreshBootstrap = trueinchat-app.tsx), visible as an extraGET /v1/bootstrap. - Telemetry delivery failure retains pending events and logs a content-free warning; retries back off to one minute. Storage denial, browser eviction or quota exhaustion can still lose observations. Credentials never enter the outbox. Data over 16 KiB is replaced with an omission marker.
- Unit (vitest,
environment: "node", includelib/**/*.test.ts,components/**/*.test.ts,app/**/*.test.ts,plugins/**/*.test.ts):lib/messages/messages.test.ts,lib/messages/messages.integration.test.ts,lib/messages/message-db.test.ts(usesfake-indexeddb),lib/api/choruz-api.test.ts,choruz-api-fetch.test.ts,choruz-api-runtime.test.ts,lib/api/choruz-trace.test.ts,telemetry-sanitize.test.ts,mentions.test.ts,terminal-bindings.test.ts,conversation-flags.test.ts,sidebar-conversations.test.ts,team-templates.test.ts,team-template-renderer.test.ts,team-template-validation.test.ts,create-agent-template-flow.test.ts,create-group-template-flow.test.ts,group-provisioning-*.test.ts,pixel-world-logic.test.ts,pixel-world-pathfinding.test.ts,pixel-animations.test.ts,pixel-houses.test.ts,pixel-recolorer.test.ts,pixel-tiles.test.ts,components/chat/message-list.test.ts,components/chat/message-bubble.test.ts,plugins/registry.test.ts,plugins/server-plugin.test.ts, and theroute.test.tsfiles underapp/api/*andapp/auth/session-invalid. - E2E (Playwright,
testDir: ./tests, projectchromium, pluschromium-reduced-motionwhenCHORUZ_E2E_EXTENDED=1):tests/e2e/app-smoke.spec.ts(the default spec forweb_e2e.sh),tests/e2e/dashboard.spec.ts,messaging.spec.ts,websocket.spec.ts,indexeddb.spec.ts,telemetry.spec.ts,docs.spec.ts,api-routes.spec.ts,plugins.spec.ts,modals.spec.ts,sidebar.spec.ts,search.spec.ts,terminal.spec.ts,pixel-world.spec.ts,team-collaboration.spec.ts,user-journeys.spec.ts,responsive.spec.ts,theme.spec.ts,keyboard.spec.ts, plustests/e2e/message-dedup.spec.ts,tests/e2e/outbox-reply.spec.ts,tests/pixel-world-*.spec.ts,tests/e2e/quotes.spec.ts,tests/e2e/threads.spec.tsand the thetests/e2e/sweep-*.spec.tssweeps. - Fixtures:
tests/fixtures/auth.ts(API_BASE,WEB_BASE,CREDENTIALSfromCHORUZ_OPERATOR_USER/CHORUZ_OPERATOR_PASSWORD) andtests/fixtures/api.ts. - Remote fixture:
infra/host/web_e2e.shdispatchesremote_web_e2e.mjswhenremote-file-transport.spec.tsoronline.spec.tsis selected, orCHORUZ_WEB_E2E_FULL=1. The wrapper owns a loopback Worker with private temporary Durable Object/D1 state, applies the account schema, waits on health, and stops it before removing the state directory.tests/fixtures/remote-dashboard.tsserves a separate dashboard origin with no API. - Harness:
infra/host/web_e2e.shstarts Postgres viainfra/host/start.sh, runsmigrate.sh resetandup, builds and startschoruz-api-gatewayandchoruz-pipeline, startspnpm devonCHORUZ_WEB_PORT(default 3100), then runspnpm e2e "$@"; with no arguments and withoutCHORUZ_WEB_E2E_FULL=1it runs onlytests/e2e/app-smoke.spec.ts. CI (.github/workflows/ci.yml) runsvitest relatedon changed files and shardedweb_e2e.shinvocations.
- sync-feed.md — the
/v1/ws/synccontract this client consumes. - api-gateway.md — the
/v1routes behindapiBaseUrl(). - threads.md —
lib/messages/threads.ts,lib/messages/thread-unreads.ts,components/chat/thread-panel.tsx. - channel-tasks.md — the kanban plugin, board components and template receipts.
- agent-protocol.md — the
crates/choruz-host-runtime/assets/agent-templates/fragments the web provisioning path composes. - Agent Notes: Board tasks created receipt.