diff --git a/apps/web/src/bench/bench-drawer.tsx b/apps/web/src/bench/bench-drawer.tsx index f4874d107..780572e1b 100644 --- a/apps/web/src/bench/bench-drawer.tsx +++ b/apps/web/src/bench/bench-drawer.tsx @@ -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 = [ @@ -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>; }) { 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; @@ -62,13 +78,13 @@ export function BenchDrawer({

{title}

-

{subtitle}

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

{subtitle}

}
-
+
{DRAWER_TABS.map((name) => (
- + ); } -/** Overview of the bench: latest activity, what's waiting on the person, - * running schedules, and who is in it. */ +/** Overview of the bench: what it is, what's happening now, what's waiting + * on the person, and its live workflows and latest files. */ export function InformationTab({ workbenchTenantId, - latestMessage, + worker, + status, participants, }: { readonly workbenchTenantId: string; - readonly latestMessage: WorkbenchMessage | undefined; + readonly worker: WorkbenchParticipant | undefined; + readonly status: WorkerStatus; readonly participants: readonly WorkbenchParticipant[]; }) { const anyAgentStarting = participants.some((p) => p.kind === "agent" && p.address === ""); @@ -70,34 +97,51 @@ export function InformationTab({ }); const pendingApprovals = approvalsQuery.kind === "ready" ? approvalsQuery.data : null; + const flows = useQuery({ + queryKey: tenantKeys.routines(workbenchTenantId), + queryFn: () => listScheduledWorkflows(workbenchTenantId), + }); + const liveFlows = (flows.data ?? []).filter((flow) => flow.status === "deployed"); + + const artifacts = useAPIQuery( + `/api/tenants/${workbenchTenantId}/artifacts`, + ArtifactListPageSchema, + ); + const latest = + artifacts.kind === "ready" + ? [...artifacts.data.artifacts] + .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)) + .slice(0, 2) + : []; + return ( <> -
-
-

Latest activity

-
- {latestMessage === undefined ? ( -

Nothing yet — say something to get started.

- ) : ( -

- {resolveParticipantName(latestMessage, participants)} ·{" "} - {formatRelativeTime(latestMessage.at)} -

- )} -
+ {worker !== undefined && status.tone === "working" ? ( +
+
+ +
+ {worker.name} + {status.text} +
+
+
+ ) : null} -
-
-

Approvals

-
+
{approvalsQuery.kind === "loading" ? : null} {approvalsQuery.kind === "error" ? (

{approvalsQuery.message}

) : null} {pendingApprovals !== null && pendingApprovals.length === 0 ? ( -

Nothing waiting on you.

+

Nothing is waiting on you.

) : null} {pendingApprovals !== null && pendingApprovals.length > 0 ? (
    @@ -106,29 +150,58 @@ export function InformationTab({ ))}
) : null} -
+
-
-
-

Participants

+
+ {flows.isLoading ? : null} + {flows.isSuccess && liveFlows.length === 0 ? ( +

No workflows are running here.

+ ) : null} +
+ {liveFlows.map((flow) => ( + +
-
    - {participants.map((participant) => ( -
  • - - +
+ +
+ {artifacts.kind === "loading" ? : null} + {artifacts.kind === "ready" && latest.length === 0 ? ( +

Nothing saved here yet.

+ ) : null} +
+ {latest.map((artifact) => ( + + + + + {artifact.title} + {artifact.kind} - {participant.name} - + {formatRelativeTime(artifact.updatedAt)} + ))} - -
+
+
); } 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 91e2cd389..9d4ead8dc 100644 --- a/apps/web/src/pages/workbench-page.tsx +++ b/apps/web/src/pages/workbench-page.tsx @@ -255,7 +255,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; @@ -386,18 +385,25 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string } Information: ( ), Artifacts: , Tools: , - Grants: , + Grants: ( + + ), Insights: , Members: ( ), Workflows: ,