feat(app): stopped panel paints last screen + Resume/Restart fresh controls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 16:09:39 +07:00
parent 375e4c5c92
commit ff0ad7a648
+59 -14
View File
@@ -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 <SplitView split={node.split} path={path} {...rest} />;
}
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<string, string>) {
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<string, string> | null }) {
const hostRef = useRef<HTMLDivElement | null>(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 <div ref={hostRef} style={{ position: "absolute", inset: 0, opacity: 0.45, pointerEvents: "none" }} />;
}
function Leaf({ id, workspaceId, running, states, surfaces, focusedId, onFocus, zoomed, drop, onStartPanelDrag, searchSurfaceId, searchNonce, onCloseSearch, font, palette }: Omit<NodeProps, "node" | "path"> & { 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(
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%", width: "100%", color: COLORS.textSecondary, flexDirection: "column", gap: 10 }}>
<div style={{ fontFamily: FONT.mono, fontSize: 13 }}>Process exited</div>
<div style={{ display: "flex", gap: 8 }}>
<button onClick={() => void restartSurface(id)}
style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 14px", background: COLORS.bgElevated, color: COLORS.textPrimary, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 7, fontSize: 12 }}>
<RotateCw size={13} /> Restart
</button>
{zoomed === id && (
<button onClick={() => void setZoom(workspaceId, null)}
style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 14px", background: "transparent", color: COLORS.textSecondary, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 7, fontSize: 12 }}>
<Minimize2 size={13} /> Exit zoom
<div style={{ position: "relative", height: "100%", width: "100%" }}>
<StoppedSnapshot surfaceId={id} font={font} palette={palette} />
<div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column", gap: 10, color: COLORS.textSecondary, background: "rgba(0,0,0,0.35)" }}>
<div style={{ fontFamily: FONT.mono, fontSize: 13 }}>Stopped</div>
<div style={{ display: "flex", gap: 8 }}>
<button onClick={() => void restartSurface(id, true)}
style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 14px", background: COLORS.accent, color: COLORS.bgApp, border: "none", borderRadius: 7, fontSize: 12, fontWeight: 600 }}>
<Play size={13} /> Resume
</button>
)}
<button onClick={() => void restartSurface(id, false)}
style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 14px", background: COLORS.bgElevated, color: COLORS.textPrimary, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 7, fontSize: 12 }}>
<RotateCw size={13} /> Restart fresh
</button>
{zoomed === id && (
<button onClick={() => void setZoom(workspaceId, null)}
style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 14px", background: "transparent", color: COLORS.textSecondary, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 7, fontSize: 12 }}>
<Minimize2 size={13} /> Exit zoom
</button>
)}
</div>
</div>
</div>
);