From 6f0fb9f055ec26dc4be66876d370f056bf8ccf12 Mon Sep 17 00:00:00 2001 From: "Ben Houston (via MyCoder)" Date: Mon, 24 Mar 2025 20:13:20 +0000 Subject: [PATCH] feat: add job cancellation in interactive mode (Ctrl+X) --- README.md | 4 +- .../agent/src/core/toolAgent/toolAgentCore.ts | 52 +++++++++++-- .../src/tools/interaction/userMessage.ts | 3 + packages/agent/src/utils/interactiveInput.ts | 74 ++++++++++++++++++- packages/cli/src/commands/$default.ts | 2 +- packages/cli/src/options.ts | 2 +- 6 files changed, 127 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 7f1c7e2..a9092f3 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,9 @@ MyCoder supports sending corrections to the main agent while it's running. This mycoder --interactive "Implement a React component" ``` -2. While the agent is running, press `Ctrl+M` to enter correction mode +2. While the agent is running, you can: + - Press `Ctrl+M` to enter correction mode and send additional context + - Press `Ctrl+X` to cancel the current job and provide new instructions 3. Type your correction or additional context 4. Press Enter to send the correction to the agent diff --git a/packages/agent/src/core/toolAgent/toolAgentCore.ts b/packages/agent/src/core/toolAgent/toolAgentCore.ts index a3d568b..1ac01e6 100644 --- a/packages/agent/src/core/toolAgent/toolAgentCore.ts +++ b/packages/agent/src/core/toolAgent/toolAgentCore.ts @@ -105,10 +105,23 @@ export const toolAgent = async ( // Import this at the top of the file try { // Dynamic import to avoid circular dependencies - const { userMessages } = await import( + const { userMessages, cancelJobFlag } = await import( '../../tools/interaction/userMessage.js' ); + // Check if job cancellation was requested + if (cancelJobFlag.value) { + cancelJobFlag.value = false; // Reset the flag + logger.info('Job cancellation requested by user'); + + // If there are no new instructions in userMessages, we'll add a default message + if (userMessages.length === 0) { + userMessages.push( + '[CANCEL JOB] Please stop the current task and wait for new instructions.', + ); + } + } + if (userMessages && userMessages.length > 0) { // Get all user messages and clear the queue const pendingUserMessages = [...userMessages]; @@ -116,11 +129,38 @@ export const toolAgent = async ( // Add each message to the conversation for (const message of pendingUserMessages) { - logger.info(`Message from user: ${message}`); - messages.push({ - role: 'user', - content: `[Correction from user]: ${message}`, - }); + if (message.startsWith('[CANCEL JOB]')) { + // For cancel job messages, we'll clear the conversation history and start fresh + const newInstruction = message.replace('[CANCEL JOB]', '').trim(); + logger.info( + `Job cancelled by user. New instruction: ${newInstruction}`, + ); + + // Clear the message history except for the system message + const systemMessage = messages.find((msg) => msg.role === 'system'); + messages.length = 0; + + // Add back the system message if it existed + if (systemMessage) { + messages.push(systemMessage); + } + + // Add a message explaining what happened + messages.push({ + role: 'user', + content: `The previous task was cancelled by the user. Please stop whatever you were doing before and focus on this new task: ${newInstruction}`, + }); + + // Reset interactions counter to avoid hitting limits + interactions = 0; + } else { + // Regular correction + logger.info(`Message from user: ${message}`); + messages.push({ + role: 'user', + content: `[Correction from user]: ${message}`, + }); + } } } } catch (error) { diff --git a/packages/agent/src/tools/interaction/userMessage.ts b/packages/agent/src/tools/interaction/userMessage.ts index 0c471b6..b163c8b 100644 --- a/packages/agent/src/tools/interaction/userMessage.ts +++ b/packages/agent/src/tools/interaction/userMessage.ts @@ -6,6 +6,9 @@ import { Tool } from '../../core/types.js'; // Track the messages sent to the main agent export const userMessages: string[] = []; +// Flag to indicate if the job should be cancelled +export const cancelJobFlag = { value: false }; + const parameterSchema = z.object({ message: z .string() diff --git a/packages/agent/src/utils/interactiveInput.ts b/packages/agent/src/utils/interactiveInput.ts index 7e0db80..c80208a 100644 --- a/packages/agent/src/utils/interactiveInput.ts +++ b/packages/agent/src/utils/interactiveInput.ts @@ -4,7 +4,10 @@ import { Writable } from 'stream'; import chalk from 'chalk'; -import { userMessages } from '../tools/interaction/userMessage.js'; +import { + userMessages, + cancelJobFlag, +} from '../tools/interaction/userMessage.js'; // Custom output stream to intercept console output class OutputInterceptor extends Writable { @@ -69,6 +72,75 @@ export const initInteractiveInput = () => { process.exit(0); } + // Check for Ctrl+X to cancel job + if (key.ctrl && key.name === 'x') { + // Pause output + interceptor.pause(); + + // Create a readline interface for input + const inputRl = createInterface({ + input: process.stdin, + output: originalStdout, + }); + + try { + // Reset cursor position and clear line + originalStdout.write('\r\n'); + originalStdout.write( + chalk.yellow( + 'Are you sure you want to cancel the current job? (y/n):\n', + ) + '> ', + ); + + // Get user confirmation + const confirmation = await inputRl.question(''); + + if (confirmation.trim().toLowerCase() === 'y') { + // Set cancel flag to true + cancelJobFlag.value = true; + + // Create a readline interface for new instructions + originalStdout.write( + chalk.green('\nJob cancelled. Enter new instructions:\n') + '> ', + ); + + // Get new instructions + const newInstructions = await inputRl.question(''); + + // Add message to queue if not empty + if (newInstructions.trim()) { + userMessages.push(`[CANCEL JOB] ${newInstructions}`); + originalStdout.write( + chalk.green( + '\nNew instructions sent. Resuming with new task...\n\n', + ), + ); + } else { + originalStdout.write( + chalk.yellow( + '\nNo new instructions provided. Job will still be cancelled...\n\n', + ), + ); + userMessages.push( + '[CANCEL JOB] Please stop the current task and wait for new instructions.', + ); + } + } else { + originalStdout.write( + chalk.green('\nCancellation aborted. Resuming output...\n\n'), + ); + } + } catch (error) { + originalStdout.write(chalk.red(`\nError cancelling job: ${error}\n\n`)); + } finally { + // Close input readline interface + inputRl.close(); + + // Resume output + interceptor.resume(); + } + } + // Check for Ctrl+M to enter message mode if (key.ctrl && key.name === 'm') { // Pause output diff --git a/packages/cli/src/commands/$default.ts b/packages/cli/src/commands/$default.ts index 2b9cfe0..69cd85e 100644 --- a/packages/cli/src/commands/$default.ts +++ b/packages/cli/src/commands/$default.ts @@ -172,7 +172,7 @@ export async function executePrompt( if (config.interactive) { logger.info( chalk.green( - 'Interactive correction mode enabled. Press Ctrl+M to send a correction to the agent.', + 'Interactive mode enabled. Press Ctrl+M to send a correction to the agent, Ctrl+X to cancel job.', ), ); cleanupInteractiveInput = initInteractiveInput(); diff --git a/packages/cli/src/options.ts b/packages/cli/src/options.ts index e0627c4..d165d0c 100644 --- a/packages/cli/src/options.ts +++ b/packages/cli/src/options.ts @@ -52,7 +52,7 @@ export const sharedOptions = { type: 'boolean', alias: 'i', description: - 'Run in interactive mode, asking for prompts and enabling corrections during execution (use Ctrl+M to send corrections)', + 'Run in interactive mode, asking for prompts and enabling corrections during execution (use Ctrl+M to send corrections, Ctrl+X to cancel job)', default: false, } as const, file: {