missions: extract MissionLivePane to stay under 1500-LoC CI budget
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 10s
ci / frontend (push) Failing after 19s
ci / e2e (push) Skipped
ci / publish (push) Skipped

Previous push hit the gates job's file-size gate — MissionCanvas.tsx
was 1517 lines (17 over). The Live Pane xterm subcomponent is
cleanly separable (no shared state with the parent, just takes
nodeId + visible props), so it lifts into its own file at zero
behavior cost.

  frontend/src/components/dashboard/MissionLivePane.tsx  (new, 106 LoC)
  frontend/src/components/dashboard/MissionCanvas.tsx    (1517 → 1424)

Also drops the xterm.css + useResilientTerminal imports from
MissionCanvas since only MissionLivePane needs them now.
This commit is contained in:
Omar Sobh
2026-07-20 11:55:47 -07:00
parent d4efb0dba2
commit 6ffbe978b2
2 changed files with 109 additions and 95 deletions
@@ -0,0 +1,107 @@
"use client";
// Live Pane tab body for missions running on a fleet node via Herdr.
// Attaches xterm.js to the node's Herdr TUI (WebRTC direct → WS-relay
// fallback). Rendered inline in MissionCanvas when
// mission.runtime_kind='local_herdr' and tab='pane'.
import { useEffect, useState } from "react";
import {
nodeHerdrConnector,
useResilientTerminal,
type TermMode,
} from "@/components/computer/apps/terminal/core";
import "@xterm/xterm/css/xterm.css";
const mono =
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
export function MissionLivePane({
nodeId,
visible,
}: {
nodeId: string | null;
visible: boolean;
}) {
if (!nodeId) {
return (
<div
style={{
padding: 40,
textAlign: "center",
color: "#8a8a92",
fontSize: 13,
}}
>
This mission has no target node.
</div>
);
}
return <LivePaneInner nodeId={nodeId} visible={visible} />;
}
function LivePaneInner({
nodeId,
visible,
}: {
nodeId: string;
visible: boolean;
}) {
const [mode, setMode] = useState<TermMode>("connecting");
const { hostRef, refit } = useResilientTerminal(
{
connect: nodeHerdrConnector(nodeId, setMode),
autoFocus: false,
visible: () => visible,
},
[nodeId],
);
useEffect(() => {
if (visible) refit();
}, [visible, refit]);
return (
<div
style={{
position: "relative",
height: "70vh",
minHeight: 480,
background: "#0a0a0d",
borderRadius: 10,
border: "1px solid rgba(255,255,255,.06)",
overflow: "hidden",
}}
>
<div
style={{
position: "absolute",
top: 8,
right: 8,
zIndex: 5,
padding: "2px 8px",
borderRadius: 6,
fontFamily: mono,
fontSize: 10,
letterSpacing: ".1em",
textTransform: "uppercase",
color:
mode === "direct"
? "#5fd08a"
: mode === "relayed"
? "#8a8a92"
: "#e8b465",
background:
mode === "direct"
? "rgba(95,208,138,.12)"
: "rgba(255,255,255,.05)",
border: `1px solid ${
mode === "direct" ? "rgba(95,208,138,.3)" : "rgba(255,255,255,.1)"
}`,
}}
>
{mode === "direct" ? "direct" : mode === "relayed" ? "relayed" : "connecting…"}
</div>
<div ref={hostRef} style={{ position: "absolute", inset: 0, padding: 8 }} />
</div>
);
}