From 8d19cc456b6a19e6f1c51b2250a2d6b55d1e4416 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 3 Nov 2025 22:15:30 -0500 Subject: [PATCH] ensure command id defined for non copilot terminals (#274954) --- .../common/capabilities/capabilities.ts | 6 ---- .../commandDetection/terminalCommand.ts | 3 +- .../commandDetectionCapability.ts | 36 +++++++++---------- .../terminal/browser/xterm/decorationAddon.ts | 4 ++- .../commandDetectionCapability.test.ts | 1 + 5 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index c35cb703601..5f9d77f65cd 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -276,12 +276,6 @@ 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 { diff --git a/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts b/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts index 30fd67a237d..1bb18a79537 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts @@ -6,6 +6,7 @@ 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; @@ -284,7 +285,7 @@ export class PartialTerminalCommand implements ICurrentPartialCommand { private readonly _xterm: Terminal, id?: string ) { - this.id = id; + this.id = id ?? generateUuid(); } 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 56b2820565d..63dc1b5ef4a 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -7,13 +7,13 @@ import { RunOnceScheduler } from '../../../../base/common/async.js'; import { debounce } from '../../../../base/common/decorators.js'; import { Emitter } from '../../../../base/common/event.js'; import { Disposable, MandatoryMutableDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; import { ILogService } from '../../../log/common/log.js'; import { CommandInvalidationReason, ICommandDetectionCapability, ICommandInvalidationRequest, IHandleCommandOptions, ISerializedCommandDetectionCapability, ISerializedTerminalCommand, ITerminalCommand, TerminalCapability } from './capabilities.js'; import { ITerminalOutputMatcher } from '../terminal.js'; import { ICurrentPartialCommand, PartialTerminalCommand, TerminalCommand } from './commandDetection/terminalCommand.js'; import { PromptInputModel, type IPromptInputModel } from './commandDetection/promptInputModel.js'; import type { IBuffer, IDisposable, IMarker, Terminal } from '@xterm/headless'; -import { isString } from '../../../../base/common/types.js'; interface ITerminalDimensions { cols: number; @@ -365,20 +365,7 @@ 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 && - isString(this.currentCommand.command) && - isString(this._nextCommandId.command) && - this.currentCommand.command.trim() === this._nextCommandId.command.trim() - ) { - this._currentCommand.id = this._nextCommandId.commandId; - this._nextCommandId = undefined; // Clear after use - } + this._ensureCurrentCommandId(this._currentCommand.command ?? this._currentCommand.extractCommandLine()); this._currentCommand.markFinishedTime(); this._ptyHeuristics.value.preHandleCommandFinished?.(); @@ -415,12 +402,25 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe this._logService.debug('CommandDetectionCapability#onCommandFinished', newCommand); this._onCommandFinished.fire(newCommand); } - // 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); + // Create new command for next execution + this._currentCommand = new PartialTerminalCommand(this._terminal); this._handleCommandStartOptions = undefined; } + private _ensureCurrentCommandId(commandLine: string | undefined): void { + if (this._nextCommandId?.commandId && typeof commandLine === 'string' && commandLine.trim() === this._nextCommandId.command.trim()) { + if (this._currentCommand.id !== this._nextCommandId.commandId) { + this._currentCommand.id = this._nextCommandId.commandId; + } + this._nextCommandId = undefined; + return; + } + + if (!this._currentCommand.id) { + this._currentCommand.id = generateUuid(); + } + } + setCommandLine(commandLine: string, isTrusted: boolean) { this._logService.debug('CommandDetectionCapability#setCommandLine', commandLine, isTrusted); this._currentCommand.command = commandLine; diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index d94b6164317..f60f3ba41b3 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -33,6 +33,7 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { TerminalContext } from '../../../chat/browser/actions/chatContext.js'; import { getTerminalUri, parseTerminalUri } from '../terminalUri.js'; import { URI } from '../../../../../base/common/uri.js'; +import { ChatAgentLocation } from '../../../chat/common/constants.js'; interface IDisposableDecoration { decoration: IDecoration; disposables: IDisposable[]; exitCode?: number; markProperties?: IMarkProperties } @@ -526,12 +527,13 @@ export class DecorationAddon extends Disposable implements ITerminalAddon, IDeco return { class: undefined, tooltip: labelAttachToChat, id: 'terminal.attachToChat', label: labelAttachToChat, enabled: true, run: async () => { - const widget = this._chatWidgetService.lastFocusedWidget; + const widget = this._chatWidgetService.lastFocusedWidget ?? this._chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat)?.[0]; let terminalContext: TerminalContext | undefined; if (this._resource) { const parsedUri = parseTerminalUri(this._resource); terminalContext = this._instantiationService.createInstance(TerminalContext, getTerminalUri(parsedUri.workspaceId, parsedUri.instanceId!, undefined, command.id)); } + if (terminalContext && widget && widget.attachmentCapabilities.supportsTerminalAttachments) { try { const attachment = await terminalContext.asAttachment(widget); diff --git a/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts b/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts index e3ec0059794..d824e989eab 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts @@ -35,6 +35,7 @@ suite('CommandDetectionCapability', () => { // Ensure timestamps are set and were captured recently for (const command of capability.commands) { ok(Math.abs(Date.now() - command.timestamp) < 2000); + ok(command.id, 'Expected command to have an assigned id'); } deepStrictEqual(addEvents, capability.commands); // Clear the commands to avoid re-asserting past commands