// Interactive view of a tmux pane. Embeds the real SSH terminal (the same // component as the Terminal feature) 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 "react-i18next"; import { Activity, Cpu, MemoryStick, RotateCw, SquareSplitHorizontal, SquareSplitVertical, Trash2, X, } from "lucide-react"; import { CommandHistoryProvider } from "@/features/terminal/command-history/CommandHistoryContext"; import { Terminal } from "@/features/terminal/Terminal"; import type { TerminalHandle, TerminalHostConfig, } from "@/features/terminal/Terminal"; import type { SSHHost } from "@/types/index"; import type { TmuxPaneMetrics } from "@/api/tmux-monitor-api"; import { formatMem } from "./format"; import type { SelectedPane } from "./types"; interface PanePreviewProps { host: SSHHost; 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; /** 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(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); } // For Tailscale-auth hosts the generic pane-preview attach path can fail // host-reachability checks because the connection is initiated as a plain // TCP/SSH dial rather than through the Tailscale transport that was used for // the interactive terminal. Building the hostConfig with an explicit // authType and a defensively derived SSH port ensures the backend selects // the correct Tailscale-aware PTY path for both initial attach and reattach. let terminalHostConfig: TerminalHostConfig; if (host.authType === "tailscale") { // Prefer host.sshPort when set (Tailscale SSH can be on a non-standard // port); fall back to the general host.port. const resolvedPort = host.sshPort ?? host.port; terminalHostConfig = { ...host, port: resolvedPort, sshPort: resolvedPort, // Carry authType explicitly to guard against accidental omission // in the spread (e.g. if host object shape changes upstream). authType: "tailscale", instanceId: instanceIdRef.current, } as TerminalHostConfig; } else { const resolvedPort = host.sshPort ?? host.port; terminalHostConfig = { ...host, port: resolvedPort, sshPort: resolvedPort, instanceId: instanceIdRef.current, } as TerminalHostConfig; } return ( <>
{pane.sessionName} ยท {pane.paneId} {metrics && ( <> {metrics.cpuPercent.toFixed(1)}% {formatMem(metrics.memRssKb)} {metrics.gpuMemMb > 0 && ( {metrics.gpuMemMb} MB GPU )} {metrics.topCommand && ( {metrics.topCommand} ({metrics.processCount}) )} )}
); }