Skip to content

Commit 6f0fb9f

Browse files
committed
feat: add job cancellation in interactive mode (Ctrl+X)
1 parent 9f2ce3f commit 6f0fb9f

File tree

6 files changed

+127
-10
lines changed

6 files changed

+127
-10
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,9 @@ MyCoder supports sending corrections to the main agent while it's running. This
142142
mycoder --interactive "Implement a React component"
143143
```
144144

145-
2. While the agent is running, press `Ctrl+M` to enter correction mode
145+
2. While the agent is running, you can:
146+
- Press `Ctrl+M` to enter correction mode and send additional context
147+
- Press `Ctrl+X` to cancel the current job and provide new instructions
146148
3. Type your correction or additional context
147149
4. Press Enter to send the correction to the agent
148150

packages/agent/src/core/toolAgent/toolAgentCore.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,22 +105,62 @@ export const toolAgent = async (
105105
// Import this at the top of the file
106106
try {
107107
// Dynamic import to avoid circular dependencies
108-
const { userMessages } = await import(
108+
const { userMessages, cancelJobFlag } = await import(
109109
'../../tools/interaction/userMessage.js'
110110
);
111111

112+
// Check if job cancellation was requested
113+
if (cancelJobFlag.value) {
114+
cancelJobFlag.value = false; // Reset the flag
115+
logger.info('Job cancellation requested by user');
116+
117+
// If there are no new instructions in userMessages, we'll add a default message
118+
if (userMessages.length === 0) {
119+
userMessages.push(
120+
'[CANCEL JOB] Please stop the current task and wait for new instructions.',
121+
);
122+
}
123+
}
124+
112125
if (userMessages && userMessages.length > 0) {
113126
// Get all user messages and clear the queue
114127
const pendingUserMessages = [...userMessages];
115128
userMessages.length = 0;
116129

117130
// Add each message to the conversation
118131
for (const message of pendingUserMessages) {
119-
logger.info(`Message from user: ${message}`);
120-
messages.push({
121-
role: 'user',
122-
content: `[Correction from user]: ${message}`,
123-
});
132+
if (message.startsWith('[CANCEL JOB]')) {
133+
// For cancel job messages, we'll clear the conversation history and start fresh
134+
const newInstruction = message.replace('[CANCEL JOB]', '').trim();
135+
logger.info(
136+
`Job cancelled by user. New instruction: ${newInstruction}`,
137+
);
138+
139+
// Clear the message history except for the system message
140+
const systemMessage = messages.find((msg) => msg.role === 'system');
141+
messages.length = 0;
142+
143+
// Add back the system message if it existed
144+
if (systemMessage) {
145+
messages.push(systemMessage);
146+
}
147+
148+
// Add a message explaining what happened
149+
messages.push({
150+
role: 'user',
151+
content: `The previous task was cancelled by the user. Please stop whatever you were doing before and focus on this new task: ${newInstruction}`,
152+
});
153+
154+
// Reset interactions counter to avoid hitting limits
155+
interactions = 0;
156+
} else {
157+
// Regular correction
158+
logger.info(`Message from user: ${message}`);
159+
messages.push({
160+
role: 'user',
161+
content: `[Correction from user]: ${message}`,
162+
});
163+
}
124164
}
125165
}
126166
} catch (error) {

packages/agent/src/tools/interaction/userMessage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import { Tool } from '../../core/types.js';
66
// Track the messages sent to the main agent
77
export const userMessages: string[] = [];
88

9+
// Flag to indicate if the job should be cancelled
10+
export const cancelJobFlag = { value: false };
11+
912
const parameterSchema = z.object({
1013
message: z
1114
.string()

packages/agent/src/utils/interactiveInput.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { Writable } from 'stream';
44

55
import chalk from 'chalk';
66

7-
import { userMessages } from '../tools/interaction/userMessage.js';
7+
import {
8+
userMessages,
9+
cancelJobFlag,
10+
} from '../tools/interaction/userMessage.js';
811

912
// Custom output stream to intercept console output
1013
class OutputInterceptor extends Writable {
@@ -69,6 +72,75 @@ export const initInteractiveInput = () => {
6972
process.exit(0);
7073
}
7174

75+
// Check for Ctrl+X to cancel job
76+
if (key.ctrl && key.name === 'x') {
77+
// Pause output
78+
interceptor.pause();
79+
80+
// Create a readline interface for input
81+
const inputRl = createInterface({
82+
input: process.stdin,
83+
output: originalStdout,
84+
});
85+
86+
try {
87+
// Reset cursor position and clear line
88+
originalStdout.write('\r\n');
89+
originalStdout.write(
90+
chalk.yellow(
91+
'Are you sure you want to cancel the current job? (y/n):\n',
92+
) + '> ',
93+
);
94+
95+
// Get user confirmation
96+
const confirmation = await inputRl.question('');
97+
98+
if (confirmation.trim().toLowerCase() === 'y') {
99+
// Set cancel flag to true
100+
cancelJobFlag.value = true;
101+
102+
// Create a readline interface for new instructions
103+
originalStdout.write(
104+
chalk.green('\nJob cancelled. Enter new instructions:\n') + '> ',
105+
);
106+
107+
// Get new instructions
108+
const newInstructions = await inputRl.question('');
109+
110+
// Add message to queue if not empty
111+
if (newInstructions.trim()) {
112+
userMessages.push(`[CANCEL JOB] ${newInstructions}`);
113+
originalStdout.write(
114+
chalk.green(
115+
'\nNew instructions sent. Resuming with new task...\n\n',
116+
),
117+
);
118+
} else {
119+
originalStdout.write(
120+
chalk.yellow(
121+
'\nNo new instructions provided. Job will still be cancelled...\n\n',
122+
),
123+
);
124+
userMessages.push(
125+
'[CANCEL JOB] Please stop the current task and wait for new instructions.',
126+
);
127+
}
128+
} else {
129+
originalStdout.write(
130+
chalk.green('\nCancellation aborted. Resuming output...\n\n'),
131+
);
132+
}
133+
} catch (error) {
134+
originalStdout.write(chalk.red(`\nError cancelling job: ${error}\n\n`));
135+
} finally {
136+
// Close input readline interface
137+
inputRl.close();
138+
139+
// Resume output
140+
interceptor.resume();
141+
}
142+
}
143+
72144
// Check for Ctrl+M to enter message mode
73145
if (key.ctrl && key.name === 'm') {
74146
// Pause output

packages/cli/src/commands/$default.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export async function executePrompt(
172172
if (config.interactive) {
173173
logger.info(
174174
chalk.green(
175-
'Interactive correction mode enabled. Press Ctrl+M to send a correction to the agent.',
175+
'Interactive mode enabled. Press Ctrl+M to send a correction to the agent, Ctrl+X to cancel job.',
176176
),
177177
);
178178
cleanupInteractiveInput = initInteractiveInput();

packages/cli/src/options.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const sharedOptions = {
5252
type: 'boolean',
5353
alias: 'i',
5454
description:
55-
'Run in interactive mode, asking for prompts and enabling corrections during execution (use Ctrl+M to send corrections)',
55+
'Run in interactive mode, asking for prompts and enabling corrections during execution (use Ctrl+M to send corrections, Ctrl+X to cancel job)',
5656
default: false,
5757
} as const,
5858
file: {

0 commit comments

Comments
 (0)