// ======================================== // DashboardToolbar Component // ======================================== // Top toolbar for Terminal Dashboard V2. // Provides toggle buttons for floating panels (Issues/Queue/Inspector) // and layout preset controls. Sessions sidebar is always visible. import { useCallback, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; import { AlertCircle, ListChecks, Info, FolderOpen, LayoutGrid, Columns2, Rows2, Square, Loader2, Folder, Maximize2, Minimize2, Activity, Plus, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Badge } from '@/components/ui/Badge'; import { useIssueQueueIntegrationStore, selectAssociationChain, } from '@/stores/issueQueueIntegrationStore'; import { useIssues, useIssueQueue } from '@/hooks/useIssues'; import { useTerminalGridStore, selectTerminalGridFocusedPaneId } from '@/stores/terminalGridStore'; import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore'; import { toast } from '@/stores/notificationStore'; import { useExecutionMonitorStore, selectActiveExecutionCount } from '@/stores/executionMonitorStore'; import { useSessionManagerStore } from '@/stores/sessionManagerStore'; import { useConfigStore } from '@/stores/configStore'; import { CliConfigModal, type CliSessionConfig } from './CliConfigModal'; // ========== Types ========== export type PanelId = 'issues' | 'queue' | 'inspector' | 'execution'; interface DashboardToolbarProps { activePanel: PanelId | null; onTogglePanel: (panelId: PanelId) => void; /** Whether the file sidebar is open */ isFileSidebarOpen?: boolean; /** Callback to toggle file sidebar */ onToggleFileSidebar?: () => void; /** Whether the session sidebar is open */ isSessionSidebarOpen?: boolean; /** Callback to toggle session sidebar */ onToggleSessionSidebar?: () => void; /** Whether fullscreen mode is active */ isFullscreen?: boolean; /** Callback to toggle fullscreen mode */ onToggleFullscreen?: () => void; } // ========== Layout Presets ========== const LAYOUT_PRESETS = [ { id: 'single' as const, icon: Square, labelId: 'terminalDashboard.toolbar.layoutSingle' }, { id: 'split-h' as const, icon: Columns2, labelId: 'terminalDashboard.toolbar.layoutSplitH' }, { id: 'split-v' as const, icon: Rows2, labelId: 'terminalDashboard.toolbar.layoutSplitV' }, { id: 'grid-2x2' as const, icon: LayoutGrid, labelId: 'terminalDashboard.toolbar.layoutGrid' }, ]; // ========== Component ========== export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen, onToggleFileSidebar, isSessionSidebarOpen, onToggleSessionSidebar, isFullscreen, onToggleFullscreen }: DashboardToolbarProps) { const { formatMessage } = useIntl(); // Issues count const { openCount } = useIssues(); // Queue count const queueQuery = useIssueQueue(); const queueCount = useMemo(() => { if (!queueQuery.data) return 0; const grouped = queueQuery.data.grouped_items ?? {}; let count = 0; for (const items of Object.values(grouped)) { count += items.length; } return count; }, [queueQuery.data]); // Inspector chain indicator const associationChain = useIssueQueueIntegrationStore(selectAssociationChain); const hasChain = associationChain !== null; // Execution monitor count const executionCount = useExecutionMonitorStore(selectActiveExecutionCount); // Feature flags for panel visibility const featureFlags = useConfigStore((s) => s.featureFlags); const showQueue = featureFlags.dashboardQueuePanelEnabled; const showInspector = featureFlags.dashboardInspectorEnabled; const showExecution = featureFlags.dashboardExecutionMonitorEnabled; // Layout preset handler const resetLayout = useTerminalGridStore((s) => s.resetLayout); const handlePreset = useCallback( (preset: 'single' | 'split-h' | 'split-v' | 'grid-2x2') => { resetLayout(preset); }, [resetLayout] ); // Launch CLI handlers const projectPath = useWorkflowStore(selectProjectPath); const focusedPaneId = useTerminalGridStore(selectTerminalGridFocusedPaneId); const createSessionAndAssign = useTerminalGridStore((s) => s.createSessionAndAssign); const updateTerminalMeta = useSessionManagerStore((s) => s.updateTerminalMeta); const [isCreating, setIsCreating] = useState(false); const [isConfigOpen, setIsConfigOpen] = useState(false); // Helper to get or create a focused pane const getOrCreateFocusedPane = useCallback(() => { if (focusedPaneId) return focusedPaneId; // No focused pane - reset layout to create a single pane resetLayout('single'); // Get the new focused pane id from store return useTerminalGridStore.getState().focusedPaneId; }, [focusedPaneId]); // Open config modal const handleOpenConfig = useCallback(() => { setIsConfigOpen(true); }, []); // Create session from config modal const handleCreateConfiguredSession = useCallback(async (config: CliSessionConfig) => { if (!projectPath) throw new Error('No project path'); setIsCreating(true); try { const targetPaneId = getOrCreateFocusedPane(); if (!targetPaneId) throw new Error('Failed to create pane'); const result = await createSessionAndAssign( targetPaneId, { workingDir: config.workingDir || projectPath, preferredShell: config.preferredShell, tool: config.tool, model: config.model, launchMode: config.launchMode, settingsEndpointId: config.settingsEndpointId, }, projectPath ); // Store tag in terminalMetas for grouping if (result?.session?.sessionKey) { updateTerminalMeta(result.session.sessionKey, { tag: config.tag, title: config.tag, }); } } catch (error: unknown) { const message = error instanceof Error ? error.message : (error as { message?: string })?.message ? (error as { message: string }).message : String(error); toast.error(`CLI 会话创建失败 (${config.tool})`, message); throw error; } finally { setIsCreating(false); } }, [projectPath, createSessionAndAssign, getOrCreateFocusedPane, updateTerminalMeta]); return ( <>
{/* Launch CLI button - opens config dialog */} {/* Separator */}
{/* Session sidebar toggle */} onToggleSessionSidebar?.()} /> {/* Separator */}
{/* Panel toggle buttons */} onTogglePanel('issues')} badge={openCount > 0 ? openCount : undefined} /> {showQueue && ( onTogglePanel('queue')} badge={queueCount > 0 ? queueCount : undefined} /> )} {showInspector && ( onTogglePanel('inspector')} dot={hasChain} /> )} {showExecution && ( onTogglePanel('execution')} badge={executionCount > 0 ? executionCount : undefined} /> )} onToggleFileSidebar?.()} /> {/* Separator */}
{/* Layout presets */} {LAYOUT_PRESETS.map((preset) => ( ))} {/* Separator */}
{/* Fullscreen toggle */} {/* Right-aligned title */} {formatMessage({ id: 'terminalDashboard.page.title' })}
setIsConfigOpen(false)} defaultWorkingDir={projectPath} onCreateSession={handleCreateConfiguredSession} /> ); } // ========== Toolbar Button ========== function ToolbarButton({ icon: Icon, label, isActive, onClick, badge, dot, }: { icon: React.ComponentType<{ className?: string }>; label: string; isActive: boolean; onClick: () => void; badge?: number; dot?: boolean; }) { return ( ); }