feat(web): add chromeless TerminalView and full-screen terminal route

New `/runs/:id/terminal` route renders a bare TerminalView at viewport
size, opened outside the AppShell so there is no nav, sidebar, or run
detail tabs around it. TerminalView gains a `chromeless` prop that hides
the toolbar and decorative wrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-05-10 10:23:11 -04:00
parent 5ac9a188c6
commit cb316936e3
No known key found for this signature in database
3 changed files with 54 additions and 26 deletions

View file

@ -188,9 +188,11 @@ function StatusPill({
export default function TerminalView({
runId,
leading,
chromeless = false,
}: {
runId: string;
leading?: React.ReactNode;
chromeless?: boolean;
}) {
const { push } = useToast();
const stateQuery = useRunState(runId);
@ -349,35 +351,38 @@ export default function TerminalView({
<section
className="flex h-full min-h-0 flex-col"
aria-labelledby={headingId}
style={chromeless ? { backgroundColor: TERMINAL_BACKGROUND } : undefined}
>
<h2 id={headingId} className="sr-only">Terminal</h2>
<div className="mb-2 flex shrink-0 flex-wrap items-center gap-3">
{leading}
<StatusPill status={status} detail={sandboxDetail} />
<div className="ml-auto flex items-center gap-2">
<Tooltip label="Reconnect">
<button
type="button"
className={ICON_BUTTON_CLASS}
onClick={reconnect}
aria-label="Reconnect terminal"
>
<ArrowPathIcon className="size-4" aria-hidden="true" />
</button>
</Tooltip>
{accessCommandLabel && (
<button
type="button"
className={SECONDARY_BUTTON_CLASS}
onClick={() => void copyAccessCommand()}
aria-label={`Copy ${accessCommandLabel} command`}
>
<ClipboardDocumentIcon className="size-4" aria-hidden="true" />
{accessCommandLabel}
</button>
)}
{!chromeless && (
<div className="mb-2 flex shrink-0 flex-wrap items-center gap-3">
{leading}
<StatusPill status={status} detail={sandboxDetail} />
<div className="ml-auto flex items-center gap-2">
<Tooltip label="Reconnect">
<button
type="button"
className={ICON_BUTTON_CLASS}
onClick={reconnect}
aria-label="Reconnect terminal"
>
<ArrowPathIcon className="size-4" aria-hidden="true" />
</button>
</Tooltip>
{accessCommandLabel && (
<button
type="button"
className={SECONDARY_BUTTON_CLASS}
onClick={() => void copyAccessCommand()}
aria-label={`Copy ${accessCommandLabel} command`}
>
<ClipboardDocumentIcon className="size-4" aria-hidden="true" />
{accessCommandLabel}
</button>
)}
</div>
</div>
</div>
)}
{error ? (
<div className="flex min-h-0 flex-1 items-center justify-center" role="alert">
<ErrorState
@ -386,6 +391,8 @@ export default function TerminalView({
onRetry={error.recoverable ? reconnect : undefined}
/>
</div>
) : chromeless ? (
<div ref={terminalEl} className="h-full min-h-0 p-3" />
) : (
<div
className="min-h-0 flex-1 overflow-hidden rounded border border-line pb-3"

View file

@ -22,6 +22,7 @@ import * as RunEvents from "./routes/run-events";
import * as RunArtifacts from "./routes/run-artifacts";
import * as RunFiles from "./routes/run-files";
import * as RunSandbox from "./routes/run-sandbox";
import * as RunTerminal from "./routes/run-terminal";
import * as RunBilling from "./routes/run-billing";
import * as Insights from "./routes/insights";
import * as InsightsEditor from "./routes/insights-editor";
@ -75,6 +76,7 @@ export const routes: RouteObject[] = [
indexRoute(RedirectHome),
route("setup", Setup),
route("login", AuthLogin),
route("runs/:id/terminal", RunTerminal),
{
Component: withRouteModule({
default: AppShellModule,

View file

@ -0,0 +1,19 @@
import { useEffect } from "react";
import TerminalView from "../components/terminal-view";
export default function RunTerminal({ params }: { params: { id: string } }) {
useEffect(() => {
const previous = document.title;
document.title = `Terminal · ${params.id} · Fabro`;
return () => {
document.title = previous;
};
}, [params.id]);
return (
<div className="h-screen w-screen overflow-hidden">
<TerminalView runId={params.id} chromeless />
</div>
);
}