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
@@ -9,7 +9,10 @@ type Params = {
/**
* Every computer a bot can use: the ones it was given directly
* (`bot_computers`) plus the ones assigned to any workspace it's in
* (`workspace_computers`). Only the owner's computers, each once.
* (`workspace_computers`); the coordinator can use all of them. Only the
* owner's computers, each once. The
* one it works on now (`bots.computer_id`) comes first, else its main one,
* so `computers[0]` is where its tasks run by default.
*/
export default async function grabBotComputers({
bot_id,
@@ -19,7 +22,8 @@ export default async function grabBotComputers({
sql: `SELECT c.* FROM computers c
WHERE c.user_id = ?
AND (
c.id IN (SELECT computer_id FROM bot_computers WHERE bot_id = ?)
(SELECT kind FROM bots WHERE id = ? AND user_id = c.user_id) = 'coordinator'
OR c.id IN (SELECT computer_id FROM bot_computers WHERE bot_id = ?)
OR c.id IN (
SELECT wc.computer_id FROM workspace_computers wc
JOIN workspace_bots wb ON wb.workspace_id = wc.workspace_id
@@ -27,8 +31,9 @@ export default async function grabBotComputers({
WHERE wb.bot_id = ? AND w.archived = 0
)
)
ORDER BY c.kind = 'main' DESC, c.id ASC`,
values: [user_id, bot_id, bot_id],
ORDER BY c.id = (SELECT computer_id FROM bots WHERE id = ?) DESC,
c.kind = 'main' DESC, c.id ASC`,
values: [user_id, bot_id, bot_id, bot_id, bot_id],
});
return res.payload || [];
@@ -0,0 +1,40 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import AppError from "@/src/utils/app-error";
import grabUserBot from "../bots/grab-user-bot";
import grabBotComputers from "./grab-bot-computers";
type Params = {
bot_id: number | string;
user_id: number | string;
computer_id: number | string;
};
/**
* Switches the computer a bot works on (`bots.computer_id`) to one of its
* own (`grabBotComputers`). Returns its computers, the new one first.
*/
export default async function setBotComputer({ bot_id, user_id, computer_id }: Params) {
const bot = await grabUserBot({ bot_id, user_id });
if (!bot?.id) {
throw new AppError(`Bot not found.`);
}
const computers = await grabBotComputers({ bot_id: bot.id, user_id });
const computer = computers.find((c) => String(c.id) == String(computer_id));
if (!computer?.id) {
throw new AppError(`${bot.name || "This bot"} has no access to that computer.`);
}
await BunSQLite.sql({
sql: `UPDATE bots SET computer_id = ?, updated_at = ? WHERE id = ?`,
values: [computer.id, Date.now(), bot.id],
});
return {
bot,
computer,
computers: [computer, ...computers.filter((c) => c.id != computer.id)],
};
}