Add computer switching, crew reporting, job steps and markdown chat

- Bots have a current computer (bots.computer_id), switched from the chat
  header beside the model button or by clicking a computer the bot names
  ([computer:ID]); tasks run on it by default
- search_computers runs one read-only command on all of a bot's computers;
  chat replies know their computers (compose-computers-note)
- The coordinator can use every computer and grants or revokes crew access
  with assign_computer
- Delegated jobs record requested_by_bot_id; the worker's result or failure
  is reported back in the coordinator's conversation
- bots_queue_updates records each model turn of a job, pushed live and shown
  in the working-on panel
- Live relay: every process pushes new messages, job steps and bot changes
  from the database to its own sockets, so work done elsewhere shows live
- Bot messages render as markdown (react-markdown + remark-gfm, no raw HTML)
  with twui components; rail previews are plain text
- Fix header button overflow in the model and computer selects
- Bump @moduletrace/twui to 0.1.2

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 04:44:35 +01:00
co-authored by Claude Opus 5.5
parent 6a2665dae9
commit 7b580d782e
64 changed files with 1792 additions and 86 deletions
@@ -0,0 +1,11 @@
import React from "react";
import type { BotComputers } from "../(hooks)/use-bot-computers";
/**
* The open bot's computers and the switch, shared by the header's
* computer select and the computer buttons in its messages.
*/
export const BotComputersContext = React.createContext<BotComputers>({
computers: [],
switchComputer: async () => {},
});
@@ -0,0 +1,70 @@
import type { Components } from "react-markdown";
import Divider from "@moduletrace/twui/layout/Divider";
import H3 from "@moduletrace/twui/layout/H3";
import H4 from "@moduletrace/twui/layout/H4";
import H5 from "@moduletrace/twui/layout/H5";
import P from "@moduletrace/twui/layout/P";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import MarkdownCodeBlock from "../(partials)/markdown-code-block";
import MarkdownLink from "../(partials)/markdown-link";
import MarkdownList from "../(partials)/markdown-list";
const HeadingClassName = "my-0! text-[15px]! leading-6! tracking-normal! font-bold text-ink";
/**
* How a bot message's markdown renders: chat-sized twui elements (headings
* no bigger than the bot's name), lists through twui's `List`, code on the
* sunken surface, and links that open in a new tab or switch computers.
* Tables (GFM) have no children-based twui equivalent, so they're styled
* elements.
*/
export const MessageMarkdownComponents: Components = {
p: ({ children }) => (
<P noMargin className="text-sm leading-5 text-ink">
{children}
</P>
),
h1: ({ children }) => <H3 className={HeadingClassName}>{children}</H3>,
h2: ({ children }) => <H3 className={HeadingClassName}>{children}</H3>,
h3: ({ children }) => <H4 className={HeadingClassName}>{children}</H4>,
h4: ({ children }) => <H5 className={HeadingClassName}>{children}</H5>,
h5: ({ children }) => <H5 className={HeadingClassName}>{children}</H5>,
h6: ({ children }) => <H5 className={HeadingClassName}>{children}</H5>,
strong: ({ children }) => <Span className="font-bold">{children}</Span>,
em: ({ children }) => <Span className="italic">{children}</Span>,
del: ({ children }) => <Span className="line-through">{children}</Span>,
a: MarkdownLink,
ul: ({ children }) => <MarkdownList>{children}</MarkdownList>,
ol: ({ children, start }) => (
<MarkdownList ordered start={start}>
{children}
</MarkdownList>
),
code: ({ children }) => (
<Span className="font-mono text-[13px] px-1 py-px rounded-default-xs bg-sunken text-ink break-words">
{children}
</Span>
),
pre: ({ children }) => <MarkdownCodeBlock>{children}</MarkdownCodeBlock>,
blockquote: ({ children }) => (
<Stack className="gap-2! items-stretch pl-3 border-0 border-l-2 border-solid border-hairline-strong text-ink-secondary">
{children}
</Stack>
),
hr: () => <Divider className="my-1 border-hairline dark:border-hairline" />,
img: ({ src, alt }) => (
<MarkdownLink href={typeof src == "string" ? src : undefined}>{alt || "Image"}</MarkdownLink>
),
table: ({ children }) => (
<Stack className="w-full max-w-full overflow-x-auto items-start">
<table className="text-[13px] leading-5 text-ink border-collapse">{children}</table>
</Stack>
),
th: ({ children }) => (
<th className="px-3 py-1.5 text-left font-bold border border-solid border-hairline">{children}</th>
),
td: ({ children }) => (
<td className="px-3 py-1.5 align-top border border-solid border-hairline">{children}</td>
),
};
@@ -0,0 +1,18 @@
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import type { BUN_SQLITE_COBBLE_COMPUTERS } from "@/db/types/db";
import type { SetBotComputerRequest } from "@/src/schemas/bot-schema";
/**
* Switches the computer a bot works on. Returns its computers, the new
* one first.
*/
export default async function setBotComputer({ bot_id, computer_id }: SetBotComputerRequest) {
return await fetchApi<
SetBotComputerRequest,
APIResponseObject<BUN_SQLITE_COBBLE_COMPUTERS>
>(`/api/admin/bots/set-computer`, {
method: "POST",
body: { bot_id, computer_id },
});
}
@@ -0,0 +1,70 @@
import { useState } from "react";
import useServerEvent from "@/src/hooks/use-server-event";
import type { BotUpdatedData } from "@/src/types";
import type { BUN_SQLITE_COBBLE_COMPUTERS } from "@/db/types/db";
import setBotComputer from "../(functions)/set-bot-computer";
export type BotComputers = {
/**
* The bot's computers, the one it's on first.
*/
computers: BUN_SQLITE_COBBLE_COMPUTERS[];
/**
* The computer being switched to, while the switch runs.
*/
switching_id?: number;
error?: string;
switchComputer: (params: { computer_id: number }) => Promise<void>;
};
type Params = {
bot_id: number;
computers: BUN_SQLITE_COBBLE_COMPUTERS[];
};
/**
* Keeps the open bot's computers and switches the one it's on, in place:
* no reload, the list comes back reordered. A switch made in another tab
* arrives as `server:bot-updated` with the bot's `computer_id`.
*/
export default function useBotComputers({ bot_id, computers: initial }: Params): BotComputers {
const [computers, setComputers] = useState(initial);
const [switching_id, setSwitchingId] = useState<number>();
const [error, setError] = useState<string>();
useServerEvent({
event: "server:bot-updated",
onData: (data: BotUpdatedData) => {
const computer_id = data.computer_id;
if (data.id != bot_id || !computer_id) return;
setComputers((prev) => {
const picked = prev.find((c) => c.id == computer_id);
return !picked || prev[0]?.id == computer_id
? prev
: [picked, ...prev.filter((c) => c.id != computer_id)];
});
},
});
const switchComputer = async ({ computer_id }: { computer_id: number }) => {
if (switching_id || computers[0]?.id == computer_id) return;
setSwitchingId(computer_id);
setError(undefined);
const res = await setBotComputer({ bot_id, computer_id }).catch(() => null);
if (res?.success && res.payload?.length) {
setComputers(res.payload);
} else {
setError(res?.msg || "Couldn't switch computers");
}
setSwitchingId(undefined);
};
return { computers, switching_id, error, switchComputer };
}
@@ -0,0 +1,50 @@
import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import { Check } from "lucide-react";
import type { BUN_SQLITE_COBBLE_COMPUTERS } from "@/db/types/db";
type Props = {
computer: BUN_SQLITE_COBBLE_COMPUTERS;
/**
* The computer the bot is on.
*/
current: boolean;
onPick: () => void;
};
/**
* One computer in the header's computer select, checked when it's the
* one the bot is on.
*/
export default function BotComputerOption({ computer, current, onPick }: Props) {
return (
<Row
role="menuitemradio"
aria-checked={current}
tabIndex={0}
onClick={current ? undefined : onPick}
onKeyDown={(e) => {
if (!current && (e.key == "Enter" || e.key == " ")) {
e.preventDefault();
onPick();
}
}}
className={`w-full min-h-9 gap-2.5! flex-nowrap items-center px-4 py-1.5 text-ink transition-colors duration-[var(--m-fast)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ink ${
current ? "cursor-default" : "cursor-pointer hover:bg-sunken"
}`}
>
<Check size={16} aria-hidden className={`shrink-0 ${current ? "" : "invisible"}`} />
<Stack className="min-w-0 gap-0!">
<Span className="text-sm leading-5 text-ink truncate w-full">
{computer.name || `Computer ${computer.id}`}
</Span>
{computer.description ? (
<Span className="text-[13px] leading-[18px] text-ink-secondary truncate w-full">
{computer.description}
</Span>
) : null}
</Stack>
</Row>
);
}
@@ -0,0 +1,90 @@
import { useContext, useState } from "react";
import Dropdown from "@moduletrace/twui/elements/Dropdown";
import Paper from "@moduletrace/twui/elements/Paper";
import Button from "@moduletrace/twui/layout/Button";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import { ChevronDown, Monitor } from "lucide-react";
import type { BotWithLastMessage } from "@/src/types";
import { BotComputersContext } from "../(data)/bot-computers-context";
import BotComputerOption from "./bot-computer-option";
type Props = {
bot: BotWithLastMessage;
};
/**
* The header's computer button, beside the model button: shows the
* computer the bot is on and, with more than one, switches it.
*/
export default function BotComputerSelect({ bot }: Props) {
const { computers, switching_id, error, switchComputer } = useContext(BotComputersContext);
const [open, setOpen] = useState(false);
const current = computers[0];
if (!current?.id) return null;
const name = bot.name || "This bot";
const can_switch = computers.length > 1;
const target = (
<Button
title={
can_switch
? `${name} is on ${current.name}. Switch computers`
: `${name} is on ${current.name}, its only computer`
}
type="button"
variant="outlined"
color="gray"
size="small"
disabled={!can_switch}
loading={Boolean(switching_id)}
buttonContentProps={{ className: "min-w-0" }}
className="max-w-full min-w-0 gap-2! flex-nowrap text-[13px] text-ink-secondary hover:text-ink"
>
<Monitor size={16} aria-hidden className="shrink-0 -mt-0.5" />
<Span className="truncate">{current.name || "Computer"}</Span>
{can_switch ? <ChevronDown size={14} aria-hidden className="shrink-0" /> : null}
</Button>
);
if (!can_switch) {
return <Stack className="max-w-[200px] min-w-0 shrink">{target}</Stack>;
}
return (
<Dropdown
target={target}
position="bottom-right"
externalOpen={open}
externalSetOpen={setOpen}
targetWrapperProps={{ className: "max-w-full min-w-0" }}
className="max-w-[200px] min-w-0 shrink items-stretch"
>
<Paper
role="menu"
aria-label={`${name}'s computers`}
className="w-[260px] p-0! py-1 gap-0! items-stretch rounded-default bg-surface dark:bg-surface border-hairline dark:border-hairline shadow-popover! dark:shadow-popover!"
>
{computers.map((computer, index) => (
<BotComputerOption
key={computer.id}
computer={computer}
current={index == 0}
onPick={async () => {
setOpen(false);
await switchComputer({ computer_id: computer.id || 0 });
}}
/>
))}
{error ? (
<Span role="alert" className="px-4 py-2 text-[13px] leading-[18px] text-ink-secondary">
{error}
</Span>
) : null}
</Paper>
</Dropdown>
);
}
@@ -1,5 +1,8 @@
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import Stack from "@moduletrace/twui/layout/Stack";
import P from "@moduletrace/twui/layout/P";
import linkComputerRefs from "@/src/utils/link-computer-refs";
import { MessageMarkdownComponents } from "../(data)/message-markdown-components";
import NeedsYouChip from "./needs-you-chip";
type Props = {
@@ -7,12 +10,19 @@ type Props = {
needs_you?: boolean;
};
/**
* One bot message, rendered from markdown (GFM; raw HTML is never
* rendered). Computers it names (`computerRef`) show as buttons that
* switch the bot to them.
*/
export default function BotMessageLine({ text, needs_you }: Props) {
return (
<Stack className="gap-1.5! items-start max-w-full">
<P noMargin className="text-sm leading-5 text-ink whitespace-pre-wrap break-words">
{text}
</P>
<Stack className="w-full gap-2.5! items-start break-words">
<Markdown remarkPlugins={[remarkGfm]} components={MessageMarkdownComponents}>
{linkComputerRefs({ text })}
</Markdown>
</Stack>
{needs_you ? <NeedsYouChip /> : null}
</Stack>
);
@@ -49,6 +49,7 @@ export default function BotModelsModal({ bot, harnesses: initial }: Props) {
color="gray"
size="small"
disabled={!can_edit}
buttonContentProps={{ className: "min-w-0" }}
className="max-w-full min-w-0 gap-2! flex-nowrap text-[13px] text-ink-secondary hover:text-ink"
>
<CpuAi size={16} aria-hidden className="shrink-0 -mt-0.5" />
@@ -61,7 +62,7 @@ export default function BotModelsModal({ bot, harnesses: initial }: Props) {
if (!can_edit) {
return (
<Stack className="ml-auto max-w-[40%] min-w-0 shrink">
<Stack className="max-w-[280px] min-w-0 shrink">
{target}
</Stack>
);
@@ -71,7 +72,7 @@ export default function BotModelsModal({ bot, harnesses: initial }: Props) {
<Modal
target={target}
targetWrapperProps={{
className: "ml-auto max-w-[40%] min-w-0 shrink",
className: "max-w-[280px] min-w-0 shrink",
}}
open={open}
setOpen={setOpen}
@@ -0,0 +1,56 @@
import { useContext } from "react";
import Button from "@moduletrace/twui/layout/Button";
import Span from "@moduletrace/twui/layout/Span";
import { Check, Monitor } from "lucide-react";
import { BotComputersContext } from "../(data)/bot-computers-context";
type Props = {
computer_id: number;
};
/**
* A computer the bot named in a message (`computerRef`): a button that
* switches the bot to it at once, or a checked chip when it's already on
* it. A computer the bot can't use (any more) is shown by name only.
*/
export default function ComputerRefChip({ computer_id }: Props) {
const { computers, switching_id, error, switchComputer } = useContext(BotComputersContext);
const index = computers.findIndex((c) => c.id == computer_id);
const computer = computers[index];
const name = computer?.name || `computer ${computer_id}`;
if (!computer) {
return <Span className="font-bold">{name}</Span>;
}
if (index == 0) {
return (
<Span
title="The computer this bot is on"
className="inline-flex h-6 gap-1.5 items-center px-2 align-middle rounded-default-xs bg-sunken text-[13px] font-bold leading-4 text-ink whitespace-nowrap"
>
<Monitor size={14} aria-hidden />
{name}
<Check size={14} aria-hidden />
</Span>
);
}
return (
<Button
type="button"
variant="outlined"
color="gray"
size="smaller"
title={error && switching_id == undefined ? error : `Switch to ${name}`}
loading={switching_id == computer_id}
disabled={Boolean(switching_id)}
onClick={() => switchComputer({ computer_id })}
className="inline-flex h-6 gap-1.5! flex-nowrap align-middle px-2! text-[13px] font-bold text-ink whitespace-nowrap"
>
<Monitor size={14} aria-hidden className="shrink-0" />
{name}
</Button>
);
}
@@ -6,6 +6,7 @@ import BotStone from "@/src/components/general/bot-stone";
import type { BotFace, BotHue } from "@/src/dict/bot-dict";
import type { BotWithLastMessage, HarnessOverview } from "@/src/types";
import { crewStatusSentence } from "@/src/utils/crew-status";
import BotComputerSelect from "./bot-computer-select";
import BotModelsModal from "./bot-models-modal";
type Props = {
@@ -19,8 +20,9 @@ type Props = {
/**
* 64px header. The coordinator's status is the crew sentence. On the
* right, the model button that opens the bot's models (the defaults on
* the coordinator, for super admins).
* right, the computer the bot is on (`BotComputerSelect`), then the model
* button that opens the bot's models (the defaults on the coordinator,
* for super admins).
*/
export default function ConversationHeader({ bot, bots, harnesses }: Props) {
const status =
@@ -43,7 +45,10 @@ export default function ConversationHeader({ bot, bots, harnesses }: Props) {
{status}
</Span>
</Stack>
<BotModelsModal key={bot.id} bot={bot} harnesses={harnesses} />
<Row className="ml-auto min-w-0 shrink gap-2! flex-nowrap justify-end">
<BotComputerSelect bot={bot} />
<BotModelsModal key={bot.id} bot={bot} harnesses={harnesses} />
</Row>
</Row>
);
}
@@ -0,0 +1,26 @@
import { isValidElement, type ReactNode } from "react";
import CodeBlock from "@moduletrace/twui/elements/CodeBlock";
/**
* A fenced code block in a bot message, on the sunken surface with
* twui's copy button (its icon is drawn white; recoloured to the theme).
*/
export default function MarkdownCodeBlock({ children }: { children?: ReactNode }) {
const code = isValidElement(children)
? (children.props as { children?: ReactNode }).children
: children;
return (
<CodeBlock
singleBlock
backgroundColor="var(--color-sunken)"
wrapperProps={{
className:
"max-w-full! outline-hairline dark:outline-hairline [&_.twui-code-block-header_svg]:stroke-ink-secondary",
}}
className="font-mono text-[13px] leading-5 text-ink py-2 pl-2 pr-10"
>
{code}
</CodeBlock>
);
}
@@ -0,0 +1,27 @@
import type { ComponentProps } from "react";
import Link from "@moduletrace/twui/layout/Link";
import { ComputerRefHrefPrefix } from "@/src/utils/link-computer-refs";
import ComputerRefChip from "./computer-ref-chip";
/**
* A link in a bot message. A computer the bot named (`linkComputerRefs`)
* becomes its switch button; anything else opens in a new tab.
*/
export default function MarkdownLink({ href, children }: ComponentProps<"a">) {
if (href?.startsWith(ComputerRefHrefPrefix)) {
const computer_id = Number(href.slice(ComputerRefHrefPrefix.length));
if (computer_id) return <ComputerRefChip computer_id={computer_id} />;
}
return (
<Link
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-ink underline underline-offset-2 hover:opacity-80 break-words"
>
{children}
</Link>
);
}
@@ -0,0 +1,29 @@
import { Children, isValidElement, type ReactNode } from "react";
import List from "@moduletrace/twui/layout/List";
type Props = {
children?: ReactNode;
ordered?: boolean;
start?: number;
};
/**
* A list in a bot message, through twui's `List` (which takes items, not
* `li` children): each item's content is lifted out of its `li`.
*/
export default function MarkdownList({ children, ordered, start }: Props) {
const items = Children.toArray(children)
.filter(isValidElement)
.map((item) => ({
content: (item.props as { children?: ReactNode }).children,
}));
return (
<List
ordered={ordered}
start={start}
items={items}
className={`w-full gap-1! pl-5 my-0 text-sm leading-5 text-ink ${ordered ? "list-decimal" : "list-disc"}`}
/>
);
}
@@ -5,12 +5,16 @@ import { AppContext } from "@/src/pages/__root";
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
import useLiveBots from "@/src/hooks/use-live-bots";
import useServerEvent from "@/src/hooks/use-server-event";
import { BotComputersContext } from "../(data)/bot-computers-context";
import useBotComputers from "../(hooks)/use-bot-computers";
import ConversationHeader from "../(partials)/conversation-header";
import MessageThread from "../(partials)/message-thread";
import Composer from "../(partials)/composer";
/**
* The 900px conversation column: header, bottom-anchored thread, composer.
* The bot's computers are shared with the header's select and the computer
* buttons in the thread, so a switch from either shows in both.
*/
export default function Conversation() {
const { pageProps } = useContext(AppContext);
@@ -22,6 +26,10 @@ export default function Conversation() {
const bots = useLiveBots({ bots: pageProps?.bots || [], open_bot_id: bot?.id || undefined });
const live_bot = bots.find((b) => b.id == bot?.id) || bot;
const bot_computers = useBotComputers({
bot_id: bot?.id || 0,
computers: pageProps?.bot_computers || [],
});
useServerEvent({
event: "server:bot-message",
@@ -44,21 +52,23 @@ export default function Conversation() {
if (!bot?.id || !live_bot) return null;
return (
<Stack className="grow min-w-0 h-full gap-0! items-stretch">
<ConversationHeader
bot={live_bot}
bots={bots}
harnesses={pageProps?.bot_harness_overview?.harnesses}
/>
<MessageThread bot={live_bot} messages={messages} />
<Composer
bot={bot}
onSent={({ message }) =>
setMessages((prev) =>
prev.some((m) => m.id == message.id) ? prev : [...prev, message],
)
}
/>
</Stack>
<BotComputersContext.Provider value={bot_computers}>
<Stack className="grow min-w-0 h-full gap-0! items-stretch">
<ConversationHeader
bot={live_bot}
bots={bots}
harnesses={pageProps?.bot_harness_overview?.harnesses}
/>
<MessageThread bot={live_bot} messages={messages} />
<Composer
bot={bot}
onSent={({ message }) =>
setMessages((prev) =>
prev.some((m) => m.id == message.id) ? prev : [...prev, message],
)
}
/>
</Stack>
</BotComputersContext.Provider>
);
}
@@ -4,13 +4,17 @@ import Span from "@moduletrace/twui/layout/Span";
import H2 from "@moduletrace/twui/layout/H2";
import { AppContext } from "@/src/pages/__root";
import useServerEvent from "@/src/hooks/use-server-event";
import type { BotUpdatedData } from "@/src/types";
import { AppData } from "@/src/data/app-data";
import type { BotJobUpdateData, BotUpdatedData } from "@/src/types";
import TaskStepRow from "../(partials)/task-step-row";
import OpenComputerButton from "../(partials)/open-computer-button";
/**
* 280px panel listing the steps the bot is taking, kept current by the
* active task each `server:bot-updated` push carries. Hidden below 1280px.
* active task each `server:bot-updated` push carries. Without planned
* steps it shows what the task's job has done (`bots_queue_updates`), the
* newest as in progress, appended live from `server:bot-job-update`.
* Hidden below 1280px.
*/
export default function WorkingOnPanel() {
const { pageProps } = useContext(AppContext);
@@ -24,6 +28,17 @@ export default function WorkingOnPanel() {
},
});
useServerEvent({
event: "server:bot-job-update",
onData: (data: BotJobUpdateData) => {
setTask((prev) =>
prev?.id && prev.id == data.task_id && !prev.updates.some((u) => u.id == data.id)
? { ...prev, updates: [...prev.updates, data].slice(-AppData.BotJobUpdatesShown) }
: prev,
);
},
});
return (
<Stack
className="hidden xl:flex w-[280px] shrink-0 h-full gap-0! items-stretch px-6 border-0 border-l border-solid border-hairline"
@@ -38,13 +53,21 @@ export default function WorkingOnPanel() {
{task.title}
</H2>
<Stack className="mt-4 gap-0! items-stretch">
{task.steps.map((step) => (
<TaskStepRow
key={step.id}
label={step.label || ""}
state={step.state || "pending"}
/>
))}
{task.steps.length
? task.steps.map((step) => (
<TaskStepRow
key={step.id}
label={step.label || ""}
state={step.state || "pending"}
/>
))
: task.updates.map((update, index) => (
<TaskStepRow
key={update.id}
label={update.text || ""}
state={index == task.updates.length - 1 ? "active" : "done"}
/>
))}
</Stack>
</>
) : (
@@ -3,6 +3,7 @@ import grabBots from "@/src/functions/backend/db/bots/grab-bots";
import grabBotMessages from "@/src/functions/backend/db/bots/grab-bot-messages";
import grabActiveBotTask from "@/src/functions/backend/db/bots/grab-active-bot-task";
import markBotRead from "@/src/functions/backend/db/bots/mark-bot-read";
import grabBotComputers from "@/src/functions/backend/db/computers/grab-bot-computers";
import grabBotHarnessOverview from "@/src/functions/backend/db/harness/grab-bot-harness-overview";
import grabHarnessOverview from "@/src/functions/backend/db/harness/grab-harness-overview";
import type { PagePropsType, PageQueryObject } from "@/src/types";
@@ -40,13 +41,14 @@ export const server: BunextPageServerFn<PagePropsType> = async (params) => {
* The header's models modal: a worker's own settings, or for the
* coordinator the defaults it runs on (without the providers).
*/
const [bot_messages, bot_task, bot_harness_overview] = await Promise.all([
const [bot_messages, bot_task, bot_harness_overview, bot_computers] = await Promise.all([
grabBotMessages({ bot_id: bot.id }),
grabActiveBotTask({ bot_id: bot.id }),
(bot.kind == "coordinator"
? grabHarnessOverview().then(({ harnesses }) => ({ harnesses }))
: grabBotHarnessOverview({ bot_id: bot.id })
).catch(() => null),
grabBotComputers({ bot_id: bot.id, user_id: user.id }),
]);
return {
@@ -56,6 +58,7 @@ export const server: BunextPageServerFn<PagePropsType> = async (params) => {
bot_messages,
bot_task,
bot_harness_overview,
bot_computers,
},
};
};