diff --git a/app/src/LayoutEngine.tsx b/app/src/LayoutEngine.tsx
index 74b6570..63b62f3 100644
--- a/app/src/LayoutEngine.tsx
+++ b/app/src/LayoutEngine.tsx
@@ -1,11 +1,12 @@
import { useEffect, useRef, useState } from "react";
-import { Maximize2, Minimize2, RotateCw, GripVertical } from "lucide-react";
+import { Maximize2, Minimize2, RotateCw, GripVertical, Play } from "lucide-react";
+import { Terminal } from "@xterm/xterm";
import { TerminalView } from "./TerminalView";
import { SearchBar } from "./SearchBar";
import { StatusRing } from "./StatusRing";
import { COLORS, FONT, STATE_COLOR } from "./theme";
import type { LayoutNode, SurfaceState, SurfaceView } from "./layoutTypes";
-import { setRatios, restartSurface, setZoom, moveSurface } from "./socketBridge";
+import { setRatios, restartSurface, setZoom, moveSurface, attachSurface } from "./socketBridge";
interface Props {
workspaceId: string;
@@ -116,6 +117,43 @@ function Node({ node, path, ...rest }: NodeProps) {
return ;
}
+const NERD_FALLBACK_LE = "'Symbols Nerd Font Mono'";
+const fontStackLE = (family: string | null) =>
+ family ? `'${family}', ${NERD_FALLBACK_LE}, monospace`
+ : `'JetBrains Mono Variable', 'JetBrains Mono', ${NERD_FALLBACK_LE}, monospace`;
+
+function xtermThemeLE(p: Record) {
+ return {
+ background: p["bg-panel"],
+ foreground: p["text-primary"],
+ cursor: p["text-primary"],
+ selectionBackground: p["search-match"],
+ };
+}
+
+function StoppedSnapshot({ surfaceId, font, palette }: { surfaceId: string; font: { family: string; size: number } | null; palette: Record | null }) {
+ const hostRef = useRef(null);
+ useEffect(() => {
+ const host = hostRef.current;
+ if (!host) return;
+ const term = new Terminal({
+ fontFamily: fontStackLE(font?.family ?? null),
+ fontSize: font?.size ?? 13,
+ theme: palette ? xtermThemeLE(palette) : undefined,
+ cursorBlink: false,
+ disableStdin: true,
+ scrollback: 0,
+ });
+ term.open(host);
+ let disposed = false;
+ void attachSurface(surfaceId, () => {}).then((res) => {
+ if (!disposed && res.snapshot) term.write(res.snapshot);
+ });
+ return () => { disposed = true; term.dispose(); };
+ }, [surfaceId, font, palette]); // eslint-disable-line react-hooks/exhaustive-deps
+ return ;
+}
+
function Leaf({ id, workspaceId, running, states, surfaces, focusedId, onFocus, zoomed, drop, onStartPanelDrag, searchSurfaceId, searchNonce, onCloseSearch, font, palette }: Omit & { id: string }) {
const focused = focusedId === id;
const dropEdge = drop && drop.id === id ? drop.edge : null;
@@ -142,19 +180,26 @@ function Leaf({ id, workspaceId, running, states, surfaces, focusedId, onFocus,
if (running[id] === false) {
return card(
-
-
Process exited
-
-
- {zoomed === id && (
-