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,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>
);
}