diff --git a/apps/web/src/bench/grants-tab.tsx b/apps/web/src/bench/grants-tab.tsx new file mode 100644 index 000000000..115ff30c2 --- /dev/null +++ b/apps/web/src/bench/grants-tab.tsx @@ -0,0 +1,80 @@ +import { Badge, ConfirmButton, toast } from "@corbits/react-ui"; +import type { GrantEffect } from "@intx/types"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { QueryView, describeApiError, toAPIQuery } from "@/lib/api-query"; +import { tenantKeys } from "@/query-client"; +import { listGrants, revokeGrant, type Grant } from "../settings/tenancy-api"; + +const MODE: Record< + GrantEffect, + { readonly label: string; readonly tone: "success" | "danger" | "info" } +> = { + allow: { label: "Always allow", tone: "success" }, + ask: { label: "Ask", tone: "info" }, + deny: { label: "Deny", tone: "danger" }, +}; + +/** This workbench's own grants; they never inherit from the workspace. */ +export function GrantsTab({ workbenchTenantId }: { readonly workbenchTenantId: string }) { + const queryClient = useQueryClient(); + const query = toAPIQuery( + useQuery({ + queryKey: tenantKeys.grants(workbenchTenantId), + queryFn: () => listGrants(workbenchTenantId, {}), + }), + ); + const revoke = useMutation({ + mutationFn: (grant: Grant) => revokeGrant(workbenchTenantId, grant.id), + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: tenantKeys.grants(workbenchTenantId) }); + toast("Grant revoked."); + }, + onError: (cause: unknown) => { + toast(describeApiError(cause, "revoking this grant")); + }, + }); + + return ( +
+
+

What workers may do here

+
+

+ Set per workbench. Never inherited from the workspace. +

+ + {(grants) => + grants.length === 0 ? ( +

No grants here yet.

+ ) : ( +
    + {grants.map((grant) => ( +
  • + + {grant.action} + + {grant.resource} · {grant.roleName ?? grant.principalName ?? "—"} + + + {MODE[grant.effect].label} + { + revoke.mutate(grant); + }} + > + Revoke + +
  • + ))} +
+ ) + } +
+
+ ); +} diff --git a/apps/web/src/bench/styles.css b/apps/web/src/bench/styles.css index e8bf9ecf1..079dd1686 100644 --- a/apps/web/src/bench/styles.css +++ b/apps/web/src/bench/styles.css @@ -1,3 +1,45 @@ /* Reserved stylesheet entry for `@corbits/bench-ui/styles.css`. The switcher / create / members layout glue that lived here was deleted with those unused components; apps/web still imports this path. */ + +.bench-tab-list { + display: flex; + flex-direction: column; + margin: 0; + padding: 0; + list-style: none; +} +.bench-tab-row { + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.6rem 0; + border-top: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent); +} +.bench-tab-text { + display: flex; + flex: 1; + flex-direction: column; + min-width: 0; +} +.bench-tab-text > * { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.bench-tab-tile { + display: grid; + place-items: center; + width: 2rem; + height: 2rem; + background: var(--muted); + color: var(--muted-foreground); +} +.bench-tab-mono { + font-family: var(--font-mono, "Space Mono", monospace); + font-size: 0.8rem; +} +.bench-tab-link { + font-size: 0.8rem; + text-decoration: underline; +} diff --git a/apps/web/src/bench/tools-tab.tsx b/apps/web/src/bench/tools-tab.tsx new file mode 100644 index 000000000..566ae9f64 --- /dev/null +++ b/apps/web/src/bench/tools-tab.tsx @@ -0,0 +1,53 @@ +import { Badge, Button } from "@corbits/react-ui"; + +import { QueryView } from "@/lib/api-query"; +import { Plugs } from "@/lib/icons"; +import { Link } from "../navigation"; +import { useMcpServers } from "../tools/mcp-servers-query"; + +/** Tools the workspace catalog makes available here; "Connected" once an + * agent in this workbench binds the server. */ +export function ToolsTab({ workbenchTenantId }: { readonly workbenchTenantId: string }) { + const query = useMcpServers(workbenchTenantId); + const manage = `/tools?from=${encodeURIComponent(workbenchTenantId)}`; + + return ( +
+
+

Tools

+
+ + {(servers) => + servers.length === 0 ? ( + <> +

No tools are connected yet.

+ + + ) : ( +
    + {servers.map((server) => ( +
  • + + + {server.name} + {server.url} + + 0 ? "success" : "neutral"}> + {server.agentNames.length > 0 ? "Connected" : "Available"} + + + Manage + +
  • + ))} +
+ ) + } +
+
+ ); +} diff --git a/apps/web/src/pages/workbench-page.tsx b/apps/web/src/pages/workbench-page.tsx index d3a493660..c33eeb8be 100644 --- a/apps/web/src/pages/workbench-page.tsx +++ b/apps/web/src/pages/workbench-page.tsx @@ -25,7 +25,9 @@ import { } from "@/chat/threads-api"; import { BenchDrawer } from "../bench/bench-drawer"; import { BenchPill } from "../bench/bench-pill"; +import { GrantsTab } from "../bench/grants-tab"; import { InformationTab } from "../bench/information-tab"; +import { ToolsTab } from "../bench/tools-tab"; import { useBench } from "../bench-context"; import { createFetchStockHub } from "../needs-converge"; import { workbenchKeys } from "../chat-path"; @@ -312,6 +314,8 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string } participants={participants.data ?? []} /> ), + Tools: , + Grants: , }} />