mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-11-01 16:15:24 +00:00
feat: convert tmux monitoring into a plugin (B10)
This commit is contained in:
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user