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
@@ -0,0 +1,29 @@
import { ComputerViewModes, type ComputerViewMode } from "@/src/dict/computer-dict";
import computerViewUrl from "@/src/utils/computer-view-url";
type Props = {
computer_id: number;
name: string;
mode: ComputerViewMode;
/**
* The mode on show; the others stay loaded but hidden.
*/
active: boolean;
};
/**
* One of a computer's apps in a frame. A raw iframe: twui has no
* equivalent.
*/
export default function ComputerViewFrame({ computer_id, name, mode, active }: Props) {
const title = ComputerViewModes.find((m) => m.value == mode)?.title || mode;
return (
<iframe
src={computerViewUrl({ computer_id, mode })}
title={`${name}: ${title}`}
allow="clipboard-read; clipboard-write; fullscreen"
className={`absolute inset-0 w-full h-full border-0 bg-sunken ${active ? "" : "hidden"}`}
/>
);
}
@@ -0,0 +1,31 @@
import Button from "@moduletrace/twui/layout/Button";
type Props = {
title: string;
active: boolean;
onPick: () => void;
};
/**
* One mode in a computer view's toolbar (Browser, Terminal, Code).
*/
export default function ComputerViewModeButton({ title, active, onPick }: Props) {
return (
<Button
type="button"
title={title}
role="tab"
aria-selected={active}
variant="ghost"
color="text"
onClick={onPick}
className={`px-3! py-1.5! text-[13px] leading-[18px] rounded-default transition-colors duration-[var(--m-fast)] focus-visible:ring-2 focus-visible:ring-ink ${
active
? "bg-sunken dark:bg-sunken text-ink dark:text-ink font-bold"
: "text-ink-secondary dark:text-ink-secondary hover:text-ink hover:dark:text-ink hover:bg-sunken hover:dark:bg-sunken"
}`}
>
{title}
</Button>
);
}
+77
View File
@@ -0,0 +1,77 @@
import { useState } from "react";
import Link from "@moduletrace/twui/layout/Link";
import Row from "@moduletrace/twui/layout/Row";
import Stack from "@moduletrace/twui/layout/Stack";
import { ExternalLink } from "lucide-react";
import { ComputerViewModes, type ComputerViewMode } from "@/src/dict/computer-dict";
import computerViewUrl from "@/src/utils/computer-view-url";
import ComputerViewFrame from "./computer-view-frame";
import ComputerViewModeButton from "./computer-view-mode-button";
type Props = {
computer_id: number;
/**
* The computer's name, for the frame titles.
*/
name: string;
/**
* The mode it opens in.
*/
default_mode?: ComputerViewMode;
className?: string;
};
/**
* A computer the user can work on: its browser, terminal or code editor
* (`ComputerViewModes`), served through the app (`/computers/<id>/`).
* A mode stays open once visited, so switching back keeps its state.
*/
export default function ComputerView({ computer_id, name, default_mode, className }: Props) {
const [mode, setMode] = useState<ComputerViewMode>(
default_mode || ComputerViewModes[0].value,
);
const [visited, setVisited] = useState<ComputerViewMode[]>([mode]);
const pick = ({ value }: { value: ComputerViewMode }) => {
setMode(value);
setVisited((prev) => (prev.includes(value) ? prev : [...prev, value]));
};
return (
<Stack className={`w-full min-h-0 gap-0! items-stretch ${className || ""}`}>
<Row className="w-full gap-1! flex-nowrap items-center px-3 py-2 border-0 border-b border-solid border-hairline">
<Row role="tablist" aria-label={`${name} modes`} className="gap-1! flex-nowrap">
{ComputerViewModes.map((item) => (
<ComputerViewModeButton
key={item.value}
title={item.title}
active={item.value == mode}
onPick={() => pick({ value: item.value })}
/>
))}
</Row>
<Link
href={computerViewUrl({ computer_id, mode })}
target="_blank"
rel="noopener noreferrer"
title="Open in a new tab"
aria-label="Open in a new tab"
className="ml-auto p-2 text-ink-secondary hover:text-ink rounded-default hover:bg-sunken"
>
<ExternalLink size={16} aria-hidden />
</Link>
</Row>
<Stack className="relative grow min-h-0 gap-0! items-stretch bg-sunken">
{visited.map((value) => (
<ComputerViewFrame
key={value}
computer_id={computer_id}
name={name}
mode={value}
active={value == mode}
/>
))}
</Stack>
</Stack>
);
}