From 4b0142eff627658318625eaa480598790eaa2e6d Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 30 Sep 2026 14:28:47 -0700 Subject: [PATCH] fix(web): the drawer's Grants, Tools and Members tabs match the mockup (CL-9665) --- apps/web/src/bench/drawer.css | 58 +++++++++++++++ apps/web/src/bench/grants-tab.tsx | 97 ++++++++++--------------- apps/web/src/bench/members-tab.tsx | 113 ++++++++++++++++++----------- apps/web/src/bench/tools-tab.tsx | 80 ++++++++++---------- 4 files changed, 210 insertions(+), 138 deletions(-) diff --git a/apps/web/src/bench/drawer.css b/apps/web/src/bench/drawer.css index 8bf1ab329..7ce602fa0 100644 --- a/apps/web/src/bench/drawer.css +++ b/apps/web/src/bench/drawer.css @@ -80,3 +80,61 @@ border-radius: 0; background: none; } + +.drawer-perm, +.drawer-tool { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 0; + border-top: 1px solid var(--line); +} +.drawer-tool { + padding: 8px 0; +} +.drawer-perm-name { + min-width: 0; + overflow: hidden; + font-family: var(--font-mono, "Space Mono", monospace); + font-size: 12.5px; + font-weight: 400; + text-overflow: ellipsis; + white-space: nowrap; +} +.drawer-seg { + display: inline-flex; + flex-shrink: 0; + gap: 2px; + padding: 2px; + border-radius: 999px; + background: var(--surface-sunk); +} +.drawer-seg button { + height: 26px; + padding: 0 10px; + border: 0; + border-radius: 999px; + background: none; + font-size: 12.5px; + font-weight: 600; + color: var(--ink-2); + cursor: pointer; +} +.drawer-seg button.active { + background: var(--surface); + color: var(--ink); + box-shadow: var(--raised, 0 1px 2px rgb(0 0 0 / 12%)); +} +.drawer-seg button:disabled { + cursor: default; +} +.drawer-list-link { + justify-content: space-between; +} +.drawer-list-link > .drawer-list-text { + flex: 1; +} +.drawer-list-text > b { + font-weight: 800; +} diff --git a/apps/web/src/bench/grants-tab.tsx b/apps/web/src/bench/grants-tab.tsx index 2656de2d6..3bfe7fc3c 100644 --- a/apps/web/src/bench/grants-tab.tsx +++ b/apps/web/src/bench/grants-tab.tsx @@ -1,4 +1,3 @@ -import { ConfirmButton } from "@corbits/react-ui"; import { toast } from "@corbits/react-ui/ui/toast"; import type { GrantEffect } from "@intx/types"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; @@ -8,31 +7,23 @@ import { QueryView, describeApiError, toAPIQuery } from "@/lib/api-query"; import { tenantKeys } from "@/query-client"; import { GRANT_RESOURCE_LABEL, type GrantResource } from "../settings/resource-vocabulary"; import { principalLabel } from "../settings/identity"; -import { listGrants, revokeGrant, type Grant } from "../settings/tenancy-api"; +import { listGrants, updateGrant, type Grant } from "../settings/tenancy-api"; import { useGrantNames } from "./grant-names"; +import "./drawer.css"; -const MODE: Record = { - allow: "Always allow", - ask: "Ask first", - deny: "Deny", -}; +const EFFECTS: readonly { readonly effect: GrantEffect; readonly label: string }[] = [ + { effect: "allow", label: "Allow" }, + { effect: "ask", label: "Ask" }, + { effect: "deny", label: "Deny" }, +]; const TOOL_PREFIX = "tool:"; -/** "tool:artifact_link_file" reads "Artifact link file"; other resources keep - * their vocabulary label. */ +/** A tool grant shows the tool's native name; other resources keep their + * vocabulary label. */ function resourceName(resource: string): string { - if (!resource.startsWith(TOOL_PREFIX)) { - return GRANT_RESOURCE_LABEL[resource as GrantResource] ?? resource; - } - const text = resource - .slice(TOOL_PREFIX.length) - .split(/[_.\-\s]+/) - .filter( - (word, index, all) => word !== "" && word.toLowerCase() !== all[index - 1]?.toLowerCase(), - ) - .join(" "); - return text.charAt(0).toUpperCase() + text.slice(1); + if (resource.startsWith(TOOL_PREFIX)) return resource.slice(TOOL_PREFIX.length); + return GRANT_RESOURCE_LABEL[resource as GrantResource] ?? resource; } /** This workbench's own grants; they never inherit from the workspace. */ @@ -50,8 +41,7 @@ export function GrantsTab({ if (known !== undefined) return known.name; if (grant.principalName === undefined || grant.principalName === null) return "Everyone here"; const named = names.replaceTenantIds(grant.principalName); - // A raw run id is never shown, and never turned into words; with no - // resolvable name the row has no subtitle. + // A raw run id is never shown; with no resolvable name the row has no hint. return principalLabel(named).raw === null ? named : undefined; }; const queryClient = useQueryClient(); @@ -61,14 +51,14 @@ export function GrantsTab({ queryFn: () => listGrants(workbenchTenantId, {}), }), ); - const revoke = useMutation({ - mutationFn: (grant: Grant) => revokeGrant(workbenchTenantId, grant.id), - onSuccess: () => { + const setEffect = useMutation({ + mutationFn: (input: { readonly grant: Grant; readonly effect: GrantEffect }) => + updateGrant(workbenchTenantId, input.grant.id, { effect: input.effect }), + onSettled: () => { void queryClient.invalidateQueries({ queryKey: tenantKeys.grants(workbenchTenantId) }); - toast("Grant revoked."); }, onError: (cause: unknown) => { - toast(describeApiError(cause, "revoking this grant")); + toast(describeApiError(cause, "changing this grant")); }, }); @@ -85,40 +75,33 @@ export function GrantsTab({ grants.length === 0 ? (

No grants here yet.

) : ( - + ) } diff --git a/apps/web/src/bench/members-tab.tsx b/apps/web/src/bench/members-tab.tsx index 61621c822..c8905868b 100644 --- a/apps/web/src/bench/members-tab.tsx +++ b/apps/web/src/bench/members-tab.tsx @@ -1,22 +1,74 @@ import { RichEmptyState, Skeleton } from "@corbits/react-ui"; import { useQuery } from "@tanstack/react-query"; -import { IdentityAvatar, WorkbenchAvatar } from "@/chat/avatar"; -import type { WorkbenchParticipant } from "@/chat/threads-api"; +import { WorkbenchAvatar } from "@/chat/avatar"; +import { listChatAgents, type WorkbenchParticipant } from "@/chat/threads-api"; +import { CaretRight } from "@/lib/icons"; import { Link } from "../navigation"; import { tenantKeys } from "../query-client"; import { principalLabel } from "../settings/identity"; import { listPrincipals } from "../settings/tenancy-api"; +import { useWorkerRole } from "../worker-role-query"; +import { useWorkerStatus } from "../worker-status"; +import "./drawer.css"; -/** Humans are the bench tenant's user principals; agents are the bench's - * workers, already listed as participants. */ +function capitalize(text: string): string { + return text.charAt(0).toUpperCase() + text.slice(1); +} + +function WorkerRow({ + workbenchTenantId, + participant, +}: { + readonly workbenchTenantId: string; + readonly participant: WorkbenchParticipant; +}) { + const agents = useQuery({ + queryKey: tenantKeys.agents(workbenchTenantId), + queryFn: () => listChatAgents(workbenchTenantId), + }); + const agent = agents.data?.find((a) => a.id === participant.id); + const status = useWorkerStatus(workbenchTenantId, participant.id); + const body = ( + <> + + + {participant.name} + {agent === undefined ? null : ( + + )} + +