Open computers from the chat, let bots ask for a take-over, show task progress

- Serve each computer to its owner under /computers/<id>/ (HTTP and
  websockets proxied to the AIO sandbox) and show it in a shared
  ComputerView with browser, terminal and code modes, opened from the
  computers modal or a take-over button in the thread.
- ask_user_to_take_over lets a bot hand a computer to the user for a
  login or captcha; the message gets a take-over button and needs-you,
  and the viewer hands it back with a chat message.
- While a task runs the thread shows "<bot> is working..." with the
  task's latest step; steps read as plain words, not tool names.
- A coordinator with no workers no longer claims to delegate, and bots
  describe started tasks in the first person.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 05:35:44 +01:00
co-authored by Claude Opus 5.5
parent b1023ba8ff
commit 56daf0c462
44 changed files with 1289 additions and 75 deletions
@@ -2,8 +2,19 @@ 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 type { ComputerViewMode } from "@/src/dict/computer-dict";
import setBotComputer from "../(functions)/set-bot-computer";
/**
* The computer open in the viewer, in which mode, and whether the bot
* asked the user to take it over (the viewer then offers to hand it back).
*/
export type ComputerViewing = {
computer_id: number;
mode: ComputerViewMode;
take_over?: boolean;
};
export type BotComputers = {
/**
* The bot's computers, the one it's on first.
@@ -15,6 +26,9 @@ export type BotComputers = {
switching_id?: number;
error?: string;
switchComputer: (params: { computer_id: number }) => Promise<void>;
viewing?: ComputerViewing;
openComputer: (params: ComputerViewing) => void;
closeComputer: () => void;
};
type Params = {
@@ -25,12 +39,14 @@ type Params = {
/**
* 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`.
* arrives as `server:bot-updated` with the bot's `computer_id`. Also holds
* the computer open in the viewer (`ComputerViewerModal`).
*/
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>();
const [viewing, setViewing] = useState<ComputerViewing>();
useServerEvent({
event: "server:bot-updated",
@@ -66,5 +82,13 @@ export default function useBotComputers({ bot_id, computers: initial }: Params):
setSwitchingId(undefined);
};
return { computers, switching_id, error, switchComputer };
return {
computers,
switching_id,
error,
switchComputer,
viewing,
openComputer: (params) => setViewing(params),
closeComputer: () => setViewing(undefined),
};
}
@@ -0,0 +1,54 @@
import { useState } from "react";
import useServerEvent from "@/src/hooks/use-server-event";
import type { BotJobUpdateData, BotTaskWithSteps, BotUpdatedData } from "@/src/types";
type Params = {
bot_id: number;
/**
* The bot's active task when the page loaded.
*/
task: BotTaskWithSteps | null;
};
type Progress = {
task_id: number | null;
text: string | null;
};
function fromTask({ task }: { task: BotTaskWithSteps | null }): Progress {
return {
task_id: task?.id || null,
text: task?.updates[task.updates.length - 1]?.text || null,
};
}
/**
* The latest step of the open bot's active task (a `bots_queue_updates`
* row's text), kept current from `server:bot-job-update`. Starts over when
* `server:bot-updated` brings a different task, or none.
*/
export default function useTaskProgress({ bot_id, task }: Params) {
const [progress, setProgress] = useState<Progress>(() => fromTask({ task }));
useServerEvent({
event: "server:bot-job-update",
onData: (update: BotJobUpdateData) => {
if (update.bot_id != bot_id || !update.task_id || !update.text) return;
setProgress({ task_id: update.task_id, text: update.text });
},
});
useServerEvent({
event: "server:bot-updated",
onData: (data: BotUpdatedData) => {
if (data.id != bot_id) return;
setProgress((prev) =>
(data.task?.id || null) == prev.task_id ? prev : fromTask({ task: data.task }),
);
},
});
return progress.text;
}