From e6b7f84cd7a24658d8b31ac0b3fe2fae256ceccb Mon Sep 17 00:00:00 2001 From: Sawyer Date: Tue, 29 Sep 2026 22:56:10 -0700 Subject: [PATCH] fix(web): bench drawer tabs and Information match the mockup (CL-9587) Tab strip scrolls the active tab into view; Information is unboxed sections; Grants show names with quiet badges; Members shows a skeleton until the roster resolves. --- apps/web/src/bench/bench-drawer.tsx | 22 +++- apps/web/src/bench/drawer.css | 56 ++++++++ apps/web/src/bench/grants-tab.tsx | 57 ++++++-- apps/web/src/bench/information-tab.tsx | 172 ++++++++++++++++++------- apps/web/src/bench/members-tab.tsx | 8 +- apps/web/src/pages/workbench-page.tsx | 13 +- 6 files changed, 263 insertions(+), 65 deletions(-) create mode 100644 apps/web/src/bench/drawer.css diff --git a/apps/web/src/bench/bench-drawer.tsx b/apps/web/src/bench/bench-drawer.tsx index 6356963f5..eb8e06ad8 100644 --- a/apps/web/src/bench/bench-drawer.tsx +++ b/apps/web/src/bench/bench-drawer.tsx @@ -1,5 +1,6 @@ import { Button } from "@corbits/react-ui"; import { X } from "@/lib/icons"; +import "./drawer.css"; import { useEffect, useRef, useState, type ReactNode } from "react"; export const DRAWER_TABS = [ @@ -26,12 +27,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>; }) { const [tab, setTab] = useState("Information"); const ref = useRef(null); + const stripRef = useRef(null); + + // Never scrollIntoView: it scrolls the clipped work sheet and jerks the view. + useEffect(() => { + const strip = stripRef.current; + const active = strip?.querySelector('[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; @@ -61,13 +77,13 @@ export function BenchDrawer({

{title}

-

{subtitle}

+ {subtitle === undefined || subtitle === "" ? null :

{subtitle}

}
-
+
{DRAWER_TABS.map((name) => (
+ ); } diff --git a/apps/web/src/bench/members-tab.tsx b/apps/web/src/bench/members-tab.tsx index 101428f90..395713098 100644 --- a/apps/web/src/bench/members-tab.tsx +++ b/apps/web/src/bench/members-tab.tsx @@ -13,8 +13,11 @@ import { listPrincipals } from "../settings/tenancy-api"; export function MembersTab({ workbenchTenantId, participants, + loading, }: { readonly workbenchTenantId: string; + /** True until the roster resolves; an empty list then isn't "no agents". */ + readonly loading: boolean; readonly participants: readonly WorkbenchParticipant[]; }) { // A distinct key from Settings -> People, which caches a different shape @@ -60,7 +63,10 @@ export function MembersTab({

Agents

- {agents.length === 0 ?

No agents yet.

: null} + {loading ? : null} + {!loading && agents.length === 0 ? ( +

No agents yet.

+ ) : null}
    {agents.map((agent) => (
  • diff --git a/apps/web/src/pages/workbench-page.tsx b/apps/web/src/pages/workbench-page.tsx index 06b3ae700..ce9fbff89 100644 --- a/apps/web/src/pages/workbench-page.tsx +++ b/apps/web/src/pages/workbench-page.tsx @@ -239,7 +239,6 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string } }); const messages = timeline.data ?? []; - const latestMessage = [...messages].sort((a, b) => Date.parse(b.at) - Date.parse(a.at))[0]; const openedChain = openThread === null ? [] : ancestorChain(messages, openThread); const opened = openedChain.at(-1); const failure: unknown = timeline.error ?? participants.error; @@ -361,24 +360,30 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string } ), Artifacts: , Tools: , - Grants: , + Grants: ( + + ), Insights: , Members: ( ), Workflows: ,