Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions apps/web/src/bench/bench-drawer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Button } from "@corbits/react-ui";
import "./drawer-narrow.css";
import { X } from "@/lib/icons";
import "./drawer.css";
import { useEffect, useRef, useState, type ReactNode } from "react";

export const DRAWER_TABS = [
Expand All @@ -27,12 +28,27 @@ export function BenchDrawer({
}: {
readonly open: boolean;
readonly title: string;
readonly subtitle: string;
/** The bench description; nothing renders without one. */
readonly subtitle?: string | undefined;
readonly onClose: () => void;
readonly tabs: Partial<Record<DrawerTab, ReactNode>>;
}) {
const [tab, setTab] = useState<DrawerTab>("Information");
const ref = useRef<HTMLDivElement>(null);
const stripRef = useRef<HTMLDivElement>(null);

// Never scrollIntoView: it scrolls the clipped work sheet and jerks the view.
useEffect(() => {
const strip = stripRef.current;
const active = strip?.querySelector<HTMLElement>('[aria-selected="true"]');
if (strip === null || strip === undefined || active === null || active === undefined) return;
if (
active.offsetLeft < strip.scrollLeft ||
active.offsetLeft + active.offsetWidth > strip.scrollLeft + strip.clientWidth
) {
strip.scrollLeft = active.offsetLeft - 16;
}
}, [tab, open]);

useEffect(() => {
if (!open) return;
Expand Down Expand Up @@ -62,13 +78,13 @@ export function BenchDrawer({
<div className="drawer-head">
<div>
<h2>{title}</h2>
<p>{subtitle}</p>
{subtitle === undefined || subtitle === "" ? null : <p>{subtitle}</p>}
</div>
<Button variant="ghost" size="sm" aria-label="Close drawer" onClick={onClose}>
<X size={16} aria-hidden="true" />
</Button>
</div>
<div className="drawer-tabs" role="tablist">
<div className="drawer-tabs" role="tablist" ref={stripRef}>
{DRAWER_TABS.map((name) => (
<button
key={name}
Expand Down
56 changes: 56 additions & 0 deletions apps/web/src/bench/drawer.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* Drawer chrome and tab-body sections: unboxed, per the mockup. */

.drawer .drawer-tabs {
-webkit-mask: linear-gradient(90deg, #000 calc(100% - 24px), transparent);
mask: linear-gradient(90deg, #000 calc(100% - 24px), transparent);
}
.drawer .drawer-tabs button:last-child {
margin-right: 24px;
}

.drawer-sec {
margin-bottom: 24px;
}
.drawer .workbench-info-panel {
padding: 0;
border: 0;
background: none;
margin: 0 0 24px;
}
.drawer .workbench-info-panel-header {
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.drawer .workbench-info-panel-header h2 {
font-size: 12.5px;
font-weight: 800;
color: var(--ink-2);
}
.drawer-about {
margin: 0;
font-size: 13.5px;
color: var(--ink-2);
}
.drawer-now {
display: flex;
align-items: center;
gap: 10px;
}
.drawer-now b,
.drawer-now span {
display: block;
}
.drawer-now span {
font-size: 12px;
color: var(--ink-3);
}
.drawer-grant-mode {
font-size: 12px;
font-weight: 700;
color: var(--ink-3);
white-space: nowrap;
}
.drawer-grant-mode[data-effect="deny"] {
color: var(--ink-2);
}
57 changes: 44 additions & 13 deletions apps/web/src/bench/grants-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,51 @@
import { Badge, ConfirmButton, toast } from "@corbits/react-ui";
import { ConfirmButton, toast } from "@corbits/react-ui";
import type { GrantEffect } from "@intx/types";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";

import type { WorkbenchParticipant } from "@/chat/threads-api";
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";

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" },
const MODE: Record<GrantEffect, string> = {
allow: "Always allow",
ask: "Ask first",
deny: "Deny",
};

const TOOL_PREFIX = "tool:";

/** "tool:artifact_link_file" reads "Artifact link file"; 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 words = resource
.slice(TOOL_PREFIX.length)
.replace(/[_.-]+/g, " ")
.trim();
return words.charAt(0).toUpperCase() + words.slice(1);
}

/** This workbench's own grants; they never inherit from the workspace. */
export function GrantsTab({ workbenchTenantId }: { readonly workbenchTenantId: string }) {
export function GrantsTab({
workbenchTenantId,
participants,
}: {
readonly workbenchTenantId: string;
readonly participants: readonly WorkbenchParticipant[];
}) {
const who = (grant: Grant): string => {
if (grant.roleName !== undefined && grant.roleName !== null) return grant.roleName;
const known = participants.find((p) => p.id === grant.principalId);
if (known !== undefined) return known.name;
return grant.principalName === undefined || grant.principalName === null
? "Everyone here"
: principalLabel(grant.principalName).label;
};
const queryClient = useQueryClient();
const query = toAPIQuery<readonly Grant[]>(
useQuery({
Expand Down Expand Up @@ -52,12 +81,14 @@ export function GrantsTab({ workbenchTenantId }: { readonly workbenchTenantId: s
{grants.map((grant) => (
<li key={grant.id} className="bench-tab-row">
<span className="bench-tab-text">
<span className="bench-tab-mono">{grant.action}</span>
<span className="workbench-info-cell-context">
{grant.resource} · {grant.roleName ?? grant.principalName ?? "—"}
<span className="workbench-info-cell-primary" title={grant.resource}>
{resourceName(grant.resource)}
</span>
<span className="workbench-info-cell-context">{who(grant)}</span>
</span>
<span className="drawer-grant-mode" data-effect={grant.effect}>
{MODE[grant.effect]}
</span>
<Badge tone={MODE[grant.effect].tone}>{MODE[grant.effect].label}</Badge>
<ConfirmButton
variant="ghost"
size="sm"
Expand Down
Loading
Loading