Frontend architecture for the sessionstream-backed webchat application.
The webchat frontend is a React SPA under cmd/web-chat/web/src/:
cmd/web-chat/web/src/
ws/
wsManager.ts WebSocket lifecycle + sessionstream protocol
store/
store.ts Redux store configuration
appSlice.ts App-level state (status, errors)
timelineSlice.ts Timeline entity state (upsert, delete, clear)
webchat/
ChatWidget.tsx Root chat widget component
rendererRegistry.ts Entity kind → React component mapping
cards.tsx Card renderers (message, agent mode, etc.)
timelinePropsRegistry.ts Props normalization before rendering
The frontend receives data through the sessionstream WebSocket transport:
/api/chat/ws.ClientFrame shape such as { type: "subscribe", sessionId, sinceSnapshotOrdinal: "0" }.ServerFrame snapshot with snapshotOrdinal and entities — the full current state.ServerFrame UI events with eventOrdinal, name, and payload — live updates.On the backend these frames and payloads are protobuf-backed: sessionstream uses registered protobuf message schemas and serializes them to JSON for browser delivery. The frontend currently treats the decoded frame as a canonical JSON object and maps it into local Redux state.
snapshot frame
-> clear store, map registered timeline entities, upsert all entities
ui-event frame
-> derive mutation (upsert entity, delete entity, update status)
-> dispatch to timelineSlice
-> rendererRegistry resolves component by local entity kind
-> React re-renders
The production frontend does not parse historical SEM envelopes. Chatapp backend events and timeline entities are defined by proto/pinocchio/chatapp/v1/chat.proto and by registered ChatPlugin schemas.
WebSocket frame
-> wsManager.ts
-> timelineSlice.upsertEntity / deleteEntity
-> Redux store
-> React components (ChatWidget, cards)
Entities are rendered by kind. Register a renderer for each entity kind:
import { registerTimelineRenderer } from './rendererRegistry';
registerTimelineRenderer('message', MessageCard);
registerTimelineRenderer('agent_mode', AgentModeCard);
registerTimelineRenderer('tool_call', ToolCallCard);
registerTimelineRenderer('tool_result', ToolResultCard);
Props are normalized through timelinePropsRegistry.ts before reaching renderers, protecting against schema drift between protobuf JSON payloads and local React prop names.
The default web frontend maps registered backend entity kinds into local renderer kinds. For example, ChatMessage snapshot entities become local message entities, thinking rows are message entities with role: "thinking", and shared tool-call plugin entities can be normalized into tool_call / tool_result renderers by downstream apps.
cmd/web-chat/web/src/ws/wsManager.ts — WebSocket lifecycle and frame→state mappingcmd/web-chat/web/src/store/timelineSlice.ts — Redux slice for timeline entitiescmd/web-chat/web/src/webchat/ChatWidget.tsx — Root componentcmd/web-chat/web/src/webchat/rendererRegistry.ts — Kind → component registry