feat(api-client): conversation + agent hooks + pure SSE streaming transport #345

Merged
james merged 1 commit from 344-api-client-agent-hooks into main 2026-06-29 09:43:19 +00:00
Owner

The built-in agent's full API exists (conversations #338, streaming #340, proposal read #343, commit/reject #51) but @carol/api-client had no hooks for it. This adds the data layer the chat UI (next ticket) consumes, keeping the package platform-neutral.

What's in it

  • hooks/conversations.ts — TanStack Query hooks over the typed client, mirroring hooks/notes.ts: useConversations (+useConversationsInfinite), useConversation, useCreateConversation, useSendMessageJson (the non-streaming JSON turn — works on every platform), useResumeConversation, useProposal, useCommitProposal, useRejectProposal. Every DTO from the generated contract; cache invalidation on mutations.
  • agent-stream.ts — a pure, React-free, platform-neutral SSE transport (streamConversationTurn) with an injected fetchImpl/url/getAuthHeader. No expo/react-native import — the app wires expo/fetch + the runtime server URL in the chat-UI ticket. Parses event:/data: frames into a typed ConversationStreamEvent union (mirrors the backend's ConversationEvent; can't import across the app boundary). Handles frames split across reads, malformed data: (skip, never throw), comment/keepalive lines, CRLF framing, no-body, and non-OK HTTP (→ onError, not a throw); in-band error events flow through onEvent.
  • keys.tsconversations + proposals key namespaces.

Verification (run locally on this branch)

  • typecheck ✓ · lint ✓ (no disables, no non-null assertions) · check ✓ ("generated client is up to date" — no spec/codegen change)
  • test ✓ — 8 files / 40 passed, incl. 12 behavioral SSE-transport tests over a fake fetch + ReadableStream (ordered deltas, tool call/result, awaiting-confirmation, message, done, in-band error, split-frame reassembly, comment/blank-line skip, malformed-data skip, non-OK → onError)
  • semgrep (CI pack set) ✓ 0 findings on both new src files

Note (potential small follow-up)

MessageDto isn't a named component in the generated OpenAPI types — openapi-typescript inlines it inside TurnResponseDto.messages[], so the stream event's message payload is derived as TurnResponseDto["messages"][number] (still contract-sourced, documented inline). Naming zMessageDto via .openapi("MessageDto") in the backend spec would let consumers reference components["schemas"]["MessageDto"] directly — out of scope here (client-only PR, no spec change), worth a tiny backend follow-up if the UI wants the named type.

Out of scope

  • App wiring of the transport (expo/fetch, runtime URL, streaming send-from-context) — chat-UI ticket.
  • The chat UI itself.

No openapi.json change. Part of epic #47; unblocks the chat UI.

Closes #344

🤖 Generated with Claude Code

The built-in agent's full API exists (conversations #338, streaming #340, proposal read #343, commit/reject #51) but `@carol/api-client` had no hooks for it. This adds the data layer the chat UI (next ticket) consumes, keeping the package platform-neutral. ## What's in it - **`hooks/conversations.ts`** — TanStack Query hooks over the typed client, mirroring `hooks/notes.ts`: `useConversations` (+`useConversationsInfinite`), `useConversation`, `useCreateConversation`, `useSendMessageJson` (the **non-streaming** JSON turn — works on every platform), `useResumeConversation`, `useProposal`, `useCommitProposal`, `useRejectProposal`. Every DTO from the generated contract; cache invalidation on mutations. - **`agent-stream.ts`** — a pure, React-free, platform-neutral **SSE transport** (`streamConversationTurn`) with an injected `fetchImpl`/`url`/`getAuthHeader`. No `expo`/`react-native` import — the app wires `expo/fetch` + the runtime server URL in the chat-UI ticket. Parses `event:/data:` frames into a typed `ConversationStreamEvent` union (mirrors the backend's `ConversationEvent`; can't import across the app boundary). Handles frames split across reads, malformed `data:` (skip, never throw), comment/keepalive lines, CRLF framing, no-body, and non-OK HTTP (→ `onError`, not a throw); in-band `error` events flow through `onEvent`. - **`keys.ts`** — `conversations` + `proposals` key namespaces. ## Verification (run locally on this branch) - `typecheck` ✓ · `lint` ✓ (no disables, no non-null assertions) · `check` ✓ ("generated client is up to date" — no spec/codegen change) - `test` ✓ — 8 files / 40 passed, incl. **12 behavioral SSE-transport tests** over a fake `fetch` + `ReadableStream` (ordered deltas, tool call/result, awaiting-confirmation, message, done, in-band error, split-frame reassembly, comment/blank-line skip, malformed-data skip, non-OK → onError) - semgrep (CI pack set) ✓ 0 findings on both new src files ## Note (potential small follow-up) `MessageDto` isn't a *named* component in the generated OpenAPI types — `openapi-typescript` inlines it inside `TurnResponseDto.messages[]`, so the stream event's `message` payload is derived as `TurnResponseDto["messages"][number]` (still contract-sourced, documented inline). Naming `zMessageDto` via `.openapi("MessageDto")` in the backend spec would let consumers reference `components["schemas"]["MessageDto"]` directly — out of scope here (client-only PR, no spec change), worth a tiny backend follow-up if the UI wants the named type. ## Out of scope - App wiring of the transport (`expo/fetch`, runtime URL, streaming send-from-context) — chat-UI ticket. - The chat UI itself. No `openapi.json` change. Part of epic #47; unblocks the chat UI. Closes #344 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(api-client): conversation + agent hooks + pure SSE streaming transport
All checks were successful
Commits / Conventional Commits (pull_request) Successful in 45s
PR / Static analysis (pull_request) Successful in 1m25s
PR / OSV-Scanner (pull_request) Successful in 19s
PR / Trivy (image) (pull_request) Successful in 1m36s
PR / pnpm audit (pull_request) Successful in 5m8s
PR / Lint (pull_request) Successful in 6m12s
PR / OpenAPI (pull_request) Successful in 6m20s
PR / Package age policy (soft) (pull_request) Successful in 25s
PR / Typecheck (pull_request) Successful in 6m53s
Secrets / gitleaks (pull_request) Successful in 47s
PR / Test (sqlite) (pull_request) Successful in 7m8s
PR / Build (pull_request) Successful in 7m12s
PR / Client (web export smoke) (pull_request) Successful in 7m19s
PR / Test (postgres) (pull_request) Successful in 7m31s
PR / Coverage (soft) (pull_request) Successful in 4m43s
PR / E2E (Playwright) (pull_request) Successful in 11m15s
2f40657e02
Add the data layer the chat UI (epic #47) needs. The full backend
surface already exists (conversations #338, streaming #340, proposal
read #343, commit/reject #51); this wires @carol/api-client to it.

- hooks/conversations.ts: TanStack Query hooks over the typed client —
  useConversations(+Infinite), useConversation, useCreateConversation,
  useSendMessageJson (non-streaming JSON turn, works on every platform),
  useResumeConversation, useProposal, useCommitProposal,
  useRejectProposal. Mirrors hooks/notes.ts idioms; all DTOs from the
  generated contract.
- agent-stream.ts: a pure, React-free, platform-neutral SSE transport
  (streamConversationTurn) with an injected fetchImpl/url/auth — the app
  supplies expo/fetch + the runtime URL later. Parses the event:/data:
  frames into a typed ConversationStreamEvent union (mirrors the
  backend's ConversationEvent; can't import across the app boundary).
  Handles frames split across reads, malformed data (skip), comment
  lines, CRLF framing, and non-OK HTTP (→ onError, not throw).
- keys.ts: conversations + proposals namespaces.

Tests: thin structural hook coverage + 12 behavioral SSE-transport tests
over a fake fetch + ReadableStream. No openapi.json change (endpoints
already in the spec); api-client `check` stays green.

Closes #344

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 664a2fb17b into main 2026-06-29 09:43:19 +00:00
james deleted branch 344-api-client-agent-hooks 2026-06-29 09:43:20 +00:00
Sign in to join this conversation.
No description provided.