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:
@@ -1,11 +1,12 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
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 { TerminalView } from "./TerminalView";
|
||||||
import { SearchBar } from "./SearchBar";
|
import { SearchBar } from "./SearchBar";
|
||||||
import { StatusRing } from "./StatusRing";
|
import { StatusRing } from "./StatusRing";
|
||||||
import { COLORS, FONT, STATE_COLOR } from "./theme";
|
import { COLORS, FONT, STATE_COLOR } from "./theme";
|
||||||
import type { LayoutNode, SurfaceState, SurfaceView } from "./layoutTypes";
|
import type { LayoutNode, SurfaceState, SurfaceView } from "./layoutTypes";
|
||||||
import { setRatios, restartSurface, setZoom, moveSurface } from "./socketBridge";
|
import { setRatios, restartSurface, setZoom, moveSurface, attachSurface } from "./socketBridge";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
@@ -116,6 +117,43 @@ function Node({ node, path, ...rest }: NodeProps) {
|
|||||||
return <SplitView split={node.split} path={path} {...rest} />;
|
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 }) {
|
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 focused = focusedId === id;
|
||||||
const dropEdge = drop && drop.id === id ? drop.edge : null;
|
const dropEdge = drop && drop.id === id ? drop.edge : null;
|
||||||
@@ -142,12 +180,18 @@ function Leaf({ id, workspaceId, running, states, surfaces, focusedId, onFocus,
|
|||||||
|
|
||||||
if (running[id] === false) {
|
if (running[id] === false) {
|
||||||
return card(
|
return card(
|
||||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%", width: "100%", color: COLORS.textSecondary, flexDirection: "column", gap: 10 }}>
|
<div style={{ position: "relative", height: "100%", width: "100%" }}>
|
||||||
<div style={{ fontFamily: FONT.mono, fontSize: 13 }}>Process exited</div>
|
<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 }}>
|
<div style={{ display: "flex", gap: 8 }}>
|
||||||
<button onClick={() => void restartSurface(id)}
|
<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 }}>
|
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
|
<RotateCw size={13} /> Restart fresh
|
||||||
</button>
|
</button>
|
||||||
{zoomed === id && (
|
{zoomed === id && (
|
||||||
<button onClick={() => void setZoom(workspaceId, null)}
|
<button onClick={() => void setZoom(workspaceId, null)}
|
||||||
@@ -157,6 +201,7 @@ function Leaf({ id, workspaceId, running, states, surfaces, focusedId, onFocus,
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user