diff --git a/.claude/workflows/intelligent-tools-strategy.md b/.claude/workflows/intelligent-tools-strategy.md index bbf98cf7..07177f9d 100644 --- a/.claude/workflows/intelligent-tools-strategy.md +++ b/.claude/workflows/intelligent-tools-strategy.md @@ -51,7 +51,7 @@ ccw cli exec "" --tool codex --mode auto --cd ./project --includeDirs ./ |---------|-------------| | `ccw cli status` | Check CLI tools availability | | `ccw cli exec ""` | Execute a CLI tool | -| `ccw cli resume [id]` | Resume a previous session | +| `ccw cli exec "" --resume [id]` | Resume a previous session | | `ccw cli history` | Show execution history | | `ccw cli detail ` | Show execution detail | @@ -122,38 +122,45 @@ ccw cli exec "" --tool codex --mode auto --cd ./project --includeDirs ./ **Default MODE**: No default, must be explicitly specified -### Session Management +### Session Resume -**Resume Commands** (unified via CCW): +**Resume via `--resume` parameter** (integrated into exec): ```bash -# Resume last session (any tool) -ccw cli resume --last +# Resume last session with continuation prompt +ccw cli exec "Now add error handling" --resume --tool gemini +ccw cli exec "Continue analyzing security" --resume --tool gemini -# Resume last session for specific tool -ccw cli resume --last --tool gemini -ccw cli resume --last --tool codex +# Resume specific session by ID with prompt +ccw cli exec "Fix the issues you found" --resume --tool gemini -# Resume specific session by ID -ccw cli resume - -# Resume with additional prompt -ccw cli resume --last --prompt "Continue with error handling" - -# Codex native interactive picker -ccw cli resume --tool codex +# Resume last session (empty --resume = last) +ccw cli exec "Continue analysis" --resume ``` -**Resume Options**: -| Option | Description | -|--------|-------------| -| `--last` | Resume most recent session | -| `--tool ` | Filter by tool (gemini, qwen, codex) | -| `--prompt ` | Additional prompt for continuation | -| `[id]` | Specific execution ID to resume | +**Resume Parameter**: +| Value | Description | +|-------|-------------| +| `--resume` (empty) | Resume most recent session | +| `--resume ` | Resume specific execution ID | + +**Context Assembly** (automatic): +``` +=== PREVIOUS CONVERSATION === + +USER PROMPT: +[Previous prompt content] + +ASSISTANT RESPONSE: +[Previous output] + +=== CONTINUATION === + +[Your new prompt content here] +``` **Tool-Specific Behavior**: -- **Codex**: Uses native `codex resume` command (supports interactive picker) -- **Gemini/Qwen**: Loads previous conversation context and continues with new prompt +- **Codex**: Uses native `codex resume` command +- **Gemini/Qwen**: Assembles previous prompt + response + new prompt as single context --- diff --git a/ccw/src/cli.ts b/ccw/src/cli.ts index b5f61a08..138f5966 100644 --- a/ccw/src/cli.ts +++ b/ccw/src/cli.ts @@ -160,8 +160,7 @@ export function run(argv: string[]): void { .option('--no-stream', 'Disable streaming output') .option('--limit ', 'History limit') .option('--status ', 'Filter by status') - .option('--last', 'Resume most recent session') - .option('--prompt ', 'Additional prompt for resume continuation') + .option('--resume [id]', 'Resume previous session (empty=last, or execution ID)') .action((subcommand, args, options) => cliCommand(subcommand, args, options)); program.parse(argv); diff --git a/ccw/src/commands/cli.ts b/ccw/src/commands/cli.ts index 1e01330d..a0c5cae1 100644 --- a/ccw/src/commands/cli.ts +++ b/ccw/src/commands/cli.ts @@ -8,8 +8,7 @@ import { cliExecutorTool, getCliToolsStatus, getExecutionHistory, - getExecutionDetail, - resumeCliSession + getExecutionDetail } from '../tools/cli-executor.js'; interface CliExecOptions { @@ -20,6 +19,7 @@ interface CliExecOptions { includeDirs?: string; timeout?: string; noStream?: boolean; + resume?: string | boolean; // true = last, string = execution ID } interface HistoryOptions { @@ -28,12 +28,6 @@ interface HistoryOptions { status?: string; } -interface ResumeOptions { - tool?: string; - last?: boolean; - prompt?: string; -} - /** * Show CLI tool status */ @@ -67,9 +61,11 @@ async function execAction(prompt: string | undefined, options: CliExecOptions): process.exit(1); } - const { tool = 'gemini', mode = 'analysis', model, cd, includeDirs, timeout, noStream } = options; + const { tool = 'gemini', mode = 'analysis', model, cd, includeDirs, timeout, noStream, resume } = options; - console.log(chalk.cyan(`\n Executing ${tool} (${mode} mode)...\n`)); + // Show execution mode + const resumeInfo = resume ? (typeof resume === 'string' ? ` resuming ${resume}` : ' resuming last') : ''; + console.log(chalk.cyan(`\n Executing ${tool} (${mode} mode${resumeInfo})...\n`)); // Streaming output handler const onOutput = noStream ? null : (chunk: any) => { @@ -84,7 +80,8 @@ async function execAction(prompt: string | undefined, options: CliExecOptions): model, cd, includeDirs, - timeout: timeout ? parseInt(timeout, 10) : 300000 + timeout: timeout ? parseInt(timeout, 10) : 300000, + resume // pass resume parameter }, onOutput); // If not streaming, print output now @@ -97,7 +94,7 @@ async function execAction(prompt: string | undefined, options: CliExecOptions): if (result.success) { console.log(chalk.green(` ✓ Completed in ${(result.execution.duration_ms / 1000).toFixed(1)}s`)); console.log(chalk.gray(` ID: ${result.execution.id}`)); - console.log(chalk.dim(` Resume: ccw cli resume ${result.execution.id}`)); + console.log(chalk.dim(` Resume: ccw cli exec "..." --resume ${result.execution.id}`)); } else { console.log(chalk.red(` ✗ Failed (${result.execution.status})`)); console.log(chalk.gray(` ID: ${result.execution.id}`)); @@ -194,64 +191,6 @@ async function detailAction(executionId: string | undefined): Promise { console.log(); } -/** - * Resume a CLI session - * @param {string | undefined} executionId - Optional execution ID to resume - * @param {Object} options - CLI options - */ -async function resumeAction(executionId: string | undefined, options: ResumeOptions): Promise { - const { tool, last, prompt } = options; - - // Determine resume mode - let resumeMode = ''; - if (executionId) { - resumeMode = `session ${executionId}`; - } else if (last) { - resumeMode = tool ? `last ${tool} session` : 'last session'; - } else if (tool === 'codex') { - resumeMode = 'codex (interactive picker)'; - } else { - console.error(chalk.red('Error: Please specify --last or provide an execution ID')); - console.error(chalk.gray('Usage: ccw cli resume [id] --last --tool gemini')); - process.exit(1); - } - - console.log(chalk.cyan(`\n Resuming ${resumeMode}...\n`)); - - try { - const result = await resumeCliSession( - process.cwd(), - { - tool, - executionId, - last, - prompt - }, - (chunk) => { - process.stdout.write(chunk.data); - } - ); - - console.log(); - if (result.success) { - console.log(chalk.green(` ✓ Completed in ${(result.execution.duration_ms / 1000).toFixed(1)}s`)); - console.log(chalk.gray(` ID: ${result.execution.id}`)); - console.log(chalk.dim(` Resume: ccw cli resume ${result.execution.id}`)); - } else { - console.log(chalk.red(` ✗ Failed (${result.execution.status})`)); - console.log(chalk.gray(` ID: ${result.execution.id}`)); - if (result.stderr) { - console.error(chalk.red(result.stderr)); - } - process.exit(1); - } - } catch (error) { - const err = error as Error; - console.error(chalk.red(` Error: ${err.message}`)); - process.exit(1); - } -} - /** * Get human-readable time ago string * @param {Date} date @@ -269,14 +208,14 @@ function getTimeAgo(date: Date): string { /** * CLI command entry point - * @param {string} subcommand - Subcommand (status, exec, history, detail, resume) + * @param {string} subcommand - Subcommand (status, exec, history, detail) * @param {string[]} args - Arguments array * @param {Object} options - CLI options */ export async function cliCommand( subcommand: string, args: string | string[], - options: CliExecOptions | HistoryOptions | ResumeOptions + options: CliExecOptions | HistoryOptions ): Promise { const argsArray = Array.isArray(args) ? args : (args ? [args] : []); @@ -297,17 +236,12 @@ export async function cliCommand( await detailAction(argsArray[0]); break; - case 'resume': - await resumeAction(argsArray[0], options as ResumeOptions); - break; - default: console.log(chalk.bold.cyan('\n CCW CLI Tool Executor\n')); console.log(' Unified interface for Gemini, Qwen, and Codex CLI tools.\n'); console.log(' Subcommands:'); console.log(chalk.gray(' status Check CLI tools availability')); console.log(chalk.gray(' exec Execute a CLI tool')); - console.log(chalk.gray(' resume [id] Resume a previous session')); console.log(chalk.gray(' history Show execution history')); console.log(chalk.gray(' detail Show execution detail')); console.log(); @@ -321,12 +255,7 @@ export async function cliCommand( console.log(chalk.gray(' → codex: --add-dir')); console.log(chalk.gray(' --timeout Timeout in milliseconds (default: 300000)')); console.log(chalk.gray(' --no-stream Disable streaming output')); - console.log(); - console.log(' Resume Options:'); - console.log(chalk.gray(' --last Resume most recent session')); - console.log(chalk.gray(' --tool Tool filter (gemini, qwen, codex)')); - console.log(chalk.gray(' --prompt Additional prompt for continuation')); - console.log(chalk.gray(' [id] Specific execution ID to resume')); + console.log(chalk.gray(' --resume [id] Resume previous session (empty=last, or execution ID)')); console.log(); console.log(' History Options:'); console.log(chalk.gray(' --limit Number of results (default: 20)')); @@ -338,10 +267,8 @@ export async function cliCommand( console.log(chalk.gray(' ccw cli exec "Analyze the auth module" --tool gemini')); console.log(chalk.gray(' ccw cli exec "Analyze with context" --tool gemini --includeDirs ../shared,../types')); console.log(chalk.gray(' ccw cli exec "Implement feature" --tool codex --mode auto --includeDirs ./lib')); - console.log(chalk.gray(' ccw cli resume --last # Resume last session')); - console.log(chalk.gray(' ccw cli resume --last --tool gemini # Resume last Gemini session')); - console.log(chalk.gray(' ccw cli resume --tool codex # Codex interactive picker')); - console.log(chalk.gray(' ccw cli resume --prompt "Continue..." # Resume specific session')); + console.log(chalk.gray(' ccw cli exec "Continue analysis" --resume # Resume last session')); + console.log(chalk.gray(' ccw cli exec "Continue..." --resume --tool gemini # Resume specific session')); console.log(chalk.gray(' ccw cli history --tool gemini --limit 10')); console.log(); } diff --git a/ccw/src/core/server.ts b/ccw/src/core/server.ts index a4ebb4f3..622e09fe 100644 --- a/ccw/src/core/server.ts +++ b/ccw/src/core/server.ts @@ -8,7 +8,7 @@ import { createHash } from 'crypto'; import { scanSessions } from './session-scanner.js'; import { aggregateData } from './data-aggregator.js'; import { resolvePath, getRecentPaths, trackRecentPath, removeRecentPath, normalizePathForDisplay, getWorkflowDir } from '../utils/path-resolver.js'; -import { getCliToolsStatus, getExecutionHistory, getExecutionDetail, deleteExecution, executeCliTool, resumeCliSession } from '../tools/cli-executor.js'; +import { getCliToolsStatus, getExecutionHistory, getExecutionDetail, deleteExecution, executeCliTool } from '../tools/cli-executor.js'; import { getAllManifests } from './manifest.js'; import { checkVenvStatus, bootstrapVenv, executeCodexLens, checkSemanticStatus, installSemantic } from '../tools/codex-lens.js'; import { listTools } from '../tools/index.js'; @@ -764,81 +764,6 @@ export async function startServer(options: ServerOptions = {}): Promise { - const { executionId, tool, last, prompt } = body as { - executionId?: string; - tool?: string; - last?: boolean; - prompt?: string; - }; - - if (!executionId && !last && tool !== 'codex') { - return { error: 'executionId or --last flag is required', status: 400 }; - } - - // Broadcast resume started - const resumeId = `${Date.now()}-resume`; - broadcastToClients({ - type: 'CLI_EXECUTION_STARTED', - payload: { - executionId: resumeId, - tool: tool || 'resume', - mode: 'resume', - resumeFrom: executionId, - timestamp: new Date().toISOString() - } - }); - - try { - const result = await resumeCliSession( - initialPath, - { tool, executionId, last, prompt }, - (chunk) => { - broadcastToClients({ - type: 'CLI_OUTPUT', - payload: { - executionId: resumeId, - chunkType: chunk.type, - data: chunk.data - } - }); - } - ); - - // Broadcast completion - broadcastToClients({ - type: 'CLI_EXECUTION_COMPLETED', - payload: { - executionId: resumeId, - success: result.success, - status: result.execution.status, - duration_ms: result.execution.duration_ms, - resumeFrom: executionId - } - }); - - return { - success: result.success, - execution: result.execution - }; - - } catch (error: unknown) { - broadcastToClients({ - type: 'CLI_EXECUTION_ERROR', - payload: { - executionId: resumeId, - error: (error as Error).message - } - }); - - return { error: (error as Error).message, status: 500 }; - } - }); - return; - } - // API: Update CLAUDE.md using CLI tools (Explorer view) if (pathname === '/api/update-claude-md' && req.method === 'POST') { handlePostRequest(req, res, async (body) => { diff --git a/ccw/src/templates/dashboard-js/views/history.js b/ccw/src/templates/dashboard-js/views/history.js index e7c091c8..2d5da4c4 100644 --- a/ccw/src/templates/dashboard-js/views/history.js +++ b/ccw/src/templates/dashboard-js/views/history.js @@ -162,19 +162,19 @@ function promptResumeExecution(executionId, tool) { async function executeResume(executionId, tool) { var promptInput = document.getElementById('resumePromptInput'); - var additionalPrompt = promptInput ? promptInput.value.trim() : ''; + var additionalPrompt = promptInput ? promptInput.value.trim() : 'Continue from previous session'; closeModal(); showRefreshToast('Resuming session...', 'info'); try { - var response = await fetch('/api/cli/resume', { + var response = await fetch('/api/cli/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - executionId: executionId, tool: tool, - prompt: additionalPrompt || undefined + prompt: additionalPrompt, + resume: executionId // execution ID to resume from }) }); @@ -197,12 +197,13 @@ async function resumeLastSession(tool) { showRefreshToast('Resuming last ' + (tool || '') + ' session...', 'info'); try { - var response = await fetch('/api/cli/resume', { + var response = await fetch('/api/cli/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - tool: tool || undefined, - last: true + tool: tool || 'gemini', + prompt: 'Continue from previous session', + resume: true // true = resume last session }) }); diff --git a/ccw/src/tools/cli-executor.ts b/ccw/src/tools/cli-executor.ts index 5545a7cb..8eb5c704 100644 --- a/ccw/src/tools/cli-executor.ts +++ b/ccw/src/tools/cli-executor.ts @@ -21,6 +21,7 @@ const ParamsSchema = z.object({ cd: z.string().optional(), includeDirs: z.string().optional(), timeout: z.number().default(300000), + resume: z.union([z.boolean(), z.string()]).optional(), // true = last, string = execution ID }); type Params = z.infer; @@ -252,7 +253,19 @@ async function executeCliTool( throw new Error(`Invalid params: ${parsed.error.message}`); } - const { tool, prompt, mode, model, cd, includeDirs, timeout } = parsed.data; + const { tool, prompt, mode, model, cd, includeDirs, timeout, resume } = parsed.data; + + // Determine working directory early (needed for resume lookup) + const workingDir = cd || process.cwd(); + + // Build final prompt (with resume context if applicable) + let finalPrompt = prompt; + if (resume) { + const previousExecution = getPreviousExecution(workingDir, tool, resume); + if (previousExecution) { + finalPrompt = buildContinuationPrompt(previousExecution, prompt); + } + } // Check tool availability const toolStatus = await checkToolAvailability(tool); @@ -263,16 +276,13 @@ async function executeCliTool( // Build command const { command, args, useStdin } = buildCommand({ tool, - prompt, + prompt: finalPrompt, mode, model, dir: cd, include: includeDirs }); - // Determine working directory - const workingDir = cd || process.cwd(); - // Create execution record const executionId = `${Date.now()}-${tool}`; const startTime = Date.now(); @@ -638,143 +648,6 @@ export async function getCliToolsStatus(): Promise void) | null -): Promise { - const { tool, executionId, last = false, prompt } = options; - - // For Codex, use native resume - if (tool === 'codex' || (!tool && !executionId)) { - return resumeCodexSession(baseDir, { last }, onOutput); - } - - // For Gemini/Qwen, load previous session and continue - let previousExecution: ExecutionRecord | null = null; - - if (executionId) { - previousExecution = getExecutionDetail(baseDir, executionId); - } else if (last) { - // Get the most recent execution for the specified tool (or any tool) - const history = getExecutionHistory(baseDir, { limit: 1, tool: tool || null }); - if (history.executions.length > 0) { - previousExecution = getExecutionDetail(baseDir, history.executions[0].id); - } - } - - if (!previousExecution) { - throw new Error('No previous session found to resume'); - } - - // Build continuation prompt with previous context - const continuationPrompt = buildContinuationPrompt(previousExecution, prompt); - - // Execute with the continuation prompt - return executeCliTool({ - tool: previousExecution.tool, - prompt: continuationPrompt, - mode: previousExecution.mode, - model: previousExecution.model !== 'default' ? previousExecution.model : undefined - }, onOutput); -} - -/** - * Resume Codex session using native command - */ -async function resumeCodexSession( - baseDir: string, - options: { last?: boolean }, - onOutput?: ((data: { type: string; data: string }) => void) | null -): Promise { - const { last = false } = options; - - // Check codex availability - const toolStatus = await checkToolAvailability('codex'); - if (!toolStatus.available) { - throw new Error('Codex CLI not available. Please ensure it is installed and in PATH.'); - } - - const args = ['resume']; - if (last) { - args.push('--last'); - } - - const isWindows = process.platform === 'win32'; - const startTime = Date.now(); - const executionId = `${Date.now()}-codex-resume`; - - return new Promise((resolve, reject) => { - const child = spawn('codex', args, { - cwd: baseDir, - shell: isWindows, - stdio: ['inherit', 'pipe', 'pipe'] // inherit stdin for interactive picker - }); - - let stdout = ''; - let stderr = ''; - - child.stdout!.on('data', (data) => { - const text = data.toString(); - stdout += text; - if (onOutput) { - onOutput({ type: 'stdout', data: text }); - } - }); - - child.stderr!.on('data', (data) => { - const text = data.toString(); - stderr += text; - if (onOutput) { - onOutput({ type: 'stderr', data: text }); - } - }); - - child.on('close', (code) => { - const duration = Date.now() - startTime; - const status: 'success' | 'error' = code === 0 ? 'success' : 'error'; - - const execution: ExecutionRecord = { - id: executionId, - timestamp: new Date(startTime).toISOString(), - tool: 'codex', - model: 'default', - mode: 'auto', - prompt: `[Resume session${last ? ' --last' : ''}]`, - status, - exit_code: code, - duration_ms: duration, - output: { - stdout: stdout.substring(0, 10240), - stderr: stderr.substring(0, 2048), - truncated: stdout.length > 10240 || stderr.length > 2048 - } - }; - - resolve({ - success: status === 'success', - execution, - stdout, - stderr - }); - }); - - child.on('error', (error) => { - reject(new Error(`Failed to resume codex session: ${error.message}`)); - }); - }); -} - /** * Build continuation prompt with previous conversation context */ @@ -802,6 +675,27 @@ function buildContinuationPrompt(previous: ExecutionRecord, additionalPrompt?: s return parts.join('\n'); } +/** + * Get previous execution for resume + * @param baseDir - Working directory + * @param tool - Tool to filter by + * @param resume - true for last, or execution ID string + */ +function getPreviousExecution(baseDir: string, tool: string, resume: boolean | string): ExecutionRecord | null { + if (typeof resume === 'string') { + // Resume specific execution by ID + return getExecutionDetail(baseDir, resume); + } else if (resume === true) { + // Resume last execution for this tool + const history = getExecutionHistory(baseDir, { limit: 1, tool }); + if (history.executions.length === 0) { + return null; + } + return getExecutionDetail(baseDir, history.executions[0].id); + } + return null; +} + /** * Get latest execution for a specific tool */