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

Merged
james merged 1 commit from 346-chat-ui into main 2026-06-29 11:21:14 +00:00
Owner

Replaces the /chat placeholder with a real streaming chat panel for the built-in agent — the last piece of epic #47's in-app surface. Hits the exit criterion: the user asks, Carol answers token-by-token, and write proposals are confirmed inline before anything changes. Consumes the merged backend (loop #338, streaming #340, proposal read #343) + api-client hooks/SSE transport (#344).

What's in it

  • lib/agent/chatReducer.ts — a pure, React-free reducer folding the ConversationStreamEvent wire into a render-ready turn snapshot (live text bubble, persisted rows, tool activity, awaiting_confirmation + proposalId, terminal done/error). The tested logic core; chat.tsx only dispatches into it.
  • lib/agent/streamUrl.ts + apiClient.tsresolveAgentUrl reuses the typed client's off-origin rule via a new exported resolveOffOriginBase() (additive — the existing per-request rewriter is untouched), so the SSE stream and the openapi-fetch client never drift on base URL.
  • lib/agent/agentFetch.tsexpo/fetch on native (RN's default fetch buffers and can't stream SSE), globalThis.fetch on web.
  • lib/agent/useAgentTurn.ts — send/resume over streamConversationTurn, with a JSON fallback when the stream errors before emitting any event, then invalidates the conversation so the persisted thread is authoritative.
  • chat.tsx — no-provider empty state linking to Settings, most-recent conversation + new/switcher, streaming bubbles, and an inline proposal confirmation card (summary + action + diff → commit/reject → resume). Mirrors notes.tsx (DS tokens, RN primitives, voice).
  • i18n — real chat namespace strings in Carol's voice; es.json untouched (per-key English fallback).

Verification (run locally on this branch)

  • typecheck ✓ · lint ✓ (no disables/non-null assertions) · test ✓ — 23 files / 149 passed, incl. 8 reducer tests + URL/fetch-picker branch tests
  • semgrep (CI pack set) ✓ 0 findings across all 8 new/changed source + test files
  • en.json valid JSON; no hardcoded user-facing strings, no raw colors in chat.tsx (all via theme.tokens)

⚠️ Needs maintainer verification (cannot be done headlessly)

Client vitest is node-env — no browser/device. Not automatically verified, please check manually:

  • The visual panel (bubbles, switcher, composer, confirmation card layout) in the PWA.
  • On-device native expo/fetch SSE streaming on Android, and the JSON fallback path (e.g. behind a buffering proxy).

Deferred (follow-ups)

  • Conversation rename/delete/search; auto-scroll-to-bottom; markdown rendering of replies; multi-pane desktop polish. Voice I/O is its own epic ticket.

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

Closes #346

🤖 Generated with Claude Code

Replaces the `/chat` placeholder with a real streaming chat panel for the built-in agent — the last piece of epic #47's in-app surface. Hits the exit criterion: the user asks, Carol answers token-by-token, and write proposals are confirmed inline before anything changes. Consumes the merged backend (loop #338, streaming #340, proposal read #343) + api-client hooks/SSE transport (#344). ## What's in it - **`lib/agent/chatReducer.ts`** — a pure, React-free reducer folding the `ConversationStreamEvent` wire into a render-ready turn snapshot (live text bubble, persisted rows, tool activity, `awaiting_confirmation` + proposalId, terminal done/error). The tested logic core; `chat.tsx` only dispatches into it. - **`lib/agent/streamUrl.ts` + `apiClient.ts`** — `resolveAgentUrl` reuses the typed client's off-origin rule via a new exported `resolveOffOriginBase()` (additive — the existing per-request rewriter is untouched), so the SSE stream and the openapi-fetch client never drift on base URL. - **`lib/agent/agentFetch.ts`** — `expo/fetch` on native (RN's default fetch buffers and can't stream SSE), `globalThis.fetch` on web. - **`lib/agent/useAgentTurn.ts`** — send/resume over `streamConversationTurn`, with a JSON fallback when the stream errors before emitting any event, then invalidates the conversation so the persisted thread is authoritative. - **`chat.tsx`** — no-provider empty state linking to Settings, most-recent conversation + new/switcher, streaming bubbles, and an inline proposal confirmation card (summary + action + diff → commit/reject → resume). Mirrors `notes.tsx` (DS tokens, RN primitives, voice). - **i18n** — real `chat` namespace strings in Carol's voice; `es.json` untouched (per-key English fallback). ## Verification (run locally on this branch) - `typecheck` ✓ · `lint` ✓ (no disables/non-null assertions) · `test` ✓ — 23 files / 149 passed, incl. **8 reducer tests** + URL/fetch-picker branch tests - semgrep (CI pack set) ✓ 0 findings across all 8 new/changed source + test files - `en.json` valid JSON; no hardcoded user-facing strings, no raw colors in `chat.tsx` (all via `theme.tokens`) ## ⚠️ Needs maintainer verification (cannot be done headlessly) Client vitest is `node`-env — no browser/device. **Not** automatically verified, please check manually: - The **visual panel** (bubbles, switcher, composer, confirmation card layout) in the PWA. - **On-device native `expo/fetch` SSE streaming** on Android, and the **JSON fallback** path (e.g. behind a buffering proxy). ## Deferred (follow-ups) - Conversation rename/delete/search; auto-scroll-to-bottom; markdown rendering of replies; multi-pane desktop polish. Voice I/O is its own epic ticket. Part of epic #47. Closes the in-app chat surface (MCP external surface already shipped). Closes #346 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(client): built-in chat UI — streaming chat panel + inline write confirmations
All checks were successful
Commits / Conventional Commits (pull_request) Successful in 8s
PR / Static analysis (pull_request) Successful in 2m26s
PR / pnpm audit (pull_request) Successful in 2m43s
PR / OpenAPI (pull_request) Successful in 3m11s
PR / OSV-Scanner (pull_request) Successful in 55s
PR / Lint (pull_request) Successful in 3m36s
PR / Typecheck (pull_request) Successful in 4m1s
PR / Client (web export smoke) (pull_request) Successful in 4m13s
PR / Build (pull_request) Successful in 4m18s
PR / Test (sqlite) (pull_request) Successful in 4m18s
PR / Test (postgres) (pull_request) Successful in 4m25s
PR / Package age policy (soft) (pull_request) Successful in 1m1s
Secrets / gitleaks (pull_request) Successful in 49s
PR / E2E (Playwright) (pull_request) Successful in 5m2s
PR / Trivy (image) (pull_request) Successful in 2m18s
PR / Coverage (soft) (pull_request) Successful in 2m12s
874f04635a
Replace the /chat placeholder with a real streaming chat panel for the
built-in agent (epic #47). Hits the in-app exit criterion: the user asks,
Carol answers token-by-token, and write proposals are confirmed inline
before anything changes.

- lib/agent/chatReducer.ts: a pure, React-free reducer that folds the
  ConversationStreamEvent wire into a render-ready turn snapshot (live
  text bubble, persisted rows, tool activity, awaiting_confirmation +
  proposalId, terminal done/error). The tested logic core — chat.tsx
  only dispatches into it.
- lib/agent/streamUrl.ts + apiClient.ts: resolveAgentUrl reuses the
  typed client's off-origin rule (new exported resolveOffOriginBase) so
  the SSE stream and the openapi-fetch client never drift on base URL.
- lib/agent/agentFetch.ts: expo/fetch on native (RN's default fetch
  buffers and can't stream SSE), globalThis.fetch on web.
- lib/agent/useAgentTurn.ts: send/resume over streamConversationTurn,
  with a JSON fallback when the stream errors before any event, then
  invalidates the conversation so the persisted thread is authoritative.
- chat.tsx: no-provider empty state linking to Settings, most-recent
  conversation + new/switcher, streaming bubbles, and an inline proposal
  confirmation card (summary + action + diff → commit/reject → resume).
- i18n: real chat namespace strings in Carol's voice; es.json untouched.

Tests: pure reducer (8 cases) + URL/fetch-picker branches. Client vitest
is node-env, so the visual panel and on-device native streaming need
maintainer verification.

Closes #346

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 79.3% ≥ 50%
Branches 71.0% ⚠️ ≥ 75%
Functions 80.3% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 79.3% ✅ | ≥ 50% | | Branches | 71.0% ⚠️ | ≥ 75% | | Functions | 80.3% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
james merged commit d535b586fa into main 2026-06-29 11:21:14 +00:00
james deleted branch 346-chat-ui 2026-06-29 11:21:14 +00:00
Sign in to join this conversation.
No description provided.