mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-13 02:41:50 +08:00
feat: integrate session resume functionality into CLI commands and documentation
This commit is contained in:
@@ -51,7 +51,7 @@ ccw cli exec "<prompt>" --tool codex --mode auto --cd ./project --includeDirs ./
|
|||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `ccw cli status` | Check CLI tools availability |
|
| `ccw cli status` | Check CLI tools availability |
|
||||||
| `ccw cli exec "<prompt>"` | Execute a CLI tool |
|
| `ccw cli exec "<prompt>"` | Execute a CLI tool |
|
||||||
| `ccw cli resume [id]` | Resume a previous session |
|
| `ccw cli exec "<prompt>" --resume [id]` | Resume a previous session |
|
||||||
| `ccw cli history` | Show execution history |
|
| `ccw cli history` | Show execution history |
|
||||||
| `ccw cli detail <id>` | Show execution detail |
|
| `ccw cli detail <id>` | Show execution detail |
|
||||||
|
|
||||||
@@ -122,38 +122,45 @@ ccw cli exec "<prompt>" --tool codex --mode auto --cd ./project --includeDirs ./
|
|||||||
|
|
||||||
**Default MODE**: No default, must be explicitly specified
|
**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
|
```bash
|
||||||
# Resume last session (any tool)
|
# Resume last session with continuation prompt
|
||||||
ccw cli resume --last
|
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
|
# Resume specific session by ID with prompt
|
||||||
ccw cli resume --last --tool gemini
|
ccw cli exec "Fix the issues you found" --resume <execution-id> --tool gemini
|
||||||
ccw cli resume --last --tool codex
|
|
||||||
|
|
||||||
# Resume specific session by ID
|
# Resume last session (empty --resume = last)
|
||||||
ccw cli resume <execution-id>
|
ccw cli exec "Continue analysis" --resume
|
||||||
|
|
||||||
# Resume with additional prompt
|
|
||||||
ccw cli resume --last --prompt "Continue with error handling"
|
|
||||||
|
|
||||||
# Codex native interactive picker
|
|
||||||
ccw cli resume --tool codex
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Resume Options**:
|
**Resume Parameter**:
|
||||||
| Option | Description |
|
| Value | Description |
|
||||||
|--------|-------------|
|
|-------|-------------|
|
||||||
| `--last` | Resume most recent session |
|
| `--resume` (empty) | Resume most recent session |
|
||||||
| `--tool <tool>` | Filter by tool (gemini, qwen, codex) |
|
| `--resume <id>` | Resume specific execution ID |
|
||||||
| `--prompt <text>` | Additional prompt for continuation |
|
|
||||||
| `[id]` | Specific execution ID to resume |
|
**Context Assembly** (automatic):
|
||||||
|
```
|
||||||
|
=== PREVIOUS CONVERSATION ===
|
||||||
|
|
||||||
|
USER PROMPT:
|
||||||
|
[Previous prompt content]
|
||||||
|
|
||||||
|
ASSISTANT RESPONSE:
|
||||||
|
[Previous output]
|
||||||
|
|
||||||
|
=== CONTINUATION ===
|
||||||
|
|
||||||
|
[Your new prompt content here]
|
||||||
|
```
|
||||||
|
|
||||||
**Tool-Specific Behavior**:
|
**Tool-Specific Behavior**:
|
||||||
- **Codex**: Uses native `codex resume` command (supports interactive picker)
|
- **Codex**: Uses native `codex resume` command
|
||||||
- **Gemini/Qwen**: Loads previous conversation context and continues with new prompt
|
- **Gemini/Qwen**: Assembles previous prompt + response + new prompt as single context
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -160,8 +160,7 @@ export function run(argv: string[]): void {
|
|||||||
.option('--no-stream', 'Disable streaming output')
|
.option('--no-stream', 'Disable streaming output')
|
||||||
.option('--limit <n>', 'History limit')
|
.option('--limit <n>', 'History limit')
|
||||||
.option('--status <status>', 'Filter by status')
|
.option('--status <status>', 'Filter by status')
|
||||||
.option('--last', 'Resume most recent session')
|
.option('--resume [id]', 'Resume previous session (empty=last, or execution ID)')
|
||||||
.option('--prompt <text>', 'Additional prompt for resume continuation')
|
|
||||||
.action((subcommand, args, options) => cliCommand(subcommand, args, options));
|
.action((subcommand, args, options) => cliCommand(subcommand, args, options));
|
||||||
|
|
||||||
program.parse(argv);
|
program.parse(argv);
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ import {
|
|||||||
cliExecutorTool,
|
cliExecutorTool,
|
||||||
getCliToolsStatus,
|
getCliToolsStatus,
|
||||||
getExecutionHistory,
|
getExecutionHistory,
|
||||||
getExecutionDetail,
|
getExecutionDetail
|
||||||
resumeCliSession
|
|
||||||
} from '../tools/cli-executor.js';
|
} from '../tools/cli-executor.js';
|
||||||
|
|
||||||
interface CliExecOptions {
|
interface CliExecOptions {
|
||||||
@@ -20,6 +19,7 @@ interface CliExecOptions {
|
|||||||
includeDirs?: string;
|
includeDirs?: string;
|
||||||
timeout?: string;
|
timeout?: string;
|
||||||
noStream?: boolean;
|
noStream?: boolean;
|
||||||
|
resume?: string | boolean; // true = last, string = execution ID
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HistoryOptions {
|
interface HistoryOptions {
|
||||||
@@ -28,12 +28,6 @@ interface HistoryOptions {
|
|||||||
status?: string;
|
status?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResumeOptions {
|
|
||||||
tool?: string;
|
|
||||||
last?: boolean;
|
|
||||||
prompt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show CLI tool status
|
* Show CLI tool status
|
||||||
*/
|
*/
|
||||||
@@ -67,9 +61,11 @@ async function execAction(prompt: string | undefined, options: CliExecOptions):
|
|||||||
process.exit(1);
|
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
|
// Streaming output handler
|
||||||
const onOutput = noStream ? null : (chunk: any) => {
|
const onOutput = noStream ? null : (chunk: any) => {
|
||||||
@@ -84,7 +80,8 @@ async function execAction(prompt: string | undefined, options: CliExecOptions):
|
|||||||
model,
|
model,
|
||||||
cd,
|
cd,
|
||||||
includeDirs,
|
includeDirs,
|
||||||
timeout: timeout ? parseInt(timeout, 10) : 300000
|
timeout: timeout ? parseInt(timeout, 10) : 300000,
|
||||||
|
resume // pass resume parameter
|
||||||
}, onOutput);
|
}, onOutput);
|
||||||
|
|
||||||
// If not streaming, print output now
|
// If not streaming, print output now
|
||||||
@@ -97,7 +94,7 @@ async function execAction(prompt: string | undefined, options: CliExecOptions):
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log(chalk.green(` ✓ Completed in ${(result.execution.duration_ms / 1000).toFixed(1)}s`));
|
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.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 {
|
} else {
|
||||||
console.log(chalk.red(` ✗ Failed (${result.execution.status})`));
|
console.log(chalk.red(` ✗ Failed (${result.execution.status})`));
|
||||||
console.log(chalk.gray(` ID: ${result.execution.id}`));
|
console.log(chalk.gray(` ID: ${result.execution.id}`));
|
||||||
@@ -194,64 +191,6 @@ async function detailAction(executionId: string | undefined): Promise<void> {
|
|||||||
console.log();
|
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<void> {
|
|
||||||
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
|
* Get human-readable time ago string
|
||||||
* @param {Date} date
|
* @param {Date} date
|
||||||
@@ -269,14 +208,14 @@ function getTimeAgo(date: Date): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* CLI command entry point
|
* 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 {string[]} args - Arguments array
|
||||||
* @param {Object} options - CLI options
|
* @param {Object} options - CLI options
|
||||||
*/
|
*/
|
||||||
export async function cliCommand(
|
export async function cliCommand(
|
||||||
subcommand: string,
|
subcommand: string,
|
||||||
args: string | string[],
|
args: string | string[],
|
||||||
options: CliExecOptions | HistoryOptions | ResumeOptions
|
options: CliExecOptions | HistoryOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const argsArray = Array.isArray(args) ? args : (args ? [args] : []);
|
const argsArray = Array.isArray(args) ? args : (args ? [args] : []);
|
||||||
|
|
||||||
@@ -297,17 +236,12 @@ export async function cliCommand(
|
|||||||
await detailAction(argsArray[0]);
|
await detailAction(argsArray[0]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'resume':
|
|
||||||
await resumeAction(argsArray[0], options as ResumeOptions);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log(chalk.bold.cyan('\n CCW CLI Tool Executor\n'));
|
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(' Unified interface for Gemini, Qwen, and Codex CLI tools.\n');
|
||||||
console.log(' Subcommands:');
|
console.log(' Subcommands:');
|
||||||
console.log(chalk.gray(' status Check CLI tools availability'));
|
console.log(chalk.gray(' status Check CLI tools availability'));
|
||||||
console.log(chalk.gray(' exec <prompt> Execute a CLI tool'));
|
console.log(chalk.gray(' exec <prompt> 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(' history Show execution history'));
|
||||||
console.log(chalk.gray(' detail <id> Show execution detail'));
|
console.log(chalk.gray(' detail <id> Show execution detail'));
|
||||||
console.log();
|
console.log();
|
||||||
@@ -321,12 +255,7 @@ export async function cliCommand(
|
|||||||
console.log(chalk.gray(' → codex: --add-dir'));
|
console.log(chalk.gray(' → codex: --add-dir'));
|
||||||
console.log(chalk.gray(' --timeout <ms> Timeout in milliseconds (default: 300000)'));
|
console.log(chalk.gray(' --timeout <ms> Timeout in milliseconds (default: 300000)'));
|
||||||
console.log(chalk.gray(' --no-stream Disable streaming output'));
|
console.log(chalk.gray(' --no-stream Disable streaming output'));
|
||||||
console.log();
|
console.log(chalk.gray(' --resume [id] Resume previous session (empty=last, or execution ID)'));
|
||||||
console.log(' Resume Options:');
|
|
||||||
console.log(chalk.gray(' --last Resume most recent session'));
|
|
||||||
console.log(chalk.gray(' --tool <tool> Tool filter (gemini, qwen, codex)'));
|
|
||||||
console.log(chalk.gray(' --prompt <text> Additional prompt for continuation'));
|
|
||||||
console.log(chalk.gray(' [id] Specific execution ID to resume'));
|
|
||||||
console.log();
|
console.log();
|
||||||
console.log(' History Options:');
|
console.log(' History Options:');
|
||||||
console.log(chalk.gray(' --limit <n> Number of results (default: 20)'));
|
console.log(chalk.gray(' --limit <n> 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 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 "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 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 exec "Continue analysis" --resume # Resume last session'));
|
||||||
console.log(chalk.gray(' ccw cli resume --last --tool gemini # Resume last Gemini session'));
|
console.log(chalk.gray(' ccw cli exec "Continue..." --resume <id> --tool gemini # Resume specific session'));
|
||||||
console.log(chalk.gray(' ccw cli resume --tool codex # Codex interactive picker'));
|
|
||||||
console.log(chalk.gray(' ccw cli resume <id> --prompt "Continue..." # Resume specific session'));
|
|
||||||
console.log(chalk.gray(' ccw cli history --tool gemini --limit 10'));
|
console.log(chalk.gray(' ccw cli history --tool gemini --limit 10'));
|
||||||
console.log();
|
console.log();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { createHash } from 'crypto';
|
|||||||
import { scanSessions } from './session-scanner.js';
|
import { scanSessions } from './session-scanner.js';
|
||||||
import { aggregateData } from './data-aggregator.js';
|
import { aggregateData } from './data-aggregator.js';
|
||||||
import { resolvePath, getRecentPaths, trackRecentPath, removeRecentPath, normalizePathForDisplay, getWorkflowDir } from '../utils/path-resolver.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 { getAllManifests } from './manifest.js';
|
||||||
import { checkVenvStatus, bootstrapVenv, executeCodexLens, checkSemanticStatus, installSemantic } from '../tools/codex-lens.js';
|
import { checkVenvStatus, bootstrapVenv, executeCodexLens, checkSemanticStatus, installSemantic } from '../tools/codex-lens.js';
|
||||||
import { listTools } from '../tools/index.js';
|
import { listTools } from '../tools/index.js';
|
||||||
@@ -764,81 +764,6 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// API: Resume CLI Session
|
|
||||||
if (pathname === '/api/cli/resume' && req.method === 'POST') {
|
|
||||||
handlePostRequest(req, res, async (body) => {
|
|
||||||
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)
|
// API: Update CLAUDE.md using CLI tools (Explorer view)
|
||||||
if (pathname === '/api/update-claude-md' && req.method === 'POST') {
|
if (pathname === '/api/update-claude-md' && req.method === 'POST') {
|
||||||
handlePostRequest(req, res, async (body) => {
|
handlePostRequest(req, res, async (body) => {
|
||||||
|
|||||||
@@ -162,19 +162,19 @@ function promptResumeExecution(executionId, tool) {
|
|||||||
|
|
||||||
async function executeResume(executionId, tool) {
|
async function executeResume(executionId, tool) {
|
||||||
var promptInput = document.getElementById('resumePromptInput');
|
var promptInput = document.getElementById('resumePromptInput');
|
||||||
var additionalPrompt = promptInput ? promptInput.value.trim() : '';
|
var additionalPrompt = promptInput ? promptInput.value.trim() : 'Continue from previous session';
|
||||||
|
|
||||||
closeModal();
|
closeModal();
|
||||||
showRefreshToast('Resuming session...', 'info');
|
showRefreshToast('Resuming session...', 'info');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var response = await fetch('/api/cli/resume', {
|
var response = await fetch('/api/cli/execute', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
executionId: executionId,
|
|
||||||
tool: tool,
|
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');
|
showRefreshToast('Resuming last ' + (tool || '') + ' session...', 'info');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var response = await fetch('/api/cli/resume', {
|
var response = await fetch('/api/cli/execute', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
tool: tool || undefined,
|
tool: tool || 'gemini',
|
||||||
last: true
|
prompt: 'Continue from previous session',
|
||||||
|
resume: true // true = resume last session
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const ParamsSchema = z.object({
|
|||||||
cd: z.string().optional(),
|
cd: z.string().optional(),
|
||||||
includeDirs: z.string().optional(),
|
includeDirs: z.string().optional(),
|
||||||
timeout: z.number().default(300000),
|
timeout: z.number().default(300000),
|
||||||
|
resume: z.union([z.boolean(), z.string()]).optional(), // true = last, string = execution ID
|
||||||
});
|
});
|
||||||
|
|
||||||
type Params = z.infer<typeof ParamsSchema>;
|
type Params = z.infer<typeof ParamsSchema>;
|
||||||
@@ -252,7 +253,19 @@ async function executeCliTool(
|
|||||||
throw new Error(`Invalid params: ${parsed.error.message}`);
|
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
|
// Check tool availability
|
||||||
const toolStatus = await checkToolAvailability(tool);
|
const toolStatus = await checkToolAvailability(tool);
|
||||||
@@ -263,16 +276,13 @@ async function executeCliTool(
|
|||||||
// Build command
|
// Build command
|
||||||
const { command, args, useStdin } = buildCommand({
|
const { command, args, useStdin } = buildCommand({
|
||||||
tool,
|
tool,
|
||||||
prompt,
|
prompt: finalPrompt,
|
||||||
mode,
|
mode,
|
||||||
model,
|
model,
|
||||||
dir: cd,
|
dir: cd,
|
||||||
include: includeDirs
|
include: includeDirs
|
||||||
});
|
});
|
||||||
|
|
||||||
// Determine working directory
|
|
||||||
const workingDir = cd || process.cwd();
|
|
||||||
|
|
||||||
// Create execution record
|
// Create execution record
|
||||||
const executionId = `${Date.now()}-${tool}`;
|
const executionId = `${Date.now()}-${tool}`;
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
@@ -638,143 +648,6 @@ export async function getCliToolsStatus(): Promise<Record<string, ToolAvailabili
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resume a CLI session
|
|
||||||
* - Codex: Uses native `codex resume` command
|
|
||||||
* - Gemini/Qwen: Loads previous conversation and continues
|
|
||||||
*/
|
|
||||||
export async function resumeCliSession(
|
|
||||||
baseDir: string,
|
|
||||||
options: {
|
|
||||||
tool?: string;
|
|
||||||
executionId?: string;
|
|
||||||
last?: boolean;
|
|
||||||
prompt?: string;
|
|
||||||
},
|
|
||||||
onOutput?: ((data: { type: string; data: string }) => void) | null
|
|
||||||
): Promise<ExecutionOutput> {
|
|
||||||
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<ExecutionOutput> {
|
|
||||||
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
|
* Build continuation prompt with previous conversation context
|
||||||
*/
|
*/
|
||||||
@@ -802,6 +675,27 @@ function buildContinuationPrompt(previous: ExecutionRecord, additionalPrompt?: s
|
|||||||
return parts.join('\n');
|
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
|
* Get latest execution for a specific tool
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user