How frontend clients integrate with the sessionstream-backed chat API, WebSocket transport, and snapshot hydration.
Frontend integration uses three endpoints:
POST /api/chat/sessions/:sessionId/messages to submit user promptsWS /api/chat/ws for streaming sessionstream UI events (subscribe to a session)GET /api/chat/sessions/:sessionId for snapshot hydrationBrowser UI
-> POST /api/chat/sessions/:sessionId/messages
-> WS /api/chat/ws (subscribe with sessionId)
-> GET /api/chat/sessions/:sessionId (snapshot)
Go backend
-> ChatService handles submit/queue/idempotency
-> sessionstream Hub manages protobuf-registered event projection and UI fanout
-> WebSocket transport delivers protobuf JSON snapshot + live UI events
The sessionstream WebSocket transport uses protobuf-defined ClientFrame and ServerFrame messages serialized as JSON. The browser still sends and receives JSON objects, but the backend schema source is protobuf.
/api/chat/ws.{ type: "hello", connectionId: "conn-N" }.{ type: "subscribe", sessionId: "...", sinceSnapshotOrdinal: "0" }.{ type: "snapshot", sessionId, snapshotOrdinal, entities: [...] } with current state.{ type: "subscribed", sessionId }.{ type: "ui-event", sessionId, eventOrdinal, name, payload } for each live event.{ type: "unsubscribe", sessionId } to stop receiving events.{ type: "ping" } to check liveness (server replies { type: "pong" }).Some existing frontend helpers still accept the older sinceOrdinal / ordinal spellings while sessionstream clients are being aligned. New documentation and new clients should use the role-specific names sinceSnapshotOrdinal, snapshotOrdinal, and eventOrdinal.
wsManager (Dedicated WebSocket Lifecycle Module)Centralize WebSocket logic into a dedicated wsManager module (see pinocchio/cmd/web-chat/web/src/ws/wsManager.ts) instead of spreading it across components.
Key reasons:
After subscribing, the server sends live updates as UI event frames. The payload is the protobuf JSON form of the registered UI event message.
{
"type": "ui-event",
"sessionId": "sess-1",
"eventOrdinal": "42",
"name": "ChatMessageAppended",
"payload": {
"messageId": "chat-msg-1:text:2",
"parentMessageId": "chat-msg-1",
"segment": 2,
"segmentType": "text",
"role": "assistant",
"content": "Hi",
"status": "streaming",
"streaming": true,
"final": false
}
}
Common UI event names:
ChatMessageAccepted — user message submittedChatMessageStarted — assistant begins respondingChatMessageAppended — assistant text segment updated during streamingChatMessageFinished — assistant text segment or final response completeChatMessageStopped — response stopped (user cancel or error)ChatReasoningStarted, ChatReasoningAppended, ChatReasoningFinished — shared thinking/reasoning plugin eventsChatToolCallStarted, ChatToolCallUpdated, ChatToolCallFinished, ChatToolResultReady — shared tool-call plugin eventsChatAgentModePreviewUpdated, ChatAgentModeCommitted, ChatAgentModePreviewCleared — app-owned mode switch eventsOn subscribe, the server sends the current state. Entity payloads are protobuf JSON for their registered timeline entity kind.
{
"type": "snapshot",
"sessionId": "sess-1",
"snapshotOrdinal": "15",
"entities": [
{
"kind": "ChatMessage",
"id": "chat-msg-1:thinking:1",
"tombstone": false,
"createdOrdinal": "11",
"lastEventOrdinal": "14",
"payload": {
"messageId": "chat-msg-1:thinking:1",
"parentMessageId": "chat-msg-1",
"segment": 1,
"segmentType": "thinking",
"role": "thinking",
"content": "I should inspect the tool output first.",
"status": "finished",
"streaming": false
}
}
]
}
POST /api/chat/sessions/sess-1/messages
{
"prompt": "Hello assistant",
"profile": "default"
}
If backend runs with --root /chat, prefix all endpoints:
/chat/api/chat/sessions/:sessionId/messages/chat/api/chat/ws/chat/api/chat/sessions/:sessionIdCompute a base prefix from window.location.pathname and reuse it consistently for fetch + websocket URLs.
POST responses as user-visible failures.pinocchio/cmd/web-chat/web/src/ws/wsManager.tspinocchio/cmd/web-chat/web/src/store/timelineSlice.tspinocchio/cmd/web-chat/web/src/webchat/rendererRegistry.ts