Skip to content

refactor: rename Agents to Workspaces, Aliases to Presets#456

Open
heiwen wants to merge 9 commits into
mainfrom
claude-issue-455-20260503-0241
Open

refactor: rename Agents to Workspaces, Aliases to Presets#456
heiwen wants to merge 9 commits into
mainfrom
claude-issue-455-20260503-0241

Conversation

@heiwen
Copy link
Copy Markdown
Contributor

@heiwen heiwen commented May 3, 2026

Closes #455.

Summary

  • Drop agents + branches; introduce workspaces (per-org) and presets (per-workspace, name + slug + model)
  • URLs move to /w/:workspaceSlug/*; POST /v1/workspaces creates the Better Auth team inline
  • GET /v1/traces is top-level with optional ?workspace=slug; GreptimeDb tags are now hebo.workspace.slug / hebo.preset.slug
  • Gateway model resolver is presets-first with a 60s LRU; preset slug shadows canonical model id; BYOK providers tried first in catalog order, then platform defaults
  • API keys stamped with { workspaceId, workspaceSlug, createdByUserId } metadata; only org owners / admins can create them (hook in apps/auth/src/lib/api-key.ts)
  • Console sidebar: static Hebo.ai wordmark + Workspace switcher; nav order Overview → Presets → API Keys → Traces → Settings (API Keys hidden for member role)

Test plan

  • bun run lint (all workspaces)
  • bun run typecheck (all workspaces)
  • bun run test (gateway hook tests updated; all pass)
  • bun run build (console + services)
  • Webkit smoke: created workspace via /w/create, redirected to /w/my-first-workspace, opened Presets + Traces pages

Follow-ups explicitly deferred

  • Preset params column (Q-A)
  • Programmatic POST /v1/organizations for agent-driven sign-up (Q-I)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Introduced workspaces as the primary organizational unit, replacing agents.
    • Added presets for configurable model settings within workspaces.
    • Enabled workspace-scoped API key generation and management.
    • Updated trace and observability features to be workspace-centric.
  • Changes

    • Redesigned navigation and settings interfaces to reflect workspace-based architecture.
    • Migrated authentication and team management to support workspace contexts.

Breaking rework of the data model and URL structure:

- apps/api: drop `agents` and `branches` tables/modules; add `workspaces`
  (scoped per-org) and `presets` (scoped per-workspace; `name`, `slug`, `model`)
- apps/api: promote traces to top-level `GET /v1/traces` with optional
  `?workspace=slug`; drop the nested `/agents/:slug/branches/:slug` shape
- apps/gateway: presets-first resolver (60s LRU); preset slug can shadow a
  canonical model id; BYOK two-pass provider selection (all BYOK providers
  in catalog order before platform defaults); OTEL tags become
  `hebo.workspace.slug` / `hebo.preset.slug`
- apps/auth: rename `teams.agentSlug` to `teams.workspaceSlug`; API keys
  stamp `{ workspaceId, workspaceSlug, createdByUserId }` in metadata;
  org-owner/admin-only guard on API key creation via Better Auth hook
- packages/shared-api: expose `workspaceId` / `workspaceSlug` on request
  context from API key metadata
- apps/console: new `/w/:workspaceSlug/*` routes; "Presets" UI replaces the
  Models/Aliases page (name + slug + model only, no routing); sidebar
  becomes static "Hebo.ai" wordmark + workspace switcher; nav order is
  Overview / Presets / API Keys / Traces / Settings (API Keys hidden for
  org `member` role); default-workspace bootstrap creates the Better Auth
  team inline via `POST /v1/workspaces`

Closes #455

Co-authored-by: Heinrich Wendel <heiwen@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 3, 2026

Warning

Rate limit exceeded

@heiwen has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 53 minutes and 56 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d446dbbd-edd3-4d07-8e12-afcad3d55724

📥 Commits

Reviewing files that changed from the base of the PR and between 337527d and a30deff.

📒 Files selected for processing (11)
  • apps/api/prisma/migrations/20260508034940_workspaces/migration.sql
  • apps/api/prisma/schema.prisma
  • apps/api/src/db/prisma.ts
  • apps/api/src/index.ts
  • apps/api/src/modules/providers/index.ts
  • apps/api/src/modules/workspaces/index.ts
  • apps/api/src/modules/workspaces/presets/index.ts
  • apps/api/src/modules/workspaces/presets/types.ts
  • apps/console/app/routes/_shell.w.$workspaceSlug.presets/form.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.presets/schema.ts
  • apps/gateway/src/lib/hooks.ts
📝 Walkthrough

Walkthrough

This PR executes a comprehensive domain model refactoring from agents/branches to workspaces/presets across the entire platform. Database schema introduces workspaces table with team integration, presets become workspace-owned (replacing agent-scoped aliases), and branches are removed entirely. API endpoints migrate from /agents/:slug/branches/:slug/* to /workspaces/:slug/*. Console routes and sidebar navigation reorient around workspaces; gateway model resolution switches to preset-first lookup with LRU caching. API keys gain workspace-scoped metadata; traces become unscoped endpoints accepting optional workspace context.

Changes

Workspace & Preset Foundation

Layer / File(s) Summary
Data Schema
apps/api/prisma/migrations/..., apps/api/prisma/schema.prisma
workspaces table added with team_id and (organization_id, slug) uniqueness; presets migrate from agent-related to workspace_id-keyed with cascade delete; agents and branches tables removed.
Type Contracts
apps/api/src/modules/workspaces/types.ts, apps/api/src/modules/presets/types.ts
Zod schemas and TypeScript types for WorkspacePlain, WorkspaceCreate, PresetPlain, PresetCreate, PresetUpdate introduced with validation rules.

API Backend - Workspaces & Presets

Layer / File(s) Summary
Workspaces CRUD
apps/api/src/modules/workspaces/index.ts
workspacesModule with GET /, POST /, GET /:slug, PATCH /:slug, DELETE /:slug endpoints; POST creates team via auth client and associates workspace by team_id.
Presets CRUD
apps/api/src/modules/presets/index.ts, apps/api/src/modules/presets/types.ts
presetsModule at /workspaces/:workspaceSlug/presets with list/create/fetch/update/delete endpoints; foreign key to workspaces with cascade behavior.
Traces Refactor
apps/api/src/modules/traces/index.ts, apps/api/src/modules/traces/service.ts
tracesModule prefix changed from /agents/:agentSlug/branches/:branchSlug/traces to /traces; listTraces and getSpans accept optional workspaceSlug instead of required agent/branch slugs.
Legacy Removal
apps/api/src/modules/agents/types.ts, apps/api/src/modules/branches/index.ts, apps/api/src/modules/branches/types.ts
Agent and branch module schemas and route handlers completely removed; ModelConfigSchema removed from providers/types.

Gateway Model Resolution

Layer / File(s) Summary
Model Alias Resolution
apps/gateway/src/lib/hooks.ts
resolveModelAlias rewritten to preset-first flow: looks up preset by workspaceId and slug via Prisma with LRU cache; sets hebo.workspace.slug and hebo.preset.slug OTEL attributes.
Provider Selection
apps/gateway/src/lib/hooks.ts
selectProviderWithByokFallback refactored with Pass 1 (sequential BYOK providers) and Pass 2 (first platform-default provider); removes custom-provider slug routing.
Gateway Endpoint Context
apps/gateway/src/index.ts
/chat/completions, /embeddings, /responses, /messages handlers now pass workspaceId and workspaceSlug to gateway handler.

Authentication & API Keys

Layer / File(s) Summary
Auth Schema
apps/auth/src/better-auth.ts, apps/auth/prisma/schema.prisma, apps/auth/prisma/migrations/...
Better Auth team additionalFields renamed from agentSlug to workspaceSlug; Teams table column updated accordingly; import formatting adjusted.
API Key Metadata
apps/console/app/lib/auth/better-auth.ts, apps/console/app/lib/auth/dummy-auth.ts, apps/console/app/lib/auth/types.ts
generateApiKey extended with optional workspace parameter; metadata now includes workspaceSlug and workspaceId; listApiKeys accepts options for workspace filtering.
Auth Middleware
packages/shared-api/middlewares/auth.ts
resolveAuthContext extracts and propagates workspaceId and workspaceSlug from API key metadata in returned auth context.

Console Frontend - Shell & Navigation

Layer / File(s) Summary
Shell Layout
apps/console/app/routes/_shell/route.tsx
clientLoader now fetches only workspaces instead of agents; sidebar uses HeboWordmark and WorkspaceSelect; playground receives activeWorkspace.
Sidebar Components
apps/console/app/routes/_shell/sidebar-*.tsx
HeboWordmark replaces agent selector; WorkspaceSelect replaces branch selector; SidebarNav filters items by user role; SidebarPlatform uses workspace basePath.
Playground Sidebar
apps/console/app/routes/_shell/sidebar-playground.tsx
Accepts activeWorkspace; fetches preset slugs and canonical models; builds modelsConfig from combined entries.

Console Frontend - Workspace & Preset Routes

Layer / File(s) Summary
Workspace CRUD & Settings
apps/console/app/routes/_shell.w.*
Workspace creation/deletion, general settings, members, danger zone, and presets management; API keys scoped to workspace; traces routes simplified to unscoped endpoints.
Onboarding & Documentation
apps/console/app/routes/_shell.w.$workspaceSlug._index/route.mdx
Step 2 updated with optional preset creation guidance; code samples use <CANONICAL_MODEL_OR_PRESET_SLUG> placeholder.

Console Frontend - Mock & Test Support

Layer / File(s) Summary
Mock Database
apps/console/app/mocks/db.ts
createDb returns { workspaces, presets, providers } with cascade relations instead of agents/branches.
Mock Handlers
apps/console/app/mocks/routes/workspaces.ts, apps/console/app/mocks/routes/traces.ts
New workspaceHandlers for workspace/preset CRUD; trace handlers rewired to unscoped /api/v1/traces; span attributes use hebo.workspace.slug.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • turisanapo

🐰 Hooray, the workspace dawn! 🌅
From agents to workspaces, we've grown so far,
Presets now shine, like a bright new star ✨
Branches are gone, the paths are clear,
Workspaces take center—let's celebrate here! 🎉

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude-issue-455-20260503-0241

@heiwen
Copy link
Copy Markdown
Contributor Author

heiwen commented May 3, 2026

@claude a few refinements:
1/ when I first login, it should create a default workspace for me (slug: default; name: Default Workspace); right now it just goes to the "create new workspace" dialog
2/ workspaces don't need any random postfix anymore, since they are unique by organization
3/ no need for dropping tables in migration file, since we start fresh; just let prisma generate the clean migrations
4/ something is wrong creating api keys ("authorization denied"). actually, let's skip that permission feature for a future ticket.
5/ after creating a new preset, the new preset dialog doesn't close
6/ when creating a preset with a name that already exists, it should show a local validation error, not a global one (same as when trying to create a workspace that already exists
7/ presets should auto populate the suggested slug in the UI
8/ after creating a second workspace, the workspace dropdown data doesn't refresh

you can test functionality with playwright-cli and the respective SKILL.

@hebo-agent
Copy link
Copy Markdown

hebo-agent Bot commented May 3, 2026

Claude finished @heiwen's task in 9m 43s —— View job


Refinements applied

All 8 items addressed — pushed as eda2593.

  • 1. Default workspace on first loginapps/console/app/routes/_shell._index/route.tsx now POST /v1/workspaces { name: "Default Workspace", slug: "default" } and redirects to /w/default when the user has no workspaces.
  • 2. No random slug postfixapps/api/src/modules/workspaces/index.ts changed slugFromString(body.name, 3)slugFromString(body.name). Uniqueness is already enforced per-organization by the (organization_id, slug) composite index.
  • 3. Clean migrations — folded workspaces/presets and the default-value changes into 20260110030050_api_schema_init; deleted 20260417014449_typebox_to_zod and 20260503024200_workspaces_and_presets. Auth init renames agentSlugworkspaceSlug in place; deleted 20260503024200_rename_team_agent_slug.
  • 4. API key authz removed — dropped apiKeyAuthzPlugin from apps/auth/src/lib/api-key.ts and apps/auth/src/better-auth.ts. Any org member can now create API keys; revisit in a future ticket.
  • 5. Preset dialog closes on successapps/console/app/routes/_shell.w.$workspaceSlug.presets/form.tsx collapses the card and resets local state via useEffect watching submission.status === "success".
  • 6. Duplicate preset slug → field error — action now maps result.error.status === 409 to submission.reply({ fieldErrors: { slug: [...] } }) (matches the workspace-create pattern).
  • 7. Auto-populate preset slug — name input drives a local slug state via slugify(name, { lower: true, strict: true }) until the user types into the slug field (slugEdited latch).
  • 8. Workspace dropdown refresh_shell/route.tsx shouldRevalidate now returns true whenever workspaceSlug param changes, so the sidebar loader re-fetches /v1/workspaces after the create-redirect lands on /w/:newSlug.

Verification

  • bun run lint
  • bun run typecheck
  • bun run test ✅ (all 3 pass / 2 pass / 31 pass)
  • bun run build
  • Playwright browser testing skipped — would require spinning up Postgres + auth + api locally, which isn't viable in this CI environment. All changes are type-checked and lint-clean; please run the webkit smoke flow locally to confirm the interactions.
    • Branch: claude-issue-455-20260503-0241

github-actions Bot and others added 3 commits May 3, 2026 04:03
- Auto-create default workspace (slug: default, name: "Default Workspace")
  when the user has none, instead of redirecting to the create dialog.
- Drop random slug postfix on workspace creation; workspaces are unique
  per organization.
- Fold the workspaces/presets tables into the api init migration and
  delete the intermediate typebox_to_zod + workspaces_and_presets +
  rename_team_agent_slug migrations. Auth init renames agentSlug ->
  workspaceSlug in place.
- Remove the api-key role authz plugin so any org member can create API
  keys; granular permissions will be revisited later.
- Close the new-preset dialog and reset inputs after a successful
  submission; surface 409 duplicate-slug errors as a field error on
  slug instead of a global toast.
- Auto-populate preset slug from the name as the user types (until they
  edit it manually).
- Revalidate the shell layout's workspace list when the active slug
  changes so the sidebar dropdown refreshes after creating a new
  workspace.

Co-authored-by: Heinrich Wendel <heiwen@users.noreply.github.com>
@heiwen heiwen marked this pull request as ready for review May 3, 2026 04:15
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: db130ab493

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +131 to +132
if (workspaceId) {
const preset = await lookupPreset(prismaClient, workspaceId, modelId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve presets when workspaceId is missing

resolveModelAlias only attempts preset lookup inside if (workspaceId), so any request that has only a workspace slug skips presets entirely and falls through to MODEL_NOT_FOUND for preset slugs. This now affects real flows: console-generated keys pass workspaceSlug metadata (not workspaceId) and cookie-auth gateway calls also lack workspaceId, so preset slugs cannot be used from the playground or those API keys. Add a slug-based fallback (or guarantee workspaceId is always present in auth metadata) before rejecting unknown model IDs.

Useful? React with 👍 / 👎.

Comment thread apps/console/app/mocks/browser.ts Outdated
...providerHandlers,
...modelHandlers,
];
let handlers = [...traceHandlers, ...workspaceHandlers, ...providerHandlers];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore mocked model catalog handler in MSW setup

The MSW worker no longer includes a handler for the gateway models endpoint, but the shell loader and playground still call gateway.models.get(...). In console-only development with mocks (bun run -F @hebo/console dev), this request is now unhandled, leaving shellStore.models empty and breaking model-dependent UI (preset creation/model selection and playground model entries). Re-add the removed model handlers to the worker registration.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
apps/console/app/routes/_shell.w.$workspaceSlug._index/route.mdx (1)

14-14: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale "agent" terminology on the overview page.

Line 14 still reads "code your first agent with our gateway" — this wasn't updated as part of the agents → workspaces rename and is now inconsistent with the rest of the UI.

📝 Suggested copy fix
-Start using our interactive playground ({kbs("mod+P")}) or code your first agent with our gateway.
+Start using our interactive playground ({kbs("mod+P")}) or connect your first app with our gateway.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/app/routes/_shell.w`.$workspaceSlug._index/route.mdx at line 14,
The overview copy still uses the old "agent" term; locate the string Start using
our interactive playground ({kbs("mod+P")}) or code your first agent with our
gateway in the _shell.w.$workspaceSlug._index route MDX and update "agent" to
the new terminology (e.g., "workspace") so it reads something like Start using
our interactive playground ({kbs("mod+P")}) or code your first workspace with
our gateway; ensure braces and kbs("mod+P") remain unchanged.
apps/console/app/routes/_shell.w.create/route.tsx (1)

27-32: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Non-409 server errors fall through to result.data!.slug, which throws a TypeError at runtime.

Eden Treaty returns { data: null, error: { status, value } } for non-2xx responses rather than throwing, so the try/catch on line 23 doesn't cover a 500 or 403. When result.error?.status !== 409, execution reaches result.data!.slug with result.data === null.

🔧 Proposed fix
  if (result.error?.status === 409)
    return submission.reply({
      fieldErrors: { name: [parseError(result.error.value).message] },
    });

+ if (result.error)
+   return submission.reply({ formErrors: [parseError(result.error.value).message] });
+
  return redirect(`/w/${result.data!.slug}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/app/routes/_shell.w.create/route.tsx` around lines 27 - 32, The
current code assumes result.data is present and always redirects to
redirect(`/w/${result.data!.slug}`), which throws when Eden Treaty returns an
error object; modify the control flow to first check if result.error exists: if
result.error?.status === 409 keep the existing fieldErrors branch, else return a
submission-level error (e.g., submission.reply({ formError:
[parseError(result.error.value).message] }) or similar) so non-409 errors don't
fall through, and only call redirect when result.data is non-null (use
result.data.slug without the non-null assertion after confirming result.data).
apps/console/app/routes/_shell.w.$workspaceSlug._index/api-key.tsx (1)

12-29: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reset the generated secret when the workspace param changes.

key, error, and loading survive independently of workspaceSlug. If this route instance is reused across workspace switches, the previous workspace's API key remains visible on the new workspace page.

♻️ Suggested fix
-import { useState } from "react";
+import { useEffect, useState } from "react";
 import { useParams } from "react-router";
@@
 export function GenerateApiKey() {
   const { workspaceSlug } = useParams();
   const [loading, setLoading] = useState<"idle" | "loading" | "success">("idle");
   const [key, setKey] = useState<string | null>(null);
   const [error, setError] = useState<string | null>(null);
+
+  useEffect(() => {
+    setLoading("idle");
+    setKey(null);
+    setError(null);
+  }, [workspaceSlug]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/app/routes/_shell.w`.$workspaceSlug._index/api-key.tsx around
lines 12 - 29, Reset the per-workspace UI state when the route's workspaceSlug
changes: add a useEffect that watches workspaceSlug and calls setKey(null),
setError(null) and setLoading("idle") so previous workspace API keys or errors
aren't shown after a workspace switch; locate the state hooks (key, error,
loading) and the handleGenerateAPIKey handler in the component and implement the
effect there.
apps/api/src/modules/traces/index.ts (1)

23-39: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Clamp trace access to the authenticated workspace.

Both handlers trust query.workspace as the scope selector. With workspace-scoped API keys, a caller can omit that filter or point it at another workspace in the same org and still hit these endpoints. Derive the effective workspace from auth context first, and reject mismatches instead of letting the query param widen scope.

🔒 Suggested shape
-    async ({ greptimeDb, organizationId, query }) => {
+    async ({ greptimeDb, organizationId, query, workspaceSlug: authWorkspaceSlug }) => {
+      if (authWorkspaceSlug && query.workspace && query.workspace !== authWorkspaceSlug) {
+        return status(403, "Workspace scope mismatch");
+      }
+
+      const effectiveWorkspace = authWorkspaceSlug ?? query.workspace;
       return status(
         200,
         await listTraces(
           greptimeDb,
           organizationId!,
-          query.workspace,
+          effectiveWorkspace,
           query.from ?? DEFAULT_FROM(),
           query.to ?? DEFAULT_TO(),
           query.page!,
@@
-    async ({ greptimeDb, organizationId, params, query }) => {
-      const spans = await getSpans(greptimeDb, organizationId!, query.workspace, params.traceId);
+    async ({ greptimeDb, organizationId, params, query, workspaceSlug: authWorkspaceSlug }) => {
+      if (authWorkspaceSlug && query.workspace && query.workspace !== authWorkspaceSlug) {
+        return status(403, "Workspace scope mismatch");
+      }
+
+      const effectiveWorkspace = authWorkspaceSlug ?? query.workspace;
+      const spans = await getSpans(
+        greptimeDb,
+        organizationId!,
+        effectiveWorkspace,
+        params.traceId,
+      );
       if (spans.length === 0) return status(404, null);
       return status(200, spans);
     },

Also applies to: 49-56

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/modules/traces/index.ts` around lines 23 - 39, The handler
currently uses query.workspace directly (see the async ({ greptimeDb,
organizationId, query }) => block and listTraces call), so change it to derive
the effective workspace from the authenticated context (e.g., the request/auth
token or context-provided workspace) and use that value when calling listTraces
instead of query.workspace; if query.workspace is provided and does not match
the authenticated workspace, return an error (403 Forbidden) rejecting the
request. Apply the same change to the other handler referenced at lines 49-56 so
both use the auth-derived workspace and validate/reject mismatches rather than
trusting query.workspace.
apps/console/app/mocks/db.ts (1)

4-20: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a real workspace id to the mock schema.

Right now the mock layer can't mirror the production PresetPlain.workspace_id contract, so apps/console/app/mocks/routes/workspaces.ts has to write the workspace slug into workspace_id. The real API stores the actual workspace id there, so MSW responses are drifting from production and can hide UI bugs around preset/workspace wiring.

Also applies to: 22-31, 52-58

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/app/mocks/db.ts` around lines 4 - 20, The mock workspace schema
is missing a real workspace id field, causing MSW responses to use slug as
workspace_id and diverge from the production PresetPlain.workspace_id contract;
update workspaceSchema to include an "id" field (string) with a realistic
default (e.g., "ws-<uuid>" or "workspace-dummy") alongside existing fields so
presets (the presets getter / presetSchema) can reference workspace.id; ensure
any places that construct PresetPlain mock objects use workspace.id for
workspace_id rather than slug and update related defaults where similar schema
blocks exist (lines referenced in the review: the other workspace schema
duplicates) so MSW matches production shape.
apps/console/app/routes/_shell.w.$workspaceSlug.api-keys/route.tsx (1)

34-39: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Also stamp workspaceId when creating workspace-scoped keys.

This action only forwards { workspaceSlug }, so keys created here won't satisfy the new { workspaceId, workspaceSlug, createdByUserId } metadata contract. That leaves them scoped by slug alone and out of sync with the auth middleware/better-auth path that already supports workspaceId.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/app/routes/_shell.w`.$workspaceSlug.api-keys/route.tsx around
lines 34 - 39, The call to authService.generateApiKey only passes {
workspaceSlug: params.workspaceSlug } so generated keys lack workspaceId
metadata; update the options argument passed to generateApiKey in the route
handler to include the workspaceId (e.g., { workspaceId: <currentWorkspaceId>,
workspaceSlug: params.workspaceSlug }) so the created key meets the {
workspaceId, workspaceSlug, createdByUserId } metadata contract; locate the call
to authService.generateApiKey (and usage of submission.value.name /
API_KEY_EXPIRATION_OPTIONS) and ensure the route supplies the correct
workspaceId from the current request/context when creating workspace-scoped
keys.
apps/api/src/modules/workspaces/index.ts (1)

35-63: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid orphaning the Better Auth team on partial failure.

This provisions the team and membership before the Prisma insert. If addTeamMember() or prismaClient.workspaces.create() fails, the org is left with a dangling team that no workspace row points to. Add a compensation path that tears the team back down on later failures, or otherwise make the flow idempotent around the external side effect.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/modules/workspaces/index.ts` around lines 35 - 63, Create a
compensation path so a partially created Better Auth team isn't left orphaned:
after calling authClient!.organization.createTeam(...) and obtaining team.id,
wrap the subsequent calls to authClient!.organization.addTeamMember(...) and
prismaClient.workspaces.create(...) in a try/catch; if any of those fail, call
the appropriate teardown (e.g. authClient!.organization.deleteTeam({ teamId:
team.id }) or remove team member then delete team) to roll back the external
side effect, log any deletion errors, and rethrow the original error so failure
is visible to callers; ensure you reference team.id, workspaceSlug,
authClient.organization.addTeamMember, authClient.organization.createTeam and
prismaClient.workspaces.create when locating the code.
🧹 Nitpick comments (5)
apps/auth/prisma/schema.prisma (1)

127-139: ⚡ Quick win

Missing DB-level unique constraint for per-org workspace slug uniqueness.

The PR explicitly requires workspace slugs to be unique per organization, but the Teams model only has @@index([organizationId]). Without a @@unique([organizationId, workspaceSlug]) constraint, a TOCTOU window between the application-level uniqueness check and the insert allows two concurrent POST /workspaces calls in the same org to silently create duplicate slugs.

✅ Proposed fix
   @@index([organizationId])
+  @@unique([organizationId, workspaceSlug])
   @@map("teams")

Then run prisma migrate dev to generate the corresponding migration. Based on learnings, Prisma migrations are auto-generated by the CLI — do not edit migration files directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/auth/prisma/schema.prisma` around lines 127 - 139, The Teams model is
missing a DB-level unique constraint for per-organization workspace slugs; add a
composite unique constraint on organizationId and workspaceSlug (e.g.
@@unique([organizationId, workspaceSlug])) inside the Teams model to enforce
uniqueness at the database level and prevent TOCTOU race conditions, then run
the Prisma CLI (prisma migrate dev) to generate the migration rather than
editing migration files manually.
apps/api/src/modules/workspaces/types.ts (1)

3-11: ⚡ Quick win

slug field lacks format constraints — invalid slugs can be stored via direct API calls.

WorkspaceCreateSchema accepts an optional explicit slug. Without a regex/format constraint, values like "My Workspace" or "UPPER" pass validation and are stored verbatim, breaking URL routing. (Implicit creation via name is safe because slugFromString normalises it, but explicit slug bypasses that.)

🔧 Suggested addition
- slug: z.string(),
+ slug: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Slug must be lowercase alphanumeric with hyphens"),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/modules/workspaces/types.ts` around lines 3 - 11, The slug field
currently accepts arbitrary strings; update WorkspacePlainSchema (and the schema
used for creates, e.g., WorkspaceCreateSchema) to add a format constraint that
matches the canonical slug format (the same regex/format slugFromString
produces), e.g. enforce lowercase letters/numbers and hyphens only (no spaces or
uppercase) using z.string().regex(...). Keep existing trim/min checks and apply
the same regex to both schemas so explicit slugs are normalized/validated
consistently with implicit slug generation.
apps/console/app/routes/_shell.w.$workspaceSlug.presets/form.tsx (1)

93-95: ⚡ Quick win

Set an explicit relative action on both fetcher forms.

Both forms currently post to the current URL implicitly. That gets brittle as soon as this list is rendered alongside a nested child route, because create/delete can start targeting the child URL instead of the presets resource route.

As per coding guidelines, "Use useFetcher with explicit relative action in section components so submissions go to the resource route without navigating away".

Also applies to: 171-172

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/app/routes/_shell.w`.$workspaceSlug.presets/form.tsx around
lines 93 - 95, The fetcher.Form instances (the ones around the delete/create
preset blocks referencing preset.slug) currently rely on implicit posting to the
current URL; add an explicit relative action that targets the presets resource
route (e.g., action="./presets" or the appropriate relative path to the presets
action) on both fetcher.Form elements so submissions always go to the presets
resource handler rather than a nested child route.
apps/console/app/routes/_shell.w.$workspaceSlug.presets/route.tsx (1)

80-92: 💤 Low value

Inconsistent response shape for delete intent.

The delete intent returns { intent, error } while create/update return { intent, submission }. This breaks the pattern established by the coding guidelines (return { intent, submission: submission.reply() }). If the consuming form component filters lastResult by intent and expects a submission field, this could cause issues.

Consider aligning the delete response with the other intents, or verify that the form component (./form) handles this alternative shape correctly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/app/routes/_shell.w`.$workspaceSlug.presets/route.tsx around
lines 80 - 92, The delete branch in the route handler currently returns { intent
} or { intent, error }, which differs from the create/update responses; update
the delete handling in the intent === "delete" branch so it always returns the
same shape by including a submission field (e.g., return { intent, submission:
null } on success and return { intent, submission: null, error:
parseError(error).message } in the catch), changing the code around
api.workspaces(...).presets(...).delete() and the catch that references
parseError(error) so consumers expecting a submission field (like the form
component) receive a consistent response shape.
apps/console/app/mocks/routes/workspaces.ts (1)

1-116: 💤 Low value

Missing GET /workspaces/:workspaceSlug/presets/:presetSlug endpoint.

The API has a GET /:presetSlug endpoint (per context snippet from apps/api/src/modules/presets/index.ts:55-71) to retrieve a single preset, but this mock file doesn't implement it. If any console component fetches a single preset by slug, it will fail against the mock.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/app/mocks/routes/workspaces.ts` around lines 1 - 116, Add a GET
handler for the single-preset route so the mock supports GET
/api/v1/workspaces/:workspaceSlug/presets/:presetSlug; locate workspaceHandlers
and add an http.get for "/api/v1/workspaces/:workspaceSlug/presets/:presetSlug"
that finds the preset via db.presets.findFirst with where({ slug:
params.presetSlug, workspace_id: params.workspaceSlug }) and returns
HttpResponse.json(preset) or a 404 HttpResponse("Not found") when missing,
mirroring the logic used in the existing patch/delete handlers for presets (see
db.presets.findFirst and the route strings).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/modules/presets/index.ts`:
- Around line 22-24: Queries against presets (e.g.,
prismaClient.presets.findMany) are currently filtered only by
params.workspaceSlug and must be scoped to the caller's organization to avoid
cross-tenant access; update every presets lookup/create/update/delete to include
the authenticated org id from the request context (e.g., callerOrgId from
auth/ctx) in the where clause by qualifying the workspace relation (for example:
where: { workspace: { slug: params.workspaceSlug, organizationId: callerOrgId }
} or the equivalent shape your schema uses) and apply the same change to all
other presets queries in this file (the blocks around the other ranges
mentioned) so every operation is constrained to the caller's organization.

In `@apps/api/src/modules/workspaces/index.ts`:
- Around line 105-112: The DELETE handler for "/:workspaceSlug" only
soft-deletes the workspace row (uses prismaClient.workspaces.findFirstOrThrow
and prismaClient.workspaces.softDelete) but does not remove the backing auth
team created on POST, leaving memberships and the slug reserved; modify the
handler to look up the workspace's associated auth team identifier (e.g.,
workspaces.authTeamId or the relation created by the POST / flow), call the
appropriate teardown (e.g., auth client or prisma delete for the team and its
memberships) before/while performing the workspace softDelete, and ensure errors
from tearing down the auth team are handled/logged (try/catch) so the workspace
delete remains consistent and the slug is freed for recreation.
- Around line 32-39: The code uses body.slug ?? slugFromString(body.name) which
treats an empty string as a valid slug; instead normalize and trim the incoming
slug and only use it if non-empty. Update the creation flow around workspaceSlug
(and usages like authClient!.organization.createTeam) to compute workspaceSlug
by trimming body.slug and falling back to slugFromString(body.name) when the
trimmed value is falsy (empty/whitespace), ensuring you pass a valid slug to
createTeam and related calls.

In `@apps/console/app/mocks/routes/workspaces.ts`:
- Around line 38-44: The mock DELETE handlers return 200 but should return 204
to match the real API; update the http.delete handlers for
"/api/v1/workspaces/:workspaceSlug" (where db.workspaces.delete(...) is called
and HttpResponse is constructed) and the similar "/api/v1/presets/:presetSlug"
handler to return new HttpResponse(undefined, { status: 204 }) so the mock
status matches the API's status(204) behavior.

In `@apps/console/app/routes/_shell._index/route.tsx`:
- Around line 5-19: The code in defaultWorkspaceMiddleware calls
api.workspaces.get() but ignores the returned error and proceeds to create a
workspace on transient failures; after awaiting api.workspaces.get() inspect the
returned error (the { data, error } shape) and if error is present throw it (or
throw a new Error when data is null/unexpected) so the React Router error
boundary surfaces the failure; only call api.workspaces.post(...) and redirect
to created?.slug after confirming no error and that workspaces is an empty
array.

In `@apps/console/app/routes/_shell/sidebar-nav.tsx`:
- Around line 32-38: The useEffect calling authService.getOrganization() lacks
error handling so a rejected promise leaves role undefined and hides "API Keys";
update the chain on the getOrganization call (in the useEffect around
authService.getOrganization, setRole, members) to add a .catch handler that logs
the error and sets a safe fallback for setRole (e.g., use user.role if available
or preserve prior role) so canManage isn't permanently false; ensure the promise
still returns null at the end.

In `@apps/console/app/routes/_shell/sidebar-playground.tsx`:
- Around line 24-33: The Promise.all call that invokes
api.workspaces(...).presets.get() and gateway.models.get() lacks error handling,
which can cause unhandled rejections and leave entries stale; wrap the
Promise.all(...) chain with a .catch handler (after the .then) that checks the
cancelled flag and then updates local state appropriately (e.g., call
setEntries([]) or set an error state / show a user-facing message) and logs the
error; ensure you still return null and do not swallow the error silently. Use
the existing symbols api.workspaces(...).presets.get(), gateway.models.get(),
setEntries, and cancelled to locate and implement the fix.

---

Outside diff comments:
In `@apps/api/src/modules/traces/index.ts`:
- Around line 23-39: The handler currently uses query.workspace directly (see
the async ({ greptimeDb, organizationId, query }) => block and listTraces call),
so change it to derive the effective workspace from the authenticated context
(e.g., the request/auth token or context-provided workspace) and use that value
when calling listTraces instead of query.workspace; if query.workspace is
provided and does not match the authenticated workspace, return an error (403
Forbidden) rejecting the request. Apply the same change to the other handler
referenced at lines 49-56 so both use the auth-derived workspace and
validate/reject mismatches rather than trusting query.workspace.

In `@apps/api/src/modules/workspaces/index.ts`:
- Around line 35-63: Create a compensation path so a partially created Better
Auth team isn't left orphaned: after calling
authClient!.organization.createTeam(...) and obtaining team.id, wrap the
subsequent calls to authClient!.organization.addTeamMember(...) and
prismaClient.workspaces.create(...) in a try/catch; if any of those fail, call
the appropriate teardown (e.g. authClient!.organization.deleteTeam({ teamId:
team.id }) or remove team member then delete team) to roll back the external
side effect, log any deletion errors, and rethrow the original error so failure
is visible to callers; ensure you reference team.id, workspaceSlug,
authClient.organization.addTeamMember, authClient.organization.createTeam and
prismaClient.workspaces.create when locating the code.

In `@apps/console/app/mocks/db.ts`:
- Around line 4-20: The mock workspace schema is missing a real workspace id
field, causing MSW responses to use slug as workspace_id and diverge from the
production PresetPlain.workspace_id contract; update workspaceSchema to include
an "id" field (string) with a realistic default (e.g., "ws-<uuid>" or
"workspace-dummy") alongside existing fields so presets (the presets getter /
presetSchema) can reference workspace.id; ensure any places that construct
PresetPlain mock objects use workspace.id for workspace_id rather than slug and
update related defaults where similar schema blocks exist (lines referenced in
the review: the other workspace schema duplicates) so MSW matches production
shape.

In `@apps/console/app/routes/_shell.w`.$workspaceSlug._index/api-key.tsx:
- Around line 12-29: Reset the per-workspace UI state when the route's
workspaceSlug changes: add a useEffect that watches workspaceSlug and calls
setKey(null), setError(null) and setLoading("idle") so previous workspace API
keys or errors aren't shown after a workspace switch; locate the state hooks
(key, error, loading) and the handleGenerateAPIKey handler in the component and
implement the effect there.

In `@apps/console/app/routes/_shell.w`.$workspaceSlug._index/route.mdx:
- Line 14: The overview copy still uses the old "agent" term; locate the string
Start using our interactive playground ({kbs("mod+P")}) or code your first agent
with our gateway in the _shell.w.$workspaceSlug._index route MDX and update
"agent" to the new terminology (e.g., "workspace") so it reads something like
Start using our interactive playground ({kbs("mod+P")}) or code your first
workspace with our gateway; ensure braces and kbs("mod+P") remain unchanged.

In `@apps/console/app/routes/_shell.w`.$workspaceSlug.api-keys/route.tsx:
- Around line 34-39: The call to authService.generateApiKey only passes {
workspaceSlug: params.workspaceSlug } so generated keys lack workspaceId
metadata; update the options argument passed to generateApiKey in the route
handler to include the workspaceId (e.g., { workspaceId: <currentWorkspaceId>,
workspaceSlug: params.workspaceSlug }) so the created key meets the {
workspaceId, workspaceSlug, createdByUserId } metadata contract; locate the call
to authService.generateApiKey (and usage of submission.value.name /
API_KEY_EXPIRATION_OPTIONS) and ensure the route supplies the correct
workspaceId from the current request/context when creating workspace-scoped
keys.

In `@apps/console/app/routes/_shell.w.create/route.tsx`:
- Around line 27-32: The current code assumes result.data is present and always
redirects to redirect(`/w/${result.data!.slug}`), which throws when Eden Treaty
returns an error object; modify the control flow to first check if result.error
exists: if result.error?.status === 409 keep the existing fieldErrors branch,
else return a submission-level error (e.g., submission.reply({ formError:
[parseError(result.error.value).message] }) or similar) so non-409 errors don't
fall through, and only call redirect when result.data is non-null (use
result.data.slug without the non-null assertion after confirming result.data).

---

Nitpick comments:
In `@apps/api/src/modules/workspaces/types.ts`:
- Around line 3-11: The slug field currently accepts arbitrary strings; update
WorkspacePlainSchema (and the schema used for creates, e.g.,
WorkspaceCreateSchema) to add a format constraint that matches the canonical
slug format (the same regex/format slugFromString produces), e.g. enforce
lowercase letters/numbers and hyphens only (no spaces or uppercase) using
z.string().regex(...). Keep existing trim/min checks and apply the same regex to
both schemas so explicit slugs are normalized/validated consistently with
implicit slug generation.

In `@apps/auth/prisma/schema.prisma`:
- Around line 127-139: The Teams model is missing a DB-level unique constraint
for per-organization workspace slugs; add a composite unique constraint on
organizationId and workspaceSlug (e.g. @@unique([organizationId,
workspaceSlug])) inside the Teams model to enforce uniqueness at the database
level and prevent TOCTOU race conditions, then run the Prisma CLI (prisma
migrate dev) to generate the migration rather than editing migration files
manually.

In `@apps/console/app/mocks/routes/workspaces.ts`:
- Around line 1-116: Add a GET handler for the single-preset route so the mock
supports GET /api/v1/workspaces/:workspaceSlug/presets/:presetSlug; locate
workspaceHandlers and add an http.get for
"/api/v1/workspaces/:workspaceSlug/presets/:presetSlug" that finds the preset
via db.presets.findFirst with where({ slug: params.presetSlug, workspace_id:
params.workspaceSlug }) and returns HttpResponse.json(preset) or a 404
HttpResponse("Not found") when missing, mirroring the logic used in the existing
patch/delete handlers for presets (see db.presets.findFirst and the route
strings).

In `@apps/console/app/routes/_shell.w`.$workspaceSlug.presets/form.tsx:
- Around line 93-95: The fetcher.Form instances (the ones around the
delete/create preset blocks referencing preset.slug) currently rely on implicit
posting to the current URL; add an explicit relative action that targets the
presets resource route (e.g., action="./presets" or the appropriate relative
path to the presets action) on both fetcher.Form elements so submissions always
go to the presets resource handler rather than a nested child route.

In `@apps/console/app/routes/_shell.w`.$workspaceSlug.presets/route.tsx:
- Around line 80-92: The delete branch in the route handler currently returns {
intent } or { intent, error }, which differs from the create/update responses;
update the delete handling in the intent === "delete" branch so it always
returns the same shape by including a submission field (e.g., return { intent,
submission: null } on success and return { intent, submission: null, error:
parseError(error).message } in the catch), changing the code around
api.workspaces(...).presets(...).delete() and the catch that references
parseError(error) so consumers expecting a submission field (like the form
component) receive a consistent response shape.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fc483b54-0fad-4dba-9295-7ce07933d378

📥 Commits

Reviewing files that changed from the base of the PR and between c6ae80c and db130ab.

📒 Files selected for processing (80)
  • apps/api/prisma/migrations/20260110030050_api_schema_init/migration.sql
  • apps/api/prisma/migrations/20260417014449_typebox_to_zod/migration.sql
  • apps/api/prisma/schema.prisma
  • apps/api/src/db/json-types.d.ts
  • apps/api/src/db/prisma.ts
  • apps/api/src/index.ts
  • apps/api/src/modules/agents/types.ts
  • apps/api/src/modules/branches/index.ts
  • apps/api/src/modules/branches/types.ts
  • apps/api/src/modules/presets/index.ts
  • apps/api/src/modules/presets/types.ts
  • apps/api/src/modules/providers/index.ts
  • apps/api/src/modules/providers/types.ts
  • apps/api/src/modules/traces/index.ts
  • apps/api/src/modules/traces/service.ts
  • apps/api/src/modules/traces/types.ts
  • apps/api/src/modules/workspaces/index.ts
  • apps/api/src/modules/workspaces/types.ts
  • apps/auth/prisma/migrations/20260119080641_auth_init/migration.sql
  • apps/auth/prisma/schema.prisma
  • apps/auth/src/better-auth.ts
  • apps/console/app/lib/auth/better-auth.ts
  • apps/console/app/lib/auth/dummy-auth.ts
  • apps/console/app/lib/auth/types.ts
  • apps/console/app/mocks/browser.ts
  • apps/console/app/mocks/db.ts
  • apps/console/app/mocks/routes/agents.ts
  • apps/console/app/mocks/routes/branches.ts
  • apps/console/app/mocks/routes/models.ts
  • apps/console/app/mocks/routes/traces.ts
  • apps/console/app/mocks/routes/workspaces.ts
  • apps/console/app/routes/_shell._index/route.tsx
  • apps/console/app/routes/_shell.agent.$agentSlug.branch.$branchSlug.models/form.tsx
  • apps/console/app/routes/_shell.agent.$agentSlug.branch.$branchSlug.models/route.tsx
  • apps/console/app/routes/_shell.agent.$agentSlug.branch.$branchSlug.models/schema.ts
  • apps/console/app/routes/_shell.agent.$agentSlug.branches/create.tsx
  • apps/console/app/routes/_shell.agent.$agentSlug.branches/delete.tsx
  • apps/console/app/routes/_shell.agent.$agentSlug.branches/route.tsx
  • apps/console/app/routes/_shell.agent.$agentSlug.branches/table.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug._index/api-key.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug._index/route.mdx
  • apps/console/app/routes/_shell.w.$workspaceSlug.api-keys/create.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.api-keys/revoke.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.api-keys/route.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.api-keys/table.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.presets/form.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.presets/route.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.presets/schema.ts
  • apps/console/app/routes/_shell.w.$workspaceSlug.providers/clear.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.providers/configure.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.providers/list.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.providers/route.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.settings.danger/route.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.settings.members/invite-schema.ts
  • apps/console/app/routes/_shell.w.$workspaceSlug.settings.members/route.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.settings/danger-zone.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.settings/general.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.settings/members.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.settings/route.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.traces.($traceId)/details.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.traces.($traceId)/route.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.traces/list.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.traces/route.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.traces/search-params.ts
  • apps/console/app/routes/_shell.w.$workspaceSlug.traces/types.ts
  • apps/console/app/routes/_shell.w.$workspaceSlug.traces/utils.ts
  • apps/console/app/routes/_shell.w.$workspaceSlug/route.tsx
  • apps/console/app/routes/_shell.w.create/form.tsx
  • apps/console/app/routes/_shell.w.create/route.tsx
  • apps/console/app/routes/_shell/route.tsx
  • apps/console/app/routes/_shell/sidebar-agent.tsx
  • apps/console/app/routes/_shell/sidebar-branch.tsx
  • apps/console/app/routes/_shell/sidebar-nav.tsx
  • apps/console/app/routes/_shell/sidebar-platform.tsx
  • apps/console/app/routes/_shell/sidebar-playground.tsx
  • apps/gateway/src/index.ts
  • apps/gateway/src/lib/hooks.test.ts
  • apps/gateway/src/lib/hooks.ts
  • packages/shared-api/middlewares/auth.ts
  • packages/shared-api/package.json
💤 Files with no reviewable changes (15)
  • apps/console/app/routes/_shell.agent.$agentSlug.branch.$branchSlug.models/schema.ts
  • apps/api/src/modules/branches/index.ts
  • apps/api/src/modules/agents/types.ts
  • apps/api/src/modules/branches/types.ts
  • apps/console/app/mocks/routes/models.ts
  • apps/console/app/routes/_shell.agent.$agentSlug.branches/create.tsx
  • apps/console/app/routes/_shell.agent.$agentSlug.branch.$branchSlug.models/form.tsx
  • apps/api/src/modules/providers/types.ts
  • apps/console/app/mocks/routes/branches.ts
  • apps/console/app/routes/_shell.agent.$agentSlug.branches/table.tsx
  • apps/api/prisma/migrations/20260417014449_typebox_to_zod/migration.sql
  • apps/console/app/routes/_shell.agent.$agentSlug.branch.$branchSlug.models/route.tsx
  • apps/console/app/routes/_shell.agent.$agentSlug.branches/route.tsx
  • apps/console/app/routes/_shell.agent.$agentSlug.branches/delete.tsx
  • apps/console/app/mocks/routes/agents.ts

Comment thread apps/api/src/modules/presets/index.ts Outdated
Comment on lines 32 to 39
async ({ body, prismaClient, organizationId, userId, authClient }) => {
const agentSlug = slugFromString(body.name, 3);
const workspaceSlug = body.slug ?? slugFromString(body.name);

const { data: team, error: createTeamError } = await authClient!.organization.createTeam({
name: `${body.name}'s Team`,
organizationId: organizationId!,
agentSlug,
workspaceSlug,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize blank slug values before falling back.

body.slug ?? slugFromString(body.name) treats "" as a real slug, so a request with slug: "" skips the fallback and tries to create an empty-slug workspace/team. Tighten the schema or switch this to a trimmed truthy check.

Suggested fix
-      const workspaceSlug = body.slug ?? slugFromString(body.name);
+      const workspaceSlug = body.slug?.trim() || slugFromString(body.name);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async ({ body, prismaClient, organizationId, userId, authClient }) => {
const agentSlug = slugFromString(body.name, 3);
const workspaceSlug = body.slug ?? slugFromString(body.name);
const { data: team, error: createTeamError } = await authClient!.organization.createTeam({
name: `${body.name}'s Team`,
organizationId: organizationId!,
agentSlug,
workspaceSlug,
});
async ({ body, prismaClient, organizationId, userId, authClient }) => {
const workspaceSlug = body.slug?.trim() || slugFromString(body.name);
const { data: team, error: createTeamError } = await authClient!.organization.createTeam({
name: `${body.name}'s Team`,
organizationId: organizationId!,
workspaceSlug,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/modules/workspaces/index.ts` around lines 32 - 39, The code uses
body.slug ?? slugFromString(body.name) which treats an empty string as a valid
slug; instead normalize and trim the incoming slug and only use it if non-empty.
Update the creation flow around workspaceSlug (and usages like
authClient!.organization.createTeam) to compute workspaceSlug by trimming
body.slug and falling back to slugFromString(body.name) when the trimmed value
is falsy (empty/whitespace), ensuring you pass a valid slug to createTeam and
related calls.

Comment on lines +105 to 112
"/:workspaceSlug",
async ({ prismaClient, params }) => {
await prismaClient.agents.softDelete({ slug: params.agentSlug });
const { id } = await prismaClient.workspaces.findFirstOrThrow({
where: { slug: params.workspaceSlug },
select: { id: true },
});
await prismaClient.workspaces.softDelete({ id });
return status(204);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Tear down the backing auth team when deleting a workspace.

DELETE /:workspaceSlug only soft-deletes the app record. The Better Auth team created in POST / remains alive with its memberships, and it can still reserve the same slug, which makes recreate-after-delete fail against auth while the workspace looks deleted in Prisma.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/modules/workspaces/index.ts` around lines 105 - 112, The DELETE
handler for "/:workspaceSlug" only soft-deletes the workspace row (uses
prismaClient.workspaces.findFirstOrThrow and prismaClient.workspaces.softDelete)
but does not remove the backing auth team created on POST, leaving memberships
and the slug reserved; modify the handler to look up the workspace's associated
auth team identifier (e.g., workspaces.authTeamId or the relation created by the
POST / flow), call the appropriate teardown (e.g., auth client or prisma delete
for the team and its memberships) before/while performing the workspace
softDelete, and ensure errors from tearing down the auth team are handled/logged
(try/catch) so the workspace delete remains consistent and the slug is freed for
recreation.

Comment thread apps/console/app/mocks/routes/workspaces.ts
Comment on lines +5 to 19
async function defaultWorkspaceMiddleware() {
const { data: workspaces } = await api.workspaces.get();

// FUTURE fade-in the next page
// FUTURE remember last agent and branch in session storage
if (agents && agents.length > 0) {
throw redirect(`/agent/${agents[0].slug}/branch/main`);
if (workspaces && workspaces.length > 0) {
const preferred =
workspaces.find((w) => w.slug === "default") ?? workspaces[0];
throw redirect(`/w/${preferred.slug}`);
}
throw redirect("/agent/create");

const { data: created } = await api.workspaces.post({
name: "Default Workspace",
slug: "default",
});
throw redirect(`/w/${created?.slug ?? "default"}`);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

API error on workspaces.get() silently falls through to workspace creation.

Eden Treaty's .get() returns { data, error } rather than throwing. When api.workspaces.get() fails (5xx, network timeout), data is null and the if (workspaces && workspaces.length > 0) guard is false — so the code proceeds to POST a new "Default Workspace". An existing user who hits a transient API failure could have a spurious workspace created under their organization.

🛡️ Proposed fix
 async function defaultWorkspaceMiddleware() {
-  const { data: workspaces } = await api.workspaces.get();
+  const { data: workspaces, error } = await api.workspaces.get();
+
+  if (error) throw error;

   if (workspaces && workspaces.length > 0) {
     const preferred =
       workspaces.find((w) => w.slug === "default") ?? workspaces[0];
     throw redirect(`/w/${preferred.slug}`);
   }

Throwing error here lets React Router's error boundary surface the failure rather than masking it with an unintended workspace creation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/app/routes/_shell._index/route.tsx` around lines 5 - 19, The
code in defaultWorkspaceMiddleware calls api.workspaces.get() but ignores the
returned error and proceeds to create a workspace on transient failures; after
awaiting api.workspaces.get() inspect the returned error (the { data, error }
shape) and if error is present throw it (or throw a new Error when data is
null/unexpected) so the React Router error boundary surfaces the failure; only
call api.workspaces.post(...) and redirect to created?.slug after confirming no
error and that workspaces is an empty array.

Comment on lines +32 to +38
useEffect(() => {
if (!user) return;
void authService.getOrganization().then(({ members }) => {
setRole(members.find((m) => m.userId === user.userId)?.role);
return null;
});
}, [user]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing .catch(): authService.getOrganization() failure permanently hides "API Keys" from owners/admins.

If the call rejects, role stays undefined, canManage stays false, and the "API Keys" nav item never appears for that session — with no indication to the user.

🔧 Proposed fix
  void authService.getOrganization().then(({ members }) => {
    setRole(members.find((m) => m.userId === user.userId)?.role);
    return null;
- });
+ }).catch(() => {
+   // leave role undefined; API Keys remains hidden
+ });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!user) return;
void authService.getOrganization().then(({ members }) => {
setRole(members.find((m) => m.userId === user.userId)?.role);
return null;
});
}, [user]);
useEffect(() => {
if (!user) return;
void authService.getOrganization().then(({ members }) => {
setRole(members.find((m) => m.userId === user.userId)?.role);
return null;
}).catch(() => {
// leave role undefined; API Keys remains hidden
});
}, [user]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/app/routes/_shell/sidebar-nav.tsx` around lines 32 - 38, The
useEffect calling authService.getOrganization() lacks error handling so a
rejected promise leaves role undefined and hides "API Keys"; update the chain on
the getOrganization call (in the useEffect around authService.getOrganization,
setRole, members) to add a .catch handler that logs the error and sets a safe
fallback for setRole (e.g., use user.role if available or preserve prior role)
so canManage isn't permanently false; ensure the promise still returns null at
the end.

Comment on lines +24 to +33
void Promise.all([
api.workspaces({ workspaceSlug: slug }).presets.get(),
gateway.models.get({ query: { endpoints: true } }),
]).then(([presets, models]) => {
if (cancelled) return null;
const presetSlugs = (presets.data ?? []).map((p) => ({ alias: p.slug }));
const canonical = (models.data?.data ?? []).map((m) => ({ alias: m.id }));
setEntries([...presetSlugs, ...canonical]);
return null;
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unhandled rejection from Promise.all will silently swallow errors.

With no .catch(), any rejection (network error, auth failure) from either presets.get() or gateway.models.get() escapes as an unhandled promise rejection. The entries state stays at its previous/stale value and the user sees no feedback.

🔧 Proposed fix
  void Promise.all([
    api.workspaces({ workspaceSlug: slug }).presets.get(),
    gateway.models.get({ query: { endpoints: true } }),
  ]).then(([presets, models]) => {
    if (cancelled) return null;
    const presetSlugs = (presets.data ?? []).map((p) => ({ alias: p.slug }));
    const canonical = (models.data?.data ?? []).map((m) => ({ alias: m.id }));
    setEntries([...presetSlugs, ...canonical]);
    return null;
- });
+ }).catch(() => {
+   if (!cancelled) setEntries([]);
+ });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void Promise.all([
api.workspaces({ workspaceSlug: slug }).presets.get(),
gateway.models.get({ query: { endpoints: true } }),
]).then(([presets, models]) => {
if (cancelled) return null;
const presetSlugs = (presets.data ?? []).map((p) => ({ alias: p.slug }));
const canonical = (models.data?.data ?? []).map((m) => ({ alias: m.id }));
setEntries([...presetSlugs, ...canonical]);
return null;
});
void Promise.all([
api.workspaces({ workspaceSlug: slug }).presets.get(),
gateway.models.get({ query: { endpoints: true } }),
]).then(([presets, models]) => {
if (cancelled) return null;
const presetSlugs = (presets.data ?? []).map((p) => ({ alias: p.slug }));
const canonical = (models.data?.data ?? []).map((m) => ({ alias: m.id }));
setEntries([...presetSlugs, ...canonical]);
return null;
}).catch(() => {
if (!cancelled) setEntries([]);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/app/routes/_shell/sidebar-playground.tsx` around lines 24 - 33,
The Promise.all call that invokes api.workspaces(...).presets.get() and
gateway.models.get() lacks error handling, which can cause unhandled rejections
and leave entries stale; wrap the Promise.all(...) chain with a .catch handler
(after the .then) that checks the cancelled flag and then updates local state
appropriately (e.g., call setEntries([]) or set an error state / show a
user-facing message) and logs the error; ensure you still return null and do not
swallow the error silently. Use the existing symbols
api.workspaces(...).presets.get(), gateway.models.get(), setEntries, and
cancelled to locate and implement the fix.

@8monkey-ai 8monkey-ai deleted a comment from hebo-agent Bot May 4, 2026
@heiwen
Copy link
Copy Markdown
Contributor Author

heiwen commented May 4, 2026

@claude what do you think about codex and coderabbit review comments?

@hebo-agent
Copy link
Copy Markdown

hebo-agent Bot commented May 4, 2026

Claude finished @heiwen's task in 1m 28s —— View job


Assessment of Codex + CodeRabbit feedback

I verified each finding against the current branch. My read: a couple are real (and related), one "critical" is a false positive, and the rest are polish.


🔴 Worth fixing before merge

1. Codex P1 — preset lookup is dead when workspaceId is missingapps/gateway/src/lib/hooks.ts:131
Legitimate and actually more severe than it reads. Two user-facing flows only have workspaceSlug:

  • Console-created API keys stamp { workspaceSlug } only (see route.tsx:38).
  • Cookie-auth gateway calls (playground) don't attach any workspace info — packages/shared-api/middlewares/auth.ts:73-82 only sets userId/organizationId on the cookie path.

So preset slugs currently resolve to MODEL_NOT_FOUND everywhere except the narrow case where both metadata fields were set. Two options:

  • (a) Stamp workspaceId on every key creation and resolve workspace-by-slug on the cookie path, so workspaceId is always present. Cleanest.
  • (b) Add a slug-based fallback in lookupPreset (one extra workspaces.findFirst per miss, cache it).

I'd go with (a) — also fixes CodeRabbit's route.tsx:34 comment (missing workspaceId on key metadata) and the api-key.tsx generate path. The console can pull workspace.id from the $workspaceSlug loader it already fetches.

2. CodeRabbit — trace endpoints don't clamp to the authed workspaceapps/api/src/modules/traces/index.ts:23,49
Real cross-workspace read path: a workspace-A API key can GET /v1/traces?workspace=B (same org) or omit the param to see all traces org-wide. Fix is mechanical — when auth resolves a workspaceSlug, reject query.workspace mismatches and default the filter to the auth-derived slug.

3. CodeRabbit — workspace delete leaves the Better Auth team orphanedapps/api/src/modules/workspaces/index.ts:105
Soft-delete on workspaces doesn't touch the createTeam side effect from POST. Re-creating the same-slug workspace later will fail at team creation (slug reserved). Either hard-delete the team in the handler, or include a compensation in a try/catch.

4. CodeRabbit — "" passes as a valid slugapps/api/src/modules/workspaces/index.ts:39
One-liner: body.slug?.trim() || slugFromString(body.name). Worth doing — the current ?? also accepts whitespace-only strings.


🟡 Polish — worth a follow-up pass, not a blocker

  • _shell.w.create/route.tsx:27 — non-409 server errors fall through to result.data!.slug and throw a TypeError. Add an if (result.error) return submission.reply({ formErrors: [...] }); before the redirect.
  • _shell._index/route.tsx:5-19 — transient workspaces.get() 5xx is swallowed and the user silently gets a second "Default Workspace" created. Throw on error.
  • sidebar-nav.tsx / sidebar-playground.tsx — missing .catch() on async effects; rejections leave "API Keys" permanently hidden or stale playground entries.
  • _shell.w.$workspaceSlug._index/api-key.tsx — generated key/loading/error state survives workspace switch. useEffect reset on workspaceSlug change.
  • route.mdx:14 — still says "code your first agent with our gateway". Missed in the rename.

🟢 Nits

  • schema.prisma — add @@unique([organizationId, workspaceSlug]) on Teams to close the TOCTOU window at the DB level.
  • Mock DELETE handlers return 200 vs API's 204.
  • presets/form.tsx — explicit relative action on the fetcher forms (matches project convention).
  • Missing GET /workspaces/:slug/presets/:presetSlug mock (only matters if a console route uses it — currently none does).
  • Mock workspaces lacks a real id field — low value given we're mirroring shape, not production behaviour.
  • Delete-intent response shape differs from create/update ({ intent, error } vs { intent, submission }).
  • Workspace slug schema lacks a regex — covered implicitly when clients go through slugFromString, but direct API callers can pass "UPPER" or spaces.

❌ False positive

CodeRabbit "Critical" on apps/api/src/modules/presets/index.ts:22-24 — claims preset queries aren't org-scoped. They are: createPrismaClient (apps/api/src/db/prisma.ts:18-29) injects organization_id: organizationId into every non-create operation's where. Since presets rows carry organization_id, queries filtered only by workspace: { slug } still get clamped to the caller's org. Same applies to every other preset-scope-warning in that review. (The trace-endpoint critical is not a false positive — traces hit GreptimeDB directly and bypass Prisma, so that one stands.)


📦 Codex P2 — MSW model-catalog handler

Worth checking. sidebar-playground.tsx:24-33 still calls gateway.models.get(...) and the old modelHandlers were removed from browser.ts. If bun run -F @hebo/console dev is still a supported flow (console-only, mocked gateway), the worker needs that handler restored. If mocks are CI/Storybook only now, lower priority.


Recommendation

Happy to pick up items 1–4 on this PR (they're mostly mechanical and hang together via the workspaceId plumbing). Leave the rest for a polish PR unless you want it all in one — your call.
• Branch: claude-issue-455-20260503-0241

@8monkey-ai 8monkey-ai deleted a comment from hebo-agent Bot May 7, 2026
@8monkey-ai 8monkey-ai deleted a comment from hebo-agent Bot May 7, 2026
@heiwen
Copy link
Copy Markdown
Contributor Author

heiwen commented May 7, 2026

@claude
1/ address the codex P2 comment
2/ for "the trace endpoints don't clamp to the authed workspace", remove the workspace parameter for now, it should always come from the auth scope instead
3/ address mock delete handlers to return 204

keep the rest for later

@hebo-agent
Copy link
Copy Markdown

hebo-agent Bot commented May 7, 2026

Claude finished @heiwen's task in 7m 1s —— View job


Tasks

  • Read relevant files to understand current state
  • Address Codex P2: restore mocked model catalog handler in MSW setup
  • Remove workspace query param from trace endpoints — use auth scope
  • Update mock delete handlers to return 204
  • Run lint + typecheck + tests + build
  • Commit and push (337527d)

Summary

  • Codex P2 (MSW model catalog) — restored apps/console/app/mocks/routes/models.ts and re-registered modelHandlers in apps/console/app/mocks/browser.ts so the console-only dev flow (bun run -F @hebo/console dev) hits a handled /gateway/v1/models again. Shell loader and playground model entries now populate in mocked mode.
  • Trace endpoints → auth scope — dropped the workspace query param from TraceListQuerySchema and removed TraceDetailQuerySchema entirely. Both GET /v1/traces and GET /v1/traces/:traceId now read workspaceSlug from the auth middleware context (propagated out of API-key metadata in packages/shared-api/middlewares/auth.ts). A workspace-A key can no longer point ?workspace=B at cross-workspace traces — and cookie-auth calls, which carry no workspace, still return unscoped org-wide traces (same as before; no regression). Console loaders updated accordingly: apps/console/app/routes/_shell.w.$workspaceSlug.traces/route.tsx and .../traces.($traceId)/route.tsx no longer pass query.workspace.
  • Mock DELETE → 204apps/console/app/mocks/routes/workspaces.ts now returns new HttpResponse(null, { status: 204 }) for both workspace and preset deletes, matching status(204) from the real API.

Items explicitly deferred per your instruction: workspaceId stamping on API keys, "" slug normalization, auth-team teardown on workspace delete, non-409 Eden fallthrough, sidebar .catches, api-key state reset on workspace switch, route.mdx "agent" copy, Teams @@unique DB constraint, mock GET single-preset, delete intent response-shape alignment, slug regex.

Verification

  • bun run lint
  • bun run typecheck ✅ (all workspaces)
  • bun run test ✅ (3 + 2 + 31 pass)
  • bun run build
    • Branch: claude-issue-455-20260503-0241

- Scope GET /v1/traces and GET /v1/traces/:traceId by the authenticated
  workspaceSlug instead of a caller-supplied ?workspace= query param, so
  workspace-A API keys can no longer enumerate workspace-B traces.
- Restore the MSW model-catalog handler at /gateway/v1/models so the
  console-only dev flow (shell loader + playground) still resolves
  canonical models.
- Return 204 (not 200) from the workspace/preset DELETE mock handlers to
  match the real API contract.

Co-authored-by: Heinrich Wendel <heiwen@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/console/app/mocks/routes/workspaces.ts (1)

56-81: ⚡ Quick win

POST /presets duplicate-slug check may miss presets added via db.presets directly.

The duplicate-slug guard on line 63 checks only workspace.presets.some(p => p.slug === body.slug). If any code path creates a preset in db.presets without pushing it into workspace.presets[], the guard will miss it and the creation will silently shadow the orphaned record. Within this mock file the two-step write (create in db.presets → push to workspace.presets) is the only path, so this is low risk now, but coupling the guard to the embedded array rather than db.presets is fragile.

Consider guarding against db.presets.findFirst(q => q.where({ slug: body.slug, workspace_id: workspace.slug })) instead, which is the single source of truth for the PATCH/DELETE handlers.

♻️ Proposed refactor
-      if (workspace.presets.some((p) => p.slug === body.slug))
+      const existingPreset = db.presets.findFirst((q) =>
+        q.where({ slug: body.slug, workspace_id: workspace.slug }),
+      );
+      if (existingPreset)
         return new HttpResponse("Preset with the same slug already exists", { status: 409 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/app/mocks/routes/workspaces.ts` around lines 56 - 81, The
duplicate-slug check in the POST /presets route handler currently inspects
workspace.presets (in the http.post handler callback) which can miss presets
present in db.presets; change the guard to query the canonical store by calling
db.presets.findFirst(...) with a where filter for slug: body.slug and
workspace_id: workspace.slug (or use workspace.id if appropriate) and treat a
non-null result as a 409 conflict before creating the new preset.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/console/app/mocks/routes/workspaces.ts`:
- Around line 47-53: The GET handler for
"/api/v1/workspaces/:workspaceSlug/presets" currently returns the embedded
workspace.presets array which isn't updated when the PATCH handler calls
db.presets.update, causing edits to disappear; change the GET to read
authoritative presets from db.presets (e.g., query db.presets.findMany filtered
by workspaceId or workspaceSlug) or, after db.presets.update in the PATCH
handler, write the updated preset back into the owning workspace record's
presets array so the workspace.presets and db.presets stay in sync; update the
code around the http.get route handler and the PATCH logic that calls
db.presets.update to ensure both sources reflect the same data (use workspace.id
/ params.workspaceSlug and preset.id to locate and replace the item).

---

Nitpick comments:
In `@apps/console/app/mocks/routes/workspaces.ts`:
- Around line 56-81: The duplicate-slug check in the POST /presets route handler
currently inspects workspace.presets (in the http.post handler callback) which
can miss presets present in db.presets; change the guard to query the canonical
store by calling db.presets.findFirst(...) with a where filter for slug:
body.slug and workspace_id: workspace.slug (or use workspace.id if appropriate)
and treat a non-null result as a 409 conflict before creating the new preset.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a11e426-d493-424f-90b5-74d255d7e6e8

📥 Commits

Reviewing files that changed from the base of the PR and between db130ab and 337527d.

📒 Files selected for processing (5)
  • apps/api/src/modules/traces/index.ts
  • apps/console/app/mocks/browser.ts
  • apps/console/app/mocks/routes/workspaces.ts
  • apps/console/app/routes/_shell.w.$workspaceSlug.traces.($traceId)/route.tsx
  • apps/console/app/routes/_shell.w.$workspaceSlug.traces/route.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/console/app/routes/_shell.w.$workspaceSlug.traces.($traceId)/route.tsx
  • apps/api/src/modules/traces/index.ts
  • apps/console/app/mocks/browser.ts
  • apps/console/app/routes/_shell.w.$workspaceSlug.traces/route.tsx

Comment on lines +47 to +53
http.get<{ workspaceSlug: string }>(
"/api/v1/workspaces/:workspaceSlug/presets",
({ params }) => {
const workspace = db.workspaces.findFirst((q) => q.where({ slug: params.workspaceSlug }));
if (!workspace) return new HttpResponse("Not found", { status: 404 });
return HttpResponse.json(workspace.presets);
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

PATCH updates db.presets but GET /presets reads from workspace.presets[] — edits are invisible to subsequent reads.

GET /workspaces/:workspaceSlug/presets (line 52) returns workspace.presets, which is the embedded array on the workspace record. However, the PATCH handler (lines 95–101) only calls db.presets.update(...) and never writes back to workspace.presets. Any edit to a preset's name or model will be lost the next time the list is fetched, breaking edit-then-read test flows.

🐛 Proposed fix — sync the workspace's embedded preset array after update
       const updated = await db.presets.update(preset, {
         data(p) {
           if (body.name !== undefined) p.name = body.name;
           if (body.model !== undefined) p.model = body.model;
           p.updated_at = new Date();
         },
       });
+
+      // Keep the workspace's embedded presets list in sync so GET /presets reflects the edit.
+      const ws = db.workspaces.findFirst((q) => q.where({ slug: params.workspaceSlug }));
+      if (ws) {
+        await db.workspaces.update(ws, {
+          data(w) {
+            const idx = w.presets.findIndex((p) => p.slug === params.presetSlug);
+            if (idx !== -1) w.presets[idx] = updated;
+          },
+        });
+      }

       return HttpResponse.json(updated);

Also applies to: 83-105

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/app/mocks/routes/workspaces.ts` around lines 47 - 53, The GET
handler for "/api/v1/workspaces/:workspaceSlug/presets" currently returns the
embedded workspace.presets array which isn't updated when the PATCH handler
calls db.presets.update, causing edits to disappear; change the GET to read
authoritative presets from db.presets (e.g., query db.presets.findMany filtered
by workspaceId or workspaceSlug) or, after db.presets.update in the PATCH
handler, write the updated preset back into the owning workspace record's
presets array so the workspace.presets and db.presets stay in sync; update the
code around the http.get route handler and the PATCH logic that calls
db.presets.update to ensure both sources reflect the same data (use workspace.id
/ params.workspaceSlug and preset.id to locate and replace the item).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: Agents -> Workspaces

1 participant