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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user