From a2d7a0a4db5c1ae241aa3b6cc2235d437db93308 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Tue, 17 Sep 2024 10:40:06 -0400 Subject: [PATCH] Add shell listener to reduce occurrences of empty outputs --- src/integrations/TerminalManager.ts | 53 ++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/src/integrations/TerminalManager.ts b/src/integrations/TerminalManager.ts index f6e27fa..80ec2bd 100644 --- a/src/integrations/TerminalManager.ts +++ b/src/integrations/TerminalManager.ts @@ -67,9 +67,9 @@ with older VSCode versions. Users on older versions will automatically fall back for terminal command execution. Interestingly, some environments like Cursor enable these APIs even without the latest VSCode engine. This approach allows us to leverage advanced features when available while ensuring broad compatibility. -https://github.com/microsoft/vscode/blob/f0417069c62e20f3667506f4b7e53ca0004b4e3e/src/vscode-dts/vscode.d.ts#L7442 */ declare module "vscode" { + // https://github.com/microsoft/vscode/blob/f0417069c62e20f3667506f4b7e53ca0004b4e3e/src/vscode-dts/vscode.d.ts#L7442 interface Terminal { shellIntegration?: { cwd?: vscode.Uri @@ -78,6 +78,14 @@ declare module "vscode" { } } } + // https://github.com/microsoft/vscode/blob/f0417069c62e20f3667506f4b7e53ca0004b4e3e/src/vscode-dts/vscode.d.ts#L10794 + interface Window { + onDidEndTerminalShellExecution?: ( + listener: (e: any) => any, + thisArgs?: any, + disposables?: vscode.Disposable[] + ) => vscode.Disposable + } } // Although vscode.window.terminals provides a list of all open terminals, there's no way to know whether they're busy or not (exitStatus does not provide useful information for most commands). In order to prevent creating too many terminals, we need to keep track of terminals through the life of the extension, as well as session specific terminals for the life of a task (to get latest unretrieved output). @@ -136,6 +144,24 @@ class TerminalRegistry { export class TerminalManager { private terminalIds: Set = new Set() private processes: Map = new Map() + private disposables: vscode.Disposable[] = [] + + constructor() { + // Listening to this reduces the # of empty terminal outputs! + const disposable = (vscode.window as vscode.Window).onDidEndTerminalShellExecution?.(async (e) => { + // console.log(`Terminal shell execution ended. Command line:`, e.execution.commandLine.value) + const stream = e?.execution?.read() + if (stream) { + for await (let _ of stream) { + // console.log(`from onDidEndTerminalShellExecution, read:`, data) + } + } + }) + if (disposable) { + this.disposables.push(disposable) + } + // Oddly if we listen to `onDidStartTerminalShellExecution` or `onDidChangeTerminalShellIntegration` this hack doesn't work... + } runCommand(terminalInfo: TerminalInfo, command: string): TerminalProcessResultPromise { terminalInfo.busy = true @@ -232,6 +258,8 @@ export class TerminalManager { // } this.terminalIds.clear() this.processes.clear() + this.disposables.forEach((disposable) => disposable.dispose()) + this.disposables = [] } } @@ -280,12 +308,34 @@ export class TerminalProcess extends EventEmitter { /* The first chunk we get from this stream needs to be processed to be more human readable, ie remove vscode's custom escape sequences and identifiers, removing duplicate first char bug, etc. */ + + // bug where sometimes the command output makes its way into vscode shell integration metadata + /* + ]633 is a custom sequence number used by VSCode shell integration: + - OSC 633 ; A ST - Mark prompt start + - OSC 633 ; B ST - Mark prompt end + - OSC 633 ; C ST - Mark pre-execution (start of command output) + - OSC 633 ; D [; ] ST - Mark execution finished with optional exit code + - OSC 633 ; E ; [; ] ST - Explicitly set command line with optional nonce + */ + // if you print this data you might see something like "eecho hello worldo hello world;5ba85d14-e92a-40c4-b2fd-71525581eeb0]633;C" but this is actually just a bunch of escape sequences, ignore up to the first ;C + /* ddateb15026-6a64-40db-b21f-2a621a9830f0]633;CTue Sep 17 06:37:04 EDT 2024 % ]633;D;0]633;P;Cwd=/Users/saoud/Repositories/test */ + // Gets output between ]633;C (command start) and ]633;D (command end) + const outputBetweenSequences = this.removeLastLineArtifacts( + data.match(/\]633;C([\s\S]*?)\]633;D/)?.[1] || "" + ).trim() + + // Once we've retrieved any potential output between sequences, we can remove everything up to end of the last sequence // https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st const vscodeSequenceRegex = /\x1b\]633;.[^\x07]*\x07/g const lastMatch = [...data.matchAll(vscodeSequenceRegex)].pop() if (lastMatch && lastMatch.index !== undefined) { data = data.slice(lastMatch.index + lastMatch[0].length) } + // Place output back after removing vscode sequences + if (outputBetweenSequences) { + data = outputBetweenSequences + "\n" + data + } // remove ansi data = stripAnsi(data) // Split data by newlines @@ -313,6 +363,7 @@ export class TerminalProcess extends EventEmitter { } // first few chunks could be the command being echoed back, so we must ignore + // note this means that 'echo' commands wont work if (!didOutputNonCommand) { const lines = data.split("\n") for (let i = 0; i < lines.length; i++) {