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>
Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and
hardcoded `recent` list on the workspace-home page with real
workspace-scoped data.
* server/routers/objects.ts: add two new procedures.
- `objects.stats` returns { openTasks, containers }. Open-task count
treats null status as open; only `done` and `closed` (per
packages/shared object-statuses) are terminal. Container count
aggregates project + space + group rows.
- `objects.listRecent({ limit })` returns the N most-recently-updated
rows, descending by updated_at. Excludes archived and excludes
`workspace`/`group` from the activity feed (containers clutter
"what did I just touch" recency).
Both go through workspaceProcedure, so the workspace_id filter
comes from the middleware-resolved ctx.workspace.id rather than
any user input.
* app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new
procedures via @trpc/react-query. Adds:
- Skeleton loading state (no flash of zeros).
- Empty state with a "New task" CTA on workspaces with no objects.
- Real "X ago" labels on the recent feed.
- Click-through links from recent rows to /{slug}/{id}.
- A locally-mounted CreateObjectDialog instance independent of the
global one in AppShell so the empty-state CTA can pre-seed
defaultType="task" without coordinating shared state.
* components/ui/skeleton.tsx: new (standard shadcn pulse skeleton).
Used by the dashboard but reusable across the app.
The scaffolded "Due this week" stat is dropped: `objects` has no
due_at column and the task explicitly preferred dropping a card to
schema-creep.
`pnpm lint && pnpm type-check` clean. Closes
plans/Plan-daily-driver-finish/Epic-shipping-the-shell/
Task-wire-workspace-home-dashboard.md.
Co-authored-by: Cursor <cursoragent@cursor.com>
Block A of the EchoDo plan. Workspaces used to live as `objects(type='workspace')`,
which made it impossible to put a real RLS-friendly tenant boundary on the schema
or to give each workspace a stable URL slug. This commit:
- Adds a top-level `workspaces` table (slug unique, owner FK, plan_tier hook).
- Migrates the 8 anchor tables (objects, workspace_members, object_type_defs,
property_definitions, templates, forms, markdown_backlog_items,
cursor_sync_mappings) to FK into `workspaces.id` instead of `objects.id`,
with a hand-augmented data-copy migration that preserves IDs and slug-collision-
proofs on backfill.
- Introduces a `workspaceProcedure` tRPC middleware + `resolveWorkspace` helper
that take a UUID-or-slug `workspace` handle and expose `ctx.workspace`. All
tenant-scoped routers (objects, types, properties, templates, forms, search,
ai, relations, favorites) now flow through it.
- Updates the web app to pass `workspace` slugs from the URL (or store) instead
of the old `workspaceId`, including a workspace-sync layer that rewrites
/<UUID>/... links to /<slug>/...
- Updates the MCP tools (list_objects, create_object, search_objects) and the
workspace://{handle}/tree resource to accept either a slug or UUID so existing
agents keep working.
- Adds a Create Workspace dialog and a Workspace Settings page (rename + slug
rename with redirect, owner-only archive).
Verified locally against a fresh Postgres: migration applies cleanly, slug
uniqueness holds, tenant data is isolated by workspace_id, slug↔UUID resolution
works in both directions, and ON DELETE CASCADE cleans up child rows in the
correct workspace only.
Co-authored-by: Cursor <cursoragent@cursor.com>