ubiquitous-invention/plans/Plan-daily-driver-finish/Epic-shipping-the-shell/Task-wire-ai-chat-to-trpc.md
Randall Stillwell a1e6c863d5 feat(web): wire AI chat page to streaming /api/chat handler
Path-A task 3/5. Replaces the setTimeout mock that returned the literal
"Full AI integration is coming soon!" string with a real streaming
provider call.

* apps/web/app/api/chat/route.ts (new): POST handler that runs the
  same auth + resolveWorkspace pipeline workspaceProcedure uses, then
  streams a response from streamText().toDataStreamResponse(). Maps
  resolveWorkspace's TRPCError codes to HTTP status (401/403/404/400).
  Returns a structured 503 with a human-readable hint when
  OPENAI_API_KEY is unset, so the misconfiguration is surfaced rather
  than masked by a fake stream.

* apps/web/app/(app)/[workspaceSlug]/ai/page.tsx: replace the local
  message-state + setTimeout placeholder with useChat from
  @ai-sdk/react. workspace slug is sent on every request body so the
  server can enforce tenant scoping. Adds a ChatErrorBanner that
  parses the JSON error body the route emits and renders amber for
  the "unavailable" case, destructive for other failures.

* apps/web/package.json: pull in @ai-sdk/react as a direct dep
  (previously only transitive via `ai`).

The existing aiRouter.chat tRPC mutation is left intact — it powers
the right-panel command palette via the non-streaming generateText
path, and rebuilding that as streaming was outside the scope of
making the dedicated chat page usable.

Provider selection still flows from env per packages/ai conventions:
OPENAI_API_KEY gates availability, OPENAI_BASE_URL lets operators
route through Ollama on CT 108 transparently, OPENAI_MODEL overrides
the default gpt-4o-mini.

`pnpm lint && pnpm type-check` clean. Closes
plans/Plan-daily-driver-finish/Epic-shipping-the-shell/
Task-wire-ai-chat-to-trpc.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 00:23:21 -05:00

5.5 KiB

kind slug title plan_slug epic_slug status priority tenant_id owner cursor_todo_id updated_at
task wire-ai-chat-to-trpc Replace AI chat setTimeout placeholder with a real provider call daily-driver-finish shipping-the-shell done P0 global unassigned null 2026-06-02

Task summary

apps/web/app/(app)/[workspaceSlug]/ai/page.tsx is a setTimeout mock that returns the literal string "Full AI integration is coming soon!". Replace with a real call through apps/web/server/routers/ai.ts and packages/ai.

Description

The page already manages local chat state — message list, input ref, auto-scroll, loading state. All it's missing is the network call.

Approach

  1. Inspect routers/ai.ts to see what procedures exist. If there's already a chat / complete procedure, use it. Otherwise add one:

    • Input: { messages: Array<{ role: "user" | "assistant" | "system", content: string }>, workspaceSlug: string }
    • Output: streaming text (use the Vercel AI SDK's streamText from @tasks/ai).
    • Wrap with workspaceProcedure so workspace membership is checked.
  2. Use streaming, not request/response. The Vercel AI SDK's useChat hook is the natural fit, but it expects a /api/chat HTTP endpoint, not tRPC. Two options:

    • (a) Add a Next.js route handler at apps/web/app/api/chat/route.ts that calls into the same provider abstraction in packages/ai. Keep auth in the route handler (auth() from apps/web/lib/auth.ts). Use useChat() on the client.
    • (b) Stream through tRPC v11's subscription support. More work; only choose this if you specifically want a single API style.
    • Recommendation: (a). It's the path of least resistance and matches how every other AI SDK example is structured.
  3. Tenant isolation: the provider call MUST be scoped by the resolved workspace. Pass the workspace slug from useParams() into the request; verify on the server that the session user is a member before invoking the model.

  4. Provider selection comes from env (packages/ai already supports this — OpenAI, Anthropic, or Ollama on CT 108 via OPENAI_BASE_URL). Don't hardcode a provider in the route handler.

  5. Error handling: surface a friendly error message inline in the chat (provider down, rate limited, etc.) rather than crashing the page. Show "AI is unavailable" if no provider env is set.

  6. Delete the placeholder string — leaving the "coming soon" copy in the file makes it look unshipped even after the wire-up.

Out of scope

  • Tool-calling, retrieval, or letting the AI mutate workspace objects. That's Plan-agent-coordination work.
  • Conversation persistence (storing chats in DB). Keep messages in component state for now; persistence is a follow-up.

Subtasks

  • Audit apps/web/server/routers/ai.ts and packages/ai/src/ for existing primitives. The existing aiRouter.chat mutation uses non-streaming generateText; we kept it intact (the right-panel command palette still calls it) and built a separate streaming endpoint for the chat page.
  • Added apps/web/app/api/chat/route.ts route handler: auth() session check, resolveWorkspace (same helper workspaceProcedure uses), zod-validated body, and streamText().toDataStreamResponse(). Maps TRPCError codes from the resolver to proper HTTP status codes (401/403/404/400).
  • Replaced the setTimeout block in [workspaceSlug]/ai/page.tsx with useChat() from @ai-sdk/react, passing the workspace slug in body so every request is workspace-scoped.
  • Added an inline ChatErrorBanner that distinguishes the 503 "AI unavailable" case (amber, surfaces the env-var hint verbatim from the server) from generic failures (destructive). Parses the JSON error body the route handler emits.
  • Type-check + lint clean. Live provider verification will happen against whatever provider is configured in the operator's .env (OPENAI_API_KEY, optionally OPENAI_BASE_URL for Ollama on CT 108, OPENAI_MODEL for non-default models). The "unavailable" path was verified by construction — no key returns the structured 503.

Decisions made vs. the scaffold

  • Kept the existing aiRouter.chat mutation untouched. The scaffold suggested adding streaming to the existing tRPC route, but that's a much bigger change (tRPC v11 subscriptions / SSE adapter) and the existing mutation is still consumed by the right-panel command palette. The new /api/chat handler reuses the same auth + resolveWorkspace plumbing without disrupting that consumer.
  • Provider config via env. OPENAI_API_KEY gates availability; OPENAI_BASE_URL lets operators point at the Ollama proxy on CT 108 transparently; OPENAI_MODEL overrides the default gpt-4o-mini. No provider name is hardcoded in the route handler.
  • runtime = "nodejs" and dynamic = "force-dynamic" are explicit on the route. The auth call hits the DB; the streaming response can't be cached.

Owner or assignee

Unassigned

Status

done

Estimation

M

Acceptance criteria

  • No setTimeout mock remains in [workspaceSlug]/ai/page.tsx.
  • Sending a message streams an assistant response from a real provider (when OPENAI_API_KEY is set).
  • Server route validates session (auth()) and workspace membership (resolveWorkspace) before calling the provider.
  • Error states render an inline banner: amber "AI is unavailable" when the server returns 503 (no API key), destructive variant for any other failure, with the server-provided message surfaced verbatim.
  • Epic: ./Epic-shipping-the-shell.md
  • Plan: ../Plan-daily-driver-finish.md