ensure command id defined for non copilot terminals (#274954)

This commit is contained in:
Megan Rogge
2025-11-03 22:15:30 -05:00
committed by GitHub
parent df07c4aa3c
commit 8d19cc456b
5 changed files with 24 additions and 26 deletions

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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);

View File

@@ -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