diff --git a/src/components/general/computer-open-button.tsx b/src/components/general/computer-open-button.tsx
new file mode 100644
index 0000000..1a362b9
--- /dev/null
+++ b/src/components/general/computer-open-button.tsx
@@ -0,0 +1,76 @@
+import { useState } from "react";
+import Button from "@moduletrace/twui/layout/Button";
+import type {
+ TWUIButtonColorType,
+ TWUIButtonSizeType,
+ TWUIButtonVariantType,
+} from "@moduletrace/twui/layout/Button";
+import { AppWindow } from "lucide-react";
+import { twMerge } from "tailwind-merge";
+import ComputerViewerModal from "./computer-viewer-modal";
+
+type Props = {
+ computer_id: number;
+ /**
+ * The computer's name, for the modal and the button's title.
+ */
+ name: string;
+ /**
+ * Words after the icon; without them it's an icon button.
+ */
+ label?: string;
+ title?: string;
+ variant?: TWUIButtonVariantType;
+ color?: TWUIButtonColorType;
+ size?: TWUIButtonSizeType;
+ disabled?: boolean;
+ className?: string;
+};
+
+const IconButtonClasses =
+ "shrink-0 p-2! text-ink-secondary dark:text-ink-secondary hover:text-ink hover:dark:text-ink hover:bg-sunken hover:dark:bg-sunken rounded-default transition-colors duration-[var(--m-fast)] focus-visible:ring-2 focus-visible:ring-ink";
+
+/**
+ * Opens a computer in the viewer over the page, in its browser, terminal
+ * or code editor. Lists use it as an icon button; the computer's own page
+ * adds a label.
+ */
+export default function ComputerOpenButton({
+ computer_id,
+ name,
+ label,
+ title,
+ variant = "ghost",
+ color = "text",
+ size,
+ disabled,
+ className,
+}: Props) {
+ const [open, setOpen] = useState(false);
+ const button_title = title || `Open ${name}`;
+
+ return (
+ <>
+
+ setOpen(false)}
+ computer_id={computer_id}
+ name={name}
+ />
+ >
+ );
+}
diff --git a/src/components/general/computer-viewer-modal.tsx b/src/components/general/computer-viewer-modal.tsx
new file mode 100644
index 0000000..8d72068
--- /dev/null
+++ b/src/components/general/computer-viewer-modal.tsx
@@ -0,0 +1,82 @@
+import type { ReactNode } from "react";
+import Modal from "@moduletrace/twui/elements/Modal";
+import H2 from "@moduletrace/twui/layout/H2";
+import Row from "@moduletrace/twui/layout/Row";
+import Span from "@moduletrace/twui/layout/Span";
+import Stack from "@moduletrace/twui/layout/Stack";
+import type { ComputerViewMode } from "@/src/dict/computer-dict";
+import ComputerView from "./computer-view";
+
+type Props = {
+ open: boolean;
+ onClose: () => void;
+ computer_id: number;
+ /**
+ * The computer's name, in the header and the frame titles.
+ */
+ name: string;
+ /**
+ * The mode it opens in.
+ */
+ default_mode?: ComputerViewMode;
+ /**
+ * A line under the name, e.g. a take-over note.
+ */
+ note?: string;
+ /**
+ * Controls at the end of the header, e.g. handing it back.
+ */
+ action?: ReactNode;
+};
+
+/**
+ * A computer, large, in its browser, terminal or code editor, over the
+ * page. `ComputerOpenButton` opens it from lists; chat's viewer adds a
+ * note and the hand-back action after a take-over.
+ */
+export default function ComputerViewerModal({
+ open,
+ onClose,
+ computer_id,
+ name,
+ default_mode,
+ note,
+ action,
+}: Props) {
+ if (!open) return null;
+
+ return (
+ {
+ if (!next_open) onClose();
+ }}
+ className="max-w-[95vw]! h-[90vh] p-0 bg-paper flex flex-col overflow-hidden"
+ >
+
+
+
+ {name}
+
+ {note ? (
+
+ {note}
+
+ ) : null}
+
+ {action ? (
+
+ {action}
+
+ ) : null}
+
+
+
+ );
+}
diff --git a/src/pages/admin/bots/[bot_id]/(partials)/computer-viewer-modal.tsx b/src/pages/admin/bots/[bot_id]/(partials)/computer-viewer-modal.tsx
index fde78be..67f3a7a 100644
--- a/src/pages/admin/bots/[bot_id]/(partials)/computer-viewer-modal.tsx
+++ b/src/pages/admin/bots/[bot_id]/(partials)/computer-viewer-modal.tsx
@@ -1,12 +1,9 @@
import { useContext, useState } from "react";
import { AppContext } from "@/src/pages/__root";
-import Modal from "@moduletrace/twui/elements/Modal";
import Button from "@moduletrace/twui/layout/Button";
-import H2 from "@moduletrace/twui/layout/H2";
-import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
-import ComputerView from "@/src/components/general/computer-view";
+import SharedComputerViewerModal from "@/src/components/general/computer-viewer-modal";
import type { BotWithLastMessage } from "@/src/types";
import { BotComputersContext } from "../(data)/bot-computers-context";
import sendBotMessage from "../(functions)/send-bot-message";
@@ -53,26 +50,20 @@ export default function ComputerViewerModal({ bot }: Props) {
};
return (
- {
- if (!open) closeComputer();
- }}
- className="max-w-[95vw]! h-[90vh] p-0 bg-paper flex flex-col overflow-hidden"
- >
-
-
-
- {name}
-
- {viewing.take_over ? (
-
- {`${bot_name} asked you to take over. Hand it back when you're done.`}
-
- ) : null}
-
- {viewing.take_over ? (
-
+ onClose={closeComputer}
+ computer_id={Number(computer.id)}
+ name={name}
+ default_mode={viewing.mode}
+ note={
+ viewing.take_over
+ ? `${bot_name} asked you to take over. Hand it back when you're done.`
+ : undefined
+ }
+ action={
+ viewing.take_over ? (
+ <>
- ) : null}
-
-
-
+ >
+ ) : undefined
+ }
+ />
);
}
diff --git a/src/pages/admin/settings/(partials)/computer-list-row.tsx b/src/pages/admin/settings/(partials)/computer-list-row.tsx
new file mode 100644
index 0000000..ba1af95
--- /dev/null
+++ b/src/pages/admin/settings/(partials)/computer-list-row.tsx
@@ -0,0 +1,57 @@
+import type { ReactNode } from "react";
+import ComputerIconTile from "@/src/components/general/computer-icon-tile";
+import ComputerOpenButton from "@/src/components/general/computer-open-button";
+import type { ComputerWithStatus } from "@/src/types";
+import computerResourcesLabel from "@/src/utils/computer-resources-label";
+import computerStatusLabel from "@/src/utils/computer-status-label";
+import SettingsListRow from "./settings-list-row";
+
+type Props = {
+ computer: ComputerWithStatus;
+ badge?: string;
+ /**
+ * Defaults to its state, resources and description.
+ */
+ subtitle?: string;
+ /**
+ * What can be done with it, after the open button.
+ */
+ children?: ReactNode;
+};
+
+/**
+ * One computer in a settings list: its avatar and details, opening its
+ * settings page from the title. Every list uses it, so the row behaves the
+ * same, and the open button shows it in the viewer over the page.
+ */
+export default function ComputerListRow({ computer, badge, subtitle, children }: Props) {
+ const name = computer.name || `Computer ${computer.id}`;
+ const ready = computer.status == "running";
+
+ return (
+ }
+ href={`/admin/settings/computers/${computer.id}`}
+ title={name}
+ badge={badge}
+ subtitle={
+ subtitle ??
+ [
+ computerStatusLabel({ computer }),
+ computerResourcesLabel({ computer }),
+ computer.description,
+ ]
+ .filter(Boolean)
+ .join(" · ")
+ }
+ >
+
+ {children}
+
+ );
+}
diff --git a/src/pages/admin/settings/bots/[bot_id]/(partials)/bot-computer-row.tsx b/src/pages/admin/settings/bots/[bot_id]/(partials)/bot-computer-row.tsx
index c71adea..88bc81b 100644
--- a/src/pages/admin/settings/bots/[bot_id]/(partials)/bot-computer-row.tsx
+++ b/src/pages/admin/settings/bots/[bot_id]/(partials)/bot-computer-row.tsx
@@ -1,8 +1,5 @@
-import ComputerIconTile from "@/src/components/general/computer-icon-tile";
import type { ComputerWithStatus } from "@/src/types";
-import computerResourcesLabel from "@/src/utils/computer-resources-label";
-import computerStatusLabel from "@/src/utils/computer-status-label";
-import SettingsListRow from "../../../(partials)/settings-list-row";
+import ComputerListRow from "../../../(partials)/computer-list-row";
import SettingsRowAction from "../../../(partials)/settings-row-action";
type Props = {
@@ -22,17 +19,7 @@ export default function BotComputerRow({
const name = computer.name || `Computer ${computer.id}`;
return (
- }
- title={name}
- subtitle={[
- computerStatusLabel({ computer }),
- computerResourcesLabel({ computer }),
- computer.description,
- ]
- .filter(Boolean)
- .join(" · ")}
- >
+
{onRemove ? (
) : null}
-
+
);
}
diff --git a/src/pages/admin/settings/computers/(partials)/computer-settings-row.tsx b/src/pages/admin/settings/computers/(partials)/computer-settings-row.tsx
index 127e00a..4ee7fc2 100644
--- a/src/pages/admin/settings/computers/(partials)/computer-settings-row.tsx
+++ b/src/pages/admin/settings/computers/(partials)/computer-settings-row.tsx
@@ -1,8 +1,5 @@
-import ComputerIconTile from "@/src/components/general/computer-icon-tile";
import type { ComputerWithStatus } from "@/src/types";
-import computerResourcesLabel from "@/src/utils/computer-resources-label";
-import computerStatusLabel from "@/src/utils/computer-status-label";
-import SettingsListRow from "../../(partials)/settings-list-row";
+import ComputerListRow from "../../(partials)/computer-list-row";
import SettingsRowAction from "../../(partials)/settings-row-action";
type Props = {
@@ -29,18 +26,9 @@ export default function ComputerSettingsRow({
const busy_here = busy || computer.status == "creating";
return (
- }
- href={`/admin/settings/computers/${computer.id}`}
- title={name}
+
{is_super_admin && computer.status == "failed" ? (
) : null}
-
+
);
}
diff --git a/src/pages/admin/settings/computers/[computer_id]/(partials)/computer-overview-group.tsx b/src/pages/admin/settings/computers/[computer_id]/(partials)/computer-overview-group.tsx
index 736a42b..25e5b0a 100644
--- a/src/pages/admin/settings/computers/[computer_id]/(partials)/computer-overview-group.tsx
+++ b/src/pages/admin/settings/computers/[computer_id]/(partials)/computer-overview-group.tsx
@@ -1,13 +1,11 @@
-import Button from "@moduletrace/twui/layout/Button";
import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
-import { ExternalLink } from "lucide-react";
import ComputerIconTile from "@/src/components/general/computer-icon-tile";
+import ComputerOpenButton from "@/src/components/general/computer-open-button";
import type { ComputerWithStatus } from "@/src/types";
import computerResourcesLabel from "@/src/utils/computer-resources-label";
import computerStatusLabel from "@/src/utils/computer-status-label";
-import computerViewUrl from "@/src/utils/computer-view-url";
import SettingsGroup from "../../../(partials)/settings-group";
type Props = {
@@ -16,7 +14,7 @@ type Props = {
/**
* The computer at a glance: its state, resources and what it's for, and a
- * way to open it in the browser.
+ * way to open it in the viewer.
*/
export default function ComputerOverviewGroup({ computer }: Props) {
const name = computer.name || `Computer ${computer.id}`;
@@ -51,31 +49,20 @@ export default function ComputerOverviewGroup({ computer }: Props) {
.join(" · ")}
- }
- className="shrink-0"
- >
- Open
-
+ />
);
diff --git a/src/pages/admin/settings/workspaces/[workspace_id]/(partials)/workspace-computer-row.tsx b/src/pages/admin/settings/workspaces/[workspace_id]/(partials)/workspace-computer-row.tsx
index c40fb1d..45a5997 100644
--- a/src/pages/admin/settings/workspaces/[workspace_id]/(partials)/workspace-computer-row.tsx
+++ b/src/pages/admin/settings/workspaces/[workspace_id]/(partials)/workspace-computer-row.tsx
@@ -1,8 +1,6 @@
-import ComputerIconTile from "@/src/components/general/computer-icon-tile";
-import SettingsListRow from "@/src/pages/admin/settings/(partials)/settings-list-row";
-import SettingsRowAction from "@/src/pages/admin/settings/(partials)/settings-row-action";
import type { ComputerWithStatus } from "@/src/types";
-import computerStatusLabel from "@/src/utils/computer-status-label";
+import ComputerListRow from "@/src/pages/admin/settings/(partials)/computer-list-row";
+import SettingsRowAction from "@/src/pages/admin/settings/(partials)/settings-row-action";
type Props = {
computer: ComputerWithStatus;
@@ -32,11 +30,9 @@ export default function WorkspaceComputerRow({
const name = computer.name || `Computer ${computer.id}`;
return (
- }
- title={name}
+
{is_default ? null : (
@@ -48,6 +44,6 @@ export default function WorkspaceComputerRow({
{is_super_admin && computer.kind != "main" ? (
) : null}
-
+
);
}
diff --git a/src/pages/admin/workspaces/[workspace_id]/(partials)/workspace-dashboard-computer-row.tsx b/src/pages/admin/workspaces/[workspace_id]/(partials)/workspace-dashboard-computer-row.tsx
index 9ef7b68..02e757e 100644
--- a/src/pages/admin/workspaces/[workspace_id]/(partials)/workspace-dashboard-computer-row.tsx
+++ b/src/pages/admin/workspaces/[workspace_id]/(partials)/workspace-dashboard-computer-row.tsx
@@ -1,7 +1,5 @@
-import ComputerIconTile from "@/src/components/general/computer-icon-tile";
-import SettingsListRow from "@/src/pages/admin/settings/(partials)/settings-list-row";
+import ComputerListRow from "@/src/pages/admin/settings/(partials)/computer-list-row";
import type { ComputerWithStatus } from "@/src/types";
-import computerStatusLabel from "@/src/utils/computer-status-label";
type Props = {
computer: ComputerWithStatus;
@@ -9,17 +7,9 @@ type Props = {
};
/**
- * A computer in the workspace dashboard: its state and what it's for.
+ * A computer in the workspace dashboard: its state, resources and what
+ * it's for, opening its settings page.
*/
export default function WorkspaceDashboardComputerRow({ computer, is_default }: Props) {
- const name = computer.name || `Computer ${computer.id}`;
-
- return (
- }
- title={name}
- badge={is_default ? "Default" : undefined}
- subtitle={`${computerStatusLabel({ computer })}${computer.description ? ` · ${computer.description}` : ""}`}
- />
- );
+ return ;
}