Terminal: Quick fix ghost text with AI suggestions#295983
Terminal: Quick fix ghost text with AI suggestions#295983federicobrancasi wants to merge 3 commits intomicrosoft:mainfrom
Conversation
|
@microsoft-github-policy-service agree company="Microsoft" |
There was a problem hiding this comment.
Pull request overview
Adds “ghost text” rendering for terminal quick fixes, with optional AI-backed command corrections when no built-in quick fix is available.
Changes:
- Render first terminal-command quick fix as inline ghost text and allow accepting via Tab/Right Arrow.
- Add AI command suggestion fallback using
ILanguageModelsService(Copilot) when a command fails and no quick fix exists. - Introduce new terminal settings to control ghost text vs auto-fill behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution.ts | Implements ghost text rendering/acceptance and AI suggestion retrieval. |
| src/vs/workbench/contrib/terminalContrib/quickFix/browser/media/terminalQuickFix.css | Adds styling for a ghost-text decoration class (currently not wired up). |
| src/vs/workbench/contrib/terminal/common/terminalContextKey.ts | Adds a new context key for ghost text visibility (currently not set/used). |
| src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts | Registers the new quickFixGhostText / quickFixAutoFill settings in the schema. |
| src/vs/platform/terminal/common/terminal.ts | Adds new TerminalSettingId entries for the settings. |
Comments suppressed due to low confidence (3)
src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution.ts:203
CancellationTokenSourceis created for the chat request but never disposed/cancelled. This can leak listeners/resources and also prevents cancelling the request when the terminal instance is disposed or a newer command arrives. Wrap it intry/finally { cts.dispose(); }and consider wiring cancellation to the contribution's lifecycle or to the per-command request handling.
const cts = new CancellationTokenSource();
const response = await this._languageModelsService.sendChatRequest(
models[0],
new ExtensionIdentifier('core'),
[{ role: ChatMessageRole.User, content: [{ type: 'text', value: prompt }] }],
{},
cts.token
);
src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution.ts:143
- Ghost text is never cleared when quick fixes disappear.
TerminalQuickFixAddonfiresonDidUpdateQuickFixeswithactions: undefinedin_disposeQuickFix, and whengetQuickFixesForCommandreturnsundefinedthere is no update event at all. In both casescurrentGhostTextcan linger. EnsureclearGhostText()is called whene.actionsis empty/undefined, and consider clearing when new quick fixes are not available.
this.add(this._addon.onDidUpdateQuickFixes(async e => {
// Only track the latest command's quick fixes
this._quickFixMenuItems.value = e.actions ? xterm.decorationAddon.registerMenuItems(e.command, e.actions) : undefined;
// Get the first terminal command quick fix (filter out copilot-debug suggestions)
const terminalCommandFix = e.actions?.find(a =>
a.type === TerminalQuickFixType.TerminalCommand &&
a.command &&
!a.command.startsWith('copilot-debug')
);
// Ghost text mode: write dim text to terminal
const ghostTextEnabled = this._configurationService.getValue<boolean>(TerminalSettingId.ShellIntegrationQuickFixGhostText);
if (ghostTextEnabled && terminalCommandFix?.command) {
writeGhostText(terminalCommandFix.command);
return;
}
// Auto-fill mode: fill the prompt immediately
const autoFillEnabled = this._configurationService.getValue<boolean>(TerminalSettingId.ShellIntegrationQuickFixAutoFill);
if (autoFillEnabled && terminalCommandFix?.command) {
this._ctx.instance.runCommand(terminalCommandFix.command, false);
return;
}
// AI-powered fix: if no quick fix found but command failed, try AI (requires Copilot)
if (ghostTextEnabled && !terminalCommandFix && e.command) {
const output = e.command.getOutput() ?? '';
const commandLine = e.command.command;
const exitCode = e.command.exitCode;
// Try AI if: command failed (non-zero exit) AND output is short (< 1000 chars = likely error message)
const shouldTryAI = exitCode !== 0 && output.length < 1000;
if (shouldTryAI && commandLine) {
const aiSuggestion = await this._getAICommandSuggestion(commandLine, output);
if (aiSuggestion) {
writeGhostText(aiSuggestion);
}
}
}
}));
src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution.ts:70
- Rendering ghost text via
xterm.raw.writemutates the terminal buffer (it becomes real text in the scrollback and can affect wrapping/copy/paste and prompt positioning). For a true “ghost”/overlay UI, consider using an xterm decoration (similar to the existing quick-fix lightbulb decoration) so the suggestion doesn't become part of the buffer contents.
const writeGhostText = (text: string) => {
// Write dim text (ANSI escape: \x1b[2m = dim, \x1b[0m = reset)
// Also save cursor, write, restore cursor
xterm.raw.write(`\x1b7\x1b[2m${text}\x1b8`);
currentGhostText = text;
};
src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution.ts
Show resolved
Hide resolved
| // AI-powered fix: if no quick fix found but command failed, try AI (requires Copilot) | ||
| if (ghostTextEnabled && !terminalCommandFix && e.command) { | ||
| const output = e.command.getOutput() ?? ''; | ||
| const commandLine = e.command.command; | ||
| const exitCode = e.command.exitCode; | ||
| // Try AI if: command failed (non-zero exit) AND output is short (< 1000 chars = likely error message) | ||
| const shouldTryAI = exitCode !== 0 && output.length < 1000; | ||
| if (shouldTryAI && commandLine) { | ||
| const aiSuggestion = await this._getAICommandSuggestion(commandLine, output); | ||
| if (aiSuggestion) { | ||
| writeGhostText(aiSuggestion); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The AI request can race with subsequent commands/quick-fix updates: multiple onDidUpdateQuickFixes events can arrive while an earlier _getAICommandSuggestion is still streaming, and the stale response can call writeGhostText after a newer suggestion (or after the user typed). Consider storing a request-scoped token/sequence number and/or keeping a MutableDisposable<CancellationTokenSource> that cancels any in-flight request before starting a new one, and ensure you only apply the response if it still corresponds to the current command.
This issue also appears on line 196 of the same file.
| console.log('[QuickFix AI] User command:', command); | ||
| console.log('[QuickFix AI] Error output:', errorOutput.substring(0, 200)); | ||
|
|
||
| try { | ||
| // Select a fast model (gpt-4o-mini preferred for speed) | ||
| let models = await this._languageModelsService.selectLanguageModels({ | ||
| vendor: 'copilot', | ||
| family: 'gpt-4o-mini' | ||
| }); | ||
|
|
||
| if (models.length === 0) { | ||
| // Fallback to any available Copilot model | ||
| models = await this._languageModelsService.selectLanguageModels({ vendor: 'copilot' }); | ||
| if (models.length === 0) { | ||
| console.log('[QuickFix AI] No Copilot models available'); | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| console.log('[QuickFix AI] Using model:', models[0]); | ||
|
|
||
| const prompt = `Command: ${command} | ||
| Error: ${errorOutput.substring(0, 200)} | ||
| What is the correct shell command? Reply with ONLY the corrected command, nothing else.`; | ||
|
|
||
| const cts = new CancellationTokenSource(); | ||
| const response = await this._languageModelsService.sendChatRequest( | ||
| models[0], | ||
| new ExtensionIdentifier('core'), | ||
| [{ role: ChatMessageRole.User, content: [{ type: 'text', value: prompt }] }], | ||
| {}, | ||
| cts.token | ||
| ); | ||
|
|
||
| let result = ''; | ||
| for await (const part of response.stream) { | ||
| const parts = Array.isArray(part) ? part : [part]; | ||
| for (const p of parts) { | ||
| if (p.type === 'text') { | ||
| result += p.value; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| console.log('[QuickFix AI] Raw response:', result); | ||
|
|
||
| // Clean up the response - strip markdown code fences and extract just the command | ||
| let cleaned = result.trim(); | ||
| // Remove markdown code fences (```bash, ```sh, ```, etc.) | ||
| cleaned = cleaned.replace(/^```[\w]*\n?/gm, '').replace(/```$/gm, '').trim(); | ||
| // Take first non-empty line | ||
| cleaned = cleaned.split('\n').map(l => l.trim()).filter(l => l)[0] || ''; | ||
|
|
||
| console.log('[QuickFix AI] Parsed command:', cleaned); | ||
|
|
||
| // Don't suggest the same command | ||
| if (cleaned && cleaned !== command) { | ||
| return cleaned; | ||
| } | ||
| } catch (e) { | ||
| console.log('[QuickFix AI] Error:', e); | ||
| } |
There was a problem hiding this comment.
There are several console.log statements in _getAICommandSuggestion. These should not ship in production code; please route through the appropriate logging service (ILogService) behind a debug flag, or remove them before merging.
There was a problem hiding this comment.
Keeping for now while this is WIP/Draft. Will remove before marking ready for review.
| // Get the first terminal command quick fix (filter out copilot-debug suggestions) | ||
| const terminalCommandFix = e.actions?.find(a => | ||
| a.type === TerminalQuickFixType.TerminalCommand && | ||
| a.command && | ||
| !a.command.startsWith('copilot-debug') |
There was a problem hiding this comment.
The hard-coded filter !a.command.startsWith('copilot-debug') is the only occurrence of copilot-debug in the repo, which makes it look like leftover debug/prototype behavior. If this is intended, it should be expressed via structured metadata on the action (eg a dedicated flag/source) rather than matching a magic string prefix; otherwise remove it.
| // Get the first terminal command quick fix (filter out copilot-debug suggestions) | |
| const terminalCommandFix = e.actions?.find(a => | |
| a.type === TerminalQuickFixType.TerminalCommand && | |
| a.command && | |
| !a.command.startsWith('copilot-debug') | |
| // Get the first terminal command quick fix | |
| const terminalCommandFix = e.actions?.find(a => | |
| a.type === TerminalQuickFixType.TerminalCommand && | |
| a.command |
There was a problem hiding this comment.
This filters out the "copilot-debug" quick fix suggestions from appearing as ghost text.
| // Auto-fill mode: fill the prompt immediately | ||
| const autoFillEnabled = this._configurationService.getValue<boolean>(TerminalSettingId.ShellIntegrationQuickFixAutoFill); | ||
| if (autoFillEnabled && terminalCommandFix?.command) { | ||
| this._ctx.instance.runCommand(terminalCommandFix.command, false); | ||
| return; | ||
| } | ||
|
|
||
| // AI-powered fix: if no quick fix found but command failed, try AI (requires Copilot) | ||
| if (ghostTextEnabled && !terminalCommandFix && e.command) { | ||
| const output = e.command.getOutput() ?? ''; | ||
| const commandLine = e.command.command; | ||
| const exitCode = e.command.exitCode; | ||
| // Try AI if: command failed (non-zero exit) AND output is short (< 1000 chars = likely error message) | ||
| const shouldTryAI = exitCode !== 0 && output.length < 1000; | ||
| if (shouldTryAI && commandLine) { |
There was a problem hiding this comment.
The PR description says Copilot should be used when no built-in quick fix is available, but the implementation only attempts AI suggestions when terminal.integrated.shellIntegration.quickFixGhostText is enabled. If quickFixAutoFill is enabled (and ghost text is disabled), AI suggestions will never run. Either update the description or extend the implementation to cover the auto-fill case as well.
|
|
||
| /* Quick fix ghost text styling */ | ||
| .xterm-screen .xterm-decoration-container .xterm-decoration.quick-fix-ghost-text { | ||
| z-index: 6; | ||
| opacity: 0.5; | ||
| font-style: italic; | ||
| color: var(--vscode-editorGhostText-foreground, var(--vscode-terminal-foreground)); | ||
| pointer-events: none; | ||
| white-space: pre; | ||
| } |
There was a problem hiding this comment.
This CSS rule targets a .quick-fix-ghost-text decoration element, but the implementation renders ghost text via xterm.raw.write ANSI sequences and never creates a decoration with this class. Either remove this unused CSS or switch to an xterm decoration-based approach and apply the class from code.
| /* Quick fix ghost text styling */ | |
| .xterm-screen .xterm-decoration-container .xterm-decoration.quick-fix-ghost-text { | |
| z-index: 6; | |
| opacity: 0.5; | |
| font-style: italic; | |
| color: var(--vscode-editorGhostText-foreground, var(--vscode-terminal-foreground)); | |
| pointer-events: none; | |
| white-space: pre; | |
| } |
| @@ -93,6 +94,9 @@ export namespace TerminalContextKeys { | |||
| /** Whether the terminal's suggest widget is visible. */ | |||
| export const suggestWidgetVisible = new RawContextKey<boolean>(TerminalContextKeyStrings.SuggestWidgetVisible, false, localize('terminalSuggestWidgetVisible', "Whether the terminal's suggest widget is visible.")); | |||
|
|
|||
| /** Whether quick fix ghost text is visible in the terminal. */ | |||
| export const quickFixGhostTextVisible = new RawContextKey<boolean>(TerminalContextKeyStrings.QuickFixGhostTextVisible, false, localize('terminalQuickFixGhostTextVisible', "Whether quick fix ghost text is visible in the terminal.")); | |||
|
|
|||
There was a problem hiding this comment.
A new context key terminalQuickFixGhostTextVisible is added, but it isn't bound/set anywhere in the terminal quick fix contribution. Without setting it, it can’t be used for keybindings/when-clauses and becomes dead API surface. Either wire it up (bind to the terminal instance’s context key service and set/reset when ghost text appears) or remove it.
src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution.ts
Outdated
Show resolved
Hide resolved
| const writeGhostText = (text: string) => { | ||
| // Write dim text (ANSI escape: \x1b[2m = dim, \x1b[0m = reset) | ||
| // Also save cursor, write, restore cursor | ||
| xterm.raw.write(`\x1b7\x1b[2m${text}\x1b8`); | ||
| currentGhostText = text; | ||
| }; | ||
|
|
||
| const clearGhostText = () => { | ||
| if (currentGhostText) { | ||
| // Clear the ghost text by overwriting with spaces | ||
| xterm.raw.write(`\x1b7${' '.repeat(currentGhostText.length)}\x1b8`); | ||
| currentGhostText = undefined; | ||
| } | ||
| }; | ||
|
|
||
| const acceptGhostText = () => { | ||
| if (currentGhostText) { | ||
| const text = currentGhostText; | ||
| clearGhostText(); | ||
| // Write the command to the terminal (without executing) | ||
| this._ctx.instance.runCommand(text, false); | ||
| return true; |
There was a problem hiding this comment.
AI (and potentially extension-provided) suggestions are written directly to the terminal via xterm.raw.write and then injected into the prompt via runCommand without sanitizing control characters. If a suggestion contains escape sequences (eg \x1b]...), it could manipulate the terminal (clipboard, title, etc). Strip/escape non-printable characters (at least C0 control chars and ESC sequences) before rendering/accepting ghost text.
This issue also appears on line 65 of the same file.
0d742c8 to
22788c1
Compare
|
Hey, this is cool idea, but the problem is that it'll conflict with a user's shell configured ghost text/completion |
Fixes #295981
Description
This PR enhances the terminal quick fix feature by displaying command corrections as ghost text directly in the terminal prompt. Users can accept suggestions with Tab or Right Arrow.
When no built-in quick fix is available, it uses Copilot to suggest the correct command.
Demo
terminal.suggestions.mp4
Features
git statys→git statusld -la→ls -la,mkdor hello→mkdir helloNew Settings
terminal.integrated.shellIntegration.quickFixGhostText- Show suggestions as ghost textterminal.integrated.shellIntegration.quickFixAutoFill- Auto-fill the correction immediatelyImplementation Details
\x1b[2mfor dim text)attachCustomKeyEventHandlerILanguageModelsServiceto call Copilot (gpt-4o-mini)copilot-debugsuggestions filtered out from ghost textConsole Logging (Debug)
Known Limitations / Future Work
ls -lagets suggested asGet-ChildItem -Force(shell-aware suggestions needed)console.logstatements before mergingPrior Art
Inspired by Warp Terminal - Command Corrections
Testing
terminal.integrated.shellIntegration.quickFixGhostText: truegit statys+ Enter → ghost text appearsld -la+ Enter → AI suggestsls -la