terminal tools: detect prompts on the last output line#311765
Conversation
a91ef95 to
73c4a33
Compare
There was a problem hiding this comment.
Pull request overview
This PR addresses terminal output monitoring stalls by narrowing line-oriented prompt/help detection to only the last output line, preventing repeated regex evaluation over large multi-line tails during idle polling.
Changes:
- Scope
detectsNonInteractiveHelpPatternanddetectsInputRequiredPatternchecks to the last output line (while keeping task-finish and generic “press any key” detection on the bounded tail). - Add a regression test ensuring non-interactive help text on a non-final line doesn’t stop custom polling.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts |
Introduces last-line extraction and applies it to line-oriented detectors in idle/idle-wait paths. |
src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/outputMonitor.test.ts |
Adds a regression test covering help text not on the final line. |
meganrogge
left a comment
There was a problem hiding this comment.
Thanks for the fix! The core approach is right, but I have a few concerns about _getLastLineForPatternDetection that are worth addressing before merge:
-
Trailing newline produces an empty last line. Many CLIs flush prompts with a trailing
\n(or the pty emits\r\nafter a cursor update), e.g.Continue? [y/N]\n. With:const lastLineBreak = Math.max(output.lastIndexOf('\n'), output.lastIndexOf('\r')); return lastLineBreak === -1 ? output : output.slice(lastLineBreak + 1);
slice(lastLineBreak + 1)returns""and bothdetectsInputRequiredPatternanddetectsNonInteractiveHelpPatternwill miss it. Previously the$-anchored regexes would match the prompt against the tail. This looks like a regression — consider stripping trailing\r/\nfirst, or falling back to the previous non-empty line:const trimmed = output.replace(/[\r\n]+$/, ''); const idx = Math.max(trimmed.lastIndexOf('\n'), trimmed.lastIndexOf('\r')); return idx === -1 ? trimmed : trimmed.slice(idx + 1);
-
Bare
\rprogress updates truncate to a fragment. Tools likenpm,pip,docker pullrewrite the same line with\r.lastIndexOf('\r')will then return a mid-line fragment rather than the real prompt line. (CRLF is fine because\nwins viaMath.max, but bare\rredraws on Linux/macOS ttys will be truncated.) -
Prompts followed by explanatory text. Some tools write the prompt and then a hint line after it:
Continue? [y/N] (press Ctrl-C to abort)The last line is now the hint, so the prompt no longer matches. Probably rare, but a behavioral change vs. the old tail-wide scan.
Could we add regression tests for (a) a prompt ending in \n and (b) a \r-based progress line, to cover these cases? The stall fix for #311757 is good — just want to make sure we don't silently break input-required detection for the common trailing-newline case.
|
Addressed the review feedback in 2c69c47. Changes:
Validation:
I left the prompt-followed-by-hint behavior unchanged for now since that is a broader heuristic tradeoff rather than the concrete regression called out here. |
meganrogge
left a comment
There was a problem hiding this comment.
Thanks for the fix — scoping these detectors to the last line is the right direction since the help-pattern regexes aren't $-anchored. A few concerns:
-
Possible regression when the tail ends with a newline. If
outputTailends in\nor\r(common when a command just printed its final line),_getLastLineForPatternDetectionreturns''. Previously, severaldetectsInputRequiredPatternregexes (e.g./:\s*$/,/\?\s*...$/) could still match because\smatches\n. After this change, a prompt like"Password:\n"in the buffer will no longer be detected. Consider trimming trailing newlines first, e.g.:const trimmed = output.replace(/[\r\n]+$/, ''); const lastLineBreak = Math.max(trimmed.lastIndexOf('\n'), trimmed.lastIndexOf('\r')); return lastLineBreak === -1 ? trimmed : trimmed.slice(lastLineBreak + 1);
-
Behavior change for
detectsInputRequiredPattern. Most regexes in that function are already$-anchored, so scoping is a no-op for them. The one exception is/press a(?:ny)? key/i, which previously could match anywhere in the 1000-char tail and now only matches on the last line. That seems desirable, but worth confirming it doesn't regress any real scenarios. -
Brittle private-method access in the new test. Casting the monitor to
{ [key: string]: ... }to invoke_handleIdleStatedirectly is unusual for this codebase and couples the test to the private shape. Two suggestions:- Prefer driving through the public flow (e.g.
startMonitoring/ the poll loop) so the test survives refactors. - Or export
_getLastLineForPatternDetectionas a free function and unit-test it directly — it has nothisdependency.
Also consider adding a positive case (help text on the last line → pattern detected) so both branches are covered.
- Prefer driving through the public flow (e.g.
-
Minor.
_getLastLineForPatternDetectioncould just be named_getLastLine(or a module-local free function), since it's purely string slicing.
Nothing blocking, but (1) feels like it could bite in practice.
Fixes #311757
OutputMonitor was running line-oriented prompt detection against the last 1000 characters of terminal output. When the tail contained multiple lines, stale lines in the scrollback could be treated as the current prompt and the monitor would repeatedly run prompt regexes over large multiline output.
This change scopes non-interactive help and input-required prompt detection to the last output line while keeping task-finish and generic "press any key" detection on the bounded tail.
It also adds a regression test that covers help text appearing on a non-final line.
Testing: