diff --git a/apps/web/src/bench/bench-pill.tsx b/apps/web/src/bench/bench-pill.tsx index aae462f2d..fe309c7aa 100644 --- a/apps/web/src/bench/bench-pill.tsx +++ b/apps/web/src/bench/bench-pill.tsx @@ -7,8 +7,8 @@ function statusLine(status: WorkerStatus, workerName: string | undefined): strin if (status.text === "Working…") { return workerName === undefined ? "Working…" : `${workerName} is working…`; } - if (status.text === "Live") return "Idle"; - return status.text; + const text = status.text === "Live" ? "Idle" : status.text; + return workerName === undefined ? text : `${workerName} · ${text}`; } /** Frosted pill centered over the thread: the bench's worker, its name and diff --git a/apps/web/src/bench/description.css b/apps/web/src/bench/description.css index 435a5302e..05df3fa65 100644 --- a/apps/web/src/bench/description.css +++ b/apps/web/src/bench/description.css @@ -8,7 +8,8 @@ flex-direction: column; gap: 8px; } -.bench-description-form textarea { +.bench-description-form textarea, +.bench-description-form input { width: 100%; min-height: 72px; resize: vertical; @@ -30,3 +31,10 @@ font-size: 0.75rem; color: var(--muted-foreground); } +.bench-description-form input { + min-height: 0; +} +.bench-description-form[data-inline] { + flex-direction: row; + align-items: center; +} diff --git a/apps/web/src/bench/description.ts b/apps/web/src/bench/description.ts index fdb8a9135..b26302b40 100644 --- a/apps/web/src/bench/description.ts +++ b/apps/web/src/bench/description.ts @@ -1,8 +1,8 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { type } from "arktype"; -import { ApiQueryError } from "@/lib/api-query"; -import { TenantDetailSchema, fetchTenantDetail, type TenantDetail } from "../api"; +import { fetchTenantDetail, type TenantDetail } from "../api"; +import { patchTenantConfigKey } from "./tenant-config"; export const DESCRIPTION_MAX = 280; @@ -24,24 +24,7 @@ export function useBenchDescription(tenantId: string) { queryFn: () => fetchTenantDetail(tenantId), }); const save = useMutation({ - mutationFn: async (text: string) => { - // Read-modify-write on fresh config so other keys are never dropped. - const current = await fetchTenantDetail(tenantId); - const path = `/api/tenants/${encodeURIComponent(tenantId)}`; - const response = await fetch(path, { - method: "PATCH", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ config: { ...current.config, "workbench.description": text } }), - }); - if (!response.ok) { - throw new ApiQueryError(`The server answered ${response.status}.`, response.status, path); - } - const parsed = TenantDetailSchema(await response.json()); - if (parsed instanceof type.errors) { - throw new ApiQueryError(`Unexpected tenant response shape: ${parsed.summary}`); - } - return parsed; - }, + mutationFn: (text: string) => patchTenantConfigKey(tenantId, "workbench.description", text), onSuccess: (tenant) => queryClient.setQueryData(descriptionKey(tenantId), tenant), }); return { diff --git a/apps/web/src/bench/information-tab.tsx b/apps/web/src/bench/information-tab.tsx index b719e6f60..bda65bb1a 100644 --- a/apps/web/src/bench/information-tab.tsx +++ b/apps/web/src/bench/information-tab.tsx @@ -21,6 +21,7 @@ import { benchLink } from "../shell/page-crumbs"; import type { WorkerStatus } from "../worker-status"; import "./description.css"; import { DESCRIPTION_MAX, useBenchDescription } from "./description"; +import { DEFAULT_WORKER_NAME, WORKER_NAME_MAX, useWorkerName } from "./worker-name"; function Section({ title, @@ -79,6 +80,39 @@ function AboutSection({ workbenchTenantId }: { readonly workbenchTenantId: strin ); } +function WorkerNameSection({ workbenchTenantId }: { readonly workbenchTenantId: string }) { + const { name, save } = useWorkerName(workbenchTenantId); + const current = name ?? DEFAULT_WORKER_NAME; + const [draft, setDraft] = useState(null); + const value = draft ?? current; + const dirty = draft !== null && draft.trim() !== current; + return ( +
+
{ + event.preventDefault(); + save.mutate(value.trim(), { + onSuccess: () => setDraft(null), + onError: (cause) => toast(cause instanceof Error ? cause.message : String(cause)), + }); + }} + > + setDraft(event.target.value)} + /> + +
+
+ ); +} + /** 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({ @@ -118,6 +152,7 @@ export function InformationTab({ return ( <> + {worker !== undefined && status.tone === "working" ? (
diff --git a/apps/web/src/bench/members-tab.tsx b/apps/web/src/bench/members-tab.tsx index fffec23ee..61621c822 100644 --- a/apps/web/src/bench/members-tab.tsx +++ b/apps/web/src/bench/members-tab.tsx @@ -5,7 +5,6 @@ import { IdentityAvatar, WorkbenchAvatar } from "@/chat/avatar"; import type { WorkbenchParticipant } from "@/chat/threads-api"; import { Link } from "../navigation"; import { tenantKeys } from "../query-client"; -import { useWorkerBenches } from "../worker-benches"; import { principalLabel } from "../settings/identity"; import { listPrincipals } from "../settings/tenancy-api"; @@ -27,7 +26,6 @@ export function MembersTab({ queryKey: [...tenantKeys.principals(workbenchTenantId), "members"], queryFn: async () => (await listPrincipals(workbenchTenantId)).filter((p) => p.kind === "user"), }); - const { workerIdForAsset } = useWorkerBenches(); const agents = participants.filter((p) => p.kind === "agent"); return ( @@ -75,8 +73,7 @@ export function MembersTab({ ) : null}
    {agents.map((agent) => { - const workerId = - agent.assetName === undefined ? undefined : workerIdForAsset(agent.assetName); + const workerId = agent.assetName === undefined ? undefined : agent.id; const body = ( <> diff --git a/apps/web/src/bench/pending-opening-message.tsx b/apps/web/src/bench/pending-opening-message.tsx index 728dfedf6..b96021f5a 100644 --- a/apps/web/src/bench/pending-opening-message.tsx +++ b/apps/web/src/bench/pending-opening-message.tsx @@ -31,7 +31,12 @@ export function PendingOpeningMessage({ }, []); const send = useMutation({ - mutationFn: (content: string) => sendToWorkbench({ workbenchTenantId, participants, content }), + mutationFn: (content: string) => + sendToWorkbench({ + workbenchTenantId, + participants, + content, + }), onSuccess: () => { clearOpeningMessage(workbenchTenantId); setText(null); diff --git a/apps/web/src/bench/tenant-config.ts b/apps/web/src/bench/tenant-config.ts new file mode 100644 index 000000000..46898fbce --- /dev/null +++ b/apps/web/src/bench/tenant-config.ts @@ -0,0 +1,28 @@ +import { type } from "arktype"; + +import { ApiQueryError } from "@/lib/api-query"; +import { TenantDetailSchema, fetchTenantDetail, type TenantDetail } from "../api"; + +/** Sets one key of the stock tenant config. Read-modify-write on fresh config + * so other keys are never dropped. */ +export async function patchTenantConfigKey( + tenantId: string, + key: string, + value: string, +): Promise { + const current = await fetchTenantDetail(tenantId); + const path = `/api/tenants/${encodeURIComponent(tenantId)}`; + const response = await fetch(path, { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ config: { ...current.config, [key]: value } }), + }); + if (!response.ok) { + throw new ApiQueryError(`The server answered ${response.status}.`, response.status, path); + } + const parsed = TenantDetailSchema(await response.json()); + if (parsed instanceof type.errors) { + throw new ApiQueryError(`Unexpected tenant response shape: ${parsed.summary}`); + } + return parsed; +} diff --git a/apps/web/src/bench/worker-name.ts b/apps/web/src/bench/worker-name.ts new file mode 100644 index 000000000..89714989b --- /dev/null +++ b/apps/web/src/bench/worker-name.ts @@ -0,0 +1,111 @@ +// The worker's name is bench state in the tenant config, never a redeploy: +// the worker states it on its first reply and the person can rename it. + +import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; +import { type } from "arktype"; + +import type { WorkbenchMessage, WorkbenchParticipant } from "@/chat/threads-api"; +import { sameAddress } from "@/chat/threads-api"; +import { fetchTenantDetail, type TenantDetail } from "../api"; +import { patchTenantConfigKey } from "./tenant-config"; + +export const DEFAULT_WORKER_NAME = "New worker"; +export const WORKER_NAME_MAX = 24; +const CONFIG_KEY = "workbench.worker.name"; + +const NameConfig = type({ "workbench.worker.name?": "string" }); + +const NAME_PATTERN = /^[\p{L} -]+$/u; + +/** A name the bench accepts: trimmed, short, letters, spaces and hyphens. */ +export function validWorkerName(raw: string): string | undefined { + const name = raw.trim(); + return name.length > 0 && name.length <= WORKER_NAME_MAX && NAME_PATTERN.test(name) + ? name + : undefined; +} + +function readWorkerName(config: TenantDetail["config"]): string | undefined { + const parsed = NameConfig(config ?? {}); + if (parsed instanceof type.errors) return undefined; + const stored = parsed["workbench.worker.name"]; + return stored === undefined ? undefined : validWorkerName(stored); +} + +const nameKey = (tenantId: string) => ["workbench", tenantId, "worker-name"] as const; + +const nameQuery = (tenantId: string) => ({ + queryKey: nameKey(tenantId), + queryFn: async () => readWorkerName((await fetchTenantDetail(tenantId)).config) ?? null, +}); + +const NAME_LINE = /\n[ \t]*Name:[ \t]*([^\n]+?)[ \t]*$/i; + +/** The name a worker message ends with, when it ends with a valid + * `Name: ` line. */ +export function nameFromMessage(body: string): string | undefined { + const match = NAME_LINE.exec(body.trimEnd()); + return match?.[1] === undefined ? undefined : validWorkerName(match[1]); +} + +/** The message as rendered: a valid trailing `Name:` line is never shown. */ +export function stripNameLine(body: string): string { + const trimmed = body.trimEnd(); + return nameFromMessage(trimmed) === undefined ? body : trimmed.replace(NAME_LINE, ""); +} + +/** The stored name, or undefined until one exists; `save` renames. */ +export function useWorkerName(tenantId: string) { + const queryClient = useQueryClient(); + const query = useQuery(nameQuery(tenantId)); + const save = useMutation({ + mutationFn: (name: string) => setWorkerName(tenantId, name), + onSuccess: (name) => queryClient.setQueryData(nameKey(tenantId), name), + }); + return { + name: query.data ?? undefined, + loaded: query.isSuccess, + save, + }; +} + +export async function setWorkerName(tenantId: string, name: string): Promise { + const valid = validWorkerName(name); + if (valid === undefined) { + throw new Error("Use up to 24 letters, spaces or hyphens."); + } + await patchTenantConfigKey(tenantId, CONFIG_KEY, valid); + return valid; +} + +/** Stored names of many benches, by bench id; a bench with none reads as + * `DEFAULT_WORKER_NAME`. */ +export function useWorkerNames(tenantIds: readonly string[]): ReadonlyMap { + const results = useQueries({ queries: tenantIds.map(nameQuery) }); + return new Map(tenantIds.map((id, i) => [id, results[i]?.data ?? DEFAULT_WORKER_NAME])); +} + +/** Participants with the bench's worker shown under its stored name. */ +export function nameWorkers( + participants: readonly WorkbenchParticipant[], + name: string | undefined, +): readonly WorkbenchParticipant[] { + return participants.map((p) => + p.kind === "agent" ? { ...p, name: name ?? DEFAULT_WORKER_NAME } : p, + ); +} + +/** The first worker message that states a name, for a bench with none. */ +export function firstStatedName( + messages: readonly WorkbenchMessage[], + participants: readonly WorkbenchParticipant[], +): string | undefined { + for (const message of messages) { + if (message.author !== "other") continue; + const sender = participants.find((p) => sameAddress(p.address, message.address)); + if (sender !== undefined && sender.kind !== "agent") continue; + const name = nameFromMessage(message.body); + if (name !== undefined) return name; + } + return undefined; +} diff --git a/apps/web/src/chat/threads-api.ts b/apps/web/src/chat/threads-api.ts index 514c284a3..5206b97cb 100644 --- a/apps/web/src/chat/threads-api.ts +++ b/apps/web/src/chat/threads-api.ts @@ -512,7 +512,11 @@ export async function sendToWorkbench(input: { address: person.address, kind: "person" as const, })), - ...live.map((agent) => ({ name: agent.name, address: agent.address, kind: "agent" as const })), + ...live.map((agent) => ({ + name: agent.name, + address: agent.address, + kind: "agent" as const, + })), ]); let response: Response; try { diff --git a/apps/web/src/pages/workbench-page.tsx b/apps/web/src/pages/workbench-page.tsx index faebfff9d..d54082498 100644 --- a/apps/web/src/pages/workbench-page.tsx +++ b/apps/web/src/pages/workbench-page.tsx @@ -33,6 +33,7 @@ import { ArtifactsTab } from "../bench/artifacts-tab"; import { markTurnPending, useWorkerStatus } from "../worker-status"; import { GrantsTab } from "../bench/grants-tab"; import { useBenchDescription } from "../bench/description"; +import { firstStatedName, nameWorkers, stripNameLine, useWorkerName } from "../bench/worker-name"; import { InformationTab } from "../bench/information-tab"; import { InsightsTab } from "../bench/insights-tab"; import { MembersTab } from "../bench/members-tab"; @@ -92,7 +93,12 @@ function WorkbenchMessageRow({ // The person's own send carries a trailing roster block so agents in the // workbench can hand off to each other; it's never something a person should // see echoed back at them. - const body = message.author === "me" ? stripRoster(message.body) : message.body; + const body = + message.author === "me" + ? stripRoster(message.body) + : resolved?.kind === "agent" + ? stripNameLine(message.body) + : message.body; const { pkg, renderedBody } = resolveMessagePackage(message.attachments, body); const own = message.author === "me"; const time = new Date(message.at); @@ -238,7 +244,23 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string } [workbenchTenantId, queryClient], ); - const agents = (participants.data ?? []).filter((participant) => participant.kind === "agent"); + const workerName = useWorkerName(workbenchTenantId); + const roster = nameWorkers(participants.data ?? [], workerName.name); + const messages = timeline.data ?? []; + // The worker's first reply names it; the stored name wins from then on, and + // a bench that already has one is never overwritten. + const statedName = + workerName.loaded && workerName.name === undefined + ? firstStatedName(messages, roster) + : undefined; + const saveName = workerName.save; + useEffect(() => { + if (statedName !== undefined && !saveName.isPending && !saveName.isSuccess) { + saveName.mutate(statedName); + } + }, [statedName, saveName]); + + const agents = roster.filter((participant) => participant.kind === "agent"); // Released by a hub restart: the asset is still here but nothing is live. const releasedAgents = agents.filter( (agent): agent is WorkbenchParticipant & { assetName: string } => @@ -260,7 +282,7 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string } }) => sendToWorkbench({ workbenchTenantId, - participants: participants.data ?? [], + participants: roster, content, ...(inReplyTo !== undefined ? { inReplyTo } : {}), }), @@ -272,7 +294,6 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string } onError: () => queryClient.setQueryData(workbenchKeys.pendingTurn(workbenchTenantId), null), }); - const messages = timeline.data ?? []; const openedChain = openThread === null ? [] : ancestorChain(messages, openThread); const opened = openedChain.at(-1); const failure: unknown = timeline.error ?? participants.error; @@ -316,7 +337,7 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string } setOpenThread(target.messageId)} @@ -324,7 +345,7 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string } ))} {workerStatus.tone === "working" ? : null} @@ -367,7 +388,7 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string } @@ -406,22 +427,17 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string } workbenchTenantId={workbenchTenantId} worker={agents[0]} status={workerStatus} - participants={participants.data ?? []} + participants={roster} /> ), Artifacts: , Tools: , - Grants: ( - - ), + Grants: , Insights: , Members: ( ), diff --git a/apps/web/src/pages/worker-page.tsx b/apps/web/src/pages/worker-page.tsx index 08760825d..82b6fe759 100644 --- a/apps/web/src/pages/worker-page.tsx +++ b/apps/web/src/pages/worker-page.tsx @@ -2,16 +2,16 @@ // and the workbench it lives on. import { useState } from "react"; -import { Button, Card, CardDescription, CardTitle, PageShell } from "@corbits/react-ui"; +import { Button, Card, CardDescription, CardTitle, PageShell, Skeleton } from "@corbits/react-ui"; import { toast } from "@corbits/react-ui/ui/toast"; import { reportError } from "@corbits/error-sink"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { GrantEffect } from "@intx/types"; import { WorkbenchAvatar } from "@/chat/avatar"; -import { isDefaultWorker, listChatAgents, type ChatAgent } from "@/chat/threads-api"; +import { isDefaultWorker, type ChatAgent } from "@/chat/threads-api"; import { ChatCircle } from "@/lib/icons"; -import { QueryView, describeApiError } from "@/lib/api-query"; +import { describeApiError } from "@/lib/api-query"; import { listTopLevelRuns } from "../agents-api"; import { updateGrant, type Grant } from "../settings/tenancy-api"; import { agentSlugFromSourceAssetName, deployAgentSource } from "../agent-deploy"; @@ -23,14 +23,12 @@ import { toolEffectsToCarry, workerToolGrantsKey, } from "../worker-tool-grants"; -import { useBench } from "../bench-context"; import { Link } from "../navigation"; import { tenantKeys } from "../query-client"; -import { useTenantQuery } from "../routines-api"; import { StageTopBar } from "../shell/stage-top-bar"; import { WORKERS_PATH_PREFIX } from "../path-ids"; import { workbenchPath } from "../workbench-path"; -import { useWorkerBenches } from "../worker-benches"; +import { useBenchWorkers } from "../worker-benches"; import { StatusPill, WorkerRole, workerStatus } from "./workers-page"; function sourceKey(tenantId: string, agentId: string) { @@ -290,19 +288,21 @@ function DetailsCard({ } export function WorkerRoute({ agentId }: { readonly agentId: string }) { - const { selectedTenantId } = useBench(); - const agentsQuery = useTenantQuery( - tenantKeys.agents(selectedTenantId ?? "none"), - selectedTenantId !== null, - () => listChatAgents(selectedTenantId as string), - ); - const { byWorker } = useWorkerBenches(); + const { workers, loading, error } = useBenchWorkers(); + const found = workers.find((candidate) => candidate.agent.id === agentId); return (
    - - {(agents) => { - const agent = agents.find((candidate) => candidate.id === agentId); + {error !== undefined ? ( +

    + {error} +

    + ) : loading ? ( + + ) : ( + (() => { + const agent = found?.agent; + const selectedTenantId = found?.bench.id ?? null; if (agent === undefined || selectedTenantId === null) { return ( <> @@ -316,7 +316,7 @@ export function WorkerRoute({ agentId }: { readonly agentId: string }) { ); } const status = workerStatus(agent); - const agentBenches = byWorker.get(agent.id) ?? []; + const agentBenches = found === undefined ? [] : [found.bench]; const bench = agentBenches[0]; return ( <> @@ -387,8 +387,8 @@ export function WorkerRoute({ agentId }: { readonly agentId: string }) {
    ); - }} - + })() + )} ); } diff --git a/apps/web/src/pages/workers-page.tsx b/apps/web/src/pages/workers-page.tsx index 31786972d..636fbf87a 100644 --- a/apps/web/src/pages/workers-page.tsx +++ b/apps/web/src/pages/workers-page.tsx @@ -2,21 +2,19 @@ // the sidebar's "+" menu. import { useState } from "react"; -import { Button, RichEmptyState } from "@corbits/react-ui"; +import { Button, RichEmptyState, Skeleton } from "@corbits/react-ui"; import { toast } from "@corbits/react-ui/ui/toast"; import { Plus, Robot } from "@/lib/icons"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { reportError } from "@corbits/error-sink"; -import { QueryView } from "@/lib/api-query"; import { tenantKeys } from "../query-client"; -import { isAgentNotRunning, listChatAgents, type ChatAgent } from "@/chat/threads-api"; +import { isAgentNotRunning, type ChatAgent } from "@/chat/threads-api"; import { WorkbenchAvatar } from "@/chat/avatar"; import { describeRestartFailure, redeployWorkbenchAgent } from "../workbench-create"; import { useBench } from "../bench-context"; -import { useWorkerBenches } from "../worker-benches"; +import { useBenchWorkers, type BenchWorker } from "../worker-benches"; import { Link } from "../navigation"; -import { useTenantQuery } from "../routines-api"; import { PageLayout } from "../shell/page-layout"; import { StageTopBar } from "../shell/stage-top-bar"; import { CreateAgentPanel } from "./create-agent-panel"; @@ -82,30 +80,23 @@ const FILTERS: readonly (readonly [WorkerTone | "all", string])[] = [ const ROW_GRID = "grid items-center gap-4 md:grid-cols-[minmax(200px,1.2fr)_minmax(220px,1.6fr)_minmax(120px,1fr)_72px]"; -export function WorkersRosterList({ - tenantId, - agents, -}: { - readonly tenantId: string; - readonly agents: readonly ChatAgent[]; -}) { - const { byWorker } = useWorkerBenches(); +export function WorkersRosterList({ workers }: { readonly workers: readonly BenchWorker[] }) { const [filter, setFilter] = useState("all"); const [query, setQuery] = useState(""); const queryClient = useQueryClient(); const restart = useMutation({ - mutationFn: (agent: ChatAgent) => redeployWorkbenchAgent(tenantId, agent), - onSuccess: () => { + mutationFn: ({ agent, bench }: BenchWorker) => redeployWorkbenchAgent(bench.id, agent), + onSuccess: (_result, { bench }) => { void queryClient.invalidateQueries({ - queryKey: tenantKeys.agents(tenantId), + queryKey: tenantKeys.agents(bench.id), }); }, - onError: (cause) => { - reportError(cause, { operation: "agent_restart", tenantId }); + onError: (cause, { bench }) => { + reportError(cause, { operation: "agent_restart", tenantId: bench.id }); toast(describeRestartFailure(cause)); }, }); - if (agents.length === 0) { + if (workers.length === 0) { return ( } @@ -114,7 +105,7 @@ export function WorkersRosterList({ /> ); } - const rows = agents.map((agent) => ({ agent, status: workerStatus(agent) })); + const rows = workers.map((worker) => ({ ...worker, status: workerStatus(worker.agent) })); const needle = query.trim().toLowerCase(); const shown = rows.filter( ({ agent, status }) => @@ -166,7 +157,7 @@ export function WorkersRosterList({

    ) : (
      - {shown.map(({ agent, status }) => ( + {shown.map(({ agent, bench, status }) => (
    • {agent.name} @@ -193,20 +184,16 @@ export function WorkersRosterList({ size="sm" className="relative" disabled={restart.isPending} - onClick={() => restart.mutate(agent)} + onClick={() => restart.mutate({ agent, bench })} > - {restart.isPending && restart.variables?.id === agent.id + {restart.isPending && restart.variables?.agent.id === agent.id ? "Starting…" : "Restart"} ) : null} - {(byWorker.get(agent.id) ?? []).map((bench) => ( - - {bench.name} - - ))} + {bench.name} {agent.liveAddress === null ? "" : "Now"} @@ -221,14 +208,7 @@ export function WorkersRosterList({ export function WorkersRoute() { const { selectedTenantId } = useBench(); - const agentsQuery = useTenantQuery( - tenantKeys.agents(selectedTenantId ?? "none"), - selectedTenantId !== null, - () => listChatAgents(selectedTenantId as string), - // Keep polling while any worker is not live, so a restart landing is - // seen without a reload. - (agents) => ((agents?.some((agent) => agent.liveAddress === null) ?? false) ? 3000 : false), - ); + const { workers, loading, error } = useBenchWorkers(); const queryClient = useQueryClient(); const [createOpen, setCreateOpen] = useState(false); @@ -255,9 +235,15 @@ export function WorkersRoute() { /> ) : ( <> - - {(agents) => } - + {error !== undefined ? ( +

      + {error} +

      + ) : loading ? ( + + ) : ( + + )} void; }) { - // The bench's copy of a workspace worker shares its deploy asset name. - const status = useBenchWorkerStatus(benches, (p) => p.assetName === agent.assetName); + const { agent, bench } = worker; + const status = useBenchWorkerStatus([bench], (p) => p.id === agent.id); return (