feat: convert tmux monitoring into a plugin (B10)

This commit is contained in:
LukeGus committed 2026-09-23 22:42:56 -05:00
1 parent 79d77678c9
commit 708eedb861
145 files changed
+30308 -4458

No files matched your search

@@ -0,0 +1,169 @@
// Interactive view of a tmux pane. Embeds the real SSH terminal (through the
// terminal.view component ssh-terminal registers) and attaches to the
// selected session via the native tmux_attach flow, so the pane is fully
// usable: typing, mouse scrolling and tmux copy-mode all behave exactly like
// a normal terminal tab.
import { useRef, useState } from "react";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import {
Activity,
Cpu,
MemoryStick,
RotateCw,
SquareSplitHorizontal,
SquareSplitVertical,
Trash2,
X,
} from "lucide-react";
import { PluginComponent } from "@termix/plugin-sdk/ui";
import type { PluginHostRecord, TabHandle } from "@termix/plugin-sdk/frontend";
import type { TmuxPaneMetrics } from "./api";
import { formatMem } from "./format";
import type { SelectedPane } from "./types";
interface PanePreviewProps {
host: PluginHostRecord;
pane: SelectedPane;
metrics?: TmuxPaneMetrics;
/** Imperative handle of the embedded terminal, used by the parent to nudge
* a refit/redraw after layout-changing tmux actions. */
terminalRef?: React.RefObject<TabHandle | null>;
/** Split the window containing this pane ("h" = new pane to the right,
* "v" = below — tmux -h/-v semantics). */
onSplit: (direction: "h" | "v") => void;
/** Ask for confirmation and kill this pane. */
onKillPane: () => void;
onClose: () => void;
}
function newInstanceId(): string {
return typeof crypto.randomUUID === "function"
? crypto.randomUUID()
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
}
export function PanePreview({
host,
pane,
metrics,
terminalRef,
onSplit,
onKillPane,
onClose,
}: PanePreviewProps) {
const { t } = useTranslation();
// One PTY per mount; the parent remounts this component (via key) when the
// host or session changes. Pane switches within a session go through the
// focus endpoint instead, so the connection is reused.
const instanceIdRef = useRef<string>(newInstanceId());
// Bumping this remounts the embedded terminal with a fresh PTY — the rescue
// hatch when the attached client's rendering gets out of sync.
const [attachNonce, setAttachNonce] = useState(0);
function reattach() {
instanceIdRef.current = newInstanceId();
setAttachNonce((n) => n + 1);
}
// The host's own authType travels in the spread, so a Tailscale host still
// gets the Tailscale-aware PTY path. The SSH port may be non-standard.
const hostRecord = host as unknown as Record<string, unknown>;
const resolvedPort = hostRecord.sshPort ?? hostRecord.port;
const terminalHostConfig = {
...hostRecord,
port: resolvedPort,
sshPort: resolvedPort,
instanceId: instanceIdRef.current,
};
return (
<>
<div className="flex items-center gap-3 border-b border-border px-3 py-1.5 text-xs text-muted-foreground">
<span className="font-medium text-foreground">
{pane.sessionName} · {pane.paneId}
</span>
{metrics && (
<>
<span className="flex items-center gap-1">
<Cpu className="size-3" />
{metrics.cpuPercent.toFixed(1)}%
</span>
<span className="flex items-center gap-1">
<MemoryStick className="size-3" />
{formatMem(metrics.memRssKb)}
</span>
{metrics.gpuMemMb > 0 && (
<span className="flex items-center gap-1">
<Activity className="size-3" />
{metrics.gpuMemMb} MB GPU
</span>
)}
{metrics.topCommand && (
<span className="truncate">
{metrics.topCommand} ({metrics.processCount})
</span>
)}
</>
)}
<div className="ml-auto flex items-center gap-2">
<button
className="text-muted-foreground hover:text-foreground"
title={t("tmuxMonitor.splitRight")}
aria-label={t("tmuxMonitor.splitRight")}
onClick={() => onSplit("h")}
>
<SquareSplitHorizontal className="size-3.5" />
</button>
<button
className="text-muted-foreground hover:text-foreground"
title={t("tmuxMonitor.splitDown")}
aria-label={t("tmuxMonitor.splitDown")}
onClick={() => onSplit("v")}
>
<SquareSplitVertical className="size-3.5" />
</button>
<button
className="text-muted-foreground hover:text-destructive"
title={t("tmuxMonitor.killPane")}
aria-label={t("tmuxMonitor.killPane")}
onClick={onKillPane}
>
<Trash2 className="size-3.5" />
</button>
<div className="h-3.5 border-l border-border" />
<button
className="text-muted-foreground hover:text-foreground"
title={t("tmuxMonitor.reattach")}
aria-label={t("tmuxMonitor.reattach")}
onClick={reattach}
>
<RotateCw className="size-3.5" />
</button>
<button
className="text-muted-foreground hover:text-foreground"
title={t("tmuxMonitor.closePreview")}
aria-label={t("tmuxMonitor.closePreview")}
onClick={onClose}
>
<X className="size-3.5" />
</button>
</div>
</div>
<div className="min-h-0 flex-1 overflow-hidden">
<PluginComponent
id="terminal.view"
key={attachNonce}
handleRef={terminalRef}
hostConfig={terminalHostConfig}
isVisible={true}
title={pane.sessionName}
showTitle={false}
splitScreen={false}
tmuxAttachSession={pane.sessionName}
onClose={onClose}
/>
</div>
</>
);
}