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>
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
-
Inspect
routers/ai.tsto see what procedures exist. If there's already achat/completeprocedure, 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
streamTextfrom@tasks/ai). - Wrap with
workspaceProcedureso workspace membership is checked.
- Input:
-
Use streaming, not request/response. The Vercel AI SDK's
useChathook is the natural fit, but it expects a/api/chatHTTP endpoint, not tRPC. Two options:- (a) Add a Next.js route handler at
apps/web/app/api/chat/route.tsthat calls into the same provider abstraction inpackages/ai. Keep auth in the route handler (auth()fromapps/web/lib/auth.ts). UseuseChat()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.
- (a) Add a Next.js route handler at
-
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. -
Provider selection comes from env (
packages/aialready supports this — OpenAI, Anthropic, or Ollama on CT 108 viaOPENAI_BASE_URL). Don't hardcode a provider in the route handler. -
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.
-
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-coordinationwork. - 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.tsandpackages/ai/src/for existing primitives. The existingaiRouter.chatmutation uses non-streaminggenerateText; 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.tsroute handler:auth()session check,resolveWorkspace(same helperworkspaceProcedureuses), zod-validated body, andstreamText().toDataStreamResponse(). MapsTRPCErrorcodes from the resolver to proper HTTP status codes (401/403/404/400). - Replaced the
setTimeoutblock in[workspaceSlug]/ai/page.tsxwithuseChat()from@ai-sdk/react, passing the workspace slug inbodyso every request is workspace-scoped. - Added an inline
ChatErrorBannerthat 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, optionallyOPENAI_BASE_URLfor Ollama on CT 108,OPENAI_MODELfor 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.chatmutation 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/chathandler reuses the sameauth + resolveWorkspaceplumbing without disrupting that consumer. - Provider config via env.
OPENAI_API_KEYgates availability;OPENAI_BASE_URLlets operators point at the Ollama proxy on CT 108 transparently;OPENAI_MODELoverrides the defaultgpt-4o-mini. No provider name is hardcoded in the route handler. runtime = "nodejs"anddynamic = "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
setTimeoutmock remains in[workspaceSlug]/ai/page.tsx. - Sending a message streams an assistant response from a real provider (when
OPENAI_API_KEYis 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.
Links to related Epic / Plan
- Epic:
./Epic-shipping-the-shell.md - Plan:
../Plan-daily-driver-finish.md