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 @@ -416,9 +416,13 @@ export class OutputMonitor extends Disposable implements IOutputMonitor {
return this._state;
}

const promptResult = detectsInputRequiredPattern(currentLastLine);
// Only fast-path on high-confidence patterns (y/n, password, (END), etc.).
// Broad patterns like bare ":" or "?" are checked later in _handleIdleState
// after the terminal has naturally gone idle, avoiding false positives on
// normal command output that happens to end with those characters.
const promptResult = detectsHighConfidenceInputPattern(currentLastLine);
if (promptResult) {
this._logService.trace(`OutputMonitor: waitForIdle -> input-required pattern detected (waited=${waited}ms, lastLine=${this._formatLastLineForLog(currentTail)})`);
this._logService.trace(`OutputMonitor: waitForIdle -> high-confidence input pattern detected (waited=${waited}ms, lastLine=${this._formatLastLineForLog(currentTail)})`);
this._state = OutputMonitorState.Idle;
this._setupIdleInputListener();
return this._state;
Expand All @@ -440,6 +444,18 @@ export class OutputMonitor extends Disposable implements IOutputMonitor {
this._setupIdleInputListener();
return this._state;
}

// When the terminal has been idle (no new data) but the execution is
// still reported as active (e.g. task-backed executions), check the
// broader input-required heuristics. These patterns are too noisy to
// use during active output, but once the terminal has settled they
// reliably indicate an interactive prompt like "Enter your name: ".
if (recentlyIdle && isActive === true && detectsInputRequiredPattern(currentLastLine)) {
this._logService.trace(`OutputMonitor: waitForIdle -> broad input pattern detected while active+idle (waited=${waited}ms, lastLine=${this._formatLastLineForLog(currentTail)})`);
this._state = OutputMonitorState.Idle;
this._setupIdleInputListener();
return this._state;
}
}
} finally {
onDataDisposable.dispose();
Expand Down Expand Up @@ -513,7 +529,13 @@ export function matchTerminalPromptOption(options: readonly string[], suggestedO
return { option: undefined, index: -1 };
}

export function detectsInputRequiredPattern(cursorLine: string): boolean {
/**
* High-confidence patterns that reliably indicate the terminal is waiting for
* input. These are safe to use as a fast-path in `_waitForIdle` to skip normal
* idle detection, because they are specific enough to avoid false positives on
* normal command output (build logs, headers, etc.).
*/
export function detectsHighConfidenceInputPattern(cursorLine: string): boolean {
return [
// PowerShell-style multi-option line (supports [?] Help and optional default suffix) ending
// in whitespace. Uses [^\[]* to match each label (everything up to the next bracket),
Expand All @@ -528,23 +550,42 @@ export function detectsInputRequiredPattern(cursorLine: string): boolean {
// The trailing space indicates the cursor is positioned after the prompt awaiting input, as
// opposed to normal command output that happens to contain "(y)" followed by a newline.
/\(y\) +$/i,
// Line ends with ':' followed by at least one space. The trailing space indicates a
// waiting prompt (cursor positioned after the colon). A bare ':\n' at end of buffer is
// usually non-prompt output (e.g. a header or log line) and must not match.
/: +$/,
// Prompt with parenthesized default value e.g. "package name: (test) " or "version: (1.0.0) "
/:\s*\([^)]*\) +$/,
// Line contains (END) which is common in pagers
/\(END\)$/,
// Password prompt (must be followed by optional colon and trailing space to indicate
// an active prompt; otherwise normal output containing the word "password" would match).
/password:? +$/i,
// "Press a key" or "Press any key"
/press a(?:ny)? key/i,
].some(e => e.test(cursorLine));
}

/**
* Full set of input-required patterns including broader heuristics (bare `:` and
* `?` with trailing space). These may produce false positives on normal command
* output, so they should only be used **after** the terminal has been confirmed
* idle through normal polling (consecutive idle events with no data). In
* `_waitForIdle`, these are checked only when `recentlyIdle` is true (to handle
* active executions that are actually waiting for input). For the unconditional
* fast-path, use {@link detectsHighConfidenceInputPattern} instead.
*/
export function detectsInputRequiredPattern(cursorLine: string): boolean {
if (detectsHighConfidenceInputPattern(cursorLine)) {
return true;
}
return [
// Line ends with ':' followed by at least one space. The trailing space indicates a
// waiting prompt (cursor positioned after the colon). A bare ':\n' at end of buffer is
// usually non-prompt output (e.g. a header or log line) and must not match.
// NOTE: This is a broad pattern — only use after confirming idle state via polling.
/: +$/,
// Line ends with '?' followed by at least one space (optionally followed by a
// parenthesized hint like "Continue? (yes/no) "). Requiring trailing space avoids
// matching arbitrary command output where a line happens to end with '?'.
// NOTE: This is a broad pattern — only use after confirming idle state via polling.
/\? *(?:\([a-z\s]+\))? +$/i,
// "Press a key" or "Press any key"
/press a(?:ny)? key/i,
].some(e => e.test(cursorLine));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import * as assert from 'assert';
import { detectsGenericPressAnyKeyPattern, detectsInputRequiredPattern, detectsNonInteractiveHelpPattern, detectsVSCodeTaskFinishMessage, getLastLine, matchTerminalPromptOption, OutputMonitor } from '../../browser/tools/monitoring/outputMonitor.js';
import { detectsGenericPressAnyKeyPattern, detectsHighConfidenceInputPattern, detectsInputRequiredPattern, detectsNonInteractiveHelpPattern, detectsVSCodeTaskFinishMessage, getLastLine, matchTerminalPromptOption, OutputMonitor } from '../../browser/tools/monitoring/outputMonitor.js';
import { CancellationTokenSource } from '../../../../../../base/common/cancellation.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
import { IExecution, IPollingResult, OutputMonitorState } from '../../browser/tools/monitoring/types.js';
Expand Down Expand Up @@ -394,6 +394,37 @@ suite('OutputMonitor', () => {
});
});

suite('detectsHighConfidenceInputPattern', () => {
test('matches y/n and PowerShell prompts', () => {
assert.strictEqual(detectsHighConfidenceInputPattern('Continue? (y/N) '), true);
assert.strictEqual(detectsHighConfidenceInputPattern('Overwrite file? [Y/n] '), true);
assert.strictEqual(detectsHighConfidenceInputPattern('[Y] Yes [N] No '), true);
assert.strictEqual(detectsHighConfidenceInputPattern('[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): '), true);
});
test('matches password and press-any-key prompts', () => {
assert.strictEqual(detectsHighConfidenceInputPattern('Password: '), true);
assert.strictEqual(detectsHighConfidenceInputPattern('Press any key to continue...'), true);
});
test('matches parenthesized defaults', () => {
assert.strictEqual(detectsHighConfidenceInputPattern('package name: (test) '), true);
assert.strictEqual(detectsHighConfidenceInputPattern('version: (1.0.0) '), true);
});
test('matches (END) pager', () => {
assert.strictEqual(detectsHighConfidenceInputPattern('(END)'), true);
});
test('does NOT match bare colon prompts (too broad for fast-path)', () => {
assert.strictEqual(detectsHighConfidenceInputPattern('Enter your name: '), false);
assert.strictEqual(detectsHighConfidenceInputPattern('File to overwrite: '), false);
assert.strictEqual(detectsHighConfidenceInputPattern('Building project: '), false);
assert.strictEqual(detectsHighConfidenceInputPattern('Running tests:'), false);
});
test('does NOT match bare question prompts (too broad for fast-path)', () => {
assert.strictEqual(detectsHighConfidenceInputPattern('Continue? '), false);
assert.strictEqual(detectsHighConfidenceInputPattern('Are you sure? '), false);
assert.strictEqual(detectsHighConfidenceInputPattern('What happened?'), false);
});
});

suite('matchTerminalPromptOption', () => {
test('matches suggested option case-insensitively', () => {
assert.deepStrictEqual(matchTerminalPromptOption(['Y', 'n'], 'y'), { option: 'Y', index: 0 });
Expand Down
Loading