diff --git a/ccw/src/cli.ts b/ccw/src/cli.ts index fc441d47..b4685466 100644 --- a/ccw/src/cli.ts +++ b/ccw/src/cli.ts @@ -175,7 +175,7 @@ export function run(argv: string[]): void { .option('--model ', 'Model override') .option('--cd ', 'Working directory') .option('--includeDirs ', 'Additional directories (--include-directories for gemini/qwen, --add-dir for codex/claude)') - .option('--timeout ', 'Timeout in milliseconds', '300000') + .option('--timeout ', 'Timeout in milliseconds (0=disabled, controlled by external caller)', '0') .option('--stream', 'Enable streaming output (default: non-streaming with caching)') .option('--limit ', 'History limit') .option('--status ', 'Filter by status') diff --git a/ccw/src/commands/cli.ts b/ccw/src/commands/cli.ts index 2f2843d3..9420d1d7 100644 --- a/ccw/src/commands/cli.ts +++ b/ccw/src/commands/cli.ts @@ -783,7 +783,7 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec model, cd, includeDirs, - timeout: timeout ? parseInt(timeout, 10) : 300000, + timeout: timeout ? parseInt(timeout, 10) : 0, // 0 = no internal timeout, controlled by external caller resume, id, // custom execution ID noNative, diff --git a/ccw/src/core/routes/cli-routes.ts b/ccw/src/core/routes/cli-routes.ts index b594e8ed..d5d9f7ae 100644 --- a/ccw/src/core/routes/cli-routes.ts +++ b/ccw/src/core/routes/cli-routes.ts @@ -55,6 +55,28 @@ export interface RouteContext { broadcastToClients: (data: unknown) => void; } +// ========== Active Executions State ========== +// Stores running CLI executions for state recovery when view is opened/refreshed +interface ActiveExecution { + id: string; + tool: string; + mode: string; + prompt: string; + startTime: number; + output: string; + status: 'running' | 'completed' | 'error'; +} + +const activeExecutions = new Map(); + +/** + * Get all active CLI executions + * Used by frontend to restore state when view is opened during execution + */ +export function getActiveExecutions(): ActiveExecution[] { + return Array.from(activeExecutions.values()); +} + /** * Handle CLI routes * @returns true if route was handled, false otherwise @@ -62,6 +84,14 @@ export interface RouteContext { export async function handleCliRoutes(ctx: RouteContext): Promise { const { pathname, url, req, res, initialPath, handlePostRequest, broadcastToClients } = ctx; + // API: Get Active CLI Executions (for state recovery) + if (pathname === '/api/cli/active' && req.method === 'GET') { + const executions = getActiveExecutions(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ executions })); + return true; + } + // API: CLI Tools Status if (pathname === '/api/cli/status') { const status = await getCliToolsStatus(); @@ -504,6 +534,17 @@ export async function handleCliRoutes(ctx: RouteContext): Promise { const executionId = `${Date.now()}-${tool}`; + // Store active execution for state recovery + activeExecutions.set(executionId, { + id: executionId, + tool, + mode: mode || 'analysis', + prompt: prompt.substring(0, 500), // Truncate for display + startTime: Date.now(), + output: '', + status: 'running' + }); + // Broadcast execution started broadcastToClients({ type: 'CLI_EXECUTION_STARTED', @@ -525,11 +566,17 @@ export async function handleCliRoutes(ctx: RouteContext): Promise { model, cd: dir || initialPath, includeDirs, - timeout: timeout || 300000, + timeout: timeout || 0, // 0 = no internal timeout, controlled by external caller category: category || 'user', parentExecutionId, stream: true }, (chunk) => { + // Append chunk to active execution buffer + const activeExec = activeExecutions.get(executionId); + if (activeExec) { + activeExec.output += chunk.data || ''; + } + broadcastToClients({ type: 'CLI_OUTPUT', payload: { @@ -540,6 +587,9 @@ export async function handleCliRoutes(ctx: RouteContext): Promise { }); }); + // Remove from active executions on completion + activeExecutions.delete(executionId); + // Broadcast completion broadcastToClients({ type: 'CLI_EXECUTION_COMPLETED', @@ -557,6 +607,9 @@ export async function handleCliRoutes(ctx: RouteContext): Promise { }; } catch (error: unknown) { + // Remove from active executions on error + activeExecutions.delete(executionId); + broadcastToClients({ type: 'CLI_EXECUTION_ERROR', payload: { diff --git a/ccw/src/templates/dashboard-js/views/cli-manager.js b/ccw/src/templates/dashboard-js/views/cli-manager.js index a9d66c3c..7acdbccd 100644 --- a/ccw/src/templates/dashboard-js/views/cli-manager.js +++ b/ccw/src/templates/dashboard-js/views/cli-manager.js @@ -9,6 +9,55 @@ var ccwEndpointTools = []; var cliToolConfig = null; // Store loaded CLI config var predefinedModels = {}; // Store predefined models per tool +// ========== Active Execution Sync ========== + +/** + * Sync active CLI executions from server + * Called when view is opened to restore running execution state + */ +async function syncActiveExecutions() { + try { + var response = await fetch('/api/cli/active'); + if (!response.ok) return; + + var data = await response.json(); + if (!data.executions || data.executions.length === 0) return; + + // Restore the first active execution + var active = data.executions[0]; + + // Restore execution state + currentCliExecution = { + executionId: active.id, + tool: active.tool, + mode: active.mode, + startTime: active.startTime + }; + cliExecutionOutput = active.output || ''; + + // Update UI if output panel exists + var outputPanel = document.getElementById('cli-output-panel'); + var outputContent = document.getElementById('cli-output-content'); + + if (outputPanel && outputContent) { + outputPanel.style.display = 'block'; + outputContent.textContent = cliExecutionOutput; + outputContent.scrollTop = outputContent.scrollHeight; + + // Update status indicator + var statusIndicator = outputPanel.querySelector('.cli-status-indicator'); + if (statusIndicator) { + statusIndicator.className = 'cli-status-indicator running'; + statusIndicator.textContent = t('cli.running') || 'Running...'; + } + } + + console.log('[CLI Manager] Restored active execution:', active.id); + } catch (err) { + console.warn('[CLI Manager] Failed to sync active executions:', err); + } +} + // ========== Navigation Helpers ========== /** @@ -437,6 +486,9 @@ async function renderCliManager() { // Initialize Lucide icons if (window.lucide) lucide.createIcons(); + + // Sync active executions to restore running state + await syncActiveExecutions(); } // ========== Helper Functions ========== diff --git a/ccw/src/templates/dashboard-js/views/codexlens-manager.js b/ccw/src/templates/dashboard-js/views/codexlens-manager.js index 74dfaabb..63ec89a0 100644 --- a/ccw/src/templates/dashboard-js/views/codexlens-manager.js +++ b/ccw/src/templates/dashboard-js/views/codexlens-manager.js @@ -1854,9 +1854,9 @@ async function selectRerankerModel(modelName) { // ============================================================ /** - * Switch between Embedding and Reranker tabs + * Switch between Embedding and Reranker tabs in CodexLens manager */ -function switchModelTab(tabName) { +function switchCodexLensModelTab(tabName) { console.log('[CodexLens] Switching to tab:', tabName); // Update tab buttons using direct style manipulation for reliability @@ -3013,10 +3013,10 @@ function buildCodexLensManagerPage(config) { // Tabs for Embedding / Reranker '
' + '
' + - '' + - '' + '
' +