Chat runtime
Cross-package boundary overview for the chat-v2 runtime in `apps/web`. Implementation details live next to the code under `apps/web/docs/chat/`.
The chat feature in apps/web (route /chat-v2, plus the embedded /agent
shell) runs on a custom runtime that the team built rather than adapting an
off-the-shelf chat library. This page is the cross-package boundary
overview; the implementation details — data model, persistence schema,
scroll anchoring, in-flight reply handling — live in
apps/web/docs/chat/,
which is the canonical source for anyone touching the code.
Three-layer split
apps/web/src/features/chat-v2/ Business glue: composes store + protocol + UI
apps/web/src/lib/chat/ Data layer: zustand store, dexie persistence, SSE source
design-system/opal-ui/src/chat/ Presentation: MessageList, MessageBubble, Composer, MediaViewerThe boundary rules:
opal-ui/src/chatis purely presentational. It receives data, emits intents through callbacks. It must not import fromapps/web, the lib store, or any network code.apps/web/src/lib/chatknows nothing about React rendering. It exports plain functions and a zustand store. Components subscribe via thin selector hooks infeatures/chat-v2.apps/web/src/features/chat-v2is the only place that wires the store, the agent client, the SSE source, and theopal-uiprimitives together.
This split is enforced by code review; there are no ESLint rules pinning it today.
Live + history dual sources
| Source | Role |
|---|---|
apps/web/src/lib/chat/runtime/live-source.ts | SSE consumer — thread.token, message.new, thread.complete, etc. Applies updates into the store. |
apps/web/src/lib/chat/runtime/remote-loader.ts | HTTP history loader — fills holes when the user scrolls past loaded messages. |
Both sources feed the same MessageStore.apply(updates) API. Dedup is
handled inside the store so the live source doesn't write the same
message twice.
Where to read further
The data layer is a TypeScript port of the iOS TDChat Postbox layer
(zustand actor-equivalent with a sequential write queue; Dexie over
IndexedDB instead of GRDB). All the load-bearing concepts — MessageID
peer-scoped identity, MessageIndex sort keys, MessageWindow sliding
windows, HoleSet range merging, per-session store lifecycle, persistence
schema, token-streaming buffer — live in:
apps/web/docs/chat/architecture.md— full data-layer referenceapps/web/docs/chat/api-contract.md— the HTTP + SSE surface the client expects from the cloudapps/web/docs/chat/migration-plan.md— chat-v1 → chat-v2 migration sequence
These docs live next to the code they describe by design — they change together. This site links out rather than mirroring them to avoid drift.
Related
- API codegen —
chat-runtimeconsumes the generated agent client (apps/web/src/lib/chat/protocol/agent-client.ts) - MSW mocks — chat handlers in
apps/web/src/lib/msw/handlers/agent.tsmock the SSE stream for Storybook + unit tests - Workspace → Apps →
@todayai-labs/web— top-level package overview
API codegen
TypeScript clients are generated from the backend's OpenAPI spec via `@hey-api/openapi-ts`. `pnpm api:generate` writes to `packages/api-client/src/generated/` — never edit those files by hand.
Widget host deps
Why widget bundles resolve `react` and `@todayai-labs/tck` to the app's own module instances, what the ESM island cost before that, and which options were rejected.