Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -403,9 +403,13 @@ export class OutputMonitor extends Disposable implements IOutputMonitor {
}

private async _handleTimeoutState(_command: string, _invocationContext: IToolInvocationContext | undefined, _extended: boolean, _token: CancellationToken): Promise<boolean> {
// Stop after extended polling (2 minutes) without notifying user
if (_extended) {
this._logService.info('OutputMonitor: Extended polling timeout reached after 2 minutes');
// Extended polling (2 minutes) expired while the process was still
// running. Rather than silently cancelling, signal that input may be
// needed so the agent sees the current output and can decide how to
// proceed (e.g. answer an unrecognised interactive prompt).
this._logService.info('OutputMonitor: Extended polling timeout reached after 2 minutes, signaling potential input needed');
this._onDidDetectInputNeeded.fire();
this._state = OutputMonitorState.Cancelled;
return false;
}
Expand Down Expand Up @@ -604,6 +608,18 @@ export function detectsHighConfidenceInputPattern(cursorLine: string): boolean {
/password:? +$/i,
// "Press a key" or "Press any key"
/press a(?:ny)? key/i,
// Interactive prompt libraries (prompts, enquirer, inquirer) prefix the prompt with
// '? ' at the start of the line and end with a distinctive chevron character
// followed by optional trailing whitespace where the cursor is awaiting input.
// Anchoring the '?' to the start of the line (after optional whitespace/ANSI
// escapes) avoids false positives from normal output that contains both a '?'
// allow-any-unicode-next-line
// and a chevron (e.g. "What happened? ›").
// Examples:
// "? Do you want to install jsdom? <chevron>" (prompts)
// "? Pick a color <chevron> " (enquirer)
// allow-any-unicode-next-line
/^(?:\s|\x1b\[[0-9;]*m)*\?.*[›❯▸▶]\s*$/,
].some(e => e.test(cursorLine));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2218,15 +2218,15 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
lastInputNeededOutput = currentOutput;
lastInputNeededNotificationTime = now;
const inputAction = this._buildInputNeededSteeringText(chatSessionResource, termId, /*mentionTimeout*/ false);
const message = `[Terminal ${termId} notification: command is waiting for input.]\n${inputAction}\nTerminal output:\n${currentOutput}`;
const message = `[Terminal ${termId} notification: command may be waiting for input — assess the output below.]\n${inputAction}\nTerminal output:\n${currentOutput}`;

this._logService.debug(`RunInTerminalTool: Input needed in background terminal ${termId}, notifying chat session`);

this._chatService.sendRequest(chatSessionResource, message, {
...sendOptions,
queue: ChatRequestQueueKind.Steering,
isSystemInitiated: true,
systemInitiatedLabel: localize('terminalNeedsInput', "`{0}` needs input", commandName),
systemInitiatedLabel: localize('terminalAssessingOutput', "`{0}` may need input", commandName),
terminalExecutionId: termId,
}).catch(e => {
this._logService.warn(`RunInTerminalTool: Failed to send input-needed notification for terminal ${termId}`, e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,25 @@ suite('OutputMonitor', () => {
});
});

test('extended timeout with isActive fires onDidDetectInputNeeded', async () => {
return runWithFakedTimers({}, async () => {
// Simulate a process that stays active with output that doesn't
// match any input-required pattern — the extended timeout should
// fire onDidDetectInputNeeded so the agent can assess the output.
execution.isActive = async () => true;
execution.getOutput = () => 'Some unrecognised prompt waiting for input';

monitor = store.add(instantiationService.createInstance(OutputMonitor, execution, undefined, createTestContext('1'), cts.token, 'test command'));

let inputNeededFired = false;
store.add(monitor.onDidDetectInputNeeded(() => { inputNeededFired = true; }));

await Event.toPromise(monitor.onDidFinishCommand);
assert.strictEqual(inputNeededFired, true, 'onDidDetectInputNeeded should fire on extended timeout with active process');
assert.strictEqual(monitor.pollingResult?.state, OutputMonitorState.Cancelled);
});
});

test('non-interactive help on the last line stops monitoring before custom polling', async () => {
return runWithFakedTimers({}, async () => {
execution.getOutput = () => 'Build complete successfully\npress h + enter to show help';
Expand Down Expand Up @@ -354,6 +373,44 @@ suite('OutputMonitor', () => {
assert.strictEqual(detectsInputRequiredPattern('license: (ISC) '), true);
});

test('detects chevron prompts from prompts/enquirer/inquirer libraries', () => {
// vitest / npm-style "prompts" library uses U+203A SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
// allow-any-unicode-next-line
assert.strictEqual(detectsInputRequiredPattern('? Do you want to install jsdom? ›'), true);
// allow-any-unicode-next-line
assert.strictEqual(detectsInputRequiredPattern('? Do you want to install jsdom? › '), true);
Comment thread
meganrogge marked this conversation as resolved.
// inquirer / enquirer uses U+276F HEAVY RIGHT-POINTING ANGLE QUOTATION MARK
// allow-any-unicode-next-line
assert.strictEqual(detectsInputRequiredPattern('? Pick a color ❯ '), true);
// allow-any-unicode-next-line
assert.strictEqual(detectsInputRequiredPattern('? Pick a color ❯'), true);
// Other chevron variants prefixed with '?'
// allow-any-unicode-next-line
assert.strictEqual(detectsInputRequiredPattern('? Project name ▸ '), true);
// allow-any-unicode-next-line
assert.strictEqual(detectsInputRequiredPattern('? Choose ▶ '), true);

// No match if the user has already typed a response after the chevron
// allow-any-unicode-next-line
assert.strictEqual(detectsInputRequiredPattern('? Do you want to install jsdom? › yes'), false);
// allow-any-unicode-next-line
assert.strictEqual(detectsInputRequiredPattern('? Pick a color ❯ red'), false);

// No match for chevrons in normal output without a leading '?'
// allow-any-unicode-next-line
assert.strictEqual(detectsInputRequiredPattern(' feature/foo ❯ main'), false);
// allow-any-unicode-next-line
assert.strictEqual(detectsInputRequiredPattern('Project name ▸ '), false);
Comment thread
meganrogge marked this conversation as resolved.

// No match when '?' appears mid-line (not as a prompt prefix)
// allow-any-unicode-next-line
assert.strictEqual(detectsInputRequiredPattern('What happened? ›'), false);

// Match when prompt is prefixed with ANSI escape codes (colored output)
// allow-any-unicode-next-line
assert.strictEqual(detectsInputRequiredPattern('\x1b[32m? Choose a framework \x1b[0m›'), true);
});

test('detects trailing questions', () => {
assert.strictEqual(detectsInputRequiredPattern('Continue? '), true);
assert.strictEqual(detectsInputRequiredPattern('Proceed? '), true);
Expand Down
Loading