feat(client): built-in chat UI — streaming chat panel + inline write confirmations #346

Closed
opened 2026-06-29 09:46:32 +00:00 by james · 0 comments
Owner

The final piece of epic #47's in-app surface: replace the chat.tsx placeholder with a real streaming chat panel that talks to the built-in agent, renders token-by-token, and confirms write proposals inline. The backend (loop #338, streaming #340, proposal read #343, commit/reject #51) and the @carol/api-client hooks + SSE transport (#344) are all in; this is the app wiring + UI + copy.

Hits the epic exit criterion: "a user types 'add a note about last week's call with Sam to Sam's profile' — the agent answers and, on confirm, writes the note."

Scope

App streaming wiring (apps/client/lib/agent/)

  • Resolve the absolute turn URL reusing the app's existing base-URL logic (serverUrl.ts / runtimeShell.ts — same as apiClient.ts): relative /api/... on the same-origin PWA, spliced runtime URL on Android + the Tauri/Flatpak shell.
  • Pick the streaming fetch: expo/fetch on native (RN's default fetch buffers and can't stream a response body), globalThis.fetch on web. Inject it as the package transport's fetchImpl, with getAuthHeader.
  • A pure reducer (chatReducer.ts) (state, ConversationStreamEvent) -> state that accumulates a turn: append text deltas into a live assistant bubble, add persisted message rows, surface tool activity, flip to awaiting_confirmation with the proposalId, and settle on done/error. This is the tested core — chat.tsx only dispatches events into it.
  • A useAgentTurn orchestrator wrapping streamConversationTurn (#344) for send + resume, with a graceful fallback to the non-streaming useSendMessageJson when the stream transport errors before emitting any event (native edge cases).

chat.tsx

  • No provider configured (useLlmConfig): an empty state in Carol's voice — "I need an AI provider before we can talk." — with a button linking to Settings (/account, the LlmCard).
  • Active conversation: load the most-recent conversation on mount (or start fresh); a "new conversation" action; a lightweight switcher over recent conversations (useConversations). Full rename/delete/search deferred.
  • Messages: render user/assistant/tool turns; stream the assistant reply token-by-token into a live bubble; show tool-call activity subtly.
  • Composer: text input + send; disabled while a turn is streaming.
  • Write confirmation: when a turn pauses (awaiting_confirmation), read the pending proposalId off the assistant message's tool calls, fetch it (useProposal), and render an inline confirmation card — summary + action (safe/destructive) + a readable diff — with confirm/decline → useCommitProposal/useRejectProposal → resume (streamed). Honor expired/already-resolved states.
  • Errors: in-band error stream events and transport errors render inline, non-fatal.

i18n + voice

  • Replace the chat namespace placeholder in packages/i18n/messages/en.json with real strings (title, composer placeholder, no-provider empty state + link label, confirmation card labels, tool-activity, status/loading/error copy). Follow Carol's voice: first person, sentence case, lead with the takeaway, no emoji. No hardcoded user-facing strings.

Tests

  • chatReducer unit tests (the valuable core): a text-only turn accumulates deltas then finalizes; a read-tool round adds tool activity + messages; a write pause sets awaiting_confirmation with the right proposalId; an error event settles cleanly; out-of-order/duplicate-safe where it matters.
  • URL-resolution + fetch-picker pure-logic tests where feasible (node env, like serverUrl.test.ts).
  • Note: client vitest is node env (no browser) — the visual panel + on-device native streaming are verified manually by the maintainer, not in CI.

Out of scope / deferred follow-ups

  • Conversation rename/delete/search; multi-pane desktop layout polish.
  • Voice I/O (separate epic ticket).
  • Markdown rendering of assistant text (plain text v1) unless trivial.

Part of epic #47. Closes the in-app chat surface; the MCP external surface is already shipped.

The final piece of epic #47's in-app surface: replace the `chat.tsx` placeholder with a real streaming chat panel that talks to the built-in agent, renders token-by-token, and confirms write proposals inline. The backend (loop #338, streaming #340, proposal read #343, commit/reject #51) and the `@carol/api-client` hooks + SSE transport (#344) are all in; this is the app wiring + UI + copy. Hits the epic exit criterion: *"a user types 'add a note about last week's call with Sam to Sam's profile' — the agent answers and, on confirm, writes the note."* ## Scope ### App streaming wiring (`apps/client/lib/agent/`) - Resolve the absolute turn URL reusing the app's existing base-URL logic (`serverUrl.ts` / `runtimeShell.ts` — same as `apiClient.ts`): relative `/api/...` on the same-origin PWA, spliced runtime URL on Android + the Tauri/Flatpak shell. - Pick the streaming `fetch`: `expo/fetch` on native (RN's default fetch buffers and can't stream a response body), `globalThis.fetch` on web. Inject it as the package transport's `fetchImpl`, with `getAuthHeader`. - A **pure reducer** (`chatReducer.ts`) `(state, ConversationStreamEvent) -> state` that accumulates a turn: append text deltas into a live assistant bubble, add persisted message rows, surface tool activity, flip to `awaiting_confirmation` with the proposalId, and settle on `done`/`error`. This is the tested core — `chat.tsx` only dispatches events into it. - A `useAgentTurn` orchestrator wrapping `streamConversationTurn` (#344) for send + resume, with a graceful fallback to the non-streaming `useSendMessageJson` when the stream transport errors before emitting any event (native edge cases). ### `chat.tsx` - **No provider configured** (`useLlmConfig`): an empty state in Carol's voice — *"I need an AI provider before we can talk."* — with a button linking to Settings (`/account`, the LlmCard). - **Active conversation**: load the most-recent conversation on mount (or start fresh); a "new conversation" action; a lightweight switcher over recent conversations (`useConversations`). Full rename/delete/search deferred. - **Messages**: render user/assistant/tool turns; stream the assistant reply token-by-token into a live bubble; show tool-call activity subtly. - **Composer**: text input + send; disabled while a turn is streaming. - **Write confirmation**: when a turn pauses (`awaiting_confirmation`), read the pending `proposalId` off the assistant message's tool calls, fetch it (`useProposal`), and render an inline confirmation card — summary + action (safe/destructive) + a readable diff — with confirm/decline → `useCommitProposal`/`useRejectProposal` → resume (streamed). Honor `expired`/already-resolved states. - **Errors**: in-band `error` stream events and transport errors render inline, non-fatal. ### i18n + voice - Replace the `chat` namespace placeholder in `packages/i18n/messages/en.json` with real strings (title, composer placeholder, no-provider empty state + link label, confirmation card labels, tool-activity, status/loading/error copy). Follow Carol's voice: first person, sentence case, lead with the takeaway, no emoji. No hardcoded user-facing strings. ## Tests - **`chatReducer` unit tests (the valuable core):** a text-only turn accumulates deltas then finalizes; a read-tool round adds tool activity + messages; a write pause sets `awaiting_confirmation` with the right proposalId; an `error` event settles cleanly; out-of-order/duplicate-safe where it matters. - URL-resolution + fetch-picker pure-logic tests where feasible (node env, like `serverUrl.test.ts`). - Note: client vitest is `node` env (no browser) — the **visual** panel + on-device native streaming are verified manually by the maintainer, not in CI. ## Out of scope / deferred follow-ups - Conversation rename/delete/search; multi-pane desktop layout polish. - Voice I/O (separate epic ticket). - Markdown rendering of assistant text (plain text v1) unless trivial. Part of epic #47. Closes the in-app chat surface; the MCP external surface is already shipped.
james closed this issue 2026-06-29 11:21:14 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
james/carol#346
No description provided.