// ======================================== // SessionGroupTree Component // ======================================== // Tree view for session groups with drag-and-drop support. // Sessions can be dragged between groups. Groups are expandable sections. // Uses @hello-pangea/dnd for drag-and-drop, sessionManagerStore for state. import { useState, useCallback, useMemo } from 'react'; import { useIntl } from 'react-intl'; import { DragDropContext, Droppable, Draggable, type DropResult, } from '@hello-pangea/dnd'; import { ChevronRight, FolderOpen, Folder, Plus, Terminal, GripVertical, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useSessionManagerStore, selectGroups, selectSessionManagerActiveTerminalId, selectTerminalMetas } from '@/stores'; import { useCliSessionStore } from '@/stores/cliSessionStore'; import { useTerminalGridStore, selectTerminalGridPanes } from '@/stores/terminalGridStore'; import { Badge } from '@/components/ui/Badge'; import type { TerminalStatus } from '@/types/terminal-dashboard'; // ========== Status Dot Styles ========== const statusDotStyles: Record = { active: 'bg-green-500', idle: 'bg-gray-400', error: 'bg-red-500', paused: 'bg-yellow-500', resuming: 'bg-blue-400 animate-pulse', }; // ========== SessionGroupTree Component ========== export function SessionGroupTree() { const { formatMessage } = useIntl(); const groups = useSessionManagerStore(selectGroups); const activeTerminalId = useSessionManagerStore(selectSessionManagerActiveTerminalId); const terminalMetas = useSessionManagerStore(selectTerminalMetas); const createGroup = useSessionManagerStore((s) => s.createGroup); const moveSessionToGroup = useSessionManagerStore((s) => s.moveSessionToGroup); const setActiveTerminal = useSessionManagerStore((s) => s.setActiveTerminal); const sessions = useCliSessionStore((s) => s.sessions); // Grid store for pane management const panes = useTerminalGridStore(selectTerminalGridPanes); const assignSession = useTerminalGridStore((s) => s.assignSession); const setFocused = useTerminalGridStore((s) => s.setFocused); const [expandedGroups, setExpandedGroups] = useState>(new Set()); const toggleGroup = useCallback((groupId: string) => { setExpandedGroups((prev) => { const next = new Set(prev); if (next.has(groupId)) { next.delete(groupId); } else { next.add(groupId); } return next; }); }, []); const handleCreateGroup = useCallback(() => { const name = formatMessage({ id: 'terminalDashboard.sessionTree.defaultGroupName' }); createGroup(name); }, [createGroup, formatMessage]); const handleSessionClick = useCallback( (sessionId: string) => { // Set active terminal in session manager setActiveTerminal(sessionId); // Find pane that already has this session, or switch focused pane const paneWithSession = Object.entries(panes).find( ([, pane]) => pane.sessionId === sessionId ); if (paneWithSession) { // Focus the pane that has this session setFocused(paneWithSession[0]); } else { // Find focused pane or first pane, and assign session to it const focusedPaneId = useTerminalGridStore.getState().focusedPaneId; const targetPaneId = focusedPaneId || Object.keys(panes)[0]; if (targetPaneId) { assignSession(targetPaneId, sessionId); setFocused(targetPaneId); } } }, [setActiveTerminal, panes, setFocused, assignSession] ); const handleDragEnd = useCallback( (result: DropResult) => { const { draggableId, destination } = result; if (!destination) return; // destination.droppableId is the target group ID const targetGroupId = destination.droppableId; moveSessionToGroup(draggableId, targetGroupId); }, [moveSessionToGroup] ); // Build a lookup for session display names const sessionNames = useMemo(() => { const map: Record = {}; for (const [key, meta] of Object.entries(sessions)) { map[key] = meta.tool ? `${meta.tool} - ${meta.shellKind}` : meta.shellKind; } return map; }, [sessions]); if (groups.length === 0) { return (

{formatMessage({ id: 'terminalDashboard.sessionTree.noGroups' })}

); } return (
{/* Create group button */}
{/* Groups with drag-and-drop */}
{groups.map((group) => { const isExpanded = expandedGroups.has(group.id); return (
{/* Group header */} {/* Expanded: droppable session list */} {isExpanded && ( {(provided, snapshot) => (
{group.sessionIds.length === 0 ? (

{formatMessage({ id: 'terminalDashboard.sessionTree.emptyGroup' })}

) : ( group.sessionIds.map((sessionId, index) => { const meta = terminalMetas[sessionId]; const sessionStatus: TerminalStatus = meta?.status ?? 'idle'; return ( {(dragProvided, dragSnapshot) => (
handleSessionClick(sessionId)} > {/* Status indicator dot */} {sessionNames[sessionId] ?? sessionId}
)}
); }) )} {provided.placeholder}
)}
)}
); })}
); } export default SessionGroupTree;