From b6003e6e3da1a4e5fe94a44278915589888582d6 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Fri, 18 Sep 2026 09:17:45 -0700 Subject: [PATCH] fix(web): a room can never be the selected bench MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolveSelection/isBenchMembership used to treat any non-raw-id tenant name as a bench, but rooms are named child tenants too — once selectTenant ran with a room id, the room satisfied the same check and became the selected bench, so /new, "+ Create agent", and the Agents page all operated inside the room. A bench is now defined as a top-level tenant: GET /api/tenants/:id's parentId, fetched once per membership and never guessed from the tenant's name. BenchProvider exposes the resolved set as benchMemberships, which the command palette's switch-workbench and the global Routines aggregation now read instead of re-deriving their own name-based filter. --- apps/web/src/api.ts | 28 ++++++++ apps/web/src/bench-context-value.ts | 8 ++- apps/web/src/bench-context.test.ts | 70 +++++++++++------- apps/web/src/bench-context.tsx | 72 +++++++++++++------ apps/web/src/command-palette-provider.tsx | 13 ++-- apps/web/src/global-routines.ts | 19 ++--- .../src/pages/insights-page-render.test.tsx | 1 + apps/web/src/pending-approvals.test.tsx | 11 +++ apps/web/src/query-client.ts | 4 ++ 9 files changed, 155 insertions(+), 71 deletions(-) diff --git a/apps/web/src/api.ts b/apps/web/src/api.ts index 3362dcf92..2fe9517af 100644 --- a/apps/web/src/api.ts +++ b/apps/web/src/api.ts @@ -7,6 +7,7 @@ import { ApprovalResponse, AssetWithOriginResponse, PrincipalSummary, + TenantResponse, UserProfile, WorkflowRunSummary, paginatedSchema, @@ -22,6 +23,7 @@ import { pathToQueryKey } from "./query-client"; export const ProfileSchema = UserProfile; export const PrincipalsSchema = paginatedSchema(PrincipalSummary); export const TenantApprovalsSchema = paginatedSchema(ApprovalResponse); +export const TenantDetailSchema = TenantResponse; // `GET /api/tenants/:tenantId/assets` returns a bare array of // `AssetWithOriginResponse` rows (not the paginated envelope), so the schema @@ -73,6 +75,7 @@ export const ArtifactCountsSchema = type({ export type Profile = typeof UserProfile.infer; export type Principal = typeof PrincipalSummary.infer; +export type TenantDetail = typeof TenantResponse.infer; export type WorkflowRun = typeof WorkflowRunSummary.infer; export type Approval = typeof ApprovalResponse.infer; export type AssetRow = typeof AssetWithOriginResponse.infer; @@ -181,6 +184,31 @@ export function rejectApproval( ); } +/** + * One-shot fetch of `GET /api/tenants/:id` — the only place `parentId` + * comes from. A bench is a top-level tenant (`parentId === null`); a room + * is a named child tenant, so the raw-id/name heuristic can never tell them + * apart. `bench-context.tsx` fans this out per membership with + * `useQueries` to decide which memberships are benches. + */ +export async function fetchTenantDetail(tenantId: string): Promise { + const response = await fetch(`/api/tenants/${encodeURIComponent(tenantId)}`, { + headers: { accept: "application/json" }, + }); + if (!response.ok) { + throw new ApiQueryError( + `The server answered ${response.status}.`, + response.status, + `tenant ${tenantId}`, + ); + } + const parsed = TenantDetailSchema(await response.json()); + if (parsed instanceof type.errors) { + throw new ApiQueryError(`Unexpected tenant response shape: ${parsed.summary}`); + } + return parsed; +} + /** * Sandboxed HTML preview URL for a Library artifact — the same * path an `