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 (
+
+ );
+}
+
/** 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 (