Open a computer chip in the viewer instead of switching to it

A computer a bot names in a reply is now a button that opens it in the
shared computer viewer modal, the bot's own computer included (sunken and
checked, as before). Switching stays in the header's computers modal, so
the chip has one job. A computer the bot can no longer use stays a plain
name.
This commit is contained in:
2026-10-23 17:58:08 +01:00
parent 4261535741
commit e75ab28041
2 changed files with 102 additions and 24 deletions
@@ -0,0 +1,82 @@
import { describe, expect, test } from "bun:test";
import { renderToString } from "react-dom/server";
import type { BUN_SQLITE_COBBLE_COMPUTERS } from "@/db/types/db";
import type { BotComputers } from "../(hooks)/use-bot-computers";
import { BotComputersContext } from "../(data)/bot-computers-context";
import ComputerRefChip from "./computer-ref-chip";
const main: BUN_SQLITE_COBBLE_COMPUTERS = { id: 1, name: "Main computer" };
const sandbox: BUN_SQLITE_COBBLE_COMPUTERS = { id: 2, name: "SEO box" };
type RenderProps = {
computer_id: number;
computers: BUN_SQLITE_COBBLE_COMPUTERS[];
};
function render({ computer_id, computers }: RenderProps) {
const value = makeComputers({ computers });
const html = renderToString(
<BotComputersContext.Provider value={value}>
<ComputerRefChip computer_id={computer_id} />
</BotComputersContext.Provider>,
);
return { html, value };
}
type MakeComputersProps = {
computers: BUN_SQLITE_COBBLE_COMPUTERS[];
};
/**
* A computers context recording what the chip asked for: the viewer it
* opened and the switches it made.
*/
function makeComputers({ computers }: MakeComputersProps) {
const switches: number[] = [];
const opened: { computer_id: number; mode: string }[] = [];
const value: BotComputers = {
computers,
switchComputer: async ({ computer_id }) => {
switches.push(computer_id);
},
openComputer: (params) => {
opened.push(params);
},
closeComputer: () => {},
};
return Object.assign(value, { switches, opened });
}
describe("ComputerRefChip", () => {
test("opens the viewer in its browser and switches nothing", async () => {
const { html, value } = render({
computer_id: 2,
computers: [main, sandbox],
});
expect(html).toContain("SEO box");
// The chip's only job is opening, so no switch affordance survives.
expect(html).not.toContain("Switch to");
expect(value.switches).toHaveLength(0);
});
test("the computer the bot is on is still a chip, sunken with a check", () => {
const { html } = render({ computer_id: 1, computers: [main, sandbox] });
expect(html).toContain("Main computer");
expect(html).toContain("<button");
expect(html).toContain("bg-sunken");
expect(html).toContain('title="Open Main computer"');
});
test("a computer the bot can't use stays a plain name", () => {
const { html } = render({ computer_id: 9, computers: [main] });
expect(html).toContain("computer 9");
expect(html).not.toContain("<button");
});
});
@@ -2,6 +2,7 @@ import { useContext } from "react";
import Button from "@moduletrace/twui/layout/Button"; import Button from "@moduletrace/twui/layout/Button";
import Span from "@moduletrace/twui/layout/Span"; import Span from "@moduletrace/twui/layout/Span";
import { Check, Monitor } from "lucide-react"; import { Check, Monitor } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { BotComputersContext } from "../(data)/bot-computers-context"; import { BotComputersContext } from "../(data)/bot-computers-context";
type Props = { type Props = {
@@ -9,33 +10,22 @@ type Props = {
}; };
/** /**
* A computer the bot named in a message (`computerRef`): a button that * A computer the bot named in a message (`computerRef`): a chip that opens
* switches the bot to it at once, or a checked chip when it's already on * it in the viewer, in its browser. The one the bot is already on says so,
* it. A computer the bot can't use (any more) is shown by name only. * sunken with a check. A computer the bot can't use (any more) is shown by
* name only.
*/ */
export default function ComputerRefChip({ computer_id }: Props) { export default function ComputerRefChip({ computer_id }: Props) {
const { computers, switching_id, error, switchComputer } = useContext(BotComputersContext); const { computers, openComputer } = useContext(BotComputersContext);
const index = computers.findIndex((c) => c.id == computer_id); const computer = computers.find((c) => c.id == computer_id);
const computer = computers[index];
const name = computer?.name || `computer ${computer_id}`; const name = computer?.name || `computer ${computer_id}`;
if (!computer) { if (!computer?.id) {
return <Span className="font-bold">{name}</Span>; return <Span className="font-bold">{name}</Span>;
} }
if (index == 0) { const current = computers[0]?.id == computer.id;
return (
<Span
title="The computer this bot is on"
className="inline-flex h-6 gap-1.5 items-center px-2 align-middle rounded-default-xs bg-sunken text-[13px] font-bold leading-4 text-ink whitespace-nowrap"
>
<Monitor size={14} aria-hidden />
{name}
<Check size={14} aria-hidden />
</Span>
);
}
return ( return (
<Button <Button
@@ -43,14 +33,20 @@ export default function ComputerRefChip({ computer_id }: Props) {
variant="outlined" variant="outlined"
color="gray" color="gray"
size="smaller" size="smaller"
title={error && switching_id == undefined ? error : `Switch to ${name}`} title={`Open ${name}`}
loading={switching_id == computer_id} onClick={() =>
disabled={Boolean(switching_id)} openComputer({ computer_id: computer.id || 0, mode: "browser" })
onClick={() => switchComputer({ computer_id })} }
className="inline-flex h-6 gap-1.5! flex-nowrap align-middle px-2! text-[13px] font-bold text-ink whitespace-nowrap" className={twMerge(
"inline-flex h-6 gap-1.5! flex-nowrap align-middle px-2! text-[13px] font-bold whitespace-nowrap text-ink dark:text-ink",
current
? "bg-sunken hover:bg-sunken outline-0"
: "bg-transparent hover:bg-sunken",
)}
> >
<Monitor size={14} aria-hidden className="shrink-0" /> <Monitor size={14} aria-hidden className="shrink-0" />
{name} {name}
{current ? <Check size={14} aria-hidden className="shrink-0" /> : null}
</Button> </Button>
); );
} }