feat(web): one sidebar, one picker, one scope - #6801
Conversation
There was a problem hiding this comment.
31 issues found across 119 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/api/src/storage/virtual.ts">
<violation number="1" location="apps/api/src/storage/virtual.ts:275">
P2: When a matching VIRTUAL connection has metadata such as `liveAgentId`, this method returns its JSON string instead of an object. `/api/_me/projects/search` then cannot detect linked dev agents, so they appear in the picker; parse the stored JSON before returning it.</violation>
</file>
<file name="apps/web/src/hooks/use-project-scope.ts">
<violation number="1" location="apps/web/src/hooks/use-project-scope.ts:87">
P2: When an organization has more than 100 virtual MCPs, this hook cannot scope to valid projects after the first page. Fetch all pages or provide pagination before deriving the scope list.</violation>
</file>
<file name="apps/web/src/components/sidebar/shell.tsx">
<violation number="1" location="apps/web/src/components/sidebar/shell.tsx:58">
P2: On mobile, this sheet branch omits `text-sidebar-foreground`, so inherited picker and navigation text uses the document foreground instead of the sidebar theme. Add the sidebar foreground class to the sheet shell.</violation>
</file>
<file name="apps/web/src/layouts/main-panel-tabs/main-panel-tabs-bar.tsx">
<violation number="1" location="apps/web/src/layouts/main-panel-tabs/main-panel-tabs-bar.tsx:98">
P1: When a thread or agent has more tabs than the main header can fit, later tabs are clipped with no scroll or overflow affordance, and an active tab can be hidden. Retain an overflow mechanism (including active-tab promotion) or make the tab row provide an accessible way to reach every tab.</violation>
</file>
<file name="apps/web/src/components/sidebar/project-nav.tsx">
<violation number="1" location="apps/web/src/components/sidebar/project-nav.tsx:172">
P2: When a pinned app view is open, its sidebar row never becomes active because the URL segment is `app` while `view.panel` is the composite `app:<connectionId>:<toolName>` ID. Compare the resolved composite tab ID, including its connection and tool payload, so the pinned row reflects the current view.
(Based on your team's feedback about composite app-view tab IDs.)</violation>
</file>
<file name="apps/web/src/components/chat/hooks/use-chat-navigation.ts">
<violation number="1" location="apps/web/src/components/chat/hooks/use-chat-navigation.ts:56">
P3: This now carries the agent in `?virtualmcpid`, but `ChatNavigation.navigateToTask` still documents the old project path segment. Update that contract comment to match the new URL shape.</violation>
</file>
<file name="apps/web/src/layouts/main-panel-tabs/content-tab.tsx">
<violation number="1" location="apps/web/src/layouts/main-panel-tabs/content-tab.tsx:53">
P2: When an existing agent has no source, this picker creates and navigates to a new agent instead of connecting the repository to the current `virtualMcpId`. Use an update-existing-agent flow here, or label this action as importing a new agent.</violation>
</file>
<file name="apps/web/src/components/sidebar/index.tsx">
<violation number="1" location="apps/web/src/components/sidebar/index.tsx:103">
P2: On mobile org routes, this adds a second Settings control while `SidebarAccountFooterMobile` still renders its own footer shortcut. In project scope the two controls open different settings surfaces, so remove the footer shortcut or make it use the same scoped target.</violation>
</file>
<file name="apps/web/src/layouts/shell-layout.tsx">
<violation number="1" location="apps/web/src/layouts/shell-layout.tsx:267">
P3: After this handler moves ⌘K to the command palette, the shortcuts dialog still advertises ⌘K for opening itself and does not document `?`. Update the shortcut metadata to reflect the new bindings.</violation>
</file>
<file name="apps/web/src/layouts/agent-shell-layout/workspace-panel-group.tsx">
<violation number="1" location="apps/web/src/layouts/agent-shell-layout/workspace-panel-group.tsx:172">
P2: When an agent has more per-thread tabs than fit in the main header, the tab row now renders them all but clips the trailing buttons with no recovery affordance. Retain active-tab promotion/overflow handling or make the tab row scrollable instead of removing the `maxVisible` constraint.</violation>
</file>
<file name="apps/api/src/api/app.ts">
<violation number="1" location="apps/api/src/api/app.ts:2216">
P2: When this mounted search is used, `metadata.liveAgentId` is never detected because `searchAcrossMemberships` returns the text column as a string. Parse `connections.metadata` before `createMeRoutes` filters and serializes the results, otherwise hidden dev agents appear in the picker and metadata consumers receive a string instead of an object.</violation>
</file>
<file name="apps/web/src/layouts/task-board/index.tsx">
<violation number="1" location="apps/web/src/layouts/task-board/index.tsx:1063">
P1: When a project scope hides previously selected cards, this leaves their IDs in `selectedIds`. The SelectionBar still renders and bulk actions, including delete, operate on those hidden cards; clear or reconcile the selection when the scope changes.</violation>
</file>
<file name="apps/web/src/components/command-palette.tsx">
<violation number="1" location="apps/web/src/components/command-palette.tsx:141">
P2: When a project is scoped, selecting an organization-wide task result retains that project filter on the tasks route. Clear `virtualmcpid` (or resolve the task's target project) when opening the hit so tasks from another project are not hidden.</violation>
</file>
<file name="apps/web/src/layouts/main-panel-tabs/discover-tab.tsx">
<violation number="1" location="apps/web/src/layouts/main-panel-tabs/discover-tab.tsx:122">
P2: Because the invite step is permanently incomplete, `discover.setup.allDone` is unreachable and every workspace permanently appears to need setup. Remove this recurring action from the completion checklist or derive its completion from membership state.</violation>
<violation number="2" location="apps/web/src/layouts/main-panel-tabs/discover-tab.tsx:151">
P2: When a repo has CMS disabled or no editable `.deco` content, this card still says Content editing is ready. When storage lacks a matching file config, Assets is likewise marked ready although its tab remains unavailable. Derive these cards from the same per-project gates so Discover does not promise unavailable surfaces.</violation>
</file>
<file name="apps/api/src/tools/reports/setup.ts">
<violation number="1" location="apps/api/src/tools/reports/setup.ts:131">
P1: The reportUrl this emits no longer scopes to the Commerce Discovery agent. The router's canonical carrier for the agent scope is the `?virtualmcpid=` search param (read by useScopeId/useProjectContext/useChatNavigation and written as `virtualmcpid` by commerceReportNavTarget), but `agentPanelPath` writes the project id as `?project=`. Since `app` is a known panel segment, the agents route's `beforeLoad` never converts anything, and `?project=` is read nowhere, so the "diagnóstico completo" CTA lands on `/agents/app?connection=…&tool=…` under the Super Agent / no project scope instead of the Commerce Discovery project.</violation>
</file>
<file name="apps/api/src/tools/search/global-search.ts">
<violation number="1" location="apps/api/src/tools/search/global-search.ts:23">
P2: Adding `task` here changes the runtime tool contract, but the checked-in shared contract still rejects `types: ["task"]` and cannot represent task results. Regenerate `packages/shared/src/tools/tool-io.ts` from the updated tool schemas.</violation>
<violation number="2" location="apps/api/src/tools/search/global-search.ts:165">
P2: When callers use the documented empty query with tasks enabled, this branch returns no task cards instead of recent matches. Query recent tasks with the requested limit, or narrow the empty-query contract to exclude tasks.</violation>
<violation number="3" location="apps/api/src/tools/search/global-search.ts:172">
P2: When more than `limit` task cards match, this reports `totalCount` as only the number returned, unlike the thread total. Return a true task match count alongside the page and add that count here.</violation>
</file>
<file name="apps/web/src/components/sidebar/org-project-picker.tsx">
<violation number="1" location="apps/web/src/components/sidebar/org-project-picker.tsx:189">
P2: When the project-search request fails, the picker reports “no matches” instead of an error and provides no retry or failure feedback. Handle `isError` separately from the successful empty-results state.</violation>
<violation number="2" location="apps/web/src/components/sidebar/org-project-picker.tsx:189">
P2: While a new search term loads, stale results remain selectable because `isStale` is ignored; pressing Enter can open a project that no longer matches the input. Disable or hide stale rows until current-term results arrive.</violation>
<violation number="3" location="apps/web/src/components/sidebar/org-project-picker.tsx:206">
P1: When a project is selected from settings, this navigation sends it through a resolver that discards `virtualmcpid`; the picker exits settings without opening the chosen project. Navigate directly to the agents destination for non-null project IDs, with a separate unscoped destination for “All projects.”</violation>
<violation number="4" location="apps/web/src/components/sidebar/org-project-picker.tsx:219">
P1: When a cross-organization project is selected, `travelTo` routes through the org resolver, which ignores `projectId`; the user lands on that org’s default destination instead of the selected project. Use the agents destination with `virtualmcpid` for project travel, keeping `/$org` only for organization-only travel.</violation>
</file>
<file name="packages/e2e/tests/standalone-blocks-panel.spec.ts">
<violation number="1" location="packages/e2e/tests/standalone-blocks-panel.spec.ts:77">
P3: The Preview URL assertion was relaxed from `/\/agents\/[^/?]+\/preview/` (which verified a project segment) to `/\/agents\/preview/`, but no companion assertion checks that the project now rides in `?virtualmcpid=`. The comment states the project rides in search, yet the test no longer verifies project scoping survives entering preview, so a regression that drops the agent from the URL would go unnoticed.</violation>
</file>
<file name="apps/web/src/router.tsx">
<violation number="1" location="apps/web/src/router.tsx:498">
P1: When switching threads without an explicit `virtualMcpId` on a scoped agents route, `useThreadNavigate` no longer sees the current project and navigates to the Super Agent. Read `routeSearch.virtualmcpid` (or the route agent resolver) after moving the scope into search.</violation>
<violation number="2" location="apps/web/src/router.tsx:511">
P2: The added `beforeLoad` treats any lone `/agents/<segment>` that isn't in `isKnownPanelSegment` as a project id and redirects it to `?virtualmcpid=<segment>`. Every valid view name must be in `KNOWN_PANEL_SEGMENTS` or deep links to it (e.g. a view not yet in that set) get hijacked into a project-scope redirect. Verify `site-editor`/`coding` (and all tab-bar views) are in that set; also, when `virtualmcpid` is already retained from a previous navigation, an unrecognized legacy project segment stays as `panel` without migrating, rendering an unknown view.</violation>
</file>
<file name="apps/api/src/api/routes/me.ts">
<violation number="1" location="apps/api/src/api/routes/me.ts:86">
P2: When more than ten matching rows are hidden by `isPlumbing` or SSO filtering, the fixed headroom prevents the picker from finding later valid agents. Continue fetching until the requested number of permitted rows is filled, or move the filters into the search query.</violation>
<violation number="2" location="apps/api/src/api/routes/me.ts:99">
P2: Because this user-scoped GET lacks a private cache directive, a browser or intermediary can reuse search results for another session with the same query. Set `Cache-Control: private, no-store` on every response from this handler.</violation>
<violation number="3" location="apps/api/src/api/routes/me.ts:101">
P2: When this handler runs with an API-key-only `StudioContext`, `ctx.auth.user` is absent and the route returns 401 despite `ctx.auth.apiKey.userId` identifying the caller. Use `getUserId(ctx)` instead of reading only `auth.user`.
(Based on your team's feedback about API-key user ID resolution.)</violation>
<violation number="4" location="apps/api/src/api/routes/me.ts:135">
P2: When a member still has a membership in a soft-deleted organization, this endpoint returns its agents and organization name to the picker even though archived organizations are otherwise unavailable. Exclude archived organizations from the cross-organization search before returning items.</violation>
</file>
<file name="apps/web/src/hooks/use-navigate-to-agent.ts">
<violation number="1" location="apps/web/src/hooks/use-navigate-to-agent.ts:81">
P1: When the agent list resolves a default runtime, a fresh navigation drops it because this guard checks `options.runtime` instead of `wantedRuntime`. Write the resolved `wantedRuntime` into the thread intent so the route fallback creates the requested CMS or sandbox session.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| return ( | ||
| <div className="flex items-center min-w-0 gap-0.5"> | ||
| {visible.map((item) => ( | ||
| {items.map((item) => ( |
There was a problem hiding this comment.
P1: When a thread or agent has more tabs than the main header can fit, later tabs are clipped with no scroll or overflow affordance, and an active tab can be hidden. Retain an overflow mechanism (including active-tab promotion) or make the tab row provide an accessible way to reach every tab.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/layouts/main-panel-tabs/main-panel-tabs-bar.tsx, line 98:
<comment>When a thread or agent has more tabs than the main header can fit, later tabs are clipped with no scroll or overflow affordance, and an active tab can be hidden. Retain an overflow mechanism (including active-tab promotion) or make the tab row provide an accessible way to reach every tab.</comment>
<file context>
@@ -104,39 +87,15 @@ export function MainPanelTabsBar({
return (
<div className="flex items-center min-w-0 gap-0.5">
- {visible.map((item) => (
+ {items.map((item) => (
<HeaderTabButton
key={item.id}
</file context>
| * repo filter does not — two different questions, composed rather than | ||
| * conflated. | ||
| */ | ||
| const scopedItems = items.filter((item) => taskMatchesScope(item, scopeRepo)); |
There was a problem hiding this comment.
P1: When a project scope hides previously selected cards, this leaves their IDs in selectedIds. The SelectionBar still renders and bulk actions, including delete, operate on those hidden cards; clear or reconcile the selection when the scope changes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/layouts/task-board/index.tsx, line 1063:
<comment>When a project scope hides previously selected cards, this leaves their IDs in `selectedIds`. The SelectionBar still renders and bulk actions, including delete, operate on those hidden cards; clear or reconcile the selection when the scope changes.</comment>
<file context>
@@ -1047,9 +1055,22 @@ export function TaskBoardPage() {
+ * repo filter does not — two different questions, composed rather than
+ * conflated.
+ */
+ const scopedItems = items.filter((item) => taskMatchesScope(item, scopeRepo));
+ const visibleItems = scopedItems.filter((item) =>
taskMatchesFilters(item, filters),
</file context>
| navigate({ | ||
| to: "/$org", | ||
| params: { org: org.slug }, | ||
| search: { virtualmcpid: id ?? undefined }, |
There was a problem hiding this comment.
P1: When a project is selected from settings, this navigation sends it through a resolver that discards virtualmcpid; the picker exits settings without opening the chosen project. Navigate directly to the agents destination for non-null project IDs, with a separate unscoped destination for “All projects.”
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/components/sidebar/org-project-picker.tsx, line 206:
<comment>When a project is selected from settings, this navigation sends it through a resolver that discards `virtualmcpid`; the picker exits settings without opening the chosen project. Navigate directly to the agents destination for non-null project IDs, with a separate unscoped destination for “All projects.”</comment>
<file context>
@@ -0,0 +1,533 @@
+ navigate({
+ to: "/$org",
+ params: { org: org.slug },
+ search: { virtualmcpid: id ?? undefined },
+ });
+ } else {
</file context>
| navigate({ | ||
| to: "/$org", | ||
| params: { org: slug }, | ||
| search: projectId ? { virtualmcpid: projectId } : {}, |
There was a problem hiding this comment.
P1: When a cross-organization project is selected, travelTo routes through the org resolver, which ignores projectId; the user lands on that org’s default destination instead of the selected project. Use the agents destination with virtualmcpid for project travel, keeping /$org only for organization-only travel.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/components/sidebar/org-project-picker.tsx, line 219:
<comment>When a cross-organization project is selected, `travelTo` routes through the org resolver, which ignores `projectId`; the user lands on that org’s default destination instead of the selected project. Use the agents destination with `virtualmcpid` for project travel, keeping `/$org` only for organization-only travel.</comment>
<file context>
@@ -0,0 +1,533 @@
+ navigate({
+ to: "/$org",
+ params: { org: slug },
+ search: projectId ? { virtualmcpid: projectId } : {},
+ });
+ onClose();
</file context>
| * panel's payload (`connection`/`tool`), landing the report CTA on an empty | ||
| * app view with no error. | ||
| */ | ||
| beforeLoad: ({ params, search }) => { |
There was a problem hiding this comment.
P2: The added beforeLoad treats any lone /agents/<segment> that isn't in isKnownPanelSegment as a project id and redirects it to ?virtualmcpid=<segment>. Every valid view name must be in KNOWN_PANEL_SEGMENTS or deep links to it (e.g. a view not yet in that set) get hijacked into a project-scope redirect. Verify site-editor/coding (and all tab-bar views) are in that set; also, when virtualmcpid is already retained from a previous navigation, an unrecognized legacy project segment stays as panel without migrating, rendering an unknown view.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/router.tsx, line 511:
<comment>The added `beforeLoad` treats any lone `/agents/<segment>` that isn't in `isKnownPanelSegment` as a project id and redirects it to `?virtualmcpid=<segment>`. Every valid view name must be in `KNOWN_PANEL_SEGMENTS` or deep links to it (e.g. a view not yet in that set) get hijacked into a project-scope redirect. Verify `site-editor`/`coding` (and all tab-bar views) are in that set; also, when `virtualmcpid` is already retained from a previous navigation, an unrecognized legacy project segment stays as `panel` without migrating, rendering an unknown view.</comment>
<file context>
@@ -473,25 +483,46 @@ const orgHomeRoute = createRoute({
+ * panel's payload (`connection`/`tool`), landing the report CTA on an empty
+ * app view with no error.
+ */
+ beforeLoad: ({ params, search }) => {
+ const segment = params.panel;
+ if (!segment || isKnownPanelSegment(segment)) return;
</file context>
| return; | ||
| } | ||
| /** "?" opens the shortcuts sheet that ⌘K used to; ignored while typing. */ | ||
| if (e.key === "?" && !isTypingTarget(e.target)) { |
There was a problem hiding this comment.
P3: After this handler moves ⌘K to the command palette, the shortcuts dialog still advertises ⌘K for opening itself and does not document ?. Update the shortcut metadata to reflect the new bindings.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/layouts/shell-layout.tsx, line 267:
<comment>After this handler moves ⌘K to the command palette, the shortcuts dialog still advertises ⌘K for opening itself and does not document `?`. Update the shortcut metadata to reflect the new bindings.</comment>
<file context>
@@ -257,6 +259,12 @@ function ShellLayoutContent() {
+ return;
+ }
+ /** "?" opens the shortcuts sheet that ⌘K used to; ignored while typing. */
+ if (e.key === "?" && !isTypingTarget(e.target)) {
e.preventDefault();
setShortcutsDialogOpen(true);
</file context>
| /* The view is a path segment now: `/agents/<project>/preview`. */ | ||
| await expect(page).toHaveURL(/\/agents\/[^/?]+\/preview/); | ||
| /* The VIEW is the segment; the project rides in `?virtualmcpid=`. */ | ||
| await expect(page).toHaveURL(/\/agents\/preview/); |
There was a problem hiding this comment.
P3: The Preview URL assertion was relaxed from /\/agents\/[^/?]+\/preview/ (which verified a project segment) to /\/agents\/preview/, but no companion assertion checks that the project now rides in ?virtualmcpid=. The comment states the project rides in search, yet the test no longer verifies project scoping survives entering preview, so a regression that drops the agent from the URL would go unnoticed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/e2e/tests/standalone-blocks-panel.spec.ts, line 77:
<comment>The Preview URL assertion was relaxed from `/\/agents\/[^/?]+\/preview/` (which verified a project segment) to `/\/agents\/preview/`, but no companion assertion checks that the project now rides in `?virtualmcpid=`. The comment states the project rides in search, yet the test no longer verifies project scoping survives entering preview, so a regression that drops the agent from the URL would go unnoticed.</comment>
<file context>
@@ -73,8 +73,8 @@ test.describe("Blocks preview mode", () => {
- /* The view is a path segment now: `/agents/<project>/preview`. */
- await expect(page).toHaveURL(/\/agents\/[^/?]+\/preview/);
+ /* The VIEW is the segment; the project rides in `?virtualmcpid=`. */
+ await expect(page).toHaveURL(/\/agents\/preview/);
await expect(chat).toBeVisible();
await expect(main).toBeVisible();
</file context>
| }, | ||
| params: { org: org.slug, panel: view?.panel }, | ||
| search: { | ||
| virtualmcpid: opts?.virtualMcpId ?? virtualMcpId, |
There was a problem hiding this comment.
P3: This now carries the agent in ?virtualmcpid, but ChatNavigation.navigateToTask still documents the old project path segment. Update that contract comment to match the new URL shape.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/components/chat/hooks/use-chat-navigation.ts, line 56:
<comment>This now carries the agent in `?virtualmcpid`, but `ChatNavigation.navigateToTask` still documents the old project path segment. Update that contract comment to match the new URL shape.</comment>
<file context>
@@ -51,12 +51,9 @@ export function useChatNavigation(): ChatNavigation {
- },
+ params: { org: org.slug, panel: view?.panel },
search: {
+ virtualmcpid: opts?.virtualMcpId ?? virtualMcpId,
thread: taskId,
...(view?.payload ?? {}),
</file context>
Booting walked through four: splash, then a panel spinner, then a chat skeleton, then a spinner again. Measured on a production build, an in-app home -> settings -> back also went BACKWARDS, dropping to a full-screen splash twice after the shell had already painted. Two states now. SplashScreen is pre-shell only, and PanelLoading gates the main panel with the shell painted around it. THE SPLASH IS ONE INSTANCE. It was rendered from five places -- Providers, the root route, AuthLoading, four route pendingComponents, and the access gate -- so each handoff unmounted one element and mounted another at a different tree position, restarting its animation. A recorder asking "is a splash on screen" saw continuous coverage and reported nothing wrong. `boot-gate.tsx` reads one module-scope promise with `use()`; a settled promise returns synchronously forever after, so nothing can re-arm it. router.load() is what it waits on -- a hand-listed prerequisite set left a 376ms blank because the router still had work pending. defaultPendingComponent moves from SplashScreen to PanelLoading, which is the change that matters most: a route that FORGETS pendingComponent now degrades to a panel spinner instead of blanking the app. That is how 16 settings routes ended up rendering a min-h-screen splash inside the settings card. Nine loaders deleted or merged. Chat.Skeleton was also ChatSidePanel's ERROR fallback, so a failed load left a fake conversation up forever; that path renders the real error text now. loading-states.spec.ts counts splash MOUNTS, not presence -- measuring presence is precisely what let the replay through. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/$org/home rendered the Super Agent's own Overview board -- an agent view the org landing happened to borrow. It is now the org's roster: a greeting, one search, the agents someone actually made, and what has run recently. The filter already existed and was simply never called. scopableProjects() hides the Super Agent, the seven Studio Pack managers and dev agents; the settings Agents page filters only `id !== org.id`, which is why everything showed. Since Studio Pack is backfilled into every org, the filtered EMPTY state is the normal first run, not a rare one -- so Import from GitHub is the primary button there rather than an item in a dropdown. Search goes to the SERVER. The list tool caps at 100 rows with no cursor, so the settings page's client-side filter silently searches a prefix. The hero search opens the existing palette rather than adding a second index. Its open state moved to a module-scope store read with useSyncExternalStore, so only the two components that read it subscribe -- a context would re-render every consumer under the shell on each toggle. Two full-viewport blanking bugs fixed on the way: AddConnectionDialog and CommandPalette were both mounted unconditionally while calling useSuspenseQuery on the self-MCP client, whose entry is deliberately not warm-started. Nothing between them and the root caught the suspend, so a CLOSED dialog blanked the whole app on every cold load and org switch. /$org/home still serves double duty -- scoped, it is the agent's home and renders Overview exactly as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Connecting a client meant two route-level tabs, a nested OAuth/API-key tab strip inside each client panel, three stacked surfaces around one URL, and the same endpoint printed twice under two names. It is one page now: client tabs (Claude, Cursor, Codex, Other) with the install method as a Select on the same row, the snippet below, and API keys flat at the bottom. THE MODE WAS PER-CLIENT. Picking "API key" and switching Cursor -> Codex silently returned you to OAuth, because the tabs lived inside ClientPanel with defaultValue="oauth". It is page-level state now and persists across tabs. Claude Desktop is gone (no distinct mark exists -- the Anthropic starburst is Claude Code's, and two identical glyphs in a row of four read as a bug), "Claude Code" reads as "Claude", and "Any other MCP client" merged into the Other tab so the endpoint appears exactly once. Removing the tab strip would have DELETED THE CONNECT SIDEBAR ROW: the sidebar routes a grouped row at its first visible tab and dropped the row when there were none, unable to tell "declares no tabs" from "all tabs gated away". isActive matched only groupRoutes(), now empty. Both fixed, and ownedRoutes keeps /settings/api-keys lighting Connect for anyone holding that bookmark. Splitting the API-keys view surfaced that it had no loading or error branch -- it destructured `data = []`, so a slow list flashed "No API keys yet" and a FAILED list rendered as empty. useApiKeysList neither suspends nor throws, so the route's error fallback never fired. ConnectKeysSection is deleted: the full list is a superset (same hook, a client-side "Connect: " filter), so nothing is lost and legacy keys from retired clients still list and revoke. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Preview, Content and Code were three sibling addresses for one surface, and a puzzle-piece CMS button toggled a fourth view inside Preview. There is one segment now -- /agents/site-editor -- and Content opens on it via ?main=content, so the CMS view is finally linkable and survives a reload (editingMode was local useState before). The CMS button is gone; the switcher writes and clears the param. cmsMode collapses from off|manual|auto to off|on. manual and auto differed only in whether Preview popped the CMS open by itself, which the URL now decides. BOTH RETIRED VALUES ARE PERSISTED ON REAL AGENTS. Dropping them from the enum alone would read every one as `off` and silently take the CMS away -- and the test agent turned out to be stored as "auto", so this was exercised live, not hypothetically. normalizeCmsMode() maps them in ONE place, modelled on normalizePanelSegment; the schema widened to StoredCmsModeSchema so a pre-collapse row still parses rather than throwing. Leaving a project no longer strands you. Reports is project-only now, so clearing the scope in place landed on a scope-less /reports that the nav had just dropped its row for. routeExistsInScope() is that fact, shared with the sidebar rows rather than restated -- two hand-kept lists would drift the first time a destination is added. Everything else still un-narrows in place: scoped Tasks becomes the org's Tasks, not Home. The splash fill eases logarithmically and caps near 88%. A bezier is bounded within its own duration and cannot express an asymptote, so the stops sample ln(1+9t)/ln(10) directly. It never reaches full: a bar that fills and then sits there reads as finished-but-stuck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
14 issues found across 94 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/e2e/tests/destination-routes.spec.ts">
<violation number="1" location="packages/e2e/tests/destination-routes.spec.ts:355">
P2: The new `/agents/content` regression case does not assert Content semantics. Since `content` is classified as a known segment, this test can pass even if it renders the wrong panel; assert that the loaded view is Content (or that the app rewrites the legacy segment to `/agents/site-editor?main=content`) so the bookmark compatibility contract is covered.</violation>
</file>
<file name="apps/web/src/layouts/main-panel-tabs/org-agents-tab.tsx">
<violation number="1" location="apps/web/src/layouts/main-panel-tabs/org-agents-tab.tsx:91">
P2: When an organization has more than 100 agents, this page silently omits the remaining agents from the roster. Load all pages (or otherwise handle `hasMore`) before presenting the roster as complete.</violation>
<violation number="2" location="apps/web/src/layouts/main-panel-tabs/org-agents-tab.tsx:102">
P2: When activity exists, this page mounts three `useTaskBoardItems` instances and therefore duplicates the task-board and Decopilot SSE subscriptions. Read the board data once and pass the derived activity/columns into `RecentActivity`, or provide a non-subscribing selector for the gating check.</violation>
</file>
<file name="apps/web/src/i18n/en/settings.ts">
<violation number="1" location="apps/web/src/i18n/en/settings.ts:284">
P3: The new key `settings.connectClients.copied` is never referenced in code — the `copied` values in org-connect.tsx and install-snippet.tsx come from `useCopy()` state, not from `t()`. Remove the key (and its pt-br counterpart) or wire it into the copy button's feedback text.</violation>
</file>
<file name="apps/web/src/layouts/shell-layout.tsx">
<violation number="1" location="apps/web/src/layouts/shell-layout.tsx:423">
P2: When Cmd/Ctrl+K opens before the self-MCP prewarm finishes, this conditional mounts `CommandPalette`, whose client hook suspends and replaces the entire painted app with the root `SplashScreen`. Keep the palette connection non-suspending or add a local Suspense fallback so opening the palette never blanks the shell.</violation>
</file>
<file name="apps/web/src/layouts/main-panel-tabs/source-system-tabs.ts">
<violation number="1" location="apps/web/src/layouts/main-panel-tabs/source-system-tabs.ts:86">
P2: When a Code tab has an open file (`code:<path>`), reports-only navigation does not recognize it as a source tab. Include the encoded-file form so it deep-links to the Report Agent like the bare Code tab.</violation>
</file>
<file name="apps/web/src/hooks/use-destination-route.ts">
<violation number="1" location="apps/web/src/hooks/use-destination-route.ts:75">
P1: When no project is selected, this classification hides the org-wide Reports destination; with a project selected, it incorrectly exposes that org-wide page in the project navigation. Mark Reports `org-only` so the sidebar and scope-exit behavior match `reportsRoute` and the report's org-scoped data.</violation>
</file>
<file name="apps/web/src/layouts/main-panel-tabs/use-main-panel-tabs.ts">
<violation number="1" location="apps/web/src/layouts/main-panel-tabs/use-main-panel-tabs.ts:347">
P2: When a CMS session opens a saved or bookmarked `code` URL, this runtime gate removes Code from the surface tabs but leaves the URL-selected tab active, so the panel mounts `CodeTab` without a sandbox. Normalize explicit Code URLs to the CMS surface default before resolving `activeTab`.</violation>
</file>
<file name="packages/e2e/tests/loading-states.spec.ts">
<violation number="1" location="packages/e2e/tests/loading-states.spec.ts:287">
P1: When boot takes longer than 1.5 seconds, the splash's intentional infinite `deco-splash-wave` animation emits repeated `animationstart` events, so this assertion fails on a normal slow boot. Exclude the looping wave from replay detection and assert uniqueness only for the one-shot animations.</violation>
</file>
<file name="apps/web/src/components/sandbox/preview/preview.tsx">
<violation number="1" location="apps/web/src/components/sandbox/preview/preview.tsx:1729">
P2: When a loader, SEO edit, or page creation opens Blocks mode, the Site Editor no longer exposes a way to return to Preview because this control now renders only `urlGroup`. Keep a shared Blocks close/toggle action in both desktop and mobile controls, or route these flows through the Content tab.</violation>
</file>
<file name="apps/web/src/components/chat/side-panel-chat.tsx">
<violation number="1" location="apps/web/src/components/chat/side-panel-chat.tsx:130">
P2: When `ChatSidePanel` renders this fallback under `SidePanel`, its parent is not a flex container, so `flex-1` does not fill the panel and the alert stays at the top. Add `h-full` so the error state remains vertically centered.</violation>
</file>
<file name="apps/web/src/views/settings/org-connect.tsx">
<violation number="1" location="apps/web/src/views/settings/org-connect.tsx:359">
P1: When `api-keys:manage` is denied, this guard hides only `ApiKeysSection`, but `ClientPanel` still lets the user mint a full-organization API key. Guard the inline key-generation controls too, or wrap the entire connect page in the capability gate.</violation>
<violation number="2" location="apps/web/src/views/settings/org-connect.tsx:360">
P2: After revoking a key generated in the client panel, the snippet still displays that revoked token because `ApiKeysSection` cannot clear `newKeys`. Clear the corresponding local key after deletion or coordinate revocation with the connect panel.</violation>
</file>
<file name="apps/web/src/components/connect/install-snippet.tsx">
<violation number="1" location="apps/web/src/components/connect/install-snippet.tsx:104">
P2: This makes the installation command unavailable to screen readers: the button is named only “Copy”, while the code is now inside that button rather than exposed as readable content. Keep the snippet outside the interactive control and provide a separate copy button, or explicitly associate an accessible text description so users can review what will be copied.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| )} | ||
| </SettingsSection> | ||
|
|
||
| <RequireCapability capability="api-keys:manage" area="api-keys"> |
There was a problem hiding this comment.
P1: When api-keys:manage is denied, this guard hides only ApiKeysSection, but ClientPanel still lets the user mint a full-organization API key. Guard the inline key-generation controls too, or wrap the entire connect page in the capability gate.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/views/settings/org-connect.tsx, line 359:
<comment>When `api-keys:manage` is denied, this guard hides only `ApiKeysSection`, but `ClientPanel` still lets the user mint a full-organization API key. Guard the inline key-generation controls too, or wrap the entire connect page in the capability gate.</comment>
<file context>
@@ -255,92 +283,82 @@ function OrgConnectContent() {
+ )}
+ </SettingsSection>
+
+ <RequireCapability capability="api-keys:manage" area="api-keys">
+ <ApiKeysSection />
+ </RequireCapability>
</file context>
| DestinationRoutePath, | ||
| ScopeBinding | ||
| >([ | ||
| [DESTINATION_ROUTE.reports, "project-only"], |
There was a problem hiding this comment.
P1: When no project is selected, this classification hides the org-wide Reports destination; with a project selected, it incorrectly exposes that org-wide page in the project navigation. Mark Reports org-only so the sidebar and scope-exit behavior match reportsRoute and the report's org-scoped data.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/hooks/use-destination-route.ts, line 75:
<comment>When no project is selected, this classification hides the org-wide Reports destination; with a project selected, it incorrectly exposes that org-wide page in the project navigation. Mark Reports `org-only` so the sidebar and scope-exit behavior match `reportsRoute` and the report's org-scoped data.</comment>
<file context>
@@ -38,14 +39,51 @@ export function useLeafRoutePath(): string {
+ DestinationRoutePath,
+ ScopeBinding
+>([
+ [DESTINATION_ROUTE.reports, "project-only"],
+ [DESTINATION_ROUTE.library, "org-only"],
+]);
</file context>
| An empty list is allowed and not vacuous: the assertion above already | ||
| pinned the mount count, and `prefers-reduced-motion` legitimately runs | ||
| no animations at all. */ | ||
| expect(boot.animations).toEqual([...new Set(boot.animations)]); |
There was a problem hiding this comment.
P1: When boot takes longer than 1.5 seconds, the splash's intentional infinite deco-splash-wave animation emits repeated animationstart events, so this assertion fails on a normal slow boot. Exclude the looping wave from replay detection and assert uniqueness only for the one-shot animations.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/e2e/tests/loading-states.spec.ts, line 287:
<comment>When boot takes longer than 1.5 seconds, the splash's intentional infinite `deco-splash-wave` animation emits repeated `animationstart` events, so this assertion fails on a normal slow boot. Exclude the looping wave from replay detection and assert uniqueness only for the one-shot animations.</comment>
<file context>
@@ -0,0 +1,289 @@
+ An empty list is allowed and not vacuous: the assertion above already
+ pinned the mount count, and `prefers-reduced-motion` legitimately runs
+ no animations at all. */
+ expect(boot.animations).toEqual([...new Set(boot.animations)]);
+ });
+});
</file context>
| expect(boot.animations).toEqual([...new Set(boot.animations)]); | |
| expect(boot.animations.filter((name) => name !== "deco-splash-wave")).toEqual([ | |
| ...new Set(boot.animations.filter((name) => name !== "deco-splash-wave")), | |
| ]); |
| * empty board the roster spans the row instead of sitting beside a blank | ||
| * panel. The roster itself always renders — its empty state carries the | ||
| * import CTA, so it is the page's floor, never absent. */ | ||
| const hasActivity = useRecentTasks().length > 0; |
There was a problem hiding this comment.
P2: When activity exists, this page mounts three useTaskBoardItems instances and therefore duplicates the task-board and Decopilot SSE subscriptions. Read the board data once and pass the derived activity/columns into RecentActivity, or provide a non-subscribing selector for the gating check.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/layouts/main-panel-tabs/org-agents-tab.tsx, line 102:
<comment>When activity exists, this page mounts three `useTaskBoardItems` instances and therefore duplicates the task-board and Decopilot SSE subscriptions. Read the board data once and pass the derived activity/columns into `RecentActivity`, or provide a non-subscribing selector for the gating check.</comment>
<file context>
@@ -0,0 +1,200 @@
+ * empty board the roster spans the row instead of sitting beside a blank
+ * panel. The roster itself always renders — its empty state carries the
+ * import CTA, so it is the page's floor, never absent. */
+ const hasActivity = useRecentTasks().length > 0;
+ const codeAgents = agents.filter((a) => agentHasClonableSource(a.metadata));
+ const plainAgents = agents.filter((a) => !agentHasClonableSource(a.metadata));
</file context>
| {cmsToggle && <div className="mx-0.5 h-5 w-px shrink-0 bg-border" />} | ||
| {urlGroup} | ||
| </div> | ||
| <div className="flex min-w-0 items-center gap-0.5">{urlGroup}</div> |
There was a problem hiding this comment.
P2: When a loader, SEO edit, or page creation opens Blocks mode, the Site Editor no longer exposes a way to return to Preview because this control now renders only urlGroup. Keep a shared Blocks close/toggle action in both desktop and mobile controls, or route these flows through the Content tab.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/components/sandbox/preview/preview.tsx, line 1729:
<comment>When a loader, SEO edit, or page creation opens Blocks mode, the Site Editor no longer exposes a way to return to Preview because this control now renders only `urlGroup`. Keep a shared Blocks close/toggle action in both desktop and mobile controls, or route these flows through the Content tab.</comment>
<file context>
@@ -1815,14 +1724,9 @@ export function PreviewContent({ virtualMcpId }: { virtualMcpId: string }) {
- {cmsToggle && <div className="mx-0.5 h-5 w-px shrink-0 bg-border" />}
- {urlGroup}
- </div>
+ <div className="flex min-w-0 items-center gap-0.5">{urlGroup}</div>
) : null;
</file context>
| "Any other MCP client: paste this endpoint into it to give that runtime every connection enabled in this org, governed by your Decopilot rules.", | ||
| "settings.connectClients.apiKeyOption": "API key", | ||
| "settings.connectClients.connectAClient": "Connect a client", | ||
| "settings.connectClients.copied": "Copied", |
There was a problem hiding this comment.
P3: The new key settings.connectClients.copied is never referenced in code — the copied values in org-connect.tsx and install-snippet.tsx come from useCopy() state, not from t(). Remove the key (and its pt-br counterpart) or wire it into the copy button's feedback text.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/i18n/en/settings.ts, line 284:
<comment>The new key `settings.connectClients.copied` is never referenced in code — the `copied` values in org-connect.tsx and install-snippet.tsx come from `useCopy()` state, not from `t()`. Remove the key (and its pt-br counterpart) or wire it into the copy button's feedback text.</comment>
<file context>
@@ -279,36 +277,28 @@ export const settings = {
+ "Any other MCP client: paste this endpoint into it to give that runtime every connection enabled in this org, governed by your Decopilot rules.",
+ "settings.connectClients.apiKeyOption": "API key",
+ "settings.connectClients.connectAClient": "Connect a client",
+ "settings.connectClients.copied": "Copied",
"settings.connectClients.copy": "Copy",
- "settings.connectClients.createdAt": "Created {date}",
</file context>
CONTENT NO LONGER DEPENDS ON A FETCH. Whether the Content tab existed used to hang on `useDecofile`/`useLiveMeta` reporting editable content -- a read that lands late on a sandbox session and never on a CMS one, which has no dev server at all. So the tab blinked in, or never appeared, and the `contentTabPending` / `contentAsked` / `contentIsDefaultMain` escape hatches that papered over it disagreed with each other. It is now gated on the AGENT: CMS on offers Content, `off` takes it away, and an empty site is the view's own empty state to explain. That deleted the whole decofile subscription from the tab bar. The sidebar's Site Editor row lands on Preview, and a cms-runtime session opens with the blocks editor already split in -- derived once at mount from the thread's immutable runtime stamp, not from a per-agent auto-open flag. `cmsMode: "auto"` stays retired. The preview toolbar's page picker is removed. NOTE: it was the only entry point for CREATING a page, so `create-page-modal.tsx` / `use-create-page.ts` went with it and page creation is currently unreachable. Recover with `git checkout <this-commit>~1 -- <path>`; the Content view, which already lists Pages, is where the entry point belongs. Also note a sandbox session now offers Preview + Content + Code. Content follows the agent's CMS mode on every runtime, which is what the rule says and what the pre-existing test asserted. THE FOCUS RINGS WERE THREE MECHANISMS IN ONE ROW. "Show chat" drew an OUTSET Tailwind ring inside a header with `overflow-hidden`, so it rendered as a `)` bracket clipped at the button's own left edge. Preview and Content had no focus style at all and fell through to Chrome's stark white UA outline. The rest used shadcn's near-invisible outset ring. `INSET_FOCUS_RING` in packages/ui is now the one definition -- inset, so no ancestor can clip it. The filled SplitButton rings in `inset-ring-current`, because a token chosen to contrast with the PAGE disappears into a filled button. The CMS tour is replaced by a release tour that starts from the release card's CTA: it awaits `navigate()` to the org home, then drives. Steps are pre-filtered to visible anchors so the progress count has no gaps. The desktop filter was dropping ANY action CTA, which would have vanished the whole release in Tauri -- narrowed to the download action. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds `GET /api/_me/projects/search?q=`, which the sidebar picker calls once a term is typed. It lives under the user-scoped `/api/_me` namespace rather than `/api/:org` on purpose: that prefix runs `resolveOrgFromPath`, which rebinds `ctx.access` so a request reads exactly one tenant, and a cross-org answer served from there would break the one invariant that middleware exists to hold. Two properties worth stating: - Membership is JOINED in SQL from the session user. The request names no organizations, so there is no argument a client could forge to read a tenant it does not belong to. - It is ONE bounded query. The per-org list path selects an org's whole agent graph with no limit and filters in JavaScript, so searching N orgs that way would be N full scans of the biggest table in the account. Leaving `/api/:org` also leaves the SSO-enforcement middleware, which short-circuits when no organization is resolved — so the route re-applies that gate per organization itself. Without it, a member of an SSO-enforcing org would receive that org's agent titles without ever completing SSO, which is what IdP revocation is supposed to cut off while the `member` row still exists. The user-typed term is escaped before it reaches a LIKE pattern: unescaped, a lone `%` matches every agent in every organization the caller belongs to. The escape is a pure function with its own tests, including the ordering property that the backslash must be escaped first. Also gives `VirtualMCPStorage.list` an ORDER BY. Without one Postgres returns heap order, which an UPDATE reshuffles — so the list tool's `slice(offset, offset + limit)` could repeat one row across pages and skip another. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Navigating from `/:org/home?virtualmcpid=X` to `/:org/agents/content?...` sometimes blanked the entire viewport — sidebar included — back to the splash screen for an instant. The trigger is `ChatPrefsProvider`, which mounts above the sidebar and outside every Suspense boundary in the shell, so the nearest fallback is the router's own `SplashScreen`. It read the selected agent with a SUSPENSE query keyed on the agent id, and that id flips on exactly this navigation — the Super Agent on `/home`, the scoped agent on `/agents` — because only the agents route resolves `?virtualmcpid=`. A key that changes on a navigation whose URL param did not. It was intermittent for two reasons at once: the GET must be a cache miss (5-minute `gcTime`, so visiting the agent once hides it), AND the render must be discrete rather than a transition. TanStack does navigate inside `startTransition`, but every consumer reads location through `useSyncExternalStore`, which React schedules at sync priority to avoid tearing — and a sync render that suspends must show the fallback. React 19's "keep the previous UI" applies to transition lanes only. So the suspending reads become non-blocking rather than the boundary moving: a fallback tree would mount a second copy of the same suspending reads, and a boundary here is exactly the skeleton the sidebar is designed not to have. - `useMCPClientNonBlocking` / `useVirtualMCPNonBlocking` share the existing cache entries, so they are hits wherever the shell has mounted. - `useNavigateToAgent` no longer suspends at render or subscribes the sidebar to the thread store. It needs the agent list only to pick a default runtime, and only at CLICK time, so it reads cache when warm and fetches when cold. Passing `undefined` instead would make `runtimeMatches` match ANY empty chat, which is how a click on a CMS agent lands in a leftover sandbox session. Two claims that were already false are now true: `useVirtualMCPsNonBlocking` called the SUSPENDING `useMCPClient` (its contract held only because a parent happened to warm the client first), and the sidebar's "paints on the first frame" was gated on that same read one level up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reframes navigation around a single container. The organization is the only container; an agent is a FILTER over it, carried by `?virtualmcpid=` and read by exactly two routes. Everything else follows from that. THE PICKER. The org switcher and the agent switcher become one control in the sidebar header. They were never the same kind of thing, so the design is about keeping them apart: picking an agent writes the scope and stays put, while picking an organization changes the path, remounts the shell and drops the scope. A footer strip names the verb for the focused row before Enter commits it, since those two live one arrow-key apart. Typing searches organizations AND agents together, through one cross-org endpoint, so search never mixes a server-filtered population with an unfiltered one. Browsing lists this org's agents and the other orgs by name; another org's agents are not this org's to show. CONTEXT, NOT FALLBACK. With a scope set the sidebar is an agent's: its own rows, its own settings, and a "back to <org>" row. Without one it is the organization's, and Site Editor / CMS / Coding do not appear at all — an organization has no such surfaces, and guessing one would put an agent's rows under a header that names the org. The back row reads the RAW scope id, so an id naming an agent you cannot see still has an exit; resolved-only, that state hid the rows, Library and the exit at once, permanently, since the param is retained. ONE SHELL. Three hand-rolled shells (desktop, mobile, settings) become one with named slots, so the same back row cannot land at two different heights again. One row primitive underneath — which fixes an accessibility bug on the way: the collapsed rail sets `display:none` on the label, removing it from the accessibility tree, and a tooltip is a description, not a name. Every row in the rail announced as a bare "button". The primitive always sets `aria-label`. The settings tree joins the same shell: same header, same picker, same way out. Its full-width top bar is gone on desktop, where the sidebar already carries the org and the collapse trigger; mobile keeps it for the hamburger. Settings is now ONE row, always last, opening the agent's settings when scoped and the organization's when not. It reads the raw scope rather than a display fallback: settings is where you go to change something, and a guessed target has consequences. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clicking Settings blanked the UI, and coming back lagged. Two causes, one
structural and one a missing route option.
THE SHELL. `orgShellLayout` and `settingsLayout` are SIBLINGS under `orgLayout`,
and each owned its own sidebar row — so crossing between them destroyed an
entire shell and built another. The sidebar moves up to `orgLayout`, whose match
is in BOTH chains and whose id never changes, so React keeps that instance and
only the inset's contents and the sidebar's back/body/footer slots swap.
The branch lives INSIDE `StudioSidebar`, on the slot props. At the call site
(`{inSettings ? <SettingsSidebar/> : <StudioSidebar/>}`) it would put a different
component type at the same position, which unmounts the whole sidebar and
rebuilds it — the same bug one level down.
Two things that were silently broken fall out of this: the shells disagreed
about whether the collapse state was controlled (`open` off localStorage vs an
uncontrolled `defaultOpen`), so collapsing and then entering settings reset it;
and the width was `${width}px` on one side and the `15rem` fallback on the
other, so the sidebar jumped. One provider now, one width.
THE FALLBACK. Neither layout set a `pendingComponent`, so both fell through to
the app-wide `SplashScreen` — a `min-h-screen` box — whenever a crossing took
longer than `defaultPendingMs`. `agentShellLayout` had already learned this
("the sidebar is already mounted … so the pending state covers only the main
panel region"), and that reasoning became true for these two only once the
sidebar moved up. Both now use `ShellRouteLoading`, so the loader sits in the
panel and the sidebar stays painted.
Chat machinery stays in `orgShellLayout` deliberately: hoisting
ThreadManagerProvider and ChatPrefsProvider would make a cold link straight to
/$org/settings download ~120 KB gzip of chat code it never uses. That is why
the crossing still costs two lazy chunks and three provider mounts — this stops
the blanking, not the loading.
Verified by types, tests and the compiler; the absence of a flicker is not
something either can see.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Booting walked through four: splash, then a panel spinner, then a chat skeleton, then a spinner again. Measured on a production build, an in-app home -> settings -> back also went BACKWARDS, dropping to a full-screen splash twice after the shell had already painted. Two states now. SplashScreen is pre-shell only, and PanelLoading gates the main panel with the shell painted around it. THE SPLASH IS ONE INSTANCE. It was rendered from five places -- Providers, the root route, AuthLoading, four route pendingComponents, and the access gate -- so each handoff unmounted one element and mounted another at a different tree position, restarting its animation. A recorder asking "is a splash on screen" saw continuous coverage and reported nothing wrong. `boot-gate.tsx` reads one module-scope promise with `use()`; a settled promise returns synchronously forever after, so nothing can re-arm it. router.load() is what it waits on -- a hand-listed prerequisite set left a 376ms blank because the router still had work pending. defaultPendingComponent moves from SplashScreen to PanelLoading, which is the change that matters most: a route that FORGETS pendingComponent now degrades to a panel spinner instead of blanking the app. That is how 16 settings routes ended up rendering a min-h-screen splash inside the settings card. Nine loaders deleted or merged. Chat.Skeleton was also ChatSidePanel's ERROR fallback, so a failed load left a fake conversation up forever; that path renders the real error text now. loading-states.spec.ts counts splash MOUNTS, not presence -- measuring presence is precisely what let the replay through. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/$org/home rendered the Super Agent's own Overview board -- an agent view the org landing happened to borrow. It is now the org's roster: a greeting, one search, the agents someone actually made, and what has run recently. The filter already existed and was simply never called. scopableProjects() hides the Super Agent, the seven Studio Pack managers and dev agents; the settings Agents page filters only `id !== org.id`, which is why everything showed. Since Studio Pack is backfilled into every org, the filtered EMPTY state is the normal first run, not a rare one -- so Import from GitHub is the primary button there rather than an item in a dropdown. Search goes to the SERVER. The list tool caps at 100 rows with no cursor, so the settings page's client-side filter silently searches a prefix. The hero search opens the existing palette rather than adding a second index. Its open state moved to a module-scope store read with useSyncExternalStore, so only the two components that read it subscribe -- a context would re-render every consumer under the shell on each toggle. Two full-viewport blanking bugs fixed on the way: AddConnectionDialog and CommandPalette were both mounted unconditionally while calling useSuspenseQuery on the self-MCP client, whose entry is deliberately not warm-started. Nothing between them and the root caught the suspend, so a CLOSED dialog blanked the whole app on every cold load and org switch. /$org/home still serves double duty -- scoped, it is the agent's home and renders Overview exactly as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Connecting a client meant two route-level tabs, a nested OAuth/API-key tab strip inside each client panel, three stacked surfaces around one URL, and the same endpoint printed twice under two names. It is one page now: client tabs (Claude, Cursor, Codex, Other) with the install method as a Select on the same row, the snippet below, and API keys flat at the bottom. THE MODE WAS PER-CLIENT. Picking "API key" and switching Cursor -> Codex silently returned you to OAuth, because the tabs lived inside ClientPanel with defaultValue="oauth". It is page-level state now and persists across tabs. Claude Desktop is gone (no distinct mark exists -- the Anthropic starburst is Claude Code's, and two identical glyphs in a row of four read as a bug), "Claude Code" reads as "Claude", and "Any other MCP client" merged into the Other tab so the endpoint appears exactly once. Removing the tab strip would have DELETED THE CONNECT SIDEBAR ROW: the sidebar routes a grouped row at its first visible tab and dropped the row when there were none, unable to tell "declares no tabs" from "all tabs gated away". isActive matched only groupRoutes(), now empty. Both fixed, and ownedRoutes keeps /settings/api-keys lighting Connect for anyone holding that bookmark. Splitting the API-keys view surfaced that it had no loading or error branch -- it destructured `data = []`, so a slow list flashed "No API keys yet" and a FAILED list rendered as empty. useApiKeysList neither suspends nor throws, so the route's error fallback never fired. ConnectKeysSection is deleted: the full list is a superset (same hook, a client-side "Connect: " filter), so nothing is lost and legacy keys from retired clients still list and revoke. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Preview, Content and Code were three sibling addresses for one surface, and a puzzle-piece CMS button toggled a fourth view inside Preview. There is one segment now -- /agents/site-editor -- and Content opens on it via ?main=content, so the CMS view is finally linkable and survives a reload (editingMode was local useState before). The CMS button is gone; the switcher writes and clears the param. cmsMode collapses from off|manual|auto to off|on. manual and auto differed only in whether Preview popped the CMS open by itself, which the URL now decides. BOTH RETIRED VALUES ARE PERSISTED ON REAL AGENTS. Dropping them from the enum alone would read every one as `off` and silently take the CMS away -- and the test agent turned out to be stored as "auto", so this was exercised live, not hypothetically. normalizeCmsMode() maps them in ONE place, modelled on normalizePanelSegment; the schema widened to StoredCmsModeSchema so a pre-collapse row still parses rather than throwing. Leaving a project no longer strands you. Reports is project-only now, so clearing the scope in place landed on a scope-less /reports that the nav had just dropped its row for. routeExistsInScope() is that fact, shared with the sidebar rows rather than restated -- two hand-kept lists would drift the first time a destination is added. Everything else still un-narrows in place: scoped Tasks becomes the org's Tasks, not Home. The splash fill eases logarithmically and caps near 88%. A bezier is bounded within its own duration and cannot express an asymptote, so the stops sample ln(1+9t)/ln(10) directly. It never reaches full: a bar that fills and then sits there reads as finished-but-stuck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CONTENT NO LONGER DEPENDS ON A FETCH. Whether the Content tab existed used to hang on `useDecofile`/`useLiveMeta` reporting editable content -- a read that lands late on a sandbox session and never on a CMS one, which has no dev server at all. So the tab blinked in, or never appeared, and the `contentTabPending` / `contentAsked` / `contentIsDefaultMain` escape hatches that papered over it disagreed with each other. It is now gated on the AGENT: CMS on offers Content, `off` takes it away, and an empty site is the view's own empty state to explain. That deleted the whole decofile subscription from the tab bar. The sidebar's Site Editor row lands on Preview, and a cms-runtime session opens with the blocks editor already split in -- derived once at mount from the thread's immutable runtime stamp, not from a per-agent auto-open flag. `cmsMode: "auto"` stays retired. The preview toolbar's page picker is removed. NOTE: it was the only entry point for CREATING a page, so `create-page-modal.tsx` / `use-create-page.ts` went with it and page creation is currently unreachable. Recover with `git checkout <this-commit>~1 -- <path>`; the Content view, which already lists Pages, is where the entry point belongs. Also note a sandbox session now offers Preview + Content + Code. Content follows the agent's CMS mode on every runtime, which is what the rule says and what the pre-existing test asserted. THE FOCUS RINGS WERE THREE MECHANISMS IN ONE ROW. "Show chat" drew an OUTSET Tailwind ring inside a header with `overflow-hidden`, so it rendered as a `)` bracket clipped at the button's own left edge. Preview and Content had no focus style at all and fell through to Chrome's stark white UA outline. The rest used shadcn's near-invisible outset ring. `INSET_FOCUS_RING` in packages/ui is now the one definition -- inset, so no ancestor can clip it. The filled SplitButton rings in `inset-ring-current`, because a token chosen to contrast with the PAGE disappears into a filled button. The CMS tour is replaced by a release tour that starts from the release card's CTA: it awaits `navigate()` to the org home, then drives. Steps are pre-filtered to visible anchors so the progress count has no gaps. The desktop filter was dropping ANY action CTA, which would have vanished the whole release in Tauri -- narrowed to the download action. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
5 issues found across 49 files (changes from recent commits).
You’re at about 90% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/web/src/lib/release-feed.ts">
<violation number="1" location="apps/web/src/lib/release-feed.ts:44">
P2: The new announcement is rendered only in English because its title, bullet text, and CTA label are hard-coded in the release data. Move this copy into the English and Brazilian Portuguese translation entries and store translation keys in the release definition.</violation>
</file>
<file name="apps/web/src/layouts/main-panel-tabs/use-main-panel-tabs.ts">
<violation number="1" location="apps/web/src/layouts/main-panel-tabs/use-main-panel-tabs.ts:295">
P2: In a reports-only org viewed through a non-Report Agent shell, this exposes Content for the current source-less agent and does not deep-link it to the Report Agent. Gate Content to the Report Agent (or include `content` in the deep-link handling) while retaining the cross-agent Preview/Code behavior.</violation>
</file>
<file name="packages/ui/src/lib/focus-ring.ts">
<violation number="1" location="packages/ui/src/lib/focus-ring.ts:16">
P2: In forced-colors mode (Windows High Contrast) browsers do not render box-shadows, so the inset-ring focus indicator disappears, and `focus-visible:outline-none` has already removed the outline — leaving keyboard-focused buttons in panel headers with no focus indicator at all for those users. Since this PR is an accessibility pass, restore an outline under forced-colors, e.g. append `forced-colors:outline-2 forced-colors:outline-offset-0` (or a `forced-colors:outline-[...]` ring) to the constant so the focus ring survives when box-shadows are suppressed.</violation>
</file>
<file name="apps/web/src/components/release-channel/floating-release-card.tsx">
<violation number="1" location="apps/web/src/components/release-channel/floating-release-card.tsx:74">
P2: When the CTA is clicked from an agent-scoped route, the retained `virtualmcpid` keeps Home in agent view, so the tour never shows the organization roster it is intended to introduce. Explicitly clear the scope while navigating to the organization Home.</violation>
</file>
<file name="apps/web/src/components/layout-tour/layout-tour.ts">
<violation number="1" location="apps/web/src/components/layout-tour/layout-tour.ts:53">
P2: On mobile, the CTA starts the tour while the only `OrgProjectPicker` is inside the closed `MobileSidebarSheet`, so this filter removes the primary switcher step. Open the mobile sheet before driving or provide a visible mobile anchor before building the steps.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| id: "unified-workspace-layout", | ||
| date: "2026-09-01", | ||
| eyebrow: "Now Available", | ||
| title: "A simpler way around Studio", |
There was a problem hiding this comment.
P2: The new announcement is rendered only in English because its title, bullet text, and CTA label are hard-coded in the release data. Move this copy into the English and Brazilian Portuguese translation entries and store translation keys in the release definition.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/lib/release-feed.ts, line 44:
<comment>The new announcement is rendered only in English because its title, bullet text, and CTA label are hard-coded in the release data. Move this copy into the English and Brazilian Portuguese translation entries and store translation keys in the release definition.</comment>
<file context>
@@ -36,6 +37,30 @@ export interface Release {
+ id: "unified-workspace-layout",
+ date: "2026-09-01",
+ eyebrow: "Now Available",
+ title: "A simpler way around Studio",
+ bullets: [
+ {
</file context>
| const surfaceTabIds = resolveSurfaceTabs({ | ||
| hasSource: previewSource === "repo" || reportsOnly, | ||
| runtime, | ||
| cmsMode, |
There was a problem hiding this comment.
P2: In a reports-only org viewed through a non-Report Agent shell, this exposes Content for the current source-less agent and does not deep-link it to the Report Agent. Gate Content to the Report Agent (or include content in the deep-link handling) while retaining the cross-agent Preview/Code behavior.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/layouts/main-panel-tabs/use-main-panel-tabs.ts, line 295:
<comment>In a reports-only org viewed through a non-Report Agent shell, this exposes Content for the current source-less agent and does not deep-link it to the Report Agent. Gate Content to the Report Agent (or include `content` in the deep-link handling) while retaining the cross-agent Preview/Code behavior.</comment>
<file context>
@@ -332,70 +287,34 @@ export function useMainPanelTabs(ctx: {
+ const surfaceTabIds = resolveSurfaceTabs({
+ hasSource: previewSource === "repo" || reportsOnly,
+ runtime,
+ cmsMode,
+ });
+ const showContent = surfaceTabIds.includes("content");
</file context>
| cmsMode, | |
| cmsMode: | |
| reportsOnly && ctx.virtualMcpId !== getCommerceDiscoveryAgentId(org.id) | |
| ? "off" | |
| : cmsMode, |
| * covers every kind of button a header holds. | ||
| */ | ||
| export const INSET_FOCUS_RING = | ||
| "focus-visible:outline-none focus-visible:ring-0 focus-visible:border-border focus-visible:inset-ring-2 focus-visible:inset-ring-ring/50"; |
There was a problem hiding this comment.
P2: In forced-colors mode (Windows High Contrast) browsers do not render box-shadows, so the inset-ring focus indicator disappears, and focus-visible:outline-none has already removed the outline — leaving keyboard-focused buttons in panel headers with no focus indicator at all for those users. Since this PR is an accessibility pass, restore an outline under forced-colors, e.g. append forced-colors:outline-2 forced-colors:outline-offset-0 (or a forced-colors:outline-[...] ring) to the constant so the focus ring survives when box-shadows are suppressed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ui/src/lib/focus-ring.ts, line 16:
<comment>In forced-colors mode (Windows High Contrast) browsers do not render box-shadows, so the inset-ring focus indicator disappears, and `focus-visible:outline-none` has already removed the outline — leaving keyboard-focused buttons in panel headers with no focus indicator at all for those users. Since this PR is an accessibility pass, restore an outline under forced-colors, e.g. append `forced-colors:outline-2 forced-colors:outline-offset-0` (or a `forced-colors:outline-[...]` ring) to the constant so the focus ring survives when box-shadows are suppressed.</comment>
<file context>
@@ -0,0 +1,16 @@
+ * covers every kind of button a header holds.
+ */
+export const INSET_FOCUS_RING =
+ "focus-visible:outline-none focus-visible:ring-0 focus-visible:border-border focus-visible:inset-ring-2 focus-visible:inset-ring-ring/50";
</file context>
| "focus-visible:outline-none focus-visible:ring-0 focus-visible:border-border focus-visible:inset-ring-2 focus-visible:inset-ring-ring/50"; | |
| "focus-visible:outline-none focus-visible:ring-0 focus-visible:border-border focus-visible:inset-ring-2 focus-visible:inset-ring-ring/50 forced-colors:outline-2 forced-colors:outline-offset-0"; |
| const startTour = async () => { | ||
| markSeen(candidate.id); | ||
| if (orgSlug) { | ||
| await navigate({ to: DESTINATION_ROUTE.home, params: { org: orgSlug } }); |
There was a problem hiding this comment.
P2: When the CTA is clicked from an agent-scoped route, the retained virtualmcpid keeps Home in agent view, so the tour never shows the organization roster it is intended to introduce. Explicitly clear the scope while navigating to the organization Home.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/components/release-channel/floating-release-card.tsx, line 74:
<comment>When the CTA is clicked from an agent-scoped route, the retained `virtualmcpid` keeps Home in agent view, so the tour never shows the organization roster it is intended to introduce. Explicitly clear the scope while navigating to the organization Home.</comment>
<file context>
@@ -57,6 +62,20 @@ export function FloatingReleaseCard() {
+ const startTour = async () => {
+ markSeen(candidate.id);
+ if (orgSlug) {
+ await navigate({ to: DESTINATION_ROUTE.home, params: { org: orgSlug } });
+ }
+ startLayoutTour(t);
</file context>
| await navigate({ to: DESTINATION_ROUTE.home, params: { org: orgSlug } }); | |
| await navigate({ | |
| to: DESTINATION_ROUTE.home, | |
| params: { org: orgSlug }, | |
| search: { virtualmcpid: undefined }, | |
| }); |
| function visibleSteps(t: TFunction): DriveStep[] { | ||
| return buildSteps(t).filter( | ||
| (step) => | ||
| typeof step.element === "string" && isAnchorOnScreen(step.element), |
There was a problem hiding this comment.
P2: On mobile, the CTA starts the tour while the only OrgProjectPicker is inside the closed MobileSidebarSheet, so this filter removes the primary switcher step. Open the mobile sheet before driving or provide a visible mobile anchor before building the steps.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/components/layout-tour/layout-tour.ts, line 53:
<comment>On mobile, the CTA starts the tour while the only `OrgProjectPicker` is inside the closed `MobileSidebarSheet`, so this filter removes the primary switcher step. Open the mobile sheet before driving or provide a visible mobile anchor before building the steps.</comment>
<file context>
@@ -0,0 +1,146 @@
+function visibleSteps(t: TFunction): DriveStep[] {
+ return buildSteps(t).filter(
+ (step) =>
+ typeof step.element === "string" && isAnchorOnScreen(step.element),
+ );
+}
</file context>
eda3dc3 to
6f4177a
Compare
AN ORG-SCOPED API KEY COULD READ EVERY ORG ITS OWNER BELONGED TO. Leaving
`/api/:org` drops three gates, not one, and `/api/_me/projects/search`
re-applied only the first: SSO. It never checked the API-key organization
binding or `tokenOrganizationId`, and never called `ctx.access.check()`, so
a read-only key minted for org A returned project titles, org names and
slugs for orgs B, C and D -- while the same key against `/api/B/...` is a
hard 403. `credentialOrganizationFence` now applies the other two, sharing
the one `getApiKeyOrganizationBinding` the middleware uses rather than a
second copy, and fails closed the same way: an explicit but unreadable
binding denies, and a key and token that disagree deny. The handler also
stopped forwarding the raw `metadata` bag (github attachment,
previewServerUrl, instructions) -- the picker never read it.
THE `?project=` -> `?virtualmcpid=` RENAME WAS HALF-APPLIED IN FOUR PLACES.
The router declares only `virtualmcpid`, and its search schema strips
unknown keys, so each of these silently dropped the scope and opened on the
Super Agent: thread-route read `routeSearch.project` (always undefined, so
"New chat" wrote `virtualmcpid: undefined` and WIPED the scope);
organization-paths minted `project=` into URLSearchParams -- those URLs are
persisted per-org and already mailed; use-commerce-diagnostic put the id in
`params`, which `/$org/agents/{-$panel}` discards; and the `/$org` resolver
never read the incoming scope, overwriting cross-org travel with the
destination org's main agent. Two tests asserted the broken key and are
inverted here, which is why the suite was green on a dead contract.
I ALSO REPEATED A BUG I HAD ALREADY FIXED ONCE. `systemTabs` was computed
and never spread into `tabs`; nothing in the app navigates to `git`,
`hosting`, `e2e`, `analytics` or `cdn`, and the sidebar re-homed only Site
Editor, Assets, pinned views and Automations -- so Review changes and the
whole control-plane group were reachable only by typing the URL. Restored
with the original mapping. The comment claiming the sidebar owned them is
gone; it contradicted project-nav's own.
Also: `KEYS.projectSearch` was keyed on the term alone, and the QueryClient
is module-scope and survives sign-out, so user B saw user A's cross-org
results after typing the same term in the same tab -- the user is in the key
now. The command palette and the threads-panel dialog shared one
`globalSearch` key while caching two different shapes, so whichever opened
first decided what the other read; the key now carries limit and types, and
the dialog asks for threads only instead of dropping task rows to `null`
behind a suppressed empty state. `searchByTitle` skips attachRefs, so
`taskKey` was deriving every synced card's name from a null `jiraIssueKey`
and showing EX-333 as ACME-07. `useVirtualMCPsPage` still used the
suspending client above the shell boundary. And the tool contract was
regenerated -- it did not list the `cms` value the app now writes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 17 files (changes from recent commits).
You’re at about 96% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/web/src/layouts/main-panel-tabs/use-main-panel-tabs.ts">
<violation number="1" location="apps/web/src/layouts/main-panel-tabs/use-main-panel-tabs.ts:610">
P2: When an agent-declared tab uses a gated system ID, this spread adds the same ID twice to `tabs`, producing duplicate React keys and making the agent view resolve as the native system panel. Reserve system IDs for agent tabs or deduplicate/namespace declared IDs before building the list.</violation>
</file>
<file name="apps/api/src/api/routes/me.ts">
<violation number="1" location="apps/api/src/api/routes/me.ts:164">
P2: When a credential is organization-scoped and another organization has more recent matches, this filters a query already limited to `limit + 10`, so valid matches in the fenced organization can be omitted. Apply the organization fence in `searchAcrossMemberships` before `LIMIT`.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| const tabs: Tab[] = [ | ||
| ...leadingSystemTabs.map((t) => ({ | ||
| ...surfaceTabs, | ||
| ...systemTabs.map((t) => ({ |
There was a problem hiding this comment.
P2: When an agent-declared tab uses a gated system ID, this spread adds the same ID twice to tabs, producing duplicate React keys and making the agent view resolve as the native system panel. Reserve system IDs for agent tabs or deduplicate/namespace declared IDs before building the list.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/layouts/main-panel-tabs/use-main-panel-tabs.ts, line 610:
<comment>When an agent-declared tab uses a gated system ID, this spread adds the same ID twice to `tabs`, producing duplicate React keys and making the agent view resolve as the native system panel. Reserve system IDs for agent tabs or deduplicate/namespace declared IDs before building the list.</comment>
<file context>
@@ -595,13 +595,28 @@ export function useMainPanelTabs(ctx: {
+ * appearing in both places is fine — Site Editor already does. */
const tabs: Tab[] = [
...surfaceTabs,
+ ...systemTabs.map((t) => ({
+ id: t.id,
+ title: t.title,
</file context>
|
|
||
| const hits: ProjectSearchHit[] = matches | ||
| .filter((match) => !isPlumbing(match)) | ||
| .filter((match) => !fence || match.organization_id === fence) |
There was a problem hiding this comment.
P2: When a credential is organization-scoped and another organization has more recent matches, this filters a query already limited to limit + 10, so valid matches in the fenced organization can be omitted. Apply the organization fence in searchAcrossMemberships before LIMIT.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/api/routes/me.ts, line 164:
<comment>When a credential is organization-scoped and another organization has more recent matches, this filters a query already limited to `limit + 10`, so valid matches in the fenced organization can be omitted. Apply the organization fence in `searchAcrossMemberships` before `LIMIT`.</comment>
<file context>
@@ -122,11 +161,11 @@ export const createMeRoutes = () => {
const hits: ProjectSearchHit[] = matches
.filter((match) => !isPlumbing(match))
+ .filter((match) => !fence || match.organization_id === fence)
.map((match) => ({
id: match.id,
</file context>
The organization is the only container. An agent is a filter over it, not a
place — carried by
?virtualmcpid=and resolved by exactly two routes. Thesidebar, the picker and the settings shell all follow from that one decision.
What changes for a user
One control names where you are. The org switcher and the agent switcher
are now a single picker in the sidebar header. With an agent selected it shows
that agent — its own avatar and title — plus a Cloudflare-style ← Back to
<org> row. With none, it shows the organization.
The sidebar has two states, not a blend. In an organization: Home, Reports,
Tasks, Library, Settings. In an agent: its own surfaces (Site Editor, CMS,
Coding, Automations) and no Library, because Library lists the organization's
files and an agent's sidebar has nothing to point it at.
Search spans organizations. Typing in the picker searches agents across
every org you belong to, in one query. Browsing lists this org's agents and the
other orgs by name.
Settings is one row, always last — the agent's when scoped, the
organization's when not — and the settings tree now wears the same sidebar as
everything else, with the same picker and the same way out.
Notable in the diff
An accessibility bug, fixed by construction. The collapsed rail sets
display:noneon each row's label, which removes it from the accessibilitytree; a Radix tooltip supplies
aria-describedby, a description, not a name.Ten rows announced as a bare "button". The shared row primitive always sets
aria-label.The shell no longer unmounts when you enter Settings.
orgShellLayoutandsettingsLayoutare siblings, and each owned its own sidebar row — so crossingbetween them destroyed a whole shell and built another. The sidebar moved up to
orgLayout, whose match is in both chains, so React keeps the instance and onlythe slots swap. Two silent bugs fall out of it: the two shells disagreed about
whether the sidebar's collapse state was controlled, and about its width, so
both reset on every crossing. Neither layout set a
pendingComponenteither, soany crossing over
defaultPendingMspainted the app-wideSplashScreenover analready-mounted shell.
The whole shell could blank mid-navigation. A suspense query above the
sidebar, keyed on an agent id that flips when you cross onto the agents route,
with no boundary nearer than the router's splash screen. Diagnosed and made
non-blocking rather than papered over with a boundary — see the commit for why
a fallback tree would not have worked.
/api/_me/projects/searchis user-scoped, deliberately. Membership isjoined in SQL from the session user, so the request names no organizations. It
also re-applies the per-org SSO gate that
/api/:orgmiddleware wouldotherwise have provided, and escapes the user's term before it reaches a LIKE
pattern.
VirtualMCPStorage.listgained an ORDER BY. Without one the list tool'sslice(offset, offset + limit)could repeat a row across pages and skipanother.
Risk
Three deletions worth a reviewer's eye:
Toolbar.LogoLinkis gone (the sidebar carries identity now), which orphanedthe toolbar logo tree and, transitively,
DEFAULT_LOGO— the only reader ofconfig.logo. A deployment's configurable product logo is now renderednowhere. Say if that hook should return somewhere.
blanking is fixed; the loading is not. Chat machinery stays in
orgShellLayouton purpose — hoisting it would put ~120 KB gzip of chat codeon a cold
/settingslink.search, which removed the cross-org fan-out entirely.
/$org/$taskIdand/$org/agents/$project/$panel. The second one's URL is persisted by anexternal service and re-sent for orgs that never re-onboard, so it is not
ordinary back-compat.
Verification
bun run test8115 pass / 0 fail ·checkclean in web, api and e2e ·bun run lint0 errors ·bun run knipclean.Not verified in a browser — every claim above rests on types, tests and the
compiler. The e2e specs covering these URLs were updated but not executed.
Follow-ups deliberately not taken
agents-section.tsx(644 lines) is imported only by the header breadcrumb andno longer belongs under
components/sidebar/.sidebar-agent-groups-context.tsxis effectively dead — nothing readsorderRevision, and its one consumer renders outside the provider.The picker's
+buttons are not reachable from the keyboard; neither were thecreate rows they replaced.
🤖 Generated with Claude Code
Summary by cubic
Reworks Studio navigation around one organization shell: agents are now filters carried by
?virtualmcpid=instead of path segments, so one picker and sidebar serve organization, agent, and settings scopes. It also adds the organization home and cross-org search, consolidates Connect and Site Editor/CMS, and replaces the CMS tour with an on-demand layout tour.User-facing changes
/agents/site-editornow contains Preview, Content, and Code; Content is selected with?main=content, and its availability follows the agent's CMS mode.Reliability and compatibility
?virtualmcpid=is the one scope carrier everywhere: thread routes, deep links, and the/$orgresolver no longer write the discarded?project=key.Toolbar.LogoLinkandDEFAULT_LOGOwere removed.Written for commit fc489d1. Summary will update on new commits.