import { Plus, TerminalSquare } from 'lucide-react'; import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useI18n } from '../../application/i18n/I18nProvider'; import type { useSystemManagerBackend } from '../../application/state/useSystemManagerBackend'; import type { Snippet, TerminalSession } from '../../types'; import type { TmuxClientInfo, TmuxSessionInfo, TmuxWindowInfo } from '../../domain/systemManager/types'; import { tmuxSessionInfoEqual } from '../../domain/systemManager/pollEquals'; import { SystemPanelEmpty, SystemPanelError, SystemPanelIconButton, SystemPanelList, SystemPanelLoading, SystemPanelMetaBar, SystemPanelRefreshButton, SystemPanelSearch, SystemPanelShell, SystemPanelToolbar, } from './SystemPanelUi'; import { useAsyncRecordCache } from '../../application/state/systemManager/useAsyncRecordCache'; import { usePolling, useStableTranslate } from '../../application/state/useSystemManager'; import { TmuxNewSessionModal } from './TmuxNewSessionModal'; import { TmuxSessionCard } from './TmuxSessionCard'; import { useStableListOrder, mergePollListByKey } from './listStable'; type Backend = ReturnType; export interface TmuxSessionDetails { windows: TmuxWindowInfo[]; clients: TmuxClientInfo[]; } interface TmuxManagerTabProps { sessionId: string; parentSession: TerminalSession; isVisible: boolean; warmupEnabled?: boolean; backend: Backend; refreshIntervalSec: number; snippets: Snippet[]; onRequestTerminalFocus?: () => void; } export const TmuxManagerTab = memo(function TmuxManagerTab({ sessionId, parentSession, isVisible, warmupEnabled = false, backend, refreshIntervalSec, snippets, onRequestTerminalFocus, }: TmuxManagerTabProps) { const { t } = useI18n(); const stableT = useStableTranslate(); const [query, setQuery] = useState(''); const [modalOpen, setModalOpen] = useState(false); const [creating, setCreating] = useState(false); const [modalError, setModalError] = useState(null); const [tmuxVersion, setTmuxVersion] = useState(null); const currentSessionIdRef = useRef(sessionId); currentSessionIdRef.current = sessionId; useEffect(() => { setTmuxVersion(null); }, [sessionId]); const fetcher = useCallback(async () => { const fetchSessionId = sessionId; const result = await backend.listTmuxSessions(sessionId); const version = result.tmuxVersion ?? null; if (currentSessionIdRef.current === fetchSessionId) { setTmuxVersion((prev) => (prev === version ? prev : version)); } if (!result.success) { throw new Error(result.error || stableT('systemManager.errors.loadTmux')); } return result.sessions ?? []; }, [backend, sessionId, stableT]); const intervalMs = Math.max(2, refreshIntervalSec) * 1000; const { data: sessions, error, loading, refresh } = usePolling( fetcher, intervalMs, isVisible || warmupEnabled, (prev, next) => mergePollListByKey(prev, next, (s) => s.name, tmuxSessionInfoEqual), { poll: isVisible, resetKey: sessionId }, ); const filtered = useMemo(() => { const q = query.trim().toLowerCase(); const list = sessions ?? []; if (!q) return list; return list.filter((session) => session.name.toLowerCase().includes(q)); }, [query, sessions]); const compareSessions = useCallback( (a: TmuxSessionInfo, b: TmuxSessionInfo) => a.name.localeCompare(b.name), [], ); const displaySessions = useStableListOrder( filtered, (s) => s.name, query, compareSessions, ); const formatTmuxLoadError = useCallback(( message: string, debug?: { lastOutput?: string; tried?: string[] }, ) => { const parts = [message]; if (debug?.lastOutput) parts.push(debug.lastOutput); if (debug?.tried?.length) { parts.push(t('systemManager.tmux.lastCommand', { command: debug.tried[debug.tried.length - 1] ?? '' })); } return parts.filter(Boolean).join(' ยท '); }, [t]); const getTmuxDetailsKey = useCallback((session: TmuxSessionInfo) => ( `${sessionId}:${session.name}:${session.created}` ), [sessionId]); const fetchTmuxDetails = useCallback(async (session: TmuxSessionInfo): Promise => { const [windowsResult, clientsResult] = await Promise.all([ backend.listTmuxWindows({ sessionId, sessionName: session.name }), backend.listTmuxClients({ sessionId, sessionName: session.name }), ]); if (!windowsResult.success) { throw new Error(formatTmuxLoadError( windowsResult.error || stableT('systemManager.errors.loadTmuxWindows'), windowsResult.debug, )); } if (!clientsResult.success) { throw new Error(clientsResult.error || stableT('systemManager.errors.loadTmuxClients')); } const freshWindows = windowsResult.windows ?? []; if (freshWindows.length === 0 && session.windows > 0) { throw new Error(formatTmuxLoadError( stableT('systemManager.tmux.windowsMismatch', { count: String(session.windows) }), windowsResult.debug, )); } return { windows: freshWindows, clients: clientsResult.clients ?? [], }; }, [backend, formatTmuxLoadError, sessionId, stableT]); const { records: tmuxDetailsByName, loadRecord: loadTmuxDetails, refreshRecord: refreshTmuxDetails, } = useAsyncRecordCache({ items: sessions ?? [], enabled: isVisible && (sessions?.length ?? 0) > 0, getKey: getTmuxDetailsKey, fetchRecord: fetchTmuxDetails, prefetchLimit: 16, prefetchDelayMs: 40, staleTimeMs: 20_000, }); const handleCreate = useCallback(async (name: string, command: string) => { setCreating(true); setModalError(null); try { const result = await backend.createTmuxSession({ sessionId, name, command: command || undefined, }); if (!result.success) throw new Error(result.error); setModalOpen(false); await refresh(); } catch (err) { setModalError(err instanceof Error ? err.message : t('systemManager.errors.actionFailed')); } finally { setCreating(false); } }, [backend, refresh, sessionId, t]); return ( { setModalError(null); setModalOpen(true); }} > void refresh()} /> )} > {tmuxVersion} ) : undefined}> {t('systemManager.tmux.meta', { count: displaySessions.length })} {!error && displaySessions.length === 0 && loading && ( )} {!error && displaySessions.length === 0 && !loading && ( )} {error && ( void refresh()} retryLabel={t('history.action.retry')} loading={loading} /> )} {displaySessions.map((session) => ( ))} ); });