diff --git a/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css b/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css index 0a8c5b5b..415ed451 100644 --- a/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css +++ b/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css @@ -206,21 +206,35 @@ /* ===== Tab Bar ===== */ .cli-stream-tabs { display: flex; + flex-wrap: nowrap; gap: 2px; padding: 8px 12px; border-bottom: 1px solid hsl(var(--border)); background: hsl(var(--muted) / 0.2); overflow-x: auto; + overflow-y: hidden; scrollbar-width: thin; + scroll-behavior: smooth; + -webkit-overflow-scrolling: touch; } +/* Show scrollbar on hover for better UX */ .cli-stream-tabs::-webkit-scrollbar { - height: 4px; + height: 6px; +} + +.cli-stream-tabs::-webkit-scrollbar-track { + background: transparent; } .cli-stream-tabs::-webkit-scrollbar-thumb { background: hsl(var(--border)); - border-radius: 2px; + border-radius: 3px; + transition: background 0.2s; +} + +.cli-stream-tabs::-webkit-scrollbar-thumb:hover { + background: hsl(var(--muted-foreground) / 0.5); } .cli-stream-tab { @@ -235,6 +249,7 @@ color: hsl(var(--muted-foreground)); cursor: pointer; white-space: nowrap; + flex-shrink: 0; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); } diff --git a/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js b/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js index 6d453a1d..2e869b36 100644 --- a/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +++ b/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js @@ -12,6 +12,82 @@ let searchFilter = ''; // Search filter for output content const MAX_OUTPUT_LINES = 5000; // Prevent memory issues +// ===== State Synchronization ===== +/** + * Sync active executions from server + * Called on initialization to recover state when view is opened mid-execution + */ +async function syncActiveExecutions() { + // Only sync in server mode + if (!window.SERVER_MODE) return; + + try { + const response = await fetch('/api/cli/active'); + if (!response.ok) return; + + const { executions } = await response.json(); + if (!executions || executions.length === 0) return; + + executions.forEach(exec => { + // Skip if already tracked (avoid overwriting live data) + if (cliStreamExecutions[exec.id]) return; + + // Rebuild execution state + cliStreamExecutions[exec.id] = { + tool: exec.tool || 'cli', + mode: exec.mode || 'analysis', + output: [], + status: exec.status || 'running', + startTime: exec.startTime || Date.now(), + endTime: null + }; + + // Add system start message + cliStreamExecutions[exec.id].output.push({ + type: 'system', + content: `[${new Date(exec.startTime).toLocaleTimeString()}] CLI execution started: ${exec.tool} (${exec.mode} mode)`, + timestamp: exec.startTime + }); + + // Fill historical output (limit to last MAX_OUTPUT_LINES) + if (exec.output) { + const lines = exec.output.split('\n'); + const startIndex = Math.max(0, lines.length - MAX_OUTPUT_LINES + 1); + lines.slice(startIndex).forEach(line => { + if (line.trim()) { + cliStreamExecutions[exec.id].output.push({ + type: 'stdout', + content: line, + timestamp: Date.now() + }); + } + }); + } + }); + + // Update UI if we recovered any executions + if (executions.length > 0) { + // Set active tab to first running execution + const runningExec = executions.find(e => e.status === 'running'); + if (runningExec && !activeStreamTab) { + activeStreamTab = runningExec.id; + } + + renderStreamTabs(); + updateStreamBadge(); + + // If viewer is open, render content + if (isCliStreamViewerOpen) { + renderStreamContent(activeStreamTab); + } + } + + console.log(`[CLI Stream] Synced ${executions.length} active execution(s)`); + } catch (e) { + console.error('[CLI Stream] Sync failed:', e); + } +} + // ===== Initialization ===== function initCliStreamViewer() { // Initialize keyboard shortcuts @@ -39,6 +115,9 @@ function initCliStreamViewer() { if (content) { content.addEventListener('scroll', handleStreamContentScroll); } + + // Sync active executions from server (recover state for mid-execution joins) + syncActiveExecutions(); } // ===== Panel Control =====