feat: update CLI timeout handling and add active execution state management

This commit is contained in:
catlog22
2026-01-04 22:14:43 +08:00
parent 81f4d084b0
commit 373f1d57c1
5 changed files with 112 additions and 7 deletions

View File

@@ -175,7 +175,7 @@ export function run(argv: string[]): void {
.option('--model <model>', 'Model override') .option('--model <model>', 'Model override')
.option('--cd <path>', 'Working directory') .option('--cd <path>', 'Working directory')
.option('--includeDirs <dirs>', 'Additional directories (--include-directories for gemini/qwen, --add-dir for codex/claude)') .option('--includeDirs <dirs>', 'Additional directories (--include-directories for gemini/qwen, --add-dir for codex/claude)')
.option('--timeout <ms>', 'Timeout in milliseconds', '300000') .option('--timeout <ms>', 'Timeout in milliseconds (0=disabled, controlled by external caller)', '0')
.option('--stream', 'Enable streaming output (default: non-streaming with caching)') .option('--stream', 'Enable streaming output (default: non-streaming with caching)')
.option('--limit <n>', 'History limit') .option('--limit <n>', 'History limit')
.option('--status <status>', 'Filter by status') .option('--status <status>', 'Filter by status')

View File

@@ -783,7 +783,7 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
model, model,
cd, cd,
includeDirs, includeDirs,
timeout: timeout ? parseInt(timeout, 10) : 300000, timeout: timeout ? parseInt(timeout, 10) : 0, // 0 = no internal timeout, controlled by external caller
resume, resume,
id, // custom execution ID id, // custom execution ID
noNative, noNative,

View File

@@ -55,6 +55,28 @@ export interface RouteContext {
broadcastToClients: (data: unknown) => void; 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<string, ActiveExecution>();
/**
* 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 * Handle CLI routes
* @returns true if route was handled, false otherwise * @returns true if route was handled, false otherwise
@@ -62,6 +84,14 @@ export interface RouteContext {
export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> { export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
const { pathname, url, req, res, initialPath, handlePostRequest, broadcastToClients } = ctx; 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 // API: CLI Tools Status
if (pathname === '/api/cli/status') { if (pathname === '/api/cli/status') {
const status = await getCliToolsStatus(); const status = await getCliToolsStatus();
@@ -504,6 +534,17 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
const executionId = `${Date.now()}-${tool}`; 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 // Broadcast execution started
broadcastToClients({ broadcastToClients({
type: 'CLI_EXECUTION_STARTED', type: 'CLI_EXECUTION_STARTED',
@@ -525,11 +566,17 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
model, model,
cd: dir || initialPath, cd: dir || initialPath,
includeDirs, includeDirs,
timeout: timeout || 300000, timeout: timeout || 0, // 0 = no internal timeout, controlled by external caller
category: category || 'user', category: category || 'user',
parentExecutionId, parentExecutionId,
stream: true stream: true
}, (chunk) => { }, (chunk) => {
// Append chunk to active execution buffer
const activeExec = activeExecutions.get(executionId);
if (activeExec) {
activeExec.output += chunk.data || '';
}
broadcastToClients({ broadcastToClients({
type: 'CLI_OUTPUT', type: 'CLI_OUTPUT',
payload: { payload: {
@@ -540,6 +587,9 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
}); });
}); });
// Remove from active executions on completion
activeExecutions.delete(executionId);
// Broadcast completion // Broadcast completion
broadcastToClients({ broadcastToClients({
type: 'CLI_EXECUTION_COMPLETED', type: 'CLI_EXECUTION_COMPLETED',
@@ -557,6 +607,9 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
}; };
} catch (error: unknown) { } catch (error: unknown) {
// Remove from active executions on error
activeExecutions.delete(executionId);
broadcastToClients({ broadcastToClients({
type: 'CLI_EXECUTION_ERROR', type: 'CLI_EXECUTION_ERROR',
payload: { payload: {

View File

@@ -9,6 +9,55 @@ var ccwEndpointTools = [];
var cliToolConfig = null; // Store loaded CLI config var cliToolConfig = null; // Store loaded CLI config
var predefinedModels = {}; // Store predefined models per tool 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 ========== // ========== Navigation Helpers ==========
/** /**
@@ -437,6 +486,9 @@ async function renderCliManager() {
// Initialize Lucide icons // Initialize Lucide icons
if (window.lucide) lucide.createIcons(); if (window.lucide) lucide.createIcons();
// Sync active executions to restore running state
await syncActiveExecutions();
} }
// ========== Helper Functions ========== // ========== Helper Functions ==========

View File

@@ -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); console.log('[CodexLens] Switching to tab:', tabName);
// Update tab buttons using direct style manipulation for reliability // Update tab buttons using direct style manipulation for reliability
@@ -3013,10 +3013,10 @@ function buildCodexLensManagerPage(config) {
// Tabs for Embedding / Reranker // Tabs for Embedding / Reranker
'<div class="border-b border-border">' + '<div class="border-b border-border">' +
'<div class="flex">' + '<div class="flex">' +
'<button class="model-tab flex-1 px-4 py-2.5 text-sm font-medium border-b-2 border-primary text-primary bg-primary/5" data-tab="embedding" onclick="switchModelTab(\'embedding\')">' + '<button class="model-tab flex-1 px-4 py-2.5 text-sm font-medium border-b-2 border-primary text-primary bg-primary/5" data-tab="embedding" onclick="switchCodexLensModelTab(\'embedding\')">' +
'<i data-lucide="layers" class="w-3.5 h-3.5 inline mr-1"></i>Embedding' + '<i data-lucide="layers" class="w-3.5 h-3.5 inline mr-1"></i>Embedding' +
'</button>' + '</button>' +
'<button class="model-tab flex-1 px-4 py-2.5 text-sm font-medium border-b-2 border-transparent text-muted-foreground hover:text-foreground" data-tab="reranker" onclick="switchModelTab(\'reranker\')">' + '<button class="model-tab flex-1 px-4 py-2.5 text-sm font-medium border-b-2 border-transparent text-muted-foreground hover:text-foreground" data-tab="reranker" onclick="switchCodexLensModelTab(\'reranker\')">' +
'<i data-lucide="arrow-up-down" class="w-3.5 h-3.5 inline mr-1"></i>Reranker' + '<i data-lucide="arrow-up-down" class="w-3.5 h-3.5 inline mr-1"></i>Reranker' +
'</button>' + '</button>' +
'</div>' + '</div>' +