Skip to content

Commit fb5d471

Browse files
authored
Merge pull request microsoft#259567 from microsoft/copilot/fix-259564
Add prompt detection heuristics for no shell integration command execution timeout
2 parents 9907be7 + a8451c9 commit fb5d471

File tree

3 files changed

+184
-3
lines changed

3 files changed

+184
-3
lines changed

src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/executeStrategy.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,115 @@ export async function waitForIdle(onData: Event<unknown>, idleDurationMs: number
3636
return deferred.p.finally(() => store.dispose());
3737
}
3838

39+
export interface IPromptDetectionResult {
40+
/**
41+
* Whether a prompt was detected.
42+
*/
43+
detected: boolean;
44+
/**
45+
* The reason for logging.
46+
*/
47+
reason?: string;
48+
}
49+
50+
/**
51+
* Detects if the given text content appears to end with a common prompt pattern.
52+
*/
53+
export function detectsCommonPromptPattern(cursorLine: string): IPromptDetectionResult {
54+
if (cursorLine.trim().length === 0) {
55+
return { detected: false, reason: 'Content is empty or contains only whitespace' };
56+
}
57+
58+
// PowerShell prompt: PS C:\> or similar patterns
59+
if (/PS\s+[A-Z]:\\.*>\s*$/.test(cursorLine)) {
60+
return { detected: true, reason: `PowerShell prompt pattern detected: "${cursorLine}"` };
61+
}
62+
63+
// Command Prompt: C:\path>
64+
if (/^[A-Z]:\\.*>\s*$/.test(cursorLine)) {
65+
return { detected: true, reason: `Command Prompt pattern detected: "${cursorLine}"` };
66+
}
67+
68+
// Bash-style prompts ending with $
69+
if (/\$\s*$/.test(cursorLine)) {
70+
return { detected: true, reason: `Bash-style prompt pattern detected: "${cursorLine}"` };
71+
}
72+
73+
// Root prompts ending with #
74+
if (/#\s*$/.test(cursorLine)) {
75+
return { detected: true, reason: `Root prompt pattern detected: "${cursorLine}"` };
76+
}
77+
78+
// Python REPL prompt
79+
if (/^>>>\s*$/.test(cursorLine)) {
80+
return { detected: true, reason: `Python REPL prompt pattern detected: "${cursorLine}"` };
81+
}
82+
83+
// Custom prompts ending with the starship character (\u276f)
84+
if (/\u276f\s*$/.test(cursorLine)) {
85+
return { detected: true, reason: `Starship prompt pattern detected: "${cursorLine}"` };
86+
}
87+
88+
// Generic prompts ending with common prompt characters
89+
if (/[>%]\s*$/.test(cursorLine)) {
90+
return { detected: true, reason: `Generic prompt pattern detected: "${cursorLine}"` };
91+
}
92+
93+
return { detected: false, reason: `No common prompt pattern found in last line: "${cursorLine}"` };
94+
}
95+
96+
/**
97+
* Enhanced version of {@link waitForIdle} that uses prompt detection heuristics. After the terminal
98+
* idles for the specified period, checks if the terminal's cursor line looks like a common prompt.
99+
* If not, extends the timeout to give the command more time to complete.
100+
*/
101+
export async function waitForIdleWithPromptHeuristics(
102+
onData: Event<unknown>,
103+
instance: ITerminalInstance,
104+
idlePollIntervalMs: number,
105+
extendedTimeoutMs: number,
106+
): Promise<IPromptDetectionResult> {
107+
await waitForIdle(onData, idlePollIntervalMs);
108+
109+
const xterm = await instance.xtermReadyPromise;
110+
if (!xterm) {
111+
return { detected: false, reason: `Xterm not available, using ${idlePollIntervalMs}ms timeout` };
112+
}
113+
const startTime = Date.now();
114+
115+
// Attempt to detect a prompt pattern after idle
116+
while (Date.now() - startTime < extendedTimeoutMs) {
117+
try {
118+
let content = '';
119+
const buffer = xterm.raw.buffer.active;
120+
const line = buffer.getLine(buffer.baseY + buffer.cursorY);
121+
if (line) {
122+
content = line.translateToString(true);
123+
}
124+
const promptResult = detectsCommonPromptPattern(content);
125+
if (promptResult.detected) {
126+
return promptResult;
127+
}
128+
} catch (error) {
129+
// Continue polling even if there's an error reading terminal content
130+
}
131+
await waitForIdle(onData, Math.min(idlePollIntervalMs, extendedTimeoutMs - (Date.now() - startTime)));
132+
}
133+
134+
// Extended timeout reached without detecting a prompt
135+
try {
136+
let content = '';
137+
const buffer = xterm.raw.buffer.active;
138+
const line = buffer.getLine(buffer.baseY + buffer.cursorY);
139+
if (line) {
140+
content = line.translateToString(true) + '\n';
141+
}
142+
return { detected: false, reason: `Extended timeout reached without prompt detection. Last line: "${content.trim()}"` };
143+
} catch (error) {
144+
return { detected: false, reason: `Extended timeout reached. Error reading terminal content: ${error}` };
145+
}
146+
}
147+
39148
/**
40149
* Tracks the terminal for being idle on a prompt input. This must be called before `executeCommand`
41150
* is called.

src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { CancellationError } from '../../../../../../base/common/errors.js';
88
import { DisposableStore } from '../../../../../../base/common/lifecycle.js';
99
import { ITerminalLogService } from '../../../../../../platform/terminal/common/terminal.js';
1010
import type { ITerminalInstance } from '../../../../terminal/browser/terminal.js';
11-
import { waitForIdle, type ITerminalExecuteStrategy, type ITerminalExecuteStrategyResult } from './executeStrategy.js';
11+
import { waitForIdle, waitForIdleWithPromptHeuristics, type ITerminalExecuteStrategy, type ITerminalExecuteStrategyResult } from './executeStrategy.js';
1212

1313
/**
1414
* This strategy is used when no shell integration is available. There are very few extension APIs
@@ -63,8 +63,9 @@ export class NoneExecuteStrategy implements ITerminalExecuteStrategy {
6363
this._instance.sendText(commandLine, true);
6464

6565
// Assume the command is done when it's idle
66-
this._log('Waiting for idle');
67-
await waitForIdle(this._instance.onData, 1000);
66+
this._log('Waiting for idle with prompt heuristics');
67+
const promptResult = await waitForIdleWithPromptHeuristics(this._instance.onData, this._instance, 1000, 10000);
68+
this._log(`Prompt detection result: ${promptResult.detected ? 'detected' : 'not detected'} - ${promptResult.reason}`);
6869
if (token.isCancellationRequested) {
6970
throw new CancellationError();
7071
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { strictEqual } from 'assert';
7+
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
8+
import { detectsCommonPromptPattern } from '../../browser/executeStrategy/executeStrategy.js';
9+
10+
suite('Execute Strategy - Prompt Detection', () => {
11+
ensureNoDisposablesAreLeakedInTestSuite();
12+
13+
test('detectsCommonPromptPattern should detect PowerShell prompts', () => {
14+
strictEqual(detectsCommonPromptPattern('PS C:\\>').detected, true);
15+
strictEqual(detectsCommonPromptPattern('PS C:\\Windows\\System32>').detected, true);
16+
strictEqual(detectsCommonPromptPattern('PS C:\\Users\\test> ').detected, true);
17+
});
18+
19+
test('detectsCommonPromptPattern should detect Command Prompt', () => {
20+
strictEqual(detectsCommonPromptPattern('C:\\>').detected, true);
21+
strictEqual(detectsCommonPromptPattern('C:\\Windows\\System32>').detected, true);
22+
strictEqual(detectsCommonPromptPattern('D:\\test> ').detected, true);
23+
});
24+
25+
test('detectsCommonPromptPattern should detect Bash prompts', () => {
26+
strictEqual(detectsCommonPromptPattern('user@host:~$ ').detected, true);
27+
strictEqual(detectsCommonPromptPattern('$ ').detected, true);
28+
strictEqual(detectsCommonPromptPattern('[user@host ~]$ ').detected, true);
29+
});
30+
31+
test('detectsCommonPromptPattern should detect root prompts', () => {
32+
strictEqual(detectsCommonPromptPattern('root@host:~# ').detected, true);
33+
strictEqual(detectsCommonPromptPattern('# ').detected, true);
34+
strictEqual(detectsCommonPromptPattern('[root@host ~]# ').detected, true);
35+
});
36+
37+
test('detectsCommonPromptPattern should detect Python REPL', () => {
38+
strictEqual(detectsCommonPromptPattern('>>> ').detected, true);
39+
strictEqual(detectsCommonPromptPattern('>>>').detected, true);
40+
});
41+
42+
test('detectsCommonPromptPattern should detect starship prompts', () => {
43+
strictEqual(detectsCommonPromptPattern('~ \u276f ').detected, true);
44+
strictEqual(detectsCommonPromptPattern('/path/to/project \u276f').detected, true);
45+
});
46+
47+
test('detectsCommonPromptPattern should detect generic prompts', () => {
48+
strictEqual(detectsCommonPromptPattern('test> ').detected, true);
49+
strictEqual(detectsCommonPromptPattern('someprompt% ').detected, true);
50+
});
51+
52+
test('detectsCommonPromptPattern should handle multiline content', () => {
53+
const multilineContent = `command output line 1
54+
command output line 2
55+
user@host:~$ `;
56+
strictEqual(detectsCommonPromptPattern(multilineContent).detected, true);
57+
});
58+
59+
test('detectsCommonPromptPattern should reject non-prompt content', () => {
60+
strictEqual(detectsCommonPromptPattern('just some output').detected, false);
61+
strictEqual(detectsCommonPromptPattern('error: command not found').detected, false);
62+
strictEqual(detectsCommonPromptPattern('').detected, false);
63+
strictEqual(detectsCommonPromptPattern(' ').detected, false);
64+
});
65+
66+
test('detectsCommonPromptPattern should handle edge cases', () => {
67+
strictEqual(detectsCommonPromptPattern('output\n\n\n').detected, false);
68+
strictEqual(detectsCommonPromptPattern('\n\n$ \n\n').detected, true); // prompt with surrounding whitespace
69+
strictEqual(detectsCommonPromptPattern('output\nPS C:\\> ').detected, true); // prompt at end after output
70+
});
71+
});

0 commit comments

Comments
 (0)