diff --git a/frontend/web/src/lib/api.ts b/frontend/web/src/lib/api.ts index c7b7d78..bbb1aea 100644 --- a/frontend/web/src/lib/api.ts +++ b/frontend/web/src/lib/api.ts @@ -248,15 +248,24 @@ export function chatSync( * `image` (a base64 data: URI or URL) is attached for the vision path; the * gateway's chat body currently has no image field, so an unconfigured backend * simply ignores it. `deviceId` mirrors the client's per-browser id onto the - * persisted message (design/16 §4.5). */ + * persisted message (design/16 §4.5). `mode` is the behavior mode this turn is + * sent in (design/01 §3); omitted when empty so the backend falls back to chat. */ export function chatStream( sessionId: string, text: string, - image?: string + image?: string, + mode?: Mode ): Promise<{ stream: true }> { return request<{ stream: true }>("/chat", { method: "POST", - body: JSON.stringify({ sessionId, text, image, stream: true, deviceId: getClientId() }), + body: JSON.stringify({ + sessionId, + text, + image, + stream: true, + deviceId: getClientId(), + ...(mode ? { mode } : {}), + }), }); } diff --git a/frontend/web/src/lib/useChat.ts b/frontend/web/src/lib/useChat.ts index f32220e..c74e3c5 100644 --- a/frontend/web/src/lib/useChat.ts +++ b/frontend/web/src/lib/useChat.ts @@ -8,13 +8,13 @@ import { now } from "./util"; /** Drives one user turn: push the user bubble, blanket a streaming assistant * bubble, trigger the backend loop over REST (`stream:true`), and let the WS - * stream render into that bubble. Falls back to the WS `chat` channel when the - * REST trigger fails so the demo still works without a wired loop. + * stream render into that bubble. If the REST trigger fails, the failure is + * surfaced inline on the bubble rather than left streaming forever. * - * The behavior mode is NOT chosen by the user at send time — the agent / backend - * hot-switches it (design/01 §3), and each message carries its own mode, so the - * bubble renders per that message's mode. Until the backend advertises a - * per-message mode, a new assistant bubble defaults to the current UI mode. */ + * The behavior mode is chosen by the agent / backend hot-switch (design/01 §3); + * each message carries the mode in effect when the turn is sent, and the bubble + * renders per that message's mode. We read the current UI mode here and pass it + * on the turn so the backend persists it alongside the message (A3). */ export function useSendChat(): { send: (text: string, image?: string) => Promise; sending: boolean; @@ -46,7 +46,7 @@ export function useSendChat(): { beginAssistant(mode, clientId); try { - await chatStream(sessionId, prompt, image); + await chatStream(sessionId, prompt, image, mode); } catch (err: unknown) { // Backend loop not wired or gateway unreachable: show the failure inline // rather than leaving a permanently-streaming bubble.