diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index 51dc043abe35a..c35cb703601d3 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -251,6 +251,12 @@ export interface ICommandDetectionCapability { * always be present when running the _builtin_ SI scripts. */ setCommandLine(commandLine: string, isTrusted: boolean): void; + /** + * Sets the command ID to use for the next command that starts. + * This allows pre-assigning an ID before the shell sends the command start sequence, + * which is useful for linking commands across renderer and ptyHost. + */ + setNextCommandId(command: string, commandId: string): void; serialize(): ISerializedCommandDetectionCapability; deserialize(serialized: ISerializedCommandDetectionCapability): void; } @@ -270,6 +276,12 @@ export interface IHandleCommandOptions { * Properties for the mark */ markProperties?: IMarkProperties; + + /** + * An optional predefined command ID. When provided, this ID will be used instead of + * generating a new one, allowing commands to be linked across renderer and ptyHost. + */ + commandId?: string; } export interface INaiveCwdDetectionCapability { @@ -291,7 +303,7 @@ interface IBaseTerminalCommand { isTrusted: boolean; timestamp: number; duration: number; - id: string; + id: string | undefined; // Optional serializable cwd: string | undefined; diff --git a/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts b/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts index 10102ebc26508..30fd67a237d49 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts @@ -6,7 +6,6 @@ import { IMarkProperties, ISerializedTerminalCommand, ITerminalCommand } from '../capabilities.js'; import { ITerminalOutputMatcher, ITerminalOutputMatch } from '../../terminal.js'; import type { IBuffer, IBufferLine, IMarker, Terminal } from '@xterm/headless'; -import { generateUuid } from '../../../../../base/common/uuid.js'; export interface ITerminalCommandProperties { command: string; @@ -14,7 +13,7 @@ export interface ITerminalCommandProperties { isTrusted: boolean; timestamp: number; duration: number; - id: string; + id: string | undefined; marker: IMarker | undefined; cwd: string | undefined; exitCode: number | undefined; @@ -276,7 +275,7 @@ export class PartialTerminalCommand implements ICurrentPartialCommand { cwd?: string; command?: string; commandLineConfidence?: 'low' | 'medium' | 'high'; - id: string; + id: string | undefined; isTrusted?: boolean; isInvalid?: boolean; @@ -285,8 +284,7 @@ export class PartialTerminalCommand implements ICurrentPartialCommand { private readonly _xterm: Terminal, id?: string ) { - //TODO: this does not restore properly due to conflicting with the one created in the. PtyHost - this.id = id ?? generateUuid(); + this.id = id; } serialize(cwd: string | undefined): ISerializedTerminalCommand | undefined { diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index 0bcb3f93cbe9d..6aab13ed5a2e7 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -35,6 +35,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe private _handleCommandStartOptions?: IHandleCommandOptions; private _hasRichCommandDetection: boolean = false; get hasRichCommandDetection() { return this._hasRichCommandDetection; } + private _nextCommandId: { command: string; commandId: string | undefined } | undefined; private _ptyHeuristicsHooks: ICommandDetectionHeuristicsHooks; private readonly _ptyHeuristics: MandatoryMutableDisposable; @@ -342,6 +343,14 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe this._ptyHeuristics.value.handleCommandStart(options); } + /** + * Sets the command ID to use for the next command that starts. + * This is useful when you want to pre-assign an ID before the shell sends the command start sequence. + */ + setNextCommandId(command: string, commandId: string): void { + this._nextCommandId = { command, commandId }; + } + handleCommandExecuted(options?: IHandleCommandOptions): void { this._ptyHeuristics.value.handleCommandExecuted(options); this._currentCommand.markExecutedTime(); @@ -355,7 +364,20 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe if (!this._currentCommand.commandExecutedMarker) { this.handleCommandExecuted(); } - + // If a custom command ID is provided, use it for the current command + // Otherwise, check if there's a pending next command ID + if (options?.commandId) { + this._currentCommand.id = options.commandId; + this._nextCommandId = undefined; // Clear the pending ID + } else if ( + this._nextCommandId && + typeof this.currentCommand.command === 'string' && + typeof this._nextCommandId.command === 'string' && + this.currentCommand.command.trim() === this._nextCommandId.command.trim() + ) { + this._currentCommand.id = this._nextCommandId.commandId; + this._nextCommandId = undefined; // Clear after use + } this._currentCommand.markFinishedTime(); this._ptyHeuristics.value.preHandleCommandFinished?.(); @@ -392,7 +414,9 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe this._logService.debug('CommandDetectionCapability#onCommandFinished', newCommand); this._onCommandFinished.fire(newCommand); } - this._currentCommand = new PartialTerminalCommand(this._terminal); + // Create new command for next execution, preserving command ID if one was specified + const nextCommandId = this._handleCommandStartOptions?.commandId; + this._currentCommand = new PartialTerminalCommand(this._terminal, nextCommandId); this._handleCommandStartOptions = undefined; } diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 339ceb02883c2..317bfc846465a 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -339,6 +339,7 @@ export interface IPtyService { getInitialCwd(id: number): Promise; getCwd(id: number): Promise; acknowledgeDataEvent(id: number, charCount: number): Promise; + setNextCommandId(id: number, commandLine: string, commandId: string): Promise; setUnicodeVersion(id: number, version: '6' | '11'): Promise; processBinary(id: number, data: string): Promise; /** Confirm the process is _not_ an orphan. */ @@ -816,6 +817,12 @@ export interface ITerminalChildProcess { */ acknowledgeDataEvent(charCount: number): void; + /** + * Pre-assigns the command identifier that should be associated with the next command detected by + * shell integration. This keeps the pty host and renderer command stores aligned. + */ + setNextCommandId(commandLine: string, commandId: string): Promise; + /** * Sets the unicode version for the process, this drives the size of some characters in the * xterm-headless instance. @@ -976,6 +983,8 @@ export interface IShellIntegration { readonly onDidChangeSeenSequences: Event>; deserialize(serialized: ISerializedCommandDetectionCapability): void; + + setNextCommandId(command: string, commandId: string): void; } export interface IDecorationAddon { diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index f33f37e68e808..bfc85875cf7ab 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -376,6 +376,12 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati this._createOrGetBufferMarkDetection(terminal).getMark(vscodeMarkerId); } + setNextCommandId(command: string, commandId: string): void { + if (this._terminal) { + this._createOrGetCommandDetection(this._terminal).setNextCommandId(command, commandId); + } + } + private _markSequenceSeen(sequence: string) { if (!this._seenSequences.has(sequence)) { this._seenSequences.add(sequence); diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index 22091b450a98b..af836c1ad44b7 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -266,6 +266,9 @@ export class PtyHostService extends Disposable implements IPtyHostService { setUnicodeVersion(id: number, version: '6' | '11'): Promise { return this._proxy.setUnicodeVersion(id, version); } + setNextCommandId(id: number, commandLine: string, commandId: string): Promise { + return this._proxy.setNextCommandId(id, commandLine, commandId); + } getInitialCwd(id: number): Promise { return this._proxy.getInitialCwd(id); } diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index ba8a09b809f17..84724efd3eb9e 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -455,6 +455,11 @@ export class PtyService extends Disposable implements IPtyService { async setUnicodeVersion(id: number, version: '6' | '11'): Promise { return this._throwIfNoPty(id).setUnicodeVersion(version); } + + @traceRpc + async setNextCommandId(id: number, commandLine: string, commandId: string): Promise { + return this._throwIfNoPty(id).setNextCommandId(commandLine, commandId); + } @traceRpc async getLatency(): Promise { return []; @@ -892,6 +897,11 @@ class PersistentTerminalProcess extends Disposable { this._serializer.setUnicodeVersion?.(version); // TODO: Pass in unicode version in ctor } + + async setNextCommandId(commandLine: string, commandId: string): Promise { + this._serializer.setNextCommandId?.(commandLine, commandId); + } + acknowledgeDataEvent(charCount: number): void { if (this._inReplay) { return; @@ -1039,6 +1049,10 @@ class XtermSerializer implements ITerminalSerializer { this._xterm.clear(); } + setNextCommandId(commandLine: string, commandId: string): void { + this._shellIntegrationAddon.setNextCommandId(commandLine, commandId); + } + async generateReplayEvent(normalBufferOnly?: boolean, restoreToLastReviveBuffer?: boolean): Promise { const serialize = new (await this._getSerializeConstructor()); this._xterm.loadAddon(serialize); @@ -1126,4 +1140,5 @@ interface ITerminalSerializer { clearBuffer(): void; generateReplayEvent(normalBufferOnly?: boolean, restoreToLastReviveBuffer?: boolean): Promise; setUnicodeVersion?(version: '6' | '11'): void; + setNextCommandId?(commandLine: string, commandId: string): void; } diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index 768a931ae40d9..138540e51a5d7 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -614,6 +614,10 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess // No-op } + async setNextCommandId(commandLine: string, commandId: string): Promise { + // No-op: command IDs are tracked on the renderer and serializer only. + } + getInitialCwd(): Promise { return Promise.resolve(this._initialCwd); } diff --git a/src/vs/server/node/remoteTerminalChannel.ts b/src/vs/server/node/remoteTerminalChannel.ts index a97e55c848009..638422eb1ea44 100644 --- a/src/vs/server/node/remoteTerminalChannel.ts +++ b/src/vs/server/node/remoteTerminalChannel.ts @@ -151,6 +151,7 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel< case RemoteTerminalChannelRequest.ReviveTerminalProcesses: return this._ptyHostService.reviveTerminalProcesses.apply(this._ptyHostService, args); case RemoteTerminalChannelRequest.GetRevivedPtyNewId: return this._ptyHostService.getRevivedPtyNewId.apply(this._ptyHostService, args); case RemoteTerminalChannelRequest.SetUnicodeVersion: return this._ptyHostService.setUnicodeVersion.apply(this._ptyHostService, args); + case RemoteTerminalChannelRequest.SetNextCommandId: return this._ptyHostService.setNextCommandId.apply(this._ptyHostService, args); case RemoteTerminalChannelRequest.ReduceConnectionGraceTime: return this._reduceConnectionGraceTime(); case RemoteTerminalChannelRequest.UpdateIcon: return this._ptyHostService.updateIcon.apply(this._ptyHostService, args); case RemoteTerminalChannelRequest.UpdateTitle: return this._ptyHostService.updateTitle.apply(this._ptyHostService, args); diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 270e3633e8536..cb55cc3c7eb66 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -363,6 +363,10 @@ class ExtHostPseudoterminal implements ITerminalChildProcess { // No-op, xterm-headless isn't used for extension owned terminals. } + async setNextCommandId(commandLine: string, commandId: string): Promise { + // No-op, command IDs are only tracked on the renderer for extension terminals. + } + getInitialCwd(): Promise { return Promise.resolve(''); } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts index e3c4c84a1f837..c87a544963bf7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts @@ -38,7 +38,6 @@ import { IMarkdownRenderer } from '../../../../../../platform/markdown/browser/m import * as domSanitize from '../../../../../../base/browser/domSanitize.js'; import { DomSanitizerConfig } from '../../../../../../base/browser/domSanitize.js'; import { allowedMarkdownHtmlAttributes } from '../../../../../../base/browser/markdownRenderer.js'; -import { URI } from '../../../../../../base/common/uri.js'; const MAX_TERMINAL_OUTPUT_PREVIEW_HEIGHT = 200; @@ -164,10 +163,6 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart } this._terminalInstance = instance; this._registerInstanceListener(instance); - await this._addFocusAction(instance, terminalToolSessionId); - if (this._terminalData?.output?.html) { - this._ensureShowOutputAction(); - } }; await attachInstance(await this._terminalChatService.getTerminalInstanceByToolSessionId(terminalToolSessionId)); @@ -182,25 +177,26 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart this._register(listener); } - private async _addFocusAction(terminalInstance: ITerminalInstance, terminalToolSessionId: string) { + private async _addActions(terminalInstance: ITerminalInstance, terminalToolSessionId: string) { if (!this._actionBar.value) { return; } const isTerminalHidden = this._terminalChatService.isBackgroundTerminal(terminalToolSessionId); const command = this._getResolvedCommand(terminalInstance); - const focusAction = this._instantiationService.createInstance(FocusChatInstanceAction, terminalInstance, command, isTerminalHidden); - this._focusAction.value = focusAction; - this._actionBar.value.push(focusAction, { icon: true, label: false, index: 0 }); - this._ensureShowOutputAction(); + if (command) { + const focusAction = this._instantiationService.createInstance(FocusChatInstanceAction, terminalInstance, command, isTerminalHidden); + this._focusAction.value = focusAction; + this._actionBar.value.push(focusAction, { icon: true, label: false, index: 0 }); + this._ensureShowOutputAction(); + } } private _ensureShowOutputAction(): void { if (!this._actionBar.value) { return; } - const hasSerializedOutput = !!this._terminalData.output?.html; - const commandFinished = !!this._getResolvedCommand()?.endMarker; - if (!hasSerializedOutput && !commandFinished) { + const command = this._getResolvedCommand(); + if (!command) { return; } let showOutputAction = this._showOutputAction.value; @@ -238,34 +234,40 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart private _registerInstanceListener(terminalInstance: ITerminalInstance) { const commandDetectionListener = this._register(new MutableDisposable()); - const tryResolveCommand = (): ITerminalCommand | undefined => { + const tryResolveCommand = async (): Promise => { const resolvedCommand = this._resolveCommand(terminalInstance); if (resolvedCommand?.endMarker) { - this._ensureShowOutputAction(); + await this._addActions(terminalInstance, this._terminalData.terminalToolSessionId!); } return resolvedCommand; }; - const attachCommandDetection = (commandDetection: ICommandDetectionCapability | undefined) => { + const attachCommandDetection = async (commandDetection: ICommandDetectionCapability | undefined) => { commandDetectionListener.clear(); if (!commandDetection) { return; } - const resolvedImmediately = tryResolveCommand(); - if (resolvedImmediately?.endMarker) { - return; - } - commandDetectionListener.value = commandDetection.onCommandFinished(() => { - this._ensureShowOutputAction(); + this._addActions(terminalInstance, this._terminalData.terminalToolSessionId!); commandDetectionListener.clear(); }); + const resolvedImmediately = await tryResolveCommand(); + if (resolvedImmediately?.endMarker) { + return; + } }; attachCommandDetection(terminalInstance.capabilities.get(TerminalCapability.CommandDetection)); this._register(terminalInstance.capabilities.onDidAddCommandDetectionCapability(cd => attachCommandDetection(cd))); + this._register(this._terminalChatService.onDidRegisterTerminalInstanceWithToolSession(async () => { + const resolvedCommand = this._resolveCommand(terminalInstance); + if (resolvedCommand?.endMarker) { + await this._addActions(terminalInstance, this._terminalData.terminalToolSessionId!); + } + })); + const instanceListener = this._register(terminalInstance.onDisposed(() => { if (this._terminalInstance === terminalInstance) { this._terminalInstance = undefined; @@ -273,11 +275,8 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart commandDetectionListener.clear(); this._actionBar.clear(); this._focusAction.clear(); - const keepOutputAction = !!this._terminalData.output?.html; this._showOutputActionAdded = false; - if (!keepOutputAction) { - this._showOutputAction.clear(); - } + this._showOutputAction.clear(); this._ensureShowOutputAction(); instanceListener.dispose(); })); @@ -320,13 +319,17 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart return false; } + if (!this._terminalData.terminalToolSessionId) { + return false; + } + if (!this._terminalInstance) { - const resource = this._getTerminalResource(); - if (resource) { - this._terminalInstance = this._terminalService.getInstanceFromResource(resource); - } + this._terminalInstance = await this._terminalChatService.getTerminalInstanceByToolSessionId(this._terminalData.terminalToolSessionId); } const output = await this._collectOutput(this._terminalInstance); + if (!output) { + return false; + } const content = this._renderOutput(output); const theme = this._terminalInstance?.xterm?.getXtermTheme(); if (theme) { @@ -396,28 +399,19 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart })); } - private async _collectOutput(terminalInstance: ITerminalInstance | undefined): Promise<{ text: string; truncated: boolean }> { - const storedOutput = this._terminalData.output; - if (storedOutput?.html) { - return { text: storedOutput.html, truncated: storedOutput.truncated ?? false }; - } - if (!terminalInstance) { - return { text: '', truncated: false }; - } - const xterm = await terminalInstance.xtermReadyPromise; - if (!xterm) { - return { text: '', truncated: false }; + private async _collectOutput(terminalInstance: ITerminalInstance | undefined): Promise<{ text: string; truncated: boolean } | undefined> { + const commandDetection = terminalInstance?.capabilities.get(TerminalCapability.CommandDetection); + const commands = commandDetection?.commands; + const xterm = await terminalInstance?.xtermReadyPromise; + if (!commands || commands.length === 0 || !terminalInstance || !xterm) { + return; } - const command = this._resolveCommand(terminalInstance); + const command = commands.find(c => c.id === this._terminalData.terminalCommandId); if (!command?.endMarker) { - return { text: '', truncated: false }; - } - const text = await xterm.getCommandOutputAsHtml(command, CHAT_TERMINAL_OUTPUT_MAX_PREVIEW_LINES); - if (!text) { - return { text: '', truncated: false }; + return; } - - return { text, truncated: false }; + const result = await xterm.getCommandOutputAsHtml(command, CHAT_TERMINAL_OUTPUT_MAX_PREVIEW_LINES); + return { text: result.text, truncated: result.truncated ?? false }; } private _renderOutput(result: { text: string; truncated: boolean }): HTMLElement { @@ -439,15 +433,6 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart return container; } - private _getTerminalResource(): URI | undefined { - const commandUri = this._terminalData.terminalCommandUri; - if (!commandUri) { - return undefined; - } - return URI.isUri(commandUri) ? commandUri : URI.revive(commandUri); - } - - private _resolveCommand(instance: ITerminalInstance): ITerminalCommand | undefined { const commandDetection = instance.capabilities.get(TerminalCapability.CommandDetection); const commands = commandDetection?.commands; @@ -455,11 +440,7 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart return undefined; } - const commandId = this._terminalChatService.getTerminalCommandIdByToolSessionId(this._terminalData.terminalToolSessionId); - if (commandId) { - return commands.find(c => c.id === commandId); - } - return; + return commands.find(c => c.id === this._terminalData.terminalCommandId); } } @@ -536,7 +517,7 @@ class ToggleChatTerminalOutputAction extends Action implements IAction { export class FocusChatInstanceAction extends Action implements IAction { constructor( private readonly _instance: ITerminalInstance, - private readonly _command: ITerminalCommand | undefined, + private readonly _command: ITerminalCommand, isTerminalHidden: boolean, @ITerminalService private readonly _terminalService: ITerminalService, @ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService, @@ -560,8 +541,6 @@ export class FocusChatInstanceAction extends Action implements IAction { } this._terminalService.setActiveInstance(this._instance); await this._instance?.focusWhenReady(true); - if (this._command) { - this._instance.xterm?.markTracker.revealCommand(this._command); - } + this._instance.xterm?.markTracker.revealCommand(this._command); } } diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 85d0059b78539..b2349efb92347 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -313,12 +313,9 @@ export interface IChatTerminalToolInvocationData { alternativeRecommendation?: string; language: string; terminalToolSessionId?: string; - terminalCommandUri?: UriComponents; + /** The predefined command ID that will be used for this terminal command */ + terminalCommandId?: string; autoApproveInfo?: IMarkdownString; - output?: { - html: string; - truncated?: boolean; - }; } /** diff --git a/src/vs/workbench/contrib/terminal/browser/remotePty.ts b/src/vs/workbench/contrib/terminal/browser/remotePty.ts index 339b5abfff261..70933f071ec47 100644 --- a/src/vs/workbench/contrib/terminal/browser/remotePty.ts +++ b/src/vs/workbench/contrib/terminal/browser/remotePty.ts @@ -116,6 +116,10 @@ export class RemotePty extends BasePty implements ITerminalChildProcess { return this._remoteTerminalChannel.setUnicodeVersion(this.id, version); } + async setNextCommandId(commandLine: string, commandId: string): Promise { + return this._remoteTerminalChannel.setNextCommandId(this.id, commandLine, commandId); + } + async refreshProperty(type: T): Promise { return this._remoteTerminalChannel.refreshProperty(this.id, type); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index a43080077ff7c..f3ef15af71dcb 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -126,11 +126,6 @@ export interface ITerminalChatService { */ getTerminalInstanceByToolSessionId(terminalToolSessionId: string): Promise; - /** - * Get the terminal command ID associated with a tool session ID, if any. - */ - getTerminalCommandIdByToolSessionId(terminalToolSessionId: string | undefined): string | undefined; - /** * Returns the list of terminal instances that have been registered with a tool session id. * This is used for surfacing tool-driven/background terminals in UI (eg. quick picks). @@ -1034,7 +1029,7 @@ export interface ITerminalInstance extends IBaseTerminalInstance { */ sendPath(originalPath: string | URI, shouldExecute: boolean): Promise; - runCommand(command: string, shouldExecute?: boolean): Promise; + runCommand(command: string, shouldExecute?: boolean, commandId?: string): Promise; /** * Takes a path and returns the properly escaped path to send to a given shell. On Windows, this diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 86bdda253385c..9bb6a60a91728 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -915,7 +915,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return xterm; } - async runCommand(commandLine: string, shouldExecute: boolean): Promise { + async runCommand(commandLine: string, shouldExecute: boolean, commandId?: string): Promise { let commandDetection = this.capabilities.get(TerminalCapability.CommandDetection); const siInjectionEnabled = this._configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled) === true; const timeoutMs = getShellIntegrationTimeout( @@ -946,6 +946,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { store.dispose(); } + // If a command ID was provided and we have command detection, set it as the next command ID + // so it will be used when the shell sends the command start sequence + if (commandId && commandDetection) { + this.xterm?.shellIntegration.setNextCommandId(commandLine, commandId); + await this._processManager.setNextCommandId(commandLine, commandId); + } + // Determine whether to send ETX (ctrl+c) before running the command. This should always // happen unless command detection can reliably say that a command is being entered and // there is no content in the prompt diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts index cd71fa1cd398c..53779c3002ebe 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts @@ -140,6 +140,10 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal // No-op } + async setNextCommandId(commandLine: string, commandId: string): Promise { + // No-op + } + async processBinary(data: string): Promise { // Disabled for extension terminals this._onBinary.fire(data); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index e3f3757ee9db9..e8e5a5b5adbee 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -96,6 +96,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce private _dataFilter: SeamlessRelaunchDataFilter; private _processListeners?: IDisposable[]; private _isDisconnected: boolean = false; + private _hasLoggedSetNextCommandIdFallback = false; private _processTraits: IProcessReadyEvent | undefined; private _shellLaunchConfig?: IShellLaunchConfig; @@ -591,6 +592,35 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce return this._process?.setUnicodeVersion(version); } + async setNextCommandId(commandLine: string, commandId: string): Promise { + await this.ptyProcessReady; + const process = this._process; + if (!process) { + return; + } + try { + await process.setNextCommandId(commandLine, commandId); + } catch (error) { + if (!this._shouldIgnoreSetNextCommandIdError(error)) { + throw error; + } + } + } + + private _shouldIgnoreSetNextCommandIdError(error: unknown): boolean { + if (!(error instanceof Error) || !error.message) { + return false; + } + if (!error.message.includes('Method not found: setNextCommandId') && !error.message.includes('Method not found: $setNextCommandId')) { + return false; + } + if (!this._hasLoggedSetNextCommandIdFallback) { + this._hasLoggedSetNextCommandIdFallback = true; + this._logService.trace('setNextCommandId not supported by current terminal backend, falling back.'); + } + return true; + } + private _resize(cols: number, rows: number) { if (!this._process) { return; diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 85375c996d49d..7c0db8624db39 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -368,7 +368,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach return this._serializeAddon.serializeAsHTML(); } - async getCommandOutputAsHtml(command: ITerminalCommand, maxLines: number): Promise { + async getCommandOutputAsHtml(command: ITerminalCommand, maxLines: number): Promise<{ text: string; truncated?: boolean }> { if (!this._serializeAddon) { const Addon = await this._xtermAddonLoader.importAddon('serialize'); this._serializeAddon = new Addon(); @@ -386,7 +386,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach let endLine = command.endMarker?.line !== undefined ? command.endMarker.line - 1 : this.raw.buffer.active.length - 1; if (endLine < startLine) { - return ''; + return { text: '', truncated: false }; } // Trim empty lines from the end let emptyLinesFromEnd = 0; @@ -424,7 +424,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach const range = { startLine, endLine, startCol }; const result = this._serializeAddon.serializeAsHTML({ range }); - return result; + return { text: result, truncated: (endLine - startLine) >= maxLines }; } async getSelectionAsHtml(command?: ITerminalCommand): Promise { diff --git a/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts b/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts index 1c3ff56809ffe..c31ec29952515 100644 --- a/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts +++ b/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts @@ -225,6 +225,9 @@ export class RemoteTerminalChannelClient implements IPtyHostController { setUnicodeVersion(id: number, version: '6' | '11'): Promise { return this._channel.call(RemoteTerminalChannelRequest.SetUnicodeVersion, [id, version]); } + setNextCommandId(id: number, commandLine: string, commandId: string): Promise { + return this._channel.call(RemoteTerminalChannelRequest.SetNextCommandId, [id, commandLine, commandId]); + } shutdown(id: number, immediate: boolean): Promise { return this._channel.call(RemoteTerminalChannelRequest.Shutdown, [id, immediate]); } diff --git a/src/vs/workbench/contrib/terminal/common/remote/terminal.ts b/src/vs/workbench/contrib/terminal/common/remote/terminal.ts index 16af3dcf8b8e6..c980fad55988d 100644 --- a/src/vs/workbench/contrib/terminal/common/remote/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/remote/terminal.ts @@ -90,6 +90,7 @@ export const enum RemoteTerminalChannelRequest { ReviveTerminalProcesses = '$reviveTerminalProcesses', GetRevivedPtyNewId = '$getRevivedPtyNewId', SetUnicodeVersion = '$setUnicodeVersion', + SetNextCommandId = '$setNextCommandId', ReduceConnectionGraceTime = '$reduceConnectionGraceTime', UpdateIcon = '$updateIcon', UpdateTitle = '$updateTitle', diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 8a86ef4b30cf6..26cafd16a1e75 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -302,6 +302,7 @@ export interface ITerminalProcessManager extends IDisposable, ITerminalProcessIn setDimensions(cols: number, rows: number, sync: true): void; clearBuffer(): Promise; setUnicodeVersion(version: '6' | '11'): Promise; + setNextCommandId(commandLine: string, commandId: string): Promise; acknowledgeDataEvent(charCount: number): void; processBinary(data: string): void; diff --git a/src/vs/workbench/contrib/terminal/electron-browser/localPty.ts b/src/vs/workbench/contrib/terminal/electron-browser/localPty.ts index 6405af520543b..9388600525165 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/localPty.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/localPty.ts @@ -91,6 +91,10 @@ export class LocalPty extends BasePty implements ITerminalChildProcess { return this._proxy.setUnicodeVersion(this.id, version); } + setNextCommandId(commandLine: string, commandId: string): Promise { + return this._proxy.setNextCommandId(this.id, commandLine, commandId); + } + handleOrphanQuestion() { this._proxy.orphanQuestionReply(this.id); } diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts index 44454ca1d0dab..d120dc2d4df71 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts @@ -88,6 +88,7 @@ class TestTerminalChildProcess extends Disposable implements ITerminalChildProce clearBuffer(): void { } acknowledgeDataEvent(charCount: number): void { } async setUnicodeVersion(version: '6' | '11'): Promise { } + async setNextCommandId(commandLine: string, commandId: string): Promise { } async getInitialCwd(): Promise { return ''; } async getCwd(): Promise { return ''; } async processBinary(data: string): Promise { } diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts index 910390dee43af..7a0cece25922e 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts @@ -44,6 +44,7 @@ class TestTerminalChildProcess implements ITerminalChildProcess { clearBuffer(): void { } acknowledgeDataEvent(charCount: number): void { } async setUnicodeVersion(version: '6' | '11'): Promise { } + async setNextCommandId(commandLine: string, commandId: string): Promise { } async getInitialCwd(): Promise { return ''; } async getCwd(): Promise { return ''; } async processBinary(data: string): Promise { } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts index 4e8535ba9c6f7..c9de0afee7674 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts @@ -9,7 +9,6 @@ import { ILogService } from '../../../../../platform/log/common/log.js'; import { ITerminalChatService, ITerminalInstance, ITerminalService } from '../../../terminal/browser/terminal.js'; import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; -import { TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; import { IChatService } from '../../../chat/common/chatService.js'; import { TerminalChatContextKeys } from './terminalChat.js'; import { LocalChatSessionUri } from '../../../chat/common/chatUri.js'; @@ -27,7 +26,6 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ declare _serviceBrand: undefined; private readonly _terminalInstancesByToolSessionId = new Map(); - private readonly _commandIdByToolSessionId = new Map(); private readonly _terminalInstanceListenersByToolSessionId = this._register(new DisposableMap()); private readonly _onDidRegisterTerminalInstanceForToolSession = new Emitter(); readonly onDidRegisterTerminalInstanceWithToolSession: Event = this._onDidRegisterTerminalInstanceForToolSession.event; @@ -70,32 +68,11 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ this._persistToStorage(); this._updateHasToolTerminalContextKeys(); })); - const commandDetection = instance.capabilities.get(TerminalCapability.CommandDetection); - if (commandDetection) { - const listener = this._register(commandDetection.onCommandFinished(e => { - this._commandIdByToolSessionId.set(terminalToolSessionId, e.id); - this._persistToStorage(); - listener.dispose(); - })); - } else { - this._register(instance.capabilities.onDidAddCapability(capability => { - if (capability.id === TerminalCapability.CommandDetection) { - const commandDetection = instance.capabilities.get(TerminalCapability.CommandDetection); - if (commandDetection) { - const listener = this._register(commandDetection.onCommandFinished(e => { - this._commandIdByToolSessionId.set(terminalToolSessionId, e.id); - this._persistToStorage(); - listener.dispose(); - })); - } - } - })); - } + this._register(this._chatService.onDidDisposeSession(e => { if (LocalChatSessionUri.parseLocalSessionId(e.sessionResource) === terminalToolSessionId) { this._terminalInstancesByToolSessionId.delete(terminalToolSessionId); this._terminalInstanceListenersByToolSessionId.deleteAndDispose(terminalToolSessionId); - this._commandIdByToolSessionId.delete(terminalToolSessionId); this._persistToStorage(); this._updateHasToolTerminalContextKeys(); } @@ -104,23 +81,13 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ // Update context keys when terminal instances change (including when terminals are created, disposed, revealed, or hidden) this._register(this._terminalService.onDidChangeInstances(() => this._updateHasToolTerminalContextKeys())); - if (typeof instance.persistentProcessId === 'number') { + if (typeof instance.shellLaunchConfig?.attachPersistentProcess?.id === 'number' || typeof instance.persistentProcessId === 'number') { this._persistToStorage(); } this._updateHasToolTerminalContextKeys(); } - getTerminalCommandIdByToolSessionId(terminalToolSessionId: string | undefined): string | undefined { - if (!terminalToolSessionId) { - return undefined; - } - if (this._commandIdByToolSessionId.size === 0) { - this._restoreFromStorage(); - } - return this._commandIdByToolSessionId.get(terminalToolSessionId); - } - async getTerminalInstanceByToolSessionId(terminalToolSessionId: string | undefined): Promise { await this._terminalService.whenConnected; if (!terminalToolSessionId) { @@ -164,15 +131,6 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ this._pendingRestoredMappings.set(toolSessionId, persistentProcessId); } } - const rawCommandIds = this._storageService.get(StorageKeys.CommandIdMappings, StorageScope.WORKSPACE); - if (rawCommandIds) { - const parsedCommandIds: [string, string][] = JSON.parse(rawCommandIds); - for (const [toolSessionId, commandId] of parsedCommandIds) { - if (typeof toolSessionId === 'string' && typeof commandId === 'string') { - this._commandIdByToolSessionId.set(toolSessionId, commandId); - } - } - } } catch (err) { this._logService.warn('Failed to restore terminal chat tool session mappings', err); } @@ -182,9 +140,7 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ if (this._pendingRestoredMappings.size === 0) { return; } - if (typeof instance.persistentProcessId !== 'number') { - return; - } + for (const [toolSessionId, persistentProcessId] of this._pendingRestoredMappings) { if (persistentProcessId === instance.shellLaunchConfig.attachPersistentProcess?.id) { this._terminalInstancesByToolSessionId.set(toolSessionId, instance); @@ -215,15 +171,6 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ } else { this._storageService.remove(StorageKeys.ToolSessionMappings, StorageScope.WORKSPACE); } - const commandEntries: [string, string][] = []; - for (const [toolSessionId, commandId] of this._commandIdByToolSessionId.entries()) { - commandEntries.push([toolSessionId, commandId]); - } - if (commandEntries.length > 0) { - this._storageService.store(StorageKeys.CommandIdMappings, JSON.stringify(commandEntries), StorageScope.WORKSPACE, StorageTarget.MACHINE); - } else { - this._storageService.remove(StorageKeys.CommandIdMappings, StorageScope.WORKSPACE); - } } catch (err) { this._logService.warn('Failed to persist terminal chat tool session mappings', err); } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts index 2d2107a4b992b..b4f6102c4ff66 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts @@ -53,7 +53,7 @@ export class BasicExecuteStrategy implements ITerminalExecuteStrategy { ) { } - async execute(commandLine: string, token: CancellationToken): Promise { + async execute(commandLine: string, token: CancellationToken, commandId?: string): Promise { const store = new DisposableStore(); try { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/executeStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/executeStrategy.ts index bcc764d753e1d..04236021fd9a3 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/executeStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/executeStrategy.ts @@ -15,8 +15,11 @@ export interface ITerminalExecuteStrategy { /** * Executes a command line and gets a result designed to be passed directly to an LLM. The * result will include information about the exit code. + * @param commandLine The command line to execute + * @param token Cancellation token + * @param commandId Optional predefined command ID to link the command */ - execute(commandLine: string, token: CancellationToken): Promise; + execute(commandLine: string, token: CancellationToken, commandId?: string): Promise; readonly onDidCreateStartMarker: Event; } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts index 7592c35f76926..8dc54fd8a53e0 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts @@ -34,7 +34,7 @@ export class NoneExecuteStrategy implements ITerminalExecuteStrategy { ) { } - async execute(commandLine: string, token: CancellationToken): Promise { + async execute(commandLine: string, token: CancellationToken, commandId?: string): Promise { const store = new DisposableStore(); try { if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts index 014a9bd52c52b..8417bcb01a819 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts @@ -36,7 +36,7 @@ export class RichExecuteStrategy implements ITerminalExecuteStrategy { ) { } - async execute(commandLine: string, token: CancellationToken): Promise { + async execute(commandLine: string, token: CancellationToken, commandId?: string): Promise { const store = new DisposableStore(); try { // Ensure xterm is available @@ -76,7 +76,7 @@ export class RichExecuteStrategy implements ITerminalExecuteStrategy { // Execute the command this._log(`Executing command line \`${commandLine}\``); - this._instance.runCommand(commandLine, true); + this._instance.runCommand(commandLine, true, commandId); // Wait for the terminal to idle this._log('Waiting for done event'); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index 719a13be70774..3a6f5d445b9d5 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -13,18 +13,17 @@ import { MarkdownString, type IMarkdownString } from '../../../../../../base/com import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { basename } from '../../../../../../base/common/path.js'; import { OperatingSystem, OS } from '../../../../../../base/common/platform.js'; -import { count, escape } from '../../../../../../base/common/strings.js'; +import { count } from '../../../../../../base/common/strings.js'; import { generateUuid } from '../../../../../../base/common/uuid.js'; import { localize } from '../../../../../../nls.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService, type ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; -import { TerminalCapability, type ICommandDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; +import { TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; import { ITerminalLogService, ITerminalProfile } from '../../../../../../platform/terminal/common/terminal.js'; import { IRemoteAgentService } from '../../../../../services/remote/common/remoteAgentService.js'; import { TerminalToolConfirmationStorageKeys } from '../../../../chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.js'; import { IChatService, type IChatTerminalToolInvocationData } from '../../../../chat/common/chatService.js'; -import { CHAT_TERMINAL_OUTPUT_MAX_PREVIEW_LINES } from '../../../../chat/common/constants.js'; import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress } from '../../../../chat/common/languageModelToolsService.js'; import { ITerminalChatService, ITerminalService, type ITerminalInstance } from '../../../../terminal/browser/terminal.js'; import type { XtermTerminal } from '../../../../terminal/browser/xterm/xtermTerminal.js'; @@ -37,7 +36,7 @@ import type { ITerminalExecuteStrategy } from '../executeStrategy/executeStrateg import { NoneExecuteStrategy } from '../executeStrategy/noneExecuteStrategy.js'; import { RichExecuteStrategy } from '../executeStrategy/richExecuteStrategy.js'; import { getOutput } from '../outputHelpers.js'; -import { isFish, isPowerShell, isWindowsPowerShell, isZsh, sanitizeTerminalOutput } from '../runInTerminalHelpers.js'; +import { isFish, isPowerShell, isWindowsPowerShell, isZsh } from '../runInTerminalHelpers.js'; import { RunInTerminalToolTelemetry } from '../runInTerminalToolTelemetry.js'; import { ShellIntegrationQuality, ToolTerminalCreator, type IToolTerminal } from '../toolTerminalCreator.js'; import { TreeSitterCommandParser, TreeSitterCommandParserLanguage } from '../treeSitterCommandParser.js'; @@ -330,6 +329,8 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { const instance = context.chatSessionId ? this._sessionTerminalAssociations.get(context.chatSessionId)?.instance : undefined; const terminalToolSessionId = generateUuid(); + // Generate a custom command ID to link the command between renderer and pty host + const terminalCommandId = `tool-${generateUuid()}`; let toolEditedCommand: string | undefined = await this._commandSimplifier.rewriteIfNeeded(args, instance, shell); if (toolEditedCommand === args.command) { @@ -338,6 +339,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { const toolSpecificData: IChatTerminalToolInvocationData = { kind: 'terminal', terminalToolSessionId, + terminalCommandId, commandLine: { original: args.command, toolEdited: toolEditedCommand @@ -479,13 +481,13 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { let pollingResult: IPollingResult & { pollDurationMs: number } | undefined; try { this._logService.debug(`RunInTerminalTool: Starting background execution \`${command}\``); - const execution = new BackgroundTerminalExecution(toolTerminal.instance, xterm, command, chatSessionId); + const commandId = (toolSpecificData as IChatTerminalToolInvocationData).terminalCommandId; + const execution = new BackgroundTerminalExecution(toolTerminal.instance, xterm, command, chatSessionId, commandId); RunInTerminalTool._backgroundExecutions.set(termId, execution); outputMonitor = store.add(this._instantiationService.createInstance(OutputMonitor, execution, undefined, invocation.context!, token, command)); await Event.toPromise(outputMonitor.onDidFinishCommand); const pollingResult = outputMonitor.pollingResult; - await this._updateTerminalCommandMetadata(toolSpecificData, toolTerminal.instance, commandDetection, pollingResult?.output ? { text: pollingResult.output } : undefined); if (token.isCancellationRequested) { throw new CancellationError(); @@ -521,12 +523,6 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { error = e instanceof CancellationError ? 'canceled' : 'unexpectedException'; throw e; } finally { - await this._updateTerminalCommandMetadata( - toolSpecificData, - toolTerminal.instance, - commandDetection, - pollingResult?.output ? { text: pollingResult.output } : undefined - ); store.dispose(); this._logService.debug(`RunInTerminalTool: Finished polling \`${pollingResult?.output.length}\` lines of output in \`${pollingResult?.pollDurationMs}\``); const timingExecuteMs = Date.now() - timingStart; @@ -584,7 +580,8 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { outputMonitor = store.add(this._instantiationService.createInstance(OutputMonitor, { instance: toolTerminal.instance, sessionId: invocation.context?.sessionId, getOutput: (marker?: IXtermMarker) => getOutput(toolTerminal.instance, marker ?? startMarker) }, undefined, invocation.context, token, command)); } })); - const executeResult = await strategy.execute(command, token); + const commandId = (toolSpecificData as IChatTerminalToolInvocationData).terminalCommandId; + const executeResult = await strategy.execute(command, token, commandId); // Reset user input state after command execution completes toolTerminal.receivedUserInput = false; if (token.isCancellationRequested) { @@ -611,12 +608,6 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { error = e instanceof CancellationError ? 'canceled' : 'unexpectedException'; throw e; } finally { - await this._updateTerminalCommandMetadata( - toolSpecificData, - toolTerminal.instance, - commandDetection, - terminalResult ? { text: terminalResult } : undefined - ); store.dispose(); const timingExecuteMs = Date.now() - timingStart; this._telemetry.logInvoke(toolTerminal.instance, { @@ -667,58 +658,6 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } } - private async _updateTerminalCommandMetadata( - toolSpecificData: IChatTerminalToolInvocationData, - instance: ITerminalInstance, - commandDetection: ICommandDetectionCapability | undefined, - fallbackOutput?: { text: string; truncated?: boolean } - ): Promise { - const command = commandDetection?.commands.at(-1); - if (command?.id && !toolSpecificData.terminalCommandUri) { - const params = new URLSearchParams(instance.resource.query); - params.set('command', command.id); - const commandUri = instance.resource.with({ query: params.toString() || undefined }); - toolSpecificData.terminalCommandUri = commandUri; - } - - if (toolSpecificData.output?.html) { - return; - } - - let serializedHtml: string | undefined; - let truncated = fallbackOutput?.truncated ?? false; - - if (command?.endMarker) { - try { - const xterm = await instance.xtermReadyPromise; - if (xterm) { - const html = await xterm.getCommandOutputAsHtml(command, CHAT_TERMINAL_OUTPUT_MAX_PREVIEW_LINES); - if (html) { - serializedHtml = html; - truncated = false; - } - } - } catch (error) { - this._logService.debug('RunInTerminalTool: Failed to capture terminal HTML output for serialization', error); - } - } - - if (!serializedHtml && fallbackOutput?.text) { - const sanitized = sanitizeTerminalOutput(fallbackOutput.text); - if (sanitized) { - serializedHtml = escape(sanitized); - truncated = fallbackOutput.truncated ?? truncated; - } - } - - if (serializedHtml) { - toolSpecificData.output = { - html: serializedHtml, - truncated - }; - } - } - private _handleTerminalVisibility(toolTerminal: IToolTerminal) { if (this._configurationService.getValue(TerminalChatAgentToolsSettingId.OutputLocation) === 'terminal') { this._terminalService.setActiveInstance(toolTerminal.instance); @@ -892,12 +831,13 @@ class BackgroundTerminalExecution extends Disposable { readonly instance: ITerminalInstance, private readonly _xterm: XtermTerminal, private readonly _commandLine: string, - readonly sessionId: string + readonly sessionId: string, + commandId?: string ) { super(); this._startMarker = this._register(this._xterm.raw.registerMarker()); - this.instance.runCommand(this._commandLine, true); + this.instance.runCommand(this._commandLine, true, commandId); } getOutput(marker?: IXtermMarker): string { return getOutput(this.instance, marker ?? this._startMarker); diff --git a/src/vs/workbench/services/terminal/common/embedderTerminalService.ts b/src/vs/workbench/services/terminal/common/embedderTerminalService.ts index c93386a16de7d..37f8b31b2bb20 100644 --- a/src/vs/workbench/services/terminal/common/embedderTerminalService.ts +++ b/src/vs/workbench/services/terminal/common/embedderTerminalService.ts @@ -136,6 +136,9 @@ class EmbedderTerminalProcess extends Disposable implements ITerminalChildProces async setUnicodeVersion(): Promise { // no-op } + async setNextCommandId(): Promise { + // no-op + } async getInitialCwd(): Promise { return ''; }