feat(client): built-in chat UI — streaming chat panel + inline write confirmations #346
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol#346
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The final piece of epic #47's in-app surface: replace the
chat.tsxplaceholder 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-clienthooks + 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/)serverUrl.ts/runtimeShell.ts— same asapiClient.ts): relative/api/...on the same-origin PWA, spliced runtime URL on Android + the Tauri/Flatpak shell.fetch:expo/fetchon native (RN's default fetch buffers and can't stream a response body),globalThis.fetchon web. Inject it as the package transport'sfetchImpl, withgetAuthHeader.chatReducer.ts)(state, ConversationStreamEvent) -> statethat accumulates a turn: append text deltas into a live assistant bubble, add persisted message rows, surface tool activity, flip toawaiting_confirmationwith the proposalId, and settle ondone/error. This is the tested core —chat.tsxonly dispatches events into it.useAgentTurnorchestrator wrappingstreamConversationTurn(#344) for send + resume, with a graceful fallback to the non-streaminguseSendMessageJsonwhen the stream transport errors before emitting any event (native edge cases).chat.tsxuseLlmConfig): 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).useConversations). Full rename/delete/search deferred.awaiting_confirmation), read the pendingproposalIdoff 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). Honorexpired/already-resolved states.errorstream events and transport errors render inline, non-fatal.i18n + voice
chatnamespace placeholder inpackages/i18n/messages/en.jsonwith 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
chatReducerunit tests (the valuable core): a text-only turn accumulates deltas then finalizes; a read-tool round adds tool activity + messages; a write pause setsawaiting_confirmationwith the right proposalId; anerrorevent settles cleanly; out-of-order/duplicate-safe where it matters.serverUrl.test.ts).nodeenv (no browser) — the visual panel + on-device native streaming are verified manually by the maintainer, not in CI.Out of scope / deferred follow-ups
Part of epic #47. Closes the in-app chat surface; the MCP external surface is already shipped.