Skip to content

Commit 6e6bc29

Browse files
CopilotTyriar
andcommitted
Add prompt detection heuristics for noneExecuteStrategy timeout
Co-authored-by: Tyriar <[email protected]>
1 parent 66f6853 commit 6e6bc29

File tree

2 files changed

+113
-3
lines changed

2 files changed

+113
-3
lines changed

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

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

39+
/**
40+
* Detects if the given text content appears to end with a common prompt pattern.
41+
* This is used as a heuristic to determine if a command has finished executing.
42+
*/
43+
export function detectsCommonPromptPattern(content: string): boolean {
44+
if (!content || content.trim().length === 0) {
45+
return false;
46+
}
47+
48+
// Split content into lines and check the last non-empty line
49+
const lines = content.split('\n');
50+
let lastLine = '';
51+
for (let i = lines.length - 1; i >= 0; i--) {
52+
const line = lines[i].trim();
53+
if (line.length > 0) {
54+
lastLine = line;
55+
break;
56+
}
57+
}
58+
59+
if (!lastLine) {
60+
return false;
61+
}
62+
63+
// PowerShell prompt: PS C:\> or similar patterns
64+
if (/PS\s+[A-Z]:\\.*>\s*$/.test(lastLine)) {
65+
return true;
66+
}
67+
68+
// Command Prompt: C:\path>
69+
if (/^[A-Z]:\\.*>\s*$/.test(lastLine)) {
70+
return true;
71+
}
72+
73+
// Bash-style prompts ending with $
74+
if (/\$\s*$/.test(lastLine)) {
75+
return true;
76+
}
77+
78+
// Root prompts ending with #
79+
if (/#\s*$/.test(lastLine)) {
80+
return true;
81+
}
82+
83+
// Python REPL prompt
84+
if (/^>>>\s*$/.test(lastLine)) {
85+
return true;
86+
}
87+
88+
// Custom prompts ending with the starship character ❯ (\u276f)
89+
if (/\u276f\s*$/.test(lastLine)) {
90+
return true;
91+
}
92+
93+
// Generic prompts ending with common prompt characters
94+
if (/[>%]\s*$/.test(lastLine)) {
95+
return true;
96+
}
97+
98+
return false;
99+
}
100+
101+
/**
102+
* Enhanced version of waitForIdle that uses prompt detection heuristics.
103+
* After the initial timeout, checks if the terminal content looks like a common prompt.
104+
* If not, extends the timeout to give the command more time to complete.
105+
*/
106+
export async function waitForIdleWithPromptHeuristics(
107+
onData: Event<unknown>,
108+
instance: ITerminalInstance,
109+
initialTimeoutMs: number,
110+
extendedTimeoutMs: number = 2000
111+
): Promise<void> {
112+
// First, wait for the initial timeout period
113+
await waitForIdle(onData, initialTimeoutMs);
114+
115+
// Get the current terminal content to check for prompt patterns
116+
try {
117+
const xterm = await instance.xtermReadyPromise;
118+
if (xterm) {
119+
// Get the current visible content from the terminal
120+
const buffer = xterm.raw.buffer.active;
121+
const viewportHeight = xterm.raw.rows;
122+
let content = '';
123+
124+
// Read the last few lines of the terminal to detect prompt patterns
125+
const startLine = Math.max(0, buffer.baseY + buffer.cursorY - viewportHeight + 1);
126+
const endLine = buffer.baseY + buffer.cursorY + 1;
127+
128+
for (let i = startLine; i < endLine; i++) {
129+
const line = buffer.getLine(i);
130+
if (line) {
131+
content += line.translateToString(true) + '\n';
132+
}
133+
}
134+
135+
// If we detect a common prompt pattern, we're done
136+
if (detectsCommonPromptPattern(content)) {
137+
return;
138+
}
139+
140+
// Otherwise, wait for the extended timeout period
141+
await waitForIdle(onData, extendedTimeoutMs);
142+
}
143+
} catch (error) {
144+
// If there's an error getting terminal content, fall back to extended timeout
145+
await waitForIdle(onData, extendedTimeoutMs);
146+
}
147+
}
148+
39149
/**
40150
* Tracks the terminal for being idle on a prompt input. This must be called before `executeCommand`
41151
* is called.

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

Lines changed: 3 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,8 @@ 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+
await waitForIdleWithPromptHeuristics(this._instance.onData, this._instance, 1000);
6868
if (token.isCancellationRequested) {
6969
throw new CancellationError();
7070
}

0 commit comments

Comments
 (0)