Skip to content

Latest commit

 

History

History
169 lines (130 loc) · 26.6 KB

web-client.md

File metadata and controls

169 lines (130 loc) · 26.6 KB

Web client

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.

Layout

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).

Owns

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

Data

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.

Entry points

  • 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_in means authenticated, not connected to other people. Password fields clear after each submission and are not stored in browser persistence. online-groups.tsx creates 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 mounts ChatApp, then opens /v1/ws/sync?device_id=…&cursor=… through useChatWebSocket and re-fetches /v1/unreads on demand. Failed bootstrap requests retry in a connection screen before the chat shell mounts.

  • Gateway traffic: next.config.ts rewrites /api/v1/:path* to the gateway resolved from CHORUZ_API_BASE_URL, CHORUZ_API_URL or CHORUZ_API_PORT (default http://127.0.0.1:3000); apiBaseUrl() in lib/api/choruz-api.ts uses the same precedence server-side, and NEXT_PUBLIC_CHORUZ_API_PORT is exposed to the browser.

  • Telemetry: choruz-trace.ts stores 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 requireAuth first, then use CHORUZ_RUNTIME_DIR, CHORUZ_GIT_REPO_PATH, CHORUZ_INTERNAL_PROVISION_TOKEN, CHORUZ_DATABASE_URL / CHORUZ_PG_* and the CHORUZ_{CLAUDE,CODEX,PI,GROK,OPENCODE}_BINARY variables for provisioning and filesystem work.

  • Docs: /docs and 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.mjs then tsc --noEmit).

Invariants

  • 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. MessageList uses 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.ts checks B's bytes while hosted A refuses API calls.

  • A sync page is applied to state and persisted before its cursor is acknowledged (useChatWebSocket sends sync_ack with frame.next_cursor after onChanges resolves), so a crash during apply replays the page; pinned by tests/e2e/websocket.spec.ts ("converges on one message in two tabs", "deduplicates optimistic messages with sync confirmations").

  • Optimistic rows never reach IndexedDB: persistMessages filters server_seq < OPTIMISTIC_SERVER_SEQ; pinned by lib/messages/message-db.test.ts and lib/messages/messages.test.ts.

  • mentionedAgentIds mirrors the router's @all / @name rules so the thinking indicator matches which agents wake; pinned by lib/messages/mentions.test.ts.

  • A terminal binding stays mounted for every open tab (openTerminalBindings), so switching tabs never drops the PTY WebSocket; pinned by lib/terminal/terminal-bindings.test.ts.

  • Every Next.js API route that acts on a principal goes through requireAuth, which round-trips to GET /v1/me rather than trusting decodeSessionClaims; pinned by the route.test.ts files beside each route.

  • A client plugin renders only when the host manifest satisfies requiredHostCapabilities (hostSupportsClientPlugin); pinned by plugins/registry.test.ts and tests/e2e/plugins.spec.ts.

  • Telemetry payloads are redacted before local persistence and again at the gateway. lib/api/telemetry-sanitize.test.ts and the gateway observability tests check sensitive fields. tests/e2e/telemetry.spec.ts verifies 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 by lib/groups/team-templates.test.ts and lib/groups/team-template-renderer.test.ts.

Failure modes

  • Agent provisioning preserves gateway HTTP errors and Retry-After through 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.ts operation catches and emits trace.event("indexeddb_fallback", { op, error }); the chat path re-fetches over HTTP.
  • Sync WebSocket drop: useChatWebSocket reports status: "reconnecting" and retries with backoff from RECONNECT_BASE_MS (500 ms) to RECONNECT_MAX_MS (16 s), resuming from the persisted ack_cursor.
  • Initial bootstrap failure on /dashboard: components/chat/dashboard.tsx displays 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 answers 503 Auth service unavailable (3 s timeout) rather than 401.
  • Unknown sync change types trigger a full bootstrap refresh (refreshBootstrap = true in chat-app.tsx), visible as an extra GET /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.

Tests

  • Unit (vitest, environment: "node", include lib/**/*.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 (uses fake-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 the route.test.ts files under app/api/* and app/auth/session-invalid.
  • E2E (Playwright, testDir: ./tests, project chromium, plus chromium-reduced-motion when CHORUZ_E2E_EXTENDED=1): tests/e2e/app-smoke.spec.ts (the default spec for web_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, plus tests/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.ts and the the tests/e2e/sweep-*.spec.ts sweeps.
  • Fixtures: tests/fixtures/auth.ts (API_BASE, WEB_BASE, CREDENTIALS from CHORUZ_OPERATOR_USER / CHORUZ_OPERATOR_PASSWORD) and tests/fixtures/api.ts.
  • Remote fixture: infra/host/web_e2e.sh dispatches remote_web_e2e.mjs when remote-file-transport.spec.ts or online.spec.ts is selected, or CHORUZ_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.ts serves a separate dashboard origin with no API.
  • Harness: infra/host/web_e2e.sh starts Postgres via infra/host/start.sh, runs migrate.sh reset and up, builds and starts choruz-api-gateway and choruz-pipeline, starts pnpm dev on CHORUZ_WEB_PORT (default 3100), then runs pnpm e2e "$@"; with no arguments and without CHORUZ_WEB_E2E_FULL=1 it runs only tests/e2e/app-smoke.spec.ts. CI (.github/workflows/ci.yml) runs vitest related on changed files and sharded web_e2e.sh invocations.

Related