|
|
|
@@ -4,7 +4,6 @@
|
|
|
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
|
|
|
|
|
|
import { h } from '../../../../../../../base/browser/dom.js';
|
|
|
|
|
import { ActionBar } from '../../../../../../../base/browser/ui/actionbar/actionbar.js';
|
|
|
|
|
import { isMarkdownString, MarkdownString } from '../../../../../../../base/common/htmlContent.js';
|
|
|
|
|
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js';
|
|
|
|
|
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js';
|
|
|
|
@@ -25,7 +24,7 @@ import { ChatCollapsibleContentPart } from '../chatCollapsibleContentPart.js';
|
|
|
|
|
import { IChatRendererContent } from '../../../../common/model/chatViewModel.js';
|
|
|
|
|
import '../media/chatTerminalToolProgressPart.css';
|
|
|
|
|
import type { ICodeBlockRenderOptions } from '../codeBlockPart.js';
|
|
|
|
|
import { Action, IAction } from '../../../../../../../base/common/actions.js';
|
|
|
|
|
import { IAction } from '../../../../../../../base/common/actions.js';
|
|
|
|
|
import { timeout } from '../../../../../../../base/common/async.js';
|
|
|
|
|
import { IChatTerminalToolProgressPart, ITerminalChatService, ITerminalConfigurationService, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalService } from '../../../../../terminal/browser/terminal.js';
|
|
|
|
|
import { Disposable, DisposableStore, MutableDisposable, toDisposable, type IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
|
|
|
@@ -58,6 +57,11 @@ import { removeAnsiEscapeCodes } from '../../../../../../../base/common/strings.
|
|
|
|
|
import { PANEL_BACKGROUND } from '../../../../../../common/theme.js';
|
|
|
|
|
import { editorBackground } from '../../../../../../../platform/theme/common/colorRegistry.js';
|
|
|
|
|
import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js';
|
|
|
|
|
import { MenuWorkbenchToolBar } from '../../../../../../../platform/actions/browser/toolbar.js';
|
|
|
|
|
import { MenuRegistry } from '../../../../../../../platform/actions/common/actions.js';
|
|
|
|
|
import { CommandsRegistry } from '../../../../../../../platform/commands/common/commands.js';
|
|
|
|
|
import { MENU_CHAT_TERMINAL_TOOL_PROGRESS, TerminalChatContextKeys } from '../../../../../terminal/terminalContribChatExports.js';
|
|
|
|
|
import { ServiceCollection } from '../../../../../../../platform/instantiation/common/serviceCollection.js';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Minimum number of rows to display in the terminal output view.
|
|
|
|
@@ -94,6 +98,62 @@ const MIN_DATA_EVENTS_FOR_REAL_OUTPUT = 2;
|
|
|
|
|
*/
|
|
|
|
|
const expandedStateByInvocation = new WeakMap<IChatToolInvocation | IChatToolInvocationSerialized, boolean>();
|
|
|
|
|
|
|
|
|
|
// --- Command and menu registrations for terminal tool progress toolbar ---
|
|
|
|
|
|
|
|
|
|
CommandsRegistry.registerCommand(TerminalContribCommandId.FocusChatInstanceAction, async (_accessor: unknown, progressPart?: IChatTerminalToolProgressPart) => {
|
|
|
|
|
await progressPart?.focusTerminal();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
CommandsRegistry.registerCommand(TerminalContribCommandId.ContinueInBackground, async (_accessor: unknown, progressPart?: IChatTerminalToolProgressPart) => {
|
|
|
|
|
progressPart?.continueInBackground();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
CommandsRegistry.registerCommand(TerminalContribCommandId.ToggleChatTerminalOutput, async (_accessor: unknown, progressPart?: IChatTerminalToolProgressPart) => {
|
|
|
|
|
await progressPart?.toggleOutputFromAction();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
MenuRegistry.appendMenuItem(MENU_CHAT_TERMINAL_TOOL_PROGRESS, {
|
|
|
|
|
command: {
|
|
|
|
|
id: TerminalContribCommandId.ContinueInBackground,
|
|
|
|
|
title: localize('continueInBackground', 'Continue in Background'),
|
|
|
|
|
icon: Codicon.debugContinue,
|
|
|
|
|
},
|
|
|
|
|
when: TerminalChatContextKeys.chatToolCanContinueInBackground,
|
|
|
|
|
order: 0,
|
|
|
|
|
group: 'navigation',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
MenuRegistry.appendMenuItem(MENU_CHAT_TERMINAL_TOOL_PROGRESS, {
|
|
|
|
|
command: {
|
|
|
|
|
id: TerminalContribCommandId.FocusChatInstanceAction,
|
|
|
|
|
title: localize('focusTerminal', 'Focus Terminal'),
|
|
|
|
|
icon: Codicon.openInProduct,
|
|
|
|
|
toggled: {
|
|
|
|
|
condition: TerminalChatContextKeys.chatToolIsHiddenTerminal,
|
|
|
|
|
title: localize('showTerminal', 'Show and Focus Terminal'),
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
when: TerminalChatContextKeys.chatToolHasInstance,
|
|
|
|
|
order: 1,
|
|
|
|
|
group: 'navigation',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
MenuRegistry.appendMenuItem(MENU_CHAT_TERMINAL_TOOL_PROGRESS, {
|
|
|
|
|
command: {
|
|
|
|
|
id: TerminalContribCommandId.ToggleChatTerminalOutput,
|
|
|
|
|
title: localize('showTerminalOutput', 'Show Output'),
|
|
|
|
|
icon: Codicon.chevronRight,
|
|
|
|
|
toggled: {
|
|
|
|
|
condition: TerminalChatContextKeys.chatToolOutputExpanded,
|
|
|
|
|
title: localize('hideTerminalOutput', 'Hide Output'),
|
|
|
|
|
icon: Codicon.chevronDown,
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
when: TerminalChatContextKeys.chatToolHasOutput.isEqualTo(true),
|
|
|
|
|
order: 2,
|
|
|
|
|
group: 'navigation',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Options for configuring a terminal command decoration.
|
|
|
|
|
*/
|
|
|
|
@@ -247,8 +307,6 @@ class TerminalCommandDecoration extends Disposable {
|
|
|
|
|
export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart implements IChatTerminalToolProgressPart {
|
|
|
|
|
public readonly domNode: HTMLElement;
|
|
|
|
|
|
|
|
|
|
private readonly _actionBar: ActionBar;
|
|
|
|
|
|
|
|
|
|
private readonly _titleElement: HTMLElement;
|
|
|
|
|
private readonly _outputView: ChatTerminalToolOutputSection;
|
|
|
|
|
private readonly _terminalOutputContextKey: IContextKey<boolean>;
|
|
|
|
@@ -257,14 +315,15 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
|
|
|
|
private readonly _contentIndex: number;
|
|
|
|
|
private readonly _sessionResource: URI;
|
|
|
|
|
|
|
|
|
|
private readonly _showOutputAction = this._register(new MutableDisposable<ToggleChatTerminalOutputAction>());
|
|
|
|
|
private _showOutputActionAdded = false;
|
|
|
|
|
private readonly _focusAction = this._register(new MutableDisposable<FocusChatInstanceAction>());
|
|
|
|
|
private readonly _continueInBackgroundAction = this._register(new MutableDisposable<ContinueInBackgroundAction>());
|
|
|
|
|
// Scoped context keys that drive toolbar action visibility
|
|
|
|
|
private readonly _hasInstanceKey: IContextKey<boolean>;
|
|
|
|
|
private readonly _canContinueInBackgroundKey: IContextKey<boolean>;
|
|
|
|
|
private readonly _hasOutputKey: IContextKey<boolean>;
|
|
|
|
|
private readonly _isHiddenTerminalKey: IContextKey<boolean>;
|
|
|
|
|
private readonly _outputExpandedKey: IContextKey<boolean>;
|
|
|
|
|
|
|
|
|
|
private readonly _terminalData: IChatTerminalToolInvocationData;
|
|
|
|
|
private _terminalCommandUri: URI | undefined;
|
|
|
|
|
private _storedCommandId: string | undefined;
|
|
|
|
|
private readonly _commandText: string;
|
|
|
|
|
private readonly _isSerializedInvocation: boolean;
|
|
|
|
|
private _terminalInstance: ITerminalInstance | undefined;
|
|
|
|
@@ -302,6 +361,9 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
|
|
|
|
@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService,
|
|
|
|
|
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
|
|
|
|
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
|
|
|
|
@ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService,
|
|
|
|
|
@ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService,
|
|
|
|
|
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
|
|
|
|
) {
|
|
|
|
|
super(toolInvocation);
|
|
|
|
|
|
|
|
|
@@ -312,7 +374,6 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
|
|
|
|
terminalData = migrateLegacyTerminalToolSpecificData(terminalData);
|
|
|
|
|
this._terminalData = terminalData;
|
|
|
|
|
this._terminalCommandUri = terminalData.terminalCommandUri ? URI.revive(terminalData.terminalCommandUri) : undefined;
|
|
|
|
|
this._storedCommandId = this._terminalCommandUri ? new URLSearchParams(this._terminalCommandUri.query ?? '').get('command') ?? undefined : undefined;
|
|
|
|
|
this._isSerializedInvocation = (toolInvocation.kind === 'toolInvocationSerialized');
|
|
|
|
|
|
|
|
|
|
const elements = h('.chat-terminal-content-part@container', [
|
|
|
|
@@ -368,17 +429,52 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
|
|
|
|
this._register(this._outputView.onDidFocus(() => this._handleOutputFocus()));
|
|
|
|
|
this._register(this._outputView.onDidBlur(e => this._handleOutputBlur(e)));
|
|
|
|
|
this._register(toDisposable(() => this._handleDispose()));
|
|
|
|
|
this._register(this._keybindingService.onDidUpdateKeybindings(() => {
|
|
|
|
|
this._focusAction.value?.refreshKeybindingTooltip();
|
|
|
|
|
this._showOutputAction.value?.refreshKeybindingTooltip();
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create a scoped context key service for this toolbar so each progress part's
|
|
|
|
|
// context keys are independent from other parts.
|
|
|
|
|
const actionBarEl = h('.chat-terminal-action-bar@actionBar');
|
|
|
|
|
elements.title.append(actionBarEl.root);
|
|
|
|
|
this._actionBar = this._register(new ActionBar(actionBarEl.actionBar, {}));
|
|
|
|
|
const toolbarContextKeyService = this._register(this._contextKeyService.createScoped(actionBarEl.actionBar));
|
|
|
|
|
this._hasInstanceKey = TerminalChatContextKeys.chatToolHasInstance.bindTo(toolbarContextKeyService);
|
|
|
|
|
this._canContinueInBackgroundKey = TerminalChatContextKeys.chatToolCanContinueInBackground.bindTo(toolbarContextKeyService);
|
|
|
|
|
this._hasOutputKey = TerminalChatContextKeys.chatToolHasOutput.bindTo(toolbarContextKeyService);
|
|
|
|
|
this._isHiddenTerminalKey = TerminalChatContextKeys.chatToolIsHiddenTerminal.bindTo(toolbarContextKeyService);
|
|
|
|
|
this._outputExpandedKey = TerminalChatContextKeys.chatToolOutputExpanded.bindTo(toolbarContextKeyService);
|
|
|
|
|
const usesCollapsibleKey = TerminalChatContextKeys.chatToolUsesCollapsible.bindTo(toolbarContextKeyService);
|
|
|
|
|
|
|
|
|
|
const scopedInstantiationService = this._register(this._instantiationService.createChild(
|
|
|
|
|
new ServiceCollection([IContextKeyService, toolbarContextKeyService])
|
|
|
|
|
));
|
|
|
|
|
this._register(scopedInstantiationService.createInstance(
|
|
|
|
|
MenuWorkbenchToolBar,
|
|
|
|
|
actionBarEl.actionBar,
|
|
|
|
|
MENU_CHAT_TERMINAL_TOOL_PROGRESS,
|
|
|
|
|
{
|
|
|
|
|
menuOptions: { arg: this, shouldForwardArgs: true },
|
|
|
|
|
getKeyBinding: (action: IAction) => {
|
|
|
|
|
if (action.id === TerminalContribCommandId.FocusChatInstanceAction) {
|
|
|
|
|
return this._keybindingService.lookupKeybinding(TerminalContribCommandId.FocusMostRecentChatTerminal);
|
|
|
|
|
}
|
|
|
|
|
if (action.id === TerminalContribCommandId.ToggleChatTerminalOutput) {
|
|
|
|
|
return this._keybindingService.lookupKeybinding(TerminalContribCommandId.FocusMostRecentChatTerminalOutput);
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
));
|
|
|
|
|
this._initializeTerminalActions();
|
|
|
|
|
this._terminalService.whenConnected.then(() => this._initializeTerminalActions());
|
|
|
|
|
|
|
|
|
|
// Listen for continue in background — sets context key so toolbar auto-hides the action
|
|
|
|
|
const terminalToolSessionId = this._terminalData.terminalToolSessionId;
|
|
|
|
|
if (terminalToolSessionId) {
|
|
|
|
|
this._register(this._terminalChatService.onDidContinueInBackground(sessionId => {
|
|
|
|
|
if (sessionId === terminalToolSessionId) {
|
|
|
|
|
this._terminalData.didContinueInBackground = true;
|
|
|
|
|
this._canContinueInBackgroundKey.set(false);
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
let pastTenseMessage: string | undefined;
|
|
|
|
|
if (toolInvocation.pastTenseMessage) {
|
|
|
|
|
pastTenseMessage = `${typeof toolInvocation.pastTenseMessage === 'string' ? toolInvocation.pastTenseMessage : toolInvocation.pastTenseMessage.value}`;
|
|
|
|
@@ -420,6 +516,7 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
|
|
|
|
const requiresConfirmation = toolInvocation.kind === 'toolInvocation' && IChatToolInvocation.getConfirmationMessages(toolInvocation);
|
|
|
|
|
this._isInThinkingContainer = terminalToolsInThinking && !requiresConfirmation;
|
|
|
|
|
this._usesCollapsibleWrapper = this._isInThinkingContainer || isSimpleTerminal;
|
|
|
|
|
usesCollapsibleKey.set(this._usesCollapsibleWrapper);
|
|
|
|
|
|
|
|
|
|
if (this._usesCollapsibleWrapper) {
|
|
|
|
|
this.domNode = this._createCollapsibleWrapper(progressPart.domNode, displayCommand, toolInvocation, context);
|
|
|
|
@@ -539,7 +636,7 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
|
|
|
|
}
|
|
|
|
|
const terminalToolSessionId = this._terminalData.terminalToolSessionId;
|
|
|
|
|
if (!terminalToolSessionId) {
|
|
|
|
|
this._addActions();
|
|
|
|
|
this._updateToolbarContextKeys();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -551,7 +648,7 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
|
|
|
|
if (this._isSerializedInvocation) {
|
|
|
|
|
this._clearCommandAssociation();
|
|
|
|
|
}
|
|
|
|
|
this._addActions(undefined, terminalToolSessionId);
|
|
|
|
|
this._updateToolbarContextKeys(undefined, terminalToolSessionId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const isNewInstance = this._terminalInstance !== instance;
|
|
|
|
@@ -559,16 +656,14 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
|
|
|
|
this._terminalInstance = instance;
|
|
|
|
|
this._registerInstanceListener(instance);
|
|
|
|
|
}
|
|
|
|
|
// Always call _addActions to ensure actions are added, even if instance was set earlier
|
|
|
|
|
// (e.g., by the output view during expanded state restoration)
|
|
|
|
|
this._addActions(instance, terminalToolSessionId);
|
|
|
|
|
this._updateToolbarContextKeys(instance, terminalToolSessionId);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const initialInstance = await this._terminalChatService.getTerminalInstanceByToolSessionId(terminalToolSessionId);
|
|
|
|
|
await attachInstance(initialInstance);
|
|
|
|
|
|
|
|
|
|
if (!initialInstance) {
|
|
|
|
|
this._addActions(undefined, terminalToolSessionId);
|
|
|
|
|
this._updateToolbarContextKeys(undefined, terminalToolSessionId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this._store.isDisposed) {
|
|
|
|
@@ -587,45 +682,50 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
|
|
|
|
});
|
|
|
|
|
this._terminalSessionRegistration = this._store.add(listener);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Listen for continue in background to remove the button
|
|
|
|
|
this._store.add(this._terminalChatService.onDidContinueInBackground(sessionId => {
|
|
|
|
|
if (sessionId === terminalToolSessionId) {
|
|
|
|
|
this._terminalData.didContinueInBackground = true;
|
|
|
|
|
this._removeContinueInBackgroundAction();
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _addActions(terminalInstance?: ITerminalInstance, terminalToolSessionId?: string): void {
|
|
|
|
|
/**
|
|
|
|
|
* Updates the scoped context keys that drive toolbar action visibility.
|
|
|
|
|
* The `MenuWorkbenchToolBar` automatically shows/hides actions based on these keys.
|
|
|
|
|
*/
|
|
|
|
|
private _updateToolbarContextKeys(terminalInstance?: ITerminalInstance, terminalToolSessionId?: string): void {
|
|
|
|
|
if (this._store.isDisposed) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const actionBar = this._actionBar;
|
|
|
|
|
this._removeFocusAction();
|
|
|
|
|
const resolvedCommand = this._getResolvedCommand(terminalInstance);
|
|
|
|
|
|
|
|
|
|
this._removeContinueInBackgroundAction();
|
|
|
|
|
if (terminalInstance) {
|
|
|
|
|
const isTerminalHidden = terminalInstance && terminalToolSessionId ? this._terminalChatService.isBackgroundTerminal(terminalToolSessionId) : false;
|
|
|
|
|
const focusAction = this._instantiationService.createInstance(FocusChatInstanceAction, terminalInstance, resolvedCommand, this._terminalCommandUri, this._storedCommandId, isTerminalHidden);
|
|
|
|
|
this._focusAction.value = focusAction;
|
|
|
|
|
actionBar.push(focusAction, { icon: true, label: false, index: 0 });
|
|
|
|
|
// Focus terminal action
|
|
|
|
|
this._hasInstanceKey.set(!!terminalInstance);
|
|
|
|
|
if (terminalInstance && terminalToolSessionId) {
|
|
|
|
|
this._isHiddenTerminalKey.set(this._terminalChatService.isBackgroundTerminal(terminalToolSessionId));
|
|
|
|
|
} else {
|
|
|
|
|
this._isHiddenTerminalKey.set(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add continue in background action - only for foreground executions with running commands
|
|
|
|
|
// Note: isBackground refers to whether the tool was invoked with isBackground=true (background execution),
|
|
|
|
|
// not whether the terminal is hidden from the user
|
|
|
|
|
if (terminalToolSessionId && !this._terminalData.isBackground && !this._terminalData.didContinueInBackground) {
|
|
|
|
|
const isStillRunning = resolvedCommand?.exitCode === undefined && this._terminalData.terminalCommandState?.exitCode === undefined;
|
|
|
|
|
if (isStillRunning) {
|
|
|
|
|
const continueAction = this._instantiationService.createInstance(ContinueInBackgroundAction, terminalToolSessionId);
|
|
|
|
|
this._continueInBackgroundAction.value = continueAction;
|
|
|
|
|
actionBar.push(continueAction, { icon: true, label: false, index: 0 });
|
|
|
|
|
// Continue in background action
|
|
|
|
|
if (terminalInstance && terminalToolSessionId && !this._terminalData.isBackground && !this._terminalData.didContinueInBackground) {
|
|
|
|
|
const isStillRunning = resolvedCommand?.exitCode === undefined && this._terminalData.terminalCommandState?.exitCode === undefined;
|
|
|
|
|
this._canContinueInBackgroundKey.set(isStillRunning);
|
|
|
|
|
} else {
|
|
|
|
|
this._canContinueInBackgroundKey.set(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show output action (only when NOT using collapsible wrapper)
|
|
|
|
|
if (!this._usesCollapsibleWrapper) {
|
|
|
|
|
const hasSnapshot = !!this._terminalData.terminalCommandOutput;
|
|
|
|
|
const hasOutput = !!resolvedCommand || hasSnapshot;
|
|
|
|
|
this._hasOutputKey.set(hasOutput);
|
|
|
|
|
|
|
|
|
|
// Auto-expand on first detection of failed output
|
|
|
|
|
if (hasOutput && !this._outputView.isExpanded) {
|
|
|
|
|
const autoExpandFailures = this._configurationService.getValue<boolean>(ChatConfiguration.AutoExpandToolFailures);
|
|
|
|
|
const exitCode = resolvedCommand?.exitCode ?? this._terminalData.terminalCommandState?.exitCode;
|
|
|
|
|
if (exitCode !== undefined && exitCode !== 0 && autoExpandFailures) {
|
|
|
|
|
this._toggleOutput(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._ensureShowOutputAction(resolvedCommand);
|
|
|
|
|
this._decoration.update(resolvedCommand);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -637,52 +737,8 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
|
|
|
|
return this._resolveCommand(target);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _ensureShowOutputAction(command?: ITerminalCommand): void {
|
|
|
|
|
if (this._store.isDisposed) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// don't show dropdown when rendered with the simplified/collapsible wrapper
|
|
|
|
|
if (this._usesCollapsibleWrapper) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const resolvedCommand = command ?? this._getResolvedCommand();
|
|
|
|
|
const hasSnapshot = !!this._terminalData.terminalCommandOutput;
|
|
|
|
|
if (!resolvedCommand && !hasSnapshot) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let showOutputAction = this._showOutputAction.value;
|
|
|
|
|
if (!showOutputAction) {
|
|
|
|
|
showOutputAction = this._instantiationService.createInstance(ToggleChatTerminalOutputAction, () => this._toggleOutputFromAction());
|
|
|
|
|
this._showOutputAction.value = showOutputAction;
|
|
|
|
|
const autoExpandFailures = this._configurationService.getValue<boolean>(ChatConfiguration.AutoExpandToolFailures);
|
|
|
|
|
const exitCode = resolvedCommand?.exitCode ?? this._terminalData.terminalCommandState?.exitCode;
|
|
|
|
|
if (exitCode !== undefined && exitCode !== 0 && autoExpandFailures) {
|
|
|
|
|
this._toggleOutput(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
showOutputAction.syncPresentation(this._outputView.isExpanded);
|
|
|
|
|
|
|
|
|
|
const actionBar = this._actionBar;
|
|
|
|
|
if (this._showOutputActionAdded) {
|
|
|
|
|
const existingIndex = actionBar.viewItems.findIndex(item => item.action === showOutputAction);
|
|
|
|
|
if (existingIndex >= 0 && existingIndex !== actionBar.length() - 1) {
|
|
|
|
|
actionBar.pull(existingIndex);
|
|
|
|
|
this._showOutputActionAdded = false;
|
|
|
|
|
} else if (existingIndex >= 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this._showOutputActionAdded) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
actionBar.push([showOutputAction], { icon: true, label: false });
|
|
|
|
|
this._showOutputActionAdded = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _clearCommandAssociation(options?: { clearPersistentData?: boolean }): void {
|
|
|
|
|
this._terminalCommandUri = undefined;
|
|
|
|
|
this._storedCommandId = undefined;
|
|
|
|
|
if (options?.clearPersistentData) {
|
|
|
|
|
if (this._terminalData.terminalCommandUri) {
|
|
|
|
|
delete this._terminalData.terminalCommandUri;
|
|
|
|
@@ -719,7 +775,7 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
|
|
|
|
const commandDetectionListener = this._register(new MutableDisposable<IDisposable>());
|
|
|
|
|
const tryResolveCommand = async (): Promise<ITerminalCommand | undefined> => {
|
|
|
|
|
const resolvedCommand = this._resolveCommand(terminalInstance);
|
|
|
|
|
this._addActions(terminalInstance, this._terminalData.terminalToolSessionId);
|
|
|
|
|
this._updateToolbarContextKeys(terminalInstance, this._terminalData.terminalToolSessionId);
|
|
|
|
|
return resolvedCommand;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@@ -778,11 +834,11 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
store.add(commandDetection.onCommandExecuted(() => {
|
|
|
|
|
this._addActions(terminalInstance, this._terminalData.terminalToolSessionId);
|
|
|
|
|
this._updateToolbarContextKeys(terminalInstance, this._terminalData.terminalToolSessionId);
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
store.add(commandDetection.onCommandFinished(() => {
|
|
|
|
|
this._addActions(terminalInstance, this._terminalData.terminalToolSessionId);
|
|
|
|
|
this._updateToolbarContextKeys(terminalInstance, this._terminalData.terminalToolSessionId);
|
|
|
|
|
const resolvedCommand = this._getResolvedCommand(terminalInstance);
|
|
|
|
|
|
|
|
|
|
this._handleCommandCompletion(resolvedCommand);
|
|
|
|
@@ -810,47 +866,11 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
|
|
|
|
}
|
|
|
|
|
this._clearCommandAssociation({ clearPersistentData: true });
|
|
|
|
|
commandDetectionListener.clear();
|
|
|
|
|
if (!this._store.isDisposed) {
|
|
|
|
|
this._actionBar.clear();
|
|
|
|
|
}
|
|
|
|
|
this._removeFocusAction();
|
|
|
|
|
this._showOutputActionAdded = false;
|
|
|
|
|
this._showOutputAction.clear();
|
|
|
|
|
this._addActions(undefined, this._terminalData.terminalToolSessionId);
|
|
|
|
|
this._updateToolbarContextKeys(undefined, this._terminalData.terminalToolSessionId);
|
|
|
|
|
instanceListener.dispose();
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _removeFocusAction(): void {
|
|
|
|
|
if (this._store.isDisposed) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const actionBar = this._actionBar;
|
|
|
|
|
const focusAction = this._focusAction.value;
|
|
|
|
|
if (actionBar && focusAction) {
|
|
|
|
|
const existingIndex = actionBar.viewItems.findIndex(item => item.action === focusAction);
|
|
|
|
|
if (existingIndex >= 0) {
|
|
|
|
|
actionBar.pull(existingIndex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this._focusAction.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _removeContinueInBackgroundAction(): void {
|
|
|
|
|
if (this._store.isDisposed) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const actionBar = this._actionBar;
|
|
|
|
|
const continueAction = this._continueInBackgroundAction.value;
|
|
|
|
|
if (actionBar && continueAction) {
|
|
|
|
|
const existingIndex = actionBar.viewItems.findIndex(item => item.action === continueAction);
|
|
|
|
|
if (existingIndex >= 0) {
|
|
|
|
|
actionBar.pull(existingIndex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this._continueInBackgroundAction.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handles the completion of a terminal command by updating the UI state.
|
|
|
|
|
* This includes marking the collapsible wrapper as complete, auto-collapsing
|
|
|
|
@@ -878,7 +898,7 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
|
|
|
|
const didChange = await this._outputView.toggle(expanded);
|
|
|
|
|
const isExpanded = this._outputView.isExpanded;
|
|
|
|
|
this._titleElement.classList.toggle('chat-terminal-content-title-no-bottom-radius', isExpanded);
|
|
|
|
|
this._showOutputAction.value?.syncPresentation(isExpanded);
|
|
|
|
|
this._outputExpandedKey.set(isExpanded);
|
|
|
|
|
if (didChange) {
|
|
|
|
|
expandedStateByInvocation.set(this.toolInvocation, isExpanded);
|
|
|
|
|
}
|
|
|
|
@@ -932,15 +952,80 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async focusTerminal(): Promise<void> {
|
|
|
|
|
if (this._focusAction.value) {
|
|
|
|
|
await this._focusAction.value.run();
|
|
|
|
|
const instance = this._terminalInstance;
|
|
|
|
|
|
|
|
|
|
type FocusChatInstanceTelemetryEvent = {
|
|
|
|
|
target: 'instance' | 'commandUri' | 'none';
|
|
|
|
|
location: 'panel' | 'editor';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type FocusChatInstanceTelemetryClassification = {
|
|
|
|
|
owner: 'meganrogge';
|
|
|
|
|
comment: 'Track usage of the focus chat terminal action.';
|
|
|
|
|
target: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether focusing targeted an existing instance or opened a command URI.' };
|
|
|
|
|
location: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Location of the terminal instance when focusing.' };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let target: FocusChatInstanceTelemetryEvent['target'] = 'none';
|
|
|
|
|
let location: FocusChatInstanceTelemetryEvent['location'] = 'panel';
|
|
|
|
|
if (instance) {
|
|
|
|
|
target = 'instance';
|
|
|
|
|
location = instance.target === TerminalLocation.Editor ? 'editor' : 'panel';
|
|
|
|
|
} else if (this._terminalCommandUri) {
|
|
|
|
|
target = 'commandUri';
|
|
|
|
|
}
|
|
|
|
|
this._telemetryService.publicLog2<FocusChatInstanceTelemetryEvent, FocusChatInstanceTelemetryClassification>('terminal/chatFocusInstance', { target, location });
|
|
|
|
|
|
|
|
|
|
if (instance) {
|
|
|
|
|
this._terminalService.setActiveInstance(instance);
|
|
|
|
|
if (instance.target === TerminalLocation.Editor) {
|
|
|
|
|
this._terminalEditorService.openEditor(instance);
|
|
|
|
|
} else {
|
|
|
|
|
await this._terminalGroupService.showPanel(true);
|
|
|
|
|
}
|
|
|
|
|
this._terminalService.setActiveInstance(instance);
|
|
|
|
|
await instance.focusWhenReady(true);
|
|
|
|
|
const command = this._getResolvedCommand(instance);
|
|
|
|
|
if (command) {
|
|
|
|
|
instance.xterm?.markTracker.revealCommand(command);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this._terminalCommandUri) {
|
|
|
|
|
this._terminalService.openResource(this._terminalCommandUri);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public continueInBackground(): void {
|
|
|
|
|
const sessionId = this._terminalData.terminalToolSessionId;
|
|
|
|
|
if (sessionId) {
|
|
|
|
|
this._terminalChatService.continueInBackground(sessionId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async toggleOutputFromAction(): Promise<void> {
|
|
|
|
|
this._userToggledOutput = true;
|
|
|
|
|
|
|
|
|
|
type ToggleChatTerminalOutputTelemetryEvent = {
|
|
|
|
|
previousExpanded: boolean;
|
|
|
|
|
};
|
|
|
|
|
type ToggleChatTerminalOutputTelemetryClassification = {
|
|
|
|
|
owner: 'meganrogge';
|
|
|
|
|
comment: 'Track usage of the toggle chat terminal output action.';
|
|
|
|
|
previousExpanded: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the terminal output was expanded before the toggle.' };
|
|
|
|
|
};
|
|
|
|
|
this._telemetryService.publicLog2<ToggleChatTerminalOutputTelemetryEvent, ToggleChatTerminalOutputTelemetryClassification>('terminal/chatToggleOutput', {
|
|
|
|
|
previousExpanded: this._outputView.isExpanded
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!this._outputView.isExpanded) {
|
|
|
|
|
await this._toggleOutput(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await this._toggleOutput(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async toggleOutputFromKeyboard(): Promise<void> {
|
|
|
|
|
this._userToggledOutput = true;
|
|
|
|
|
if (!this._outputView.isExpanded) {
|
|
|
|
@@ -951,15 +1036,6 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
|
|
|
|
await this._collapseOutputAndFocusInput();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _toggleOutputFromAction(): Promise<void> {
|
|
|
|
|
this._userToggledOutput = true;
|
|
|
|
|
if (!this._outputView.isExpanded) {
|
|
|
|
|
await this._toggleOutput(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await this._toggleOutput(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _collapseOutputAndFocusInput(): Promise<void> {
|
|
|
|
|
if (this._outputView.isExpanded) {
|
|
|
|
|
await this._toggleOutput(false);
|
|
|
|
@@ -1460,178 +1536,6 @@ class ChatTerminalToolOutputSection extends Disposable {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class ToggleChatTerminalOutputAction extends Action implements IAction {
|
|
|
|
|
private _expanded = false;
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
private readonly _toggle: () => Promise<void>,
|
|
|
|
|
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
|
|
|
|
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
|
|
|
|
) {
|
|
|
|
|
super(
|
|
|
|
|
TerminalContribCommandId.ToggleChatTerminalOutput,
|
|
|
|
|
localize('showTerminalOutput', 'Show Output'),
|
|
|
|
|
ThemeIcon.asClassName(Codicon.chevronRight),
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
this._updateTooltip();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override async run(): Promise<void> {
|
|
|
|
|
type ToggleChatTerminalOutputTelemetryEvent = {
|
|
|
|
|
previousExpanded: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type ToggleChatTerminalOutputTelemetryClassification = {
|
|
|
|
|
owner: 'meganrogge';
|
|
|
|
|
comment: 'Track usage of the toggle chat terminal output action.';
|
|
|
|
|
previousExpanded: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the terminal output was expanded before the toggle.' };
|
|
|
|
|
};
|
|
|
|
|
this._telemetryService.publicLog2<ToggleChatTerminalOutputTelemetryEvent, ToggleChatTerminalOutputTelemetryClassification>('terminal/chatToggleOutput', {
|
|
|
|
|
previousExpanded: this._expanded
|
|
|
|
|
});
|
|
|
|
|
await this._toggle();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public syncPresentation(expanded: boolean): void {
|
|
|
|
|
this._expanded = expanded;
|
|
|
|
|
this._updatePresentation();
|
|
|
|
|
this._updateTooltip();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public refreshKeybindingTooltip(): void {
|
|
|
|
|
this._updateTooltip();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _updatePresentation(): void {
|
|
|
|
|
if (this._expanded) {
|
|
|
|
|
this.label = localize('hideTerminalOutput', 'Hide Output');
|
|
|
|
|
this.class = ThemeIcon.asClassName(Codicon.chevronDown);
|
|
|
|
|
} else {
|
|
|
|
|
this.label = localize('showTerminalOutput', 'Show Output');
|
|
|
|
|
this.class = ThemeIcon.asClassName(Codicon.chevronRight);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _updateTooltip(): void {
|
|
|
|
|
this.tooltip = this._keybindingService.appendKeybinding(this.label, TerminalContribCommandId.FocusMostRecentChatTerminalOutput);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class FocusChatInstanceAction extends Action implements IAction {
|
|
|
|
|
constructor(
|
|
|
|
|
private _instance: ITerminalInstance | undefined,
|
|
|
|
|
private _command: ITerminalCommand | undefined,
|
|
|
|
|
private readonly _commandUri: URI | undefined,
|
|
|
|
|
private readonly _commandId: string | undefined,
|
|
|
|
|
isTerminalHidden: boolean,
|
|
|
|
|
@ITerminalService private readonly _terminalService: ITerminalService,
|
|
|
|
|
@ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService,
|
|
|
|
|
@ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService,
|
|
|
|
|
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
|
|
|
|
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
|
|
|
|
) {
|
|
|
|
|
super(
|
|
|
|
|
TerminalContribCommandId.FocusChatInstanceAction,
|
|
|
|
|
isTerminalHidden ? localize('showTerminal', 'Show and Focus Terminal') : localize('focusTerminal', 'Focus Terminal'),
|
|
|
|
|
ThemeIcon.asClassName(Codicon.openInProduct),
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
this._updateTooltip();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override async run() {
|
|
|
|
|
this.label = this._instance?.shellLaunchConfig.hideFromUser ? localize('showAndFocusTerminal', 'Show and Focus Terminal') : localize('focusTerminal', 'Focus Terminal');
|
|
|
|
|
this._updateTooltip();
|
|
|
|
|
|
|
|
|
|
let target: FocusChatInstanceTelemetryEvent['target'] = 'none';
|
|
|
|
|
let location: FocusChatInstanceTelemetryEvent['location'] = 'panel';
|
|
|
|
|
if (this._instance) {
|
|
|
|
|
target = 'instance';
|
|
|
|
|
location = this._instance.target === TerminalLocation.Editor ? 'editor' : 'panel';
|
|
|
|
|
} else if (this._commandUri) {
|
|
|
|
|
target = 'commandUri';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type FocusChatInstanceTelemetryEvent = {
|
|
|
|
|
target: 'instance' | 'commandUri' | 'none';
|
|
|
|
|
location: 'panel' | 'editor';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type FocusChatInstanceTelemetryClassification = {
|
|
|
|
|
owner: 'meganrogge';
|
|
|
|
|
comment: 'Track usage of the focus chat terminal action.';
|
|
|
|
|
target: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether focusing targeted an existing instance or opened a command URI.' };
|
|
|
|
|
location: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Location of the terminal instance when focusing.' };
|
|
|
|
|
};
|
|
|
|
|
this._telemetryService.publicLog2<FocusChatInstanceTelemetryEvent, FocusChatInstanceTelemetryClassification>('terminal/chatFocusInstance', {
|
|
|
|
|
target,
|
|
|
|
|
location
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (this._instance) {
|
|
|
|
|
this._terminalService.setActiveInstance(this._instance);
|
|
|
|
|
if (this._instance.target === TerminalLocation.Editor) {
|
|
|
|
|
this._terminalEditorService.openEditor(this._instance);
|
|
|
|
|
} else {
|
|
|
|
|
await this._terminalGroupService.showPanel(true);
|
|
|
|
|
}
|
|
|
|
|
this._terminalService.setActiveInstance(this._instance);
|
|
|
|
|
await this._instance.focusWhenReady(true);
|
|
|
|
|
const command = this._resolveCommand();
|
|
|
|
|
if (command) {
|
|
|
|
|
this._instance.xterm?.markTracker.revealCommand(command);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this._commandUri) {
|
|
|
|
|
this._terminalService.openResource(this._commandUri);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public refreshKeybindingTooltip(): void {
|
|
|
|
|
this._updateTooltip();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _resolveCommand(): ITerminalCommand | undefined {
|
|
|
|
|
if (this._command && !this._command.endMarker?.isDisposed) {
|
|
|
|
|
return this._command;
|
|
|
|
|
}
|
|
|
|
|
if (!this._instance || !this._commandId) {
|
|
|
|
|
return this._command;
|
|
|
|
|
}
|
|
|
|
|
const commandDetection = this._instance.capabilities.get(TerminalCapability.CommandDetection);
|
|
|
|
|
const resolved = commandDetection?.commands.find(c => c.id === this._commandId);
|
|
|
|
|
if (resolved) {
|
|
|
|
|
this._command = resolved;
|
|
|
|
|
}
|
|
|
|
|
return this._command;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _updateTooltip(): void {
|
|
|
|
|
this.tooltip = this._keybindingService.appendKeybinding(this.label, TerminalContribCommandId.FocusMostRecentChatTerminal);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class ContinueInBackgroundAction extends Action implements IAction {
|
|
|
|
|
constructor(
|
|
|
|
|
private readonly _terminalToolSessionId: string,
|
|
|
|
|
@ITerminalChatService private readonly _terminalChatService: ITerminalChatService,
|
|
|
|
|
) {
|
|
|
|
|
super(
|
|
|
|
|
TerminalContribCommandId.ContinueInBackground,
|
|
|
|
|
localize('continueInBackground', 'Continue in Background'),
|
|
|
|
|
ThemeIcon.asClassName(Codicon.debugContinue),
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override async run(): Promise<void> {
|
|
|
|
|
this._terminalChatService.continueInBackground(this._terminalToolSessionId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class ChatTerminalThinkingCollapsibleWrapper extends ChatCollapsibleContentPart {
|
|
|
|
|
private readonly _terminalContentElement: HTMLElement;
|
|
|
|
|
private readonly _commandText: string;
|
|
|
|
|