From f28b6c6197a6e2472c0a89409e7e293dc3d64f11 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sat, 3 Jan 2026 23:47:02 +0800 Subject: [PATCH] feat: implement CodexLens watcher status handling and UI updates --- ccw/src/core/routes/codexlens-routes.ts | 40 +++++++++++++++++++ .../dashboard-js/components/notifications.js | 17 ++++++++ .../dashboard-js/views/codexlens-manager.js | 30 ++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/ccw/src/core/routes/codexlens-routes.ts b/ccw/src/core/routes/codexlens-routes.ts index dd1b2f01..e7738efc 100644 --- a/ccw/src/core/routes/codexlens-routes.ts +++ b/ccw/src/core/routes/codexlens-routes.ts @@ -1374,6 +1374,19 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise start_time: new Date() }; + // Capture stderr for error messages (capped at 4KB to prevent memory leak) + const MAX_STDERR_SIZE = 4096; + let stderrBuffer = ''; + if (watcherProcess.stderr) { + watcherProcess.stderr.on('data', (data: Buffer) => { + stderrBuffer += data.toString(); + // Cap buffer size to prevent memory leak in long-running watchers + if (stderrBuffer.length > MAX_STDERR_SIZE) { + stderrBuffer = stderrBuffer.slice(-MAX_STDERR_SIZE); + } + }); + } + // Handle process output for event counting if (watcherProcess.stdout) { watcherProcess.stdout.on('data', (data: Buffer) => { @@ -1386,11 +1399,38 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise }); } + // Handle spawn errors (e.g., ENOENT) + watcherProcess.on('error', (err: Error) => { + console.error(`[CodexLens] Watcher spawn error: ${err.message}`); + watcherStats.running = false; + watcherProcess = null; + broadcastToClients({ + type: 'CODEXLENS_WATCHER_STATUS', + payload: { running: false, error: `Spawn error: ${err.message}` } + }); + }); + // Handle process exit watcherProcess.on('exit', (code: number) => { watcherStats.running = false; watcherProcess = null; console.log(`[CodexLens] Watcher exited with code ${code}`); + + // Broadcast error if exited with non-zero code + if (code !== 0) { + const errorMsg = stderrBuffer.trim() || `Exited with code ${code}`; + // Use stripAnsiCodes helper for consistent ANSI cleanup + const cleanError = stripAnsiCodes(errorMsg); + broadcastToClients({ + type: 'CODEXLENS_WATCHER_STATUS', + payload: { running: false, error: cleanError } + }); + } else { + broadcastToClients({ + type: 'CODEXLENS_WATCHER_STATUS', + payload: { running: false } + }); + } }); // Broadcast watcher started diff --git a/ccw/src/templates/dashboard-js/components/notifications.js b/ccw/src/templates/dashboard-js/components/notifications.js index 14585299..f5bc0e70 100644 --- a/ccw/src/templates/dashboard-js/components/notifications.js +++ b/ccw/src/templates/dashboard-js/components/notifications.js @@ -455,6 +455,23 @@ function handleNotification(data) { console.log('[CodexLens] Index progress:', payload.stage, payload.percent + '%'); break; + case 'CODEXLENS_WATCHER_STATUS': + // Handle CodexLens file watcher status updates + if (typeof handleWatcherStatusUpdate === 'function') { + handleWatcherStatusUpdate(payload); + } + if (payload.error) { + console.error('[CodexLens] Watcher error:', payload.error); + if (typeof showRefreshToast === 'function') { + showRefreshToast('Watcher error: ' + payload.error, 'error'); + } + } else if (payload.running) { + console.log('[CodexLens] Watcher running:', payload.path); + } else { + console.log('[CodexLens] Watcher stopped'); + } + break; + default: console.log('[WS] Unknown notification type:', type); } diff --git a/ccw/src/templates/dashboard-js/views/codexlens-manager.js b/ccw/src/templates/dashboard-js/views/codexlens-manager.js index 4b1febdb..144521d8 100644 --- a/ccw/src/templates/dashboard-js/views/codexlens-manager.js +++ b/ccw/src/templates/dashboard-js/views/codexlens-manager.js @@ -4783,3 +4783,33 @@ function closeWatcherModal() { var modal = document.getElementById('watcherControlModal'); if (modal) modal.remove(); } + +/** + * Handle watcher status update from WebSocket + * @param {Object} payload - { running: boolean, path?: string, error?: string } + */ +function handleWatcherStatusUpdate(payload) { + var toggle = document.getElementById('watcherToggle'); + var statsDiv = document.getElementById('watcherStats'); + var configDiv = document.getElementById('watcherStartConfig'); + + if (payload.error) { + // Watcher failed - update UI to show stopped state + if (toggle) toggle.checked = false; + if (statsDiv) statsDiv.style.display = 'none'; + if (configDiv) configDiv.style.display = 'block'; + stopWatcherStatusPolling(); + } else if (payload.running) { + // Watcher started + if (toggle) toggle.checked = true; + if (statsDiv) statsDiv.style.display = 'block'; + if (configDiv) configDiv.style.display = 'none'; + startWatcherStatusPolling(); + } else { + // Watcher stopped normally + if (toggle) toggle.checked = false; + if (statsDiv) statsDiv.style.display = 'none'; + if (configDiv) configDiv.style.display = 'block'; + stopWatcherStatusPolling(); + } +}