import { Loader2, MonitorPlay, Pencil, Plus, Trash2, Unplug, } from 'lucide-react'; import React, { memo, useEffect, useMemo, useRef, useState } from 'react'; import { useI18n } from '../../application/i18n/I18nProvider'; import type { useSystemManagerBackend } from '../../application/state/useSystemManagerBackend'; import { buildTmuxAttachCommand } from '../../domain/systemManager/tmuxShell'; import type { TmuxManageAction, TmuxSessionInfo, } from '../../domain/systemManager/types'; import type { TerminalSession } from '../../types'; import type { AsyncRecordState } from '../../application/state/systemManager/useAsyncRecordCache'; import type { TmuxSessionDetails } from './TmuxManagerTab'; import { SystemPanelCollapsible, SystemPanelDetailStrip, SystemPanelInlineError, SystemPanelRoundButton, SystemPanelRow, SystemPanelSectionHeader, SystemPanelStatusBadge, } from './SystemPanelUi'; import { SystemPanelPromptDialog } from './SystemPanelPromptDialog'; import { SystemPanelConfirmDialog } from './SystemPanelConfirmDialog'; import { openInteractiveTerminal } from './openInteractiveTerminal'; import { showSystemManagerError } from './systemManagerToast'; import { runTmuxSessionAction } from './tmuxActionFocus'; type Backend = ReturnType; const TMUX_POPUP_ICON = { kind: 'image', src: '/system-icons/tmux.svg', alt: 'tmux', } as const; type RenamePromptTarget = | { kind: 'session' } | { kind: 'window'; windowIndex: number; currentName: string }; interface PendingTarget { action: TmuxManageAction['action']; windowIndex?: number; } interface ConfirmTmuxDetachOptions { sessionName: string; confirmMessage: string; confirm: (message: string) => boolean; runAction: (action: TmuxManageAction) => Promise; } export async function runConfirmedTmuxDetachAction({ sessionName, confirmMessage, confirm, runAction, }: ConfirmTmuxDetachOptions): Promise { if (!confirm(confirmMessage)) return false; await runAction({ action: 'detachSession', sessionName }); return true; } interface TmuxSessionCardProps { session: TmuxSessionInfo; sessionId: string; parentSession: TerminalSession; backend: Backend; detailsRecord?: AsyncRecordState; onLoadDetails: (session: TmuxSessionInfo, options?: { force?: boolean; urgent?: boolean }) => Promise; onRefreshDetails: (session: TmuxSessionInfo) => Promise; onSessionsChanged: () => Promise; onRequestTerminalFocus?: () => void; } export const TmuxSessionCard = memo(function TmuxSessionCard({ session, sessionId, parentSession, backend, detailsRecord, onLoadDetails, onRefreshDetails, onSessionsChanged, onRequestTerminalFocus, }: TmuxSessionCardProps) { const { t } = useI18n(); const [expanded, setExpanded] = useState(false); const [renamePrompt, setRenamePrompt] = useState(null); const [detachConfirmOpen, setDetachConfirmOpen] = useState(false); const [killSessionConfirmOpen, setKillSessionConfirmOpen] = useState(false); const [killWindowConfirm, setKillWindowConfirm] = useState<{ windowIndex: number; windowName: string; } | null>(null); const [newWindowOpen, setNewWindowOpen] = useState(false); const [actionError, setActionError] = useState(null); const [busy, setBusy] = useState(false); const [pending, setPending] = useState(null); const windows = detailsRecord?.data?.windows ?? []; const clients = detailsRecord?.data?.clients ?? []; const loadingDetails = detailsRecord?.loading ?? false; const windowsLoadDetail = detailsRecord?.error ?? null; const summaryKey = useMemo( () => `${session.name}|${session.created}|${session.windows}|${session.attached}|${session.activity ?? ''}`, [session.activity, session.attached, session.created, session.name, session.windows], ); const lastExpandedSummaryKeyRef = useRef(null); useEffect(() => { if (!expanded) { lastExpandedSummaryKeyRef.current = null; return; } if (lastExpandedSummaryKeyRef.current === null) { lastExpandedSummaryKeyRef.current = summaryKey; return; } if (lastExpandedSummaryKeyRef.current === summaryKey) return; lastExpandedSummaryKeyRef.current = summaryKey; void onRefreshDetails(session); }, [expanded, onRefreshDetails, session, summaryKey]); const runAction = async (action: TmuxManageAction) => { setBusy(true); setPending({ action: action.action, windowIndex: 'windowIndex' in action ? action.windowIndex : undefined, }); setActionError(null); try { const cardWillRemount = action.action === 'killSession' || action.action === 'renameSession'; const result = await runTmuxSessionAction({ sessionId, action, tmuxAction: backend.tmuxAction, onRefreshDetails: !cardWillRemount && expanded ? () => onRefreshDetails(session) : undefined, onSessionsChanged, onRequestTerminalFocus, }); if (!result.success) throw new Error(result.error || t('systemManager.errors.actionFailed')); } catch (err) { setActionError(err instanceof Error ? err.message : t('systemManager.errors.actionFailed')); } finally { setBusy(false); setPending(null); } }; const isPending = (action: TmuxManageAction['action'], windowIndex?: number) => pending !== null && pending.action === action && pending.windowIndex === windowIndex; const handleAttach = async (windowIndex?: number) => { const result = await openInteractiveTerminal( backend, parentSession, windowIndex !== undefined ? `tmux: ${session.name}:${windowIndex}` : `tmux: ${session.name}`, buildTmuxAttachCommand(session.name, windowIndex), { icon: TMUX_POPUP_ICON }, ); if (!result.success) { const message = result.error || t('systemManager.errors.actionFailed'); setActionError(message); showSystemManagerError(message, t('common.error')); } }; return ( <> { const nextExpanded = !expanded; setExpanded(nextExpanded); if (nextExpanded) { void onLoadDetails(session, { force: true, urgent: true }); } }} title={session.name} subtitle={t('systemManager.tmux.windows', { count: String(session.windows) })} trailing={(
{session.attached ? t('systemManager.tmux.attached') : t('systemManager.tmux.detached')} handleAttach()}> setRenamePrompt({ kind: 'session' })} > {session.attached && ( setDetachConfirmOpen(true)} > )} setKillSessionConfirmOpen(true)} >
)} /> {actionError && } {loadingDetails && windows.length === 0 && (
{t('systemManager.tmux.loadingDetails')}
)} {clients.length > 0 && (
{t('systemManager.tmux.clients')}: {clients.map((c) => c.tty || c.name).join(', ')}
)} setNewWindowOpen(true)} className="shrink-0 h-5 px-1.5 rounded text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/60 inline-flex items-center gap-1 disabled:opacity-40" > {isPending('createWindow') ? : } {t('systemManager.tmux.newWindow')} )} > {t('systemManager.tmux.windowList')}{windows.length > 0 ? ` ยท ${windows.length}` : ''} {windows.map((tmuxWindow) => ( handleAttach(tmuxWindow.index)} > setRenamePrompt({ kind: 'window', windowIndex: tmuxWindow.index, currentName: tmuxWindow.name, })} > setKillWindowConfirm({ windowIndex: tmuxWindow.index, windowName: tmuxWindow.name || String(tmuxWindow.index), })} > )} /> ))} {!loadingDetails && windows.length === 0 && (
{windowsLoadDetail || actionError || t('systemManager.tmux.noWindows')}
)}
{ setDetachConfirmOpen(false); void runAction({ action: 'detachSession', sessionName: session.name }); }} /> { setKillSessionConfirmOpen(false); void runAction({ action: 'killSession', sessionName: session.name }); }} /> { if (!open) setKillWindowConfirm(null); }} onConfirm={() => { const target = killWindowConfirm; setKillWindowConfirm(null); if (!target) return; void runAction({ action: 'killWindow', sessionName: session.name, windowIndex: target.windowIndex, }); }} /> { if (!open) setRenamePrompt(null); }} onSubmit={(values) => { const target = renamePrompt; setRenamePrompt(null); if (!target) return; if (target.kind === 'session') { if (values.name !== session.name) { void runAction({ action: 'renameSession', sessionName: session.name, newName: values.name }); } } else if (values.name !== target.currentName) { void runAction({ action: 'renameWindow', sessionName: session.name, windowIndex: target.windowIndex, newName: values.name, }); } }} /> { setNewWindowOpen(false); void runAction({ action: 'createWindow', sessionName: session.name, windowName: values.name || undefined, }); }} /> ); });