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
30 changes: 24 additions & 6 deletions apps/web/src/bench/members-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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";

Expand All @@ -26,6 +27,7 @@ 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 (
Expand Down Expand Up @@ -72,17 +74,33 @@ export function MembersTab({
<p className="workbench-info-empty-note">No agents yet.</p>
) : null}
<ul className="drawer-list">
{agents.map((agent) => (
<li key={agent.id}>
<Link className="drawer-list-link" to={`/workers/${encodeURIComponent(agent.id)}`}>
{agents.map((agent) => {
const workerId =
agent.assetName === undefined ? undefined : workerIdForAsset(agent.assetName);
const body = (
<>
<WorkbenchAvatar kind="worker" name={agent.name} size="md" />
<span className="drawer-list-text">
<b>{agent.name}</b>
<span>{agent.address === "" ? "Starting" : "Running"}</span>
</span>
</Link>
</li>
))}
</>
);
return (
<li key={agent.id}>
{workerId === undefined ? (
body
) : (
<Link
className="drawer-list-link"
to={`/workers/${encodeURIComponent(workerId)}`}
>
{body}
</Link>
)}
</li>
);
})}
</ul>
</section>
</div>
Expand Down
20 changes: 12 additions & 8 deletions apps/web/src/pages/worker-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ 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 { StatusPill, useWorkbenchList, workerStatus } from "./workers-page";
import { useWorkerBenches } from "../worker-benches";
import { StatusPill, workerStatus } from "./workers-page";

function sourceKey(tenantId: string, agentId: string) {
return [...tenantKeys.agents(tenantId), "source", agentId] as const;
Expand Down Expand Up @@ -307,8 +308,7 @@ export function WorkerRoute({ agentId }: { readonly agentId: string }) {
selectedTenantId !== null,
() => listChatAgents(selectedTenantId as string),
);
const benches = useWorkbenchList(selectedTenantId);
const bench = benches.data?.[0];
const { byWorker } = useWorkerBenches();

return (
<div className="flex h-full min-h-0 flex-col">
Expand All @@ -328,6 +328,8 @@ export function WorkerRoute({ agentId }: { readonly agentId: string }) {
);
}
const status = workerStatus(agent);
const agentBenches = byWorker.get(agent.id) ?? [];
const bench = agentBenches[0];
return (
<>
<StageTopBar
Expand Down Expand Up @@ -359,7 +361,7 @@ export function WorkerRoute({ agentId }: { readonly agentId: string }) {
className="inline-flex h-9 items-center gap-2 rounded-(--r-md) bg-(--primary) px-4 text-[14px] font-bold text-(--primary-foreground)"
>
<ChatCircle />
Open {bench.title}
Open {bench.name}
</Link>
)}
</div>
Expand All @@ -372,16 +374,18 @@ export function WorkerRoute({ agentId }: { readonly agentId: string }) {
<DetailsCard tenantId={selectedTenantId} agent={agent} />
<Card className="p-5">
<CardTitle className="text-[14px]">Workbench</CardTitle>
{benches.data === undefined || benches.data.length === 0 ? (
<p className="mt-2 text-[13.5px] text-(--ink-3)">No workbenches yet.</p>
{agentBenches.length === 0 ? (
<p className="mt-2 text-[13.5px] text-(--ink-3)">
Not in a workbench yet.
</p>
) : (
benches.data.map((item) => (
agentBenches.map((item) => (
<Link
key={item.id}
to={workbenchPath(item.id)}
className="mt-2 block text-[13.5px] font-semibold underline-offset-2 hover:underline"
>
{item.title}
{item.name}
</Link>
))
)}
Expand Down
18 changes: 4 additions & 14 deletions apps/web/src/pages/workers-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
import { useState } from "react";
import { Button, RichEmptyState, toast } from "@corbits/react-ui";
import { Plus, Robot } from "@/lib/icons";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
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 { WorkbenchAvatar } from "@/chat/avatar";
import { listWorkbenches } from "@/chat/workbench-tenants";
import { describeRestartFailure, redeployWorkbenchAgent } from "../workbench-create";
import { useBench } from "../bench-context";
import { useWorkerBenches } from "../worker-benches";
import { Link } from "../navigation";
import { useTenantQuery } from "../routines-api";
import { PageLayout } from "../shell/page-layout";
Expand All @@ -36,16 +36,6 @@ export function workerStatus(agent: Pick<ChatAgent, "liveAddress" | "latestStatu
: { tone: "working", text: "Starting up" };
}

/** The workspace is not a bench; a worker's workbench is one of its child
* tenants, the same rows the sidebar lists. */
export function useWorkbenchList(tenantId: string | null) {
return useQuery({
queryKey: tenantKeys.workbenches(tenantId ?? "none"),
enabled: tenantId !== null,
queryFn: () => listWorkbenches(tenantId as string),
});
}

export function workerPath(agentId: string): string {
return `${WORKERS_PATH_PREFIX}/${encodeURIComponent(agentId)}`;
}
Expand Down Expand Up @@ -85,7 +75,7 @@ export function WorkersRosterList({
readonly tenantId: string;
readonly agents: readonly ChatAgent[];
}) {
const benchName = useWorkbenchList(tenantId).data?.[0]?.title ?? "";
const { byWorker } = useWorkerBenches();
const [filter, setFilter] = useState<WorkerTone | "all">("all");
const [query, setQuery] = useState("");
const queryClient = useQueryClient();
Expand Down Expand Up @@ -187,7 +177,7 @@ export function WorkersRosterList({
) : null}
</span>
<span className="truncate text-[12.5px] font-semibold text-(--ink-2)">
{benchName}
{(byWorker.get(agent.id) ?? []).map((bench) => bench.name).join(", ")}
</span>
<span className="text-right text-[12.5px] text-(--ink-3)">
{agent.liveAddress === null ? "" : "Now"}
Expand Down
14 changes: 7 additions & 7 deletions apps/web/src/shell/workbench-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Hash } from "@/lib/icons";

import { useBench } from "../bench-context";
import { useBenchWorkerStatus } from "../worker-status";
import { useWorkerBenches } from "../worker-benches";
import { tenantKeys } from "../query-client";
import { workbenchIdFromPath, workbenchPath } from "../workbench-path";
import type { HubTenant } from "../needs-converge";
Expand Down Expand Up @@ -99,7 +100,7 @@ export function WorkbenchList({
/>
))}
</div>
<WorkerGroup path={path} onNavigate={onNavigate} benches={workbenches} />
<WorkerGroup path={path} onNavigate={onNavigate} />
</div>
);
}
Expand All @@ -110,13 +111,13 @@ function WorkerRow({
active,
onSelect,
}: {
readonly agent: { readonly name: string };
readonly agent: { readonly name: string; readonly assetName: string };
readonly benches: readonly HubTenant[];
readonly active: boolean;
readonly onSelect: () => void;
}) {
// Sidebar workers are the workspace's agents; a bench's copy shares the name.
const status = useBenchWorkerStatus(benches, (p) => p.name === agent.name);
// The bench's copy of a workspace worker shares its deploy asset name.
const status = useBenchWorkerStatus(benches, (p) => p.assetName === agent.assetName);
return (
<button
type="button"
Expand All @@ -138,12 +139,11 @@ function WorkerRow({
function WorkerGroup({
path,
onNavigate,
benches,
}: {
readonly path: string;
readonly onNavigate: (to: string) => void;
readonly benches: readonly HubTenant[];
}) {
const { byWorker } = useWorkerBenches();
const { selectedTenantId } = useBench();
const agents = useQuery({
queryKey: tenantKeys.agents(selectedTenantId ?? "none"),
Expand All @@ -161,7 +161,7 @@ function WorkerGroup({
<WorkerRow
key={agent.id}
agent={agent}
benches={benches}
benches={byWorker.get(agent.id) ?? []}
active={path === to}
onSelect={() => onNavigate(to)}
/>
Expand Down
50 changes: 50 additions & 0 deletions apps/web/src/worker-benches.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Which benches each worker runs in. A bench deploys its own copy of a
// worker, with a new asset id but the same deploy-source asset name (the
// slug is carried into the name), so the workspace worker joins to each
// bench's copy by asset name, never by display name.

import { useQueries, useQuery } from "@tanstack/react-query";

import { listChatAgents } from "@/chat/threads-api";
import { tenantKeys } from "./query-client";
import { useBench } from "./bench-context";
import type { HubTenant } from "./needs-converge";
import { useSidebarSections } from "./shell/sidebar-sections";

export type WorkerBenches = {
/** Workspace worker id -> the benches running a copy of it. */
readonly byWorker: ReadonlyMap<string, readonly HubTenant[]>;
/** The workspace worker id a bench copy (by asset name) stands for. */
readonly workerIdForAsset: (assetName: string) => string | undefined;
readonly benches: readonly HubTenant[];
};

export function useWorkerBenches(): WorkerBenches {
const { selectedTenantId } = useBench();
const sections = useSidebarSections(selectedTenantId);
const benches = sections.kind === "ready" ? sections.workbenches : [];

const workers = useQuery({
queryKey: tenantKeys.agents(selectedTenantId ?? "none"),
enabled: selectedTenantId !== null,
queryFn: () => listChatAgents(selectedTenantId as string),
});
const benchAgents = useQueries({
queries: benches.map((bench) => ({
queryKey: tenantKeys.agents(bench.id),
queryFn: () => listChatAgents(bench.id),
})),
});

const workerIdByAsset = new Map((workers.data ?? []).map((w) => [w.assetName, w.id]));
const byWorker = new Map<string, HubTenant[]>();
benches.forEach((bench, index) => {
for (const agent of benchAgents[index]?.data ?? []) {
const workerId = workerIdByAsset.get(agent.assetName);
if (workerId === undefined) continue;
byWorker.set(workerId, [...(byWorker.get(workerId) ?? []), bench]);
}
});

return { byWorker, workerIdForAsset: (assetName) => workerIdByAsset.get(assetName), benches };
}
Loading