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,41 @@
type Params = {
/**
* The sandbox's REST address (no `/mcp`).
*/
base_url: string;
command: string;
timeout_ms: number;
signal?: AbortSignal;
};
/**
* Runs one shell command on a computer over the sandbox's REST API
* (`/v1/shell/exec`), outside any MCP session. Throws when the computer
* can't be reached or refuses the command.
*/
export default async function runComputerCommand({
base_url,
command,
timeout_ms,
signal,
}: Params): Promise<{ output: string; exit_code: number | null }> {
const res = await fetch(`${base_url}/v1/shell/exec`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ command }),
signal: AbortSignal.any([
...(signal ? [signal] : []),
AbortSignal.timeout(timeout_ms),
]),
});
const body: any = await res.json().catch(() => ({}));
if (!res.ok || body?.success === false) {
throw new Error(`The computer refused the command: ${body?.message || res.status}`);
}
return {
output: String(body?.data?.output ?? ""),
exit_code: typeof body?.data?.exit_code == "number" ? body.data.exit_code : null,
};
}
@@ -0,0 +1,82 @@
import { z } from "zod";
import type { BUN_SQLITE_COBBLE_COMPUTERS } from "@/db/types/db";
import { AppData } from "@/src/data/app-data";
import grabComputerMcpUrl from "@/src/functions/backend/db/computers/grab-computer-mcp-url";
import grabComputerEnvs from "@/src/functions/backend/db/envs/grab-computer-envs";
import computerRef from "@/src/utils/computer-ref";
import redactSecrets from "@/src/utils/redact-secrets";
import type { HarnessTool } from "../../types";
import runComputerCommand from "./run-computer-command";
type Params = {
bot_id: number | string;
/**
* The bot's computers, the current one first (`grabBotComputers`).
*/
computers: BUN_SQLITE_COBBLE_COMPUTERS[];
};
type Args = {
command: string;
};
/**
* Lets a bot look for something across every computer it has: one shell
* command runs on each at once (`runComputerCommand`), and the outputs come
* back labelled with each computer's ref (`computerRef`), secrets redacted,
* so the bot can tell the user exactly which computer holds what.
*/
export default function searchComputersTool({
bot_id,
computers,
}: Params): HarnessTool<Args> {
return {
name: "search_computers",
description: `Run one read-only shell command (bash) on every computer you have at once, to find which one holds something: a file, a running app, a logged-in browser session, a setting. Each computer's output comes back under its ref, e.g. ${computerRef({ id: computers[0]?.id || 1 })}. For a logged-in site, look for its domain in the browser profiles' cookie databases (copy the file first, since the browser locks it) and list only host names, never cookie values. Don't change anything with it; use start_task for that. Output past ${AppData.ComputerSearchMaxOutputChars} characters per computer is cut, so filter in the command.`,
schema: z.object({
command: z.string().min(1).max(4000),
}),
run: async ({ args, signal }) => {
const results = await Promise.all(
computers.map(async (computer) => {
const label = `## ${computerRef({ id: computer.id || 0 })} ${computer.name || "Computer"}`;
try {
const target = await grabComputerMcpUrl({ computer_id: computer.id || 0 });
if (!target) return `${label}\nNot reachable: it has no container.`;
const [result, envs] = await Promise.all([
runComputerCommand({
base_url: target.base_url,
command: args.command,
timeout_ms: AppData.ComputerSearchTimeoutMs,
signal,
}),
grabComputerEnvs({ computer_id: computer.id || 0, bot_id }),
]);
const output = redactSecrets({
text: result.output.trim(),
secrets: envs.filter((env) => env.secret),
});
const cut = output.length > AppData.ComputerSearchMaxOutputChars;
return [
label,
`Exit code: ${result.exit_code ?? "unknown"}`,
output ? output.slice(0, AppData.ComputerSearchMaxOutputChars) : "(no output)",
cut ? `(output cut at ${AppData.ComputerSearchMaxOutputChars} characters)` : "",
]
.filter(Boolean)
.join("\n");
} catch (error) {
return `${label}\nNot reachable: ${error instanceof Error ? error.message : String(error)}`;
}
}),
);
return results.join("\n\n");
},
};
}