From 4162f5e97a99409ee25f4a1a5a43cabc61d1d111 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 31 Mar 2026 07:55:36 -0700 Subject: [PATCH 01/16] Bump distro --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index be8c8499ab0..c530f4c90a1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.115.0", - "distro": "f7bd750d5f598365ecd892bd9fd2c2b61315db0c", + "distro": "fdfcc35f4a498ffc0fae5966393153e96672dc89", "author": { "name": "Microsoft Corporation" }, From 00844ccb8b325b488c44d7cba8b089c25ba19f24 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega <48293249+osortega@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:22:04 -0700 Subject: [PATCH 02/16] Sessions: remove weird description (#306880) * Sessions: remove weird description --- .../browser/widget/input/delegationSessionPickerActionItem.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts index 97d7049abd8..68a8717ba0a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts @@ -122,9 +122,6 @@ export class DelegationSessionPickerActionItem extends SessionTypePickerActionIt } protected override _getSessionDescription(sessionTypeItem: ISessionTypeItem): string | undefined { - if (this._isSessionsWindow && sessionTypeItem.type === AgentSessionProviders.Cloud && !this._hasGitRepository()) { - return localize('chat.cloudRequiresGit', "Requires a Git repository"); - } return undefined; } From a5cc6f3c5d701f4899b5ce72254115065eb1c3f5 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 31 Mar 2026 11:26:42 -0700 Subject: [PATCH 03/16] agentHost: Fix CopilotAgent finding ripgrep (#306831) --- src/vs/platform/agentHost/node/copilot/copilotAgent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts index bf4ea118f95..688f70c6220 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts @@ -105,7 +105,7 @@ export class CopilotAgent extends Disposable implements IAgent { } } env['COPILOT_CLI_RUN_AS_NODE'] = '1'; - env['USE_BUILTIN_RIPGREP'] = '0'; + env['USE_BUILTIN_RIPGREP'] = 'false'; // Resolve the CLI entry point from node_modules. We can't use require.resolve() // because @github/copilot's exports map blocks direct subpath access. From 3bc94239ff5004757f4cc97390ecb30b86b39d85 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 31 Mar 2026 11:41:00 -0700 Subject: [PATCH 04/16] Add chat model reference inspection (#306596) * Add chat model reference inspection (Written by Copilot) * Add archive state and age to chat model inspection (Written by Copilot) * fix import * Cleanups * test --- .../browser/actions/chatDeveloperActions.ts | 87 ++++++++++++++ .../chat/browser/actions/chatForkActions.ts | 13 +- .../chat/browser/actions/chatImportExport.ts | 16 ++- .../agentSessions/agentSessionHoverWidget.ts | 2 +- .../chatSessions/chatSessions.contribution.ts | 2 +- .../chat/browser/widgetHosts/chatQuick.ts | 2 +- .../widgetHosts/editor/chatEditorInput.ts | 8 +- .../widgetHosts/viewPane/chatViewPane.ts | 10 +- .../chat/common/chatService/chatService.ts | 26 ++-- .../common/chatService/chatServiceImpl.ts | 36 +++--- .../contrib/chat/common/model/chatModel.ts | 4 +- .../chat/common/model/chatModelStore.ts | 112 ++++++++++++++++-- .../common/chatService/chatService.test.ts | 50 +++++++- .../common/chatService/mockChatService.ts | 11 +- .../test/common/model/chatModelStore.test.ts | 111 +++++++++++++++++ 15 files changed, 423 insertions(+), 67 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts index ab1429fdadd..8f0a5ef2760 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts @@ -4,12 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../../base/common/codicons.js'; +import { fromNow } from '../../../../../base/common/date.js'; import { isUriComponents, URI } from '../../../../../base/common/uri.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { localize2 } from '../../../../../nls.js'; import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { IChatService } from '../../common/chatService/chatService.js'; import { ILanguageModelsService } from '../../common/languageModels.js'; @@ -32,9 +35,64 @@ export function registerChatDeveloperActions() { registerAction2(LogChatInputHistoryAction); registerAction2(LogChatIndexAction); registerAction2(InspectChatModelAction); + registerAction2(InspectChatModelReferencesAction); registerAction2(ClearRecentlyUsedLanguageModelsAction); } +function formatChatModelReferenceInspection(accessor: ServicesAccessor): string { + const chatService = accessor.get(IChatService); + const agentSessionsService = accessor.get(IAgentSessionsService); + const debugInfo = chatService.getChatModelReferenceDebugInfo(); + const referencedModels = debugInfo.models.filter(model => model.referenceCount > 0); + const pendingEditModels = debugInfo.models.filter(model => model.hasPendingEdits); + const pendingDisposalModels = debugInfo.models.filter(model => model.pendingDisposal); + + let output = '# Chat Model References\n\n'; + output += `- Live models: ${debugInfo.totalModels}\n`; + output += `- Live references: ${debugInfo.totalReferences}\n`; + output += `- Models with active references: ${referencedModels.length}\n`; + output += `- Models with pending edits: ${pendingEditModels.length}\n`; + output += `- Models pending disposal: ${pendingDisposalModels.length}\n\n`; + output += 'Created by shows who loaded or created the model. Holders shows who currently keeps the model alive.\n\n'; + + if (!debugInfo.models.length) { + output += 'No live chat models.\n'; + return output; + } + + for (const model of debugInfo.models) { + const liveModel = chatService.getSession(model.sessionResource); + const agentSession = agentSessionsService.getSession(model.sessionResource); + const archived = agentSession ? agentSession.isArchived() : 'unknown'; + const age = liveModel ? fromNow(liveModel.timing.created, true, true, true) : 'unknown'; + + output += `## ${model.title || '(untitled)'}\n\n`; + output += `- Session: ${model.sessionResource.toString()}\n`; + output += `- Created by: ${model.createdBy}\n`; + output += `- Archived: ${archived}\n`; + output += `- Age: ${age}\n`; + output += `- Initial location: ${model.initialLocation}\n`; + output += `- Imported: ${model.isImported}\n`; + output += `- Pending edits: ${model.hasPendingEdits}\n`; + output += `- Background keep-alive enabled: ${model.willKeepAlive}\n`; + output += `- Pending disposal: ${model.pendingDisposal}\n`; + output += `- Reference count: ${model.referenceCount}\n`; + + if (model.holders.length) { + output += '- Holders:\n'; + for (const holder of model.holders) { + output += ` - ${holder.holder}: ${holder.count}\n`; + } + } else { + output += '- Holders: none\n'; + } + + output += '\n'; + } + + return output; +} + class LogChatInputHistoryAction extends Action2 { static readonly ID = 'workbench.action.chat.logInputHistory'; @@ -129,6 +187,35 @@ class InspectChatModelAction extends Action2 { } } +class InspectChatModelReferencesAction extends Action2 { + static readonly ID = 'workbench.action.chat.inspectChatModelReferences'; + + constructor() { + super({ + id: InspectChatModelReferencesAction.ID, + title: localize2('workbench.action.chat.inspectChatModelReferences.label', "Inspect Chat Model References"), + icon: Codicon.inspect, + category: Categories.Developer, + f1: true, + precondition: ChatContextKeys.enabled + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const instantiationService = accessor.get(IInstantiationService); + const editorService = accessor.get(IEditorService); + + await editorService.openEditor({ + resource: undefined, + contents: instantiationService.invokeFunction(formatChatModelReferenceInspection), + languageId: 'markdown', + options: { + pinned: true + } + }); + } +} + class ClearRecentlyUsedLanguageModelsAction extends Action2 { static readonly ID = 'workbench.action.chat.clearRecentlyUsedLanguageModels'; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatForkActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatForkActions.ts index 57b337a8ccc..c658d226b10 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatForkActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatForkActions.ts @@ -95,7 +95,7 @@ export function registerChatForkActions() { } } - const modelRef = chatService.loadSessionFromData(cleanData); + const modelRef = chatService.loadSessionFromData(cleanData, 'ChatForkActions#forkCleanSession'); // Defer navigation until after the slash command flow completes. const newSessionResource = modelRef.object.sessionResource; @@ -216,16 +216,19 @@ export function registerChatForkActions() { } } - const modelRef = chatService.loadSessionFromData(forkedData); + const modelRef = chatService.loadSessionFromData(forkedData, 'ChatForkActions#forkSession'); if (!modelRef) { return; } // Navigate to the new session in the chat view pane - const newSessionResource = modelRef.object.sessionResource; - await chatWidgetService.openSession(newSessionResource, ChatViewPaneTarget); - modelRef.dispose(); + try { + const newSessionResource = modelRef.object.sessionResource; + await chatWidgetService.openSession(newSessionResource, ChatViewPaneTarget); + } finally { + modelRef.dispose(); + } } private pendingFork = new Map>(); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatImportExport.ts b/src/vs/workbench/contrib/chat/browser/actions/chatImportExport.ts index 059f2febb5c..14dddc60283 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatImportExport.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatImportExport.ts @@ -123,17 +123,21 @@ export function registerChatExportActions() { let options: IChatEditorOptions; if (opts?.target === 'chatViewPane') { - const modelRef = chatService.loadSessionFromData(data); - sessionResource = modelRef.object.sessionResource; - resolvedTarget = ChatViewPaneTarget; - options = { pinned: true }; + const modelRef = chatService.loadSessionFromData(data, 'ChatImportExport#importToChatView'); + try { + sessionResource = modelRef.object.sessionResource; + resolvedTarget = ChatViewPaneTarget; + options = { pinned: true }; + await widgetService.openSession(sessionResource, resolvedTarget, options); + } finally { + modelRef.dispose(); + } } else { sessionResource = ChatEditorInput.getNewEditorUri(); resolvedTarget = ACTIVE_GROUP; options = { target: { data }, pinned: true }; + await widgetService.openSession(sessionResource, resolvedTarget, options); } - - await widgetService.openSession(sessionResource, resolvedTarget, options); } catch (err) { throw err; } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts index 3f90e8fd4b2..2b05a00898d 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts @@ -79,7 +79,7 @@ export class AgentSessionHoverWidget extends Disposable { } private async loadModel() { - const modelRef = await this.chatService.acquireOrLoadSession(this.session.resource, ChatAgentLocation.Chat, this.cts.token); + const modelRef = await this.chatService.acquireOrLoadSession(this.session.resource, ChatAgentLocation.Chat, this.cts.token, 'AgentSessionHoverWidget#loadModel'); if (this._store.isDisposed) { modelRef?.dispose(); return; diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index edb32b33b3d..2cdff8a4147 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -556,7 +556,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ let attachedContext = chatOptions.attachedContext; const resource = URI.revive(chatOptions.resource); - const ref = await chatService.acquireOrLoadSession(resource, ChatAgentLocation.Chat, CancellationToken.None); + const ref = await chatService.acquireOrLoadSession(resource, ChatAgentLocation.Chat, CancellationToken.None, 'ChatSessionsContribution#sendPrompt'); try { const promptFile = await resolvePromptSlashCommand(chatOptions.prompt, promptsService, toolsService); if (promptFile) { diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/chatQuick.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/chatQuick.ts index c6a61b98152..b8e5507f32a 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/chatQuick.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/chatQuick.ts @@ -394,7 +394,7 @@ class QuickChat extends Disposable { } private updateModel(): void { - this.modelRef ??= this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { disableBackgroundKeepAlive: true }); + this.modelRef ??= this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { disableBackgroundKeepAlive: true, debugOwner: 'ChatQuick#updateModel' }); const model = this.modelRef?.object; if (!model) { throw new Error('Could not start chat session'); diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts index 7a5b0d5d880..aa7c5f22a7e 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts @@ -213,16 +213,16 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler const inputType = chatSessionType ?? this.resource.authority; if (this._sessionResource) { - this.modelRef.value = await this.chatService.acquireOrLoadSession(this._sessionResource, ChatAgentLocation.Chat, CancellationToken.None); + this.modelRef.value = await this.chatService.acquireOrLoadSession(this._sessionResource, ChatAgentLocation.Chat, CancellationToken.None, 'ChatEditorInput#resolve'); // For local session only, if we find no existing session, create a new one if (!this.model && LocalChatSessionUri.parseLocalSessionId(this._sessionResource)) { - this.modelRef.value = this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { canUseTools: true }); + this.modelRef.value = this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { canUseTools: true, debugOwner: 'ChatEditorInput#resolveNewLocalSession' }); } } else if (!this.options.target) { - this.modelRef.value = this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { canUseTools: !inputType }); + this.modelRef.value = this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { canUseTools: !inputType, debugOwner: 'ChatEditorInput#resolveUntitled' }); } else if (this.options.target.data) { - this.modelRef.value = this.chatService.loadSessionFromData(this.options.target.data); + this.modelRef.value = this.chatService.loadSessionFromData(this.options.target.data, 'ChatEditorInput#resolveImportedData'); } if (!this.model || this.isDisposed()) { diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 10088ac9547..425d067ad42 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -253,7 +253,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { if (!this._widget?.viewModel && !this.restoringSession) { const sessionResource = this.getTransferredOrPersistedSessionInfo(); this.restoringSession = - (sessionResource ? this.chatService.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, CancellationToken.None) : Promise.resolve(undefined)).then(async modelRef => { + (sessionResource ? this.chatService.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, CancellationToken.None, 'ChatViewPane#onDidChangeAgents') : Promise.resolve(undefined)).then(async modelRef => { if (!this._widget) { return; // renderBody has not been called yet } @@ -699,7 +699,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private async _applyModel(): Promise { const sessionResource = this.getTransferredOrPersistedSessionInfo(); - const modelRef = sessionResource ? await this.chatService.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, CancellationToken.None) : undefined; + const modelRef = sessionResource ? await this.chatService.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, CancellationToken.None, 'ChatViewPane#applyModel') : undefined; await this.showModel(CancellationToken.None, modelRef); } @@ -714,8 +714,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { let ref: IChatModelReference | undefined; if (startNewSession) { ref = modelRef ?? (this.chatService.transferredSessionResource - ? await this.chatService.acquireOrLoadSession(this.chatService.transferredSessionResource, ChatAgentLocation.Chat, token) - : this.chatService.startNewLocalSession(ChatAgentLocation.Chat)); + ? await this.chatService.acquireOrLoadSession(this.chatService.transferredSessionResource, ChatAgentLocation.Chat, token, 'ChatViewPane#showModel') + : this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { debugOwner: 'ChatViewPane#showModel' })); if (!ref) { throw new Error('Could not start chat session'); } @@ -836,7 +836,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { const clearWidgetCancellationListener = token.onCancellationRequested(() => clearWidget.dispose()); try { - const newModelRef = await this.chatService.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, token); + const newModelRef = await this.chatService.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, token, 'ChatViewPane#loadSession'); clearWidget.dispose(); await queue; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 2fd59394973..79201b41d44 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -14,7 +14,6 @@ import { ThemeIcon } from '../../../../../base/common/themables.js'; import { hasKey } from '../../../../../base/common/types.js'; import { URI, UriComponents } from '../../../../../base/common/uri.js'; import { IRange, Range } from '../../../../../editor/common/core/range.js'; -import { HookTypeValue } from '../promptSyntax/hookTypes.js'; import { ISelection } from '../../../../../editor/common/core/selection.js'; import { Command, Location, TextEdit } from '../../../../../editor/common/languages.js'; import { FileType } from '../../../../../platform/files/common/files.js'; @@ -22,16 +21,18 @@ import { createDecorator } from '../../../../../platform/instantiation/common/in import { IAutostartResult } from '../../../mcp/common/mcpTypes.js'; import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; import { IWorkspaceSymbol } from '../../../search/common/search.js'; -import { IChatAgentCommand, IChatAgentData, IChatAgentResult, UserSelectedTools } from '../participants/chatAgents.js'; -import { IChatEditingSession } from '../editing/chatEditingService.js'; -import { IChatModel, IChatRequestModeInfo, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData } from '../model/chatModel.js'; -import { IParsedChatRequest } from '../requestParser/chatParserTypes.js'; -import { IChatParserContext } from '../requestParser/chatRequestParser.js'; import { IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js'; import { IChatRequestVariableValue } from '../attachments/chatVariables.js'; -import { ChatAgentLocation } from '../constants.js'; -import { IPreparedToolInvocation, IToolConfirmationMessages, IToolResult, IToolResultInputOutputDetails, ToolDataSource } from '../tools/languageModelToolsService.js'; import { ReadonlyChatSessionOptionsMap } from '../chatSessionsService.js'; +import { ChatAgentLocation } from '../constants.js'; +import { IChatEditingSession } from '../editing/chatEditingService.js'; +import { IChatModel, IChatRequestModeInfo, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData } from '../model/chatModel.js'; +import type { IChatModelReferenceDebugSnapshot } from '../model/chatModelStore.js'; +import { IChatAgentCommand, IChatAgentData, IChatAgentResult, UserSelectedTools } from '../participants/chatAgents.js'; +import { HookTypeValue } from '../promptSyntax/hookTypes.js'; +import { IParsedChatRequest } from '../requestParser/chatParserTypes.js'; +import { IChatParserContext } from '../requestParser/chatRequestParser.js'; +import { IPreparedToolInvocation, IToolConfirmationMessages, IToolResult, IToolResultInputOutputDetails, ToolDataSource } from '../tools/languageModelToolsService.js'; export interface IChatRequest { message: string; @@ -1440,7 +1441,7 @@ export interface IChatService { * * @returns A reference to the session's model or undefined if there is no active session for the given resource. */ - acquireExistingSession(sessionResource: URI): IChatModelReference | undefined; + acquireExistingSession(sessionResource: URI, debugOwner?: string): IChatModelReference | undefined; /** * Tries to acquire an existing a chat session for the resource. If no session exists, tries to load one for the given @@ -1448,12 +1449,14 @@ export interface IChatService { * * @returns A reference to the session's model, or undefined if the session could not be loaded */ - acquireOrLoadSession(sessionResource: URI, location: ChatAgentLocation, token: CancellationToken): Promise; + acquireOrLoadSession(sessionResource: URI, location: ChatAgentLocation, token: CancellationToken, debugOwner?: string): Promise; /** * Loads a session from exported chat data */ - loadSessionFromData(data: IExportableChatData | ISerializableChatData): IChatModelReference; + loadSessionFromData(data: IExportableChatData | ISerializableChatData, debugOwner?: string): IChatModelReference; + + getChatModelReferenceDebugInfo(): IChatModelReferenceDebugSnapshot; getChatSessionFromInternalUri(sessionResource: URI): IChatSessionContext | undefined; @@ -1543,6 +1546,7 @@ export const KEYWORD_ACTIVIATION_SETTING_ID = 'accessibility.voice.keywordActiva export interface IChatSessionStartOptions { canUseTools?: boolean; disableBackgroundKeepAlive?: boolean; + debugOwner?: string; } export const ChatStopCancellationNoopEventName = 'chat.stopCancellationNoop'; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 387af583779..99a2a4b4f97 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -370,7 +370,7 @@ export class ChatService extends Disposable implements IChatService { } sessionResource ??= LocalChatSessionUri.forSession(session.sessionId); - const sessionRef = await this.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, CancellationToken.None); + const sessionRef = await this.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, CancellationToken.None, 'ChatService#reviveSessionsWithEdits'); if (sessionRef?.object.editingSession) { await chatEditingSessionIsReady(sessionRef.object.editingSession); // the session will hold a self-reference as long as there are modified files @@ -456,7 +456,7 @@ export class ChatService extends Disposable implements IChatService { sessionResource, canUseTools: options?.canUseTools ?? true, disableBackgroundKeepAlive: options?.disableBackgroundKeepAlive - }); + }, options?.debugOwner ?? 'ChatService#startNewLocalSession'); } private _startSession(props: IStartSessionProps): ChatModel { @@ -508,13 +508,17 @@ export class ChatService extends Disposable implements IChatService { return this._sessionModels.get(sessionResource); } - acquireExistingSession(sessionResource: URI): IChatModelReference | undefined { - return this._sessionModels.acquireExisting(sessionResource); + acquireExistingSession(sessionResource: URI, debugOwner?: string): IChatModelReference | undefined { + return this._sessionModels.acquireExisting(sessionResource, debugOwner ?? 'ChatService#acquireExistingSession'); } - private async acquireOrRestoreLocalSession(sessionResource: URI): Promise { + getChatModelReferenceDebugInfo() { + return this._sessionModels.getReferenceDebugSnapshot(); + } + + private async acquireOrRestoreLocalSession(sessionResource: URI, debugOwner?: string): Promise { this.trace('acquireOrRestoreSession', `${sessionResource}`); - const existingRef = this.acquireExistingSession(sessionResource); + const existingRef = this.acquireExistingSession(sessionResource, debugOwner); if (existingRef) { return existingRef; } @@ -539,7 +543,7 @@ export class ChatService extends Disposable implements IChatService { location: sessionData.value.initialLocation ?? ChatAgentLocation.Chat, sessionResource, canUseTools: true, - }); + }, debugOwner ?? 'ChatService#acquireOrRestoreLocalSession'); return sessionRef; } @@ -556,7 +560,7 @@ export class ChatService extends Disposable implements IChatService { this._chatSessionStore.getMetadataForSessionSync(sessionResource)?.title; } - loadSessionFromData(data: IExportableChatData | ISerializableChatData): IChatModelReference { + loadSessionFromData(data: IExportableChatData | ISerializableChatData, debugOwner?: string): IChatModelReference { const sessionId = (data as ISerializableChatData).sessionId ?? generateUuid(); const sessionResource = LocalChatSessionUri.forSession(sessionId); return this._sessionModels.acquireOrCreate({ @@ -564,22 +568,22 @@ export class ChatService extends Disposable implements IChatService { location: data.initialLocation ?? ChatAgentLocation.Chat, sessionResource, canUseTools: true, - }); + }, debugOwner ?? 'ChatService#loadSessionFromData'); } - async acquireOrLoadSession(sessionResource: URI, location: ChatAgentLocation, token: CancellationToken): Promise { + async acquireOrLoadSession(sessionResource: URI, location: ChatAgentLocation, token: CancellationToken, debugOwner?: string): Promise { if (sessionResource.scheme === Schemas.vscodeLocalChatSession) { - return this.acquireOrRestoreLocalSession(sessionResource); + return this.acquireOrRestoreLocalSession(sessionResource, debugOwner); } else { - return this.loadRemoteSession(sessionResource, location, token); + return this.loadRemoteSession(sessionResource, location, token, debugOwner); } } - private async loadRemoteSession(sessionResource: URI, location: ChatAgentLocation, token: CancellationToken): Promise { + private async loadRemoteSession(sessionResource: URI, location: ChatAgentLocation, token: CancellationToken, debugOwner?: string): Promise { // Check if session already exists before resolving the provider, // so we can return a cached model even if the provider was unregistered. { - const existingRef = this.acquireExistingSession(sessionResource); + const existingRef = this.acquireExistingSession(sessionResource, debugOwner); if (existingRef) { return existingRef; } @@ -593,7 +597,7 @@ export class ChatService extends Disposable implements IChatService { // Make sure we haven't created this in the meantime { - const existingRef = this.acquireExistingSession(sessionResource); + const existingRef = this.acquireExistingSession(sessionResource, debugOwner); if (existingRef) { providedSession.dispose(); return existingRef; @@ -644,7 +648,7 @@ export class ChatService extends Disposable implements IChatService { canUseTools: false, transferEditingSession: providedSession.transferredState?.editingSession, inputState: providedSession.transferredState?.inputState, - }); + }, debugOwner ?? 'ChatService#loadRemoteSession'); // Restore permission level from metadata even when initialData was not constructed if (storedPermissionLevel && !initialData) { diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index 1c220f71fad..e874b68f49c 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -2300,7 +2300,7 @@ export class ChatModel extends Disposable implements IChatModel { const needsInput = this.requestNeedsInput.read(r); const shouldStayAlive = inProgress || !!needsInput; if (shouldStayAlive && !selfRef.value) { - selfRef.value = chatService.acquireExistingSession(this._sessionResource); + selfRef.value = chatService.acquireExistingSession(this._sessionResource, 'ChatModel#requestInProgressKeepAlive'); } else if (!shouldStayAlive && selfRef.value) { selfRef.clear(); } @@ -2324,7 +2324,7 @@ export class ChatModel extends Disposable implements IChatModel { this._register(autorun(r => { const hasModified = session.entries.read(r).some(e => e.state.read(r) === ModifiedFileEntryState.Modified); if (hasModified && !selfRef.value) { - selfRef.value = this.chatService.acquireExistingSession(this._sessionResource); + selfRef.value = this.chatService.acquireExistingSession(this._sessionResource, 'ChatModel#modifiedEditsKeepAlive'); } else if (!hasModified && selfRef.value) { selfRef.clear(); } diff --git a/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts b/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts index 24bd5cd8263..f8a64447ffa 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts @@ -9,7 +9,7 @@ import { ObservableMap } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { ChatAgentLocation } from '../constants.js'; -import { IChatEditingSession } from '../editing/chatEditingService.js'; +import { IChatEditingSession, ModifiedFileEntryState } from '../editing/chatEditingService.js'; import { ChatModel, ISerializableChatModelInputState, ISerializedChatDataReference } from './chatModel.js'; export interface IStartSessionProps { @@ -27,12 +27,39 @@ export interface ChatModelStoreDelegate { willDisposeModel: (model: ChatModel) => Promise; } +export interface IChatModelReferenceDebugHolder { + readonly holder: string; + readonly count: number; +} + +export interface IChatModelReferenceDebugInfo { + readonly sessionResource: URI; + readonly title: string; + readonly createdBy: string; + readonly initialLocation: ChatAgentLocation; + readonly isImported: boolean; + readonly willKeepAlive: boolean; + readonly hasPendingEdits: boolean; + readonly pendingDisposal: boolean; + readonly referenceCount: number; + readonly holders: readonly IChatModelReferenceDebugHolder[]; +} + +export interface IChatModelReferenceDebugSnapshot { + readonly totalModels: number; + readonly totalReferences: number; + readonly models: readonly IChatModelReferenceDebugInfo[]; +} + export class ChatModelStore extends Disposable { private readonly _refCollection: ReferenceCollection; private readonly _models = new ObservableMap(); private readonly _modelsToDispose = new Set(); private readonly _pendingDisposals = new Set>(); + private readonly _modelCreateOwners = new Map(); + private readonly _referenceOwners = new Map>(); + private _referenceOwnerIds = 0; private readonly _onDidDisposeModel = this._register(new Emitter()); public readonly onDidDisposeModel = this._onDidDisposeModel.event; @@ -48,8 +75,8 @@ export class ChatModelStore extends Disposable { const self = this; this._refCollection = new class extends ReferenceCollection { - protected createReferencedObject(key: string, props?: IStartSessionProps): ChatModel { - return self.createReferencedObject(key, props); + protected createReferencedObject(key: string, props?: IStartSessionProps, debugOwner?: string): ChatModel { + return self.createReferencedObject(key, props, debugOwner); } protected destroyReferencedObject(key: string, object: ChatModel): void { return self.destroyReferencedObject(key, object); @@ -76,19 +103,57 @@ export class ChatModelStore extends Disposable { return this._models.has(this.toKey(uri)); } - public acquireExisting(uri: URI): IReference | undefined { + public acquireExisting(uri: URI, debugOwner?: string): IReference | undefined { const key = this.toKey(uri); if (!this._models.has(key)) { return undefined; } - return this._refCollection.acquire(key); + + return this.wrapReference(key, this._refCollection.acquire(key, undefined, debugOwner), debugOwner); } - public acquireOrCreate(props: IStartSessionProps): IReference { - return this._refCollection.acquire(this.toKey(props.sessionResource), props); + public acquireOrCreate(props: IStartSessionProps, debugOwner?: string): IReference { + const key = this.toKey(props.sessionResource); + return this.wrapReference(key, this._refCollection.acquire(key, props, debugOwner), debugOwner); } - private createReferencedObject(key: string, props?: IStartSessionProps): ChatModel { + public getReferenceDebugSnapshot(): IChatModelReferenceDebugSnapshot { + const models = Array.from(this._models.values()) + .map(model => { + const key = this.toKey(model.sessionResource); + const owners = this._referenceOwners.get(key) ?? new Map(); + const countsByOwner = new Map(); + for (const owner of owners.values()) { + countsByOwner.set(owner, (countsByOwner.get(owner) ?? 0) + 1); + } + + const holders = Array.from(countsByOwner.entries()) + .map(([holder, count]) => ({ holder, count })) + .sort((a, b) => b.count - a.count || a.holder.localeCompare(b.holder)); + + return { + sessionResource: model.sessionResource, + title: model.title, + createdBy: this._modelCreateOwners.get(key) ?? 'unknown', + initialLocation: model.initialLocation, + isImported: !!model.isImported, + willKeepAlive: model.willKeepAlive, + hasPendingEdits: !!model.editingSession?.entries.get().some(entry => entry.state.get() === ModifiedFileEntryState.Modified), + pendingDisposal: this._modelsToDispose.has(key), + referenceCount: owners.size, + holders, + } satisfies IChatModelReferenceDebugInfo; + }) + .sort((a, b) => b.referenceCount - a.referenceCount || Number(b.hasPendingEdits) - Number(a.hasPendingEdits) || a.sessionResource.toString().localeCompare(b.sessionResource.toString())); + + return { + totalModels: models.length, + totalReferences: models.reduce((total, model) => total + model.referenceCount, 0), + models, + }; + } + + private createReferencedObject(key: string, props?: IStartSessionProps, debugOwner?: string): ChatModel { this._modelsToDispose.delete(key); const existingModel = this._models.get(key); if (existingModel) { @@ -101,6 +166,7 @@ export class ChatModelStore extends Disposable { this.logService.trace(`Creating chat session ${key}`); const model = this.delegate.createModel(props); + this._modelCreateOwners.set(key, debugOwner ?? 'unspecified'); if (model.sessionResource.toString() !== key) { throw new Error(`Chat session key mismatch for ${key}`); } @@ -127,6 +193,8 @@ export class ChatModelStore extends Disposable { if (this._modelsToDispose.has(key)) { this.logService.trace(`Disposing chat session ${key}`); this._models.delete(key); + this._modelCreateOwners.delete(key); + this._referenceOwners.delete(key); this._onDidDisposeModel.fire(object); object.dispose(); } @@ -134,6 +202,34 @@ export class ChatModelStore extends Disposable { } } + private wrapReference(key: string, reference: IReference, debugOwner?: string): IReference { + const ownerId = ++this._referenceOwnerIds; + let ownerEntries = this._referenceOwners.get(key); + if (!ownerEntries) { + ownerEntries = new Map(); + this._referenceOwners.set(key, ownerEntries); + } + ownerEntries.set(ownerId, debugOwner ?? 'unspecified'); + + let isDisposed = false; + return { + object: reference.object, + dispose: () => { + if (isDisposed) { + return; + } + + isDisposed = true; + const owners = this._referenceOwners.get(key); + owners?.delete(ownerId); + if (owners?.size === 0) { + this._referenceOwners.delete(key); + } + reference.dispose(); + } + }; + } + /** * For test use only */ diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts index afa76442175..8c11941ad8e 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts @@ -9,7 +9,7 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js import { Event } from '../../../../../../base/common/event.js'; import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; -import { constObservable, observableValue } from '../../../../../../base/common/observable.js'; +import { constObservable, ISettableObservable, observableValue } from '../../../../../../base/common/observable.js'; import { URI } from '../../../../../../base/common/uri.js'; import { assertSnapshot } from '../../../../../../base/test/common/snapshot.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; @@ -45,7 +45,7 @@ import { ChatDebugServiceImpl } from '../../../common/chatDebugServiceImpl.js'; import { ChatRequestQueueKind, ChatSendResult, IChatFollowup, IChatModelReference, IChatService, ResponseModelState } from '../../../common/chatService/chatService.js'; import { ChatService } from '../../../common/chatService/chatServiceImpl.js'; import { ChatAgentLocation, ChatModeKind } from '../../../common/constants.js'; -import { IChatEditingService, IChatEditingSession } from '../../../common/editing/chatEditingService.js'; +import { ChatEditingSessionState, IChatEditingService, IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; import { ChatModel, IChatModel, ISerializableChatData } from '../../../common/model/chatModel.js'; import { ChatAgentService, IChatAgent, IChatAgentData, IChatAgentImplementation, IChatAgentService } from '../../../common/participants/chatAgents.js'; import { ChatSlashCommandService, IChatSlashCommandService } from '../../../common/participants/chatSlashCommands.js'; @@ -138,6 +138,7 @@ suite('ChatService', () => { let instantiationService: TestInstantiationService; let testFileService: InMemoryTestFileService; + let editingSessionEntries: ISettableObservable; let chatAgentService: IChatAgentService; const testServices: ChatService[] = []; @@ -188,12 +189,13 @@ suite('ChatService', () => { instantiationService.stub(ILifecycleService, { onWillShutdown: Event.None }); instantiationService.stub(IWorkspaceEditingService, { onDidEnterWorkspace: Event.None }); instantiationService.stub(IChatDebugService, testDisposables.add(new ChatDebugServiceImpl())); + editingSessionEntries = observableValue('editingSessionEntries', []); instantiationService.stub(IChatEditingService, new class extends mock() { override startOrContinueGlobalEditingSession(): IChatEditingSession { return { - state: constObservable('idle'), + state: constObservable(ChatEditingSessionState.Idle), requestDisablement: observableValue('requestDisablement', []), - entries: constObservable([]), + entries: editingSessionEntries, dispose: () => { } } as unknown as IChatEditingSession; } @@ -267,6 +269,46 @@ suite('ChatService', () => { assert.deepStrictEqual(retrieved2.getRequests()[0]?.message.text, 'request 2'); }); + test('reports modified edit keep-alive holders', () => { + const testService = createChatService(); + instantiationService.stub(IChatService, testService); + const rootRef = testService.startNewLocalSession(ChatAgentLocation.Chat, { debugOwner: 'ChatServiceTest#root' }); + + const modifiedEntry = new class extends mock() { + override state = constObservable(ModifiedFileEntryState.Modified); + }(); + + editingSessionEntries.set([modifiedEntry], undefined); + + assert.deepStrictEqual(testService.getChatModelReferenceDebugInfo().models.map(model => ({ + createdBy: model.createdBy, + holders: model.holders, + hasPendingEdits: model.hasPendingEdits, + referenceCount: model.referenceCount, + })), [{ + createdBy: 'ChatServiceTest#root', + holders: [ + { holder: 'ChatModel#modifiedEditsKeepAlive', count: 1 }, + { holder: 'ChatServiceTest#root', count: 1 } + ], + hasPendingEdits: true, + referenceCount: 2, + }]); + + editingSessionEntries.set([], undefined); + assert.deepStrictEqual(testService.getChatModelReferenceDebugInfo().models.map(model => ({ + holders: model.holders, + hasPendingEdits: model.hasPendingEdits, + referenceCount: model.referenceCount, + })), [{ + holders: [{ holder: 'ChatServiceTest#root', count: 1 }], + hasPendingEdits: false, + referenceCount: 1, + }]); + + rootRef.dispose(); + }); + test('addCompleteRequest', async () => { const testService = createChatService(); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts index 07e106b2c43..6dff14b794d 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts @@ -11,6 +11,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { ChatRequestQueueKind, ChatSendResult, IChatDetail, IChatModelReference, IChatProgress, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent } from '../../../common/chatService/chatService.js'; import { ChatAgentLocation } from '../../../common/constants.js'; import { IChatModel, IChatRequestModel, IExportableChatData, ISerializableChatData } from '../../../common/model/chatModel.js'; +import type { IChatModelReferenceDebugSnapshot } from '../../../common/model/chatModelStore.js'; export class MockChatService implements IChatService { private readonly _chatModels: ISettableObservable> = observableValue('chatModels', []); @@ -96,15 +97,19 @@ export class MockChatService implements IChatService { return undefined; } - loadSessionFromData(data: IExportableChatData | ISerializableChatData): IChatModelReference { + loadSessionFromData(data: IExportableChatData | ISerializableChatData, _debugOwner?: string): IChatModelReference { throw new Error('Method not implemented.'); } - acquireOrLoadSession(_resource: URI, _position: ChatAgentLocation, _token: CancellationToken): Promise { + getChatModelReferenceDebugInfo(): IChatModelReferenceDebugSnapshot { + return { totalModels: 0, totalReferences: 0, models: [] }; + } + + acquireOrLoadSession(_resource: URI, _position: ChatAgentLocation, _token: CancellationToken, _debugOwner?: string): Promise { throw new Error('Method not implemented.'); } - acquireExistingSession(_sessionResource: URI): IChatModelReference | undefined { + acquireExistingSession(_sessionResource: URI, _debugOwner?: string): IChatModelReference | undefined { return undefined; } diff --git a/src/vs/workbench/contrib/chat/test/common/model/chatModelStore.test.ts b/src/vs/workbench/contrib/chat/test/common/model/chatModelStore.test.ts index 19bebd6283a..b6482966039 100644 --- a/src/vs/workbench/contrib/chat/test/common/model/chatModelStore.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/model/chatModelStore.test.ts @@ -172,4 +172,115 @@ suite('ChatModelStore', () => { assert.strictEqual(model.isDisposed, true); }); + + test('tracks reference owners and creation owner', async () => { + const uri = URI.parse('test://session'); + const props: IStartSessionProps = { + sessionResource: uri, + location: ChatAgentLocation.Chat, + canUseTools: true + }; + + const ref1 = testObject.acquireOrCreate(props, 'ChatModelStoreTest#create'); + const ref2 = testObject.acquireExisting(uri, 'ChatModelStoreTest#existing'); + const ref3 = testObject.acquireExisting(uri, 'ChatModelStoreTest#existing'); + + assert.deepStrictEqual(testObject.getReferenceDebugSnapshot(), { + totalModels: 1, + totalReferences: 3, + models: [{ + sessionResource: uri, + title: '', + createdBy: 'ChatModelStoreTest#create', + initialLocation: ChatAgentLocation.Chat, + isImported: false, + willKeepAlive: true, + hasPendingEdits: false, + pendingDisposal: false, + referenceCount: 3, + holders: [ + { holder: 'ChatModelStoreTest#existing', count: 2 }, + { holder: 'ChatModelStoreTest#create', count: 1 } + ] + }] + }); + + ref1.dispose(); + ref2?.dispose(); + ref3?.dispose(); + willDisposePromises[0].complete(); + await testObject.waitForModelDisposals(); + }); + + test('reports pending disposal models without holders', async () => { + const uri = URI.parse('test://session'); + const props: IStartSessionProps = { + sessionResource: uri, + location: ChatAgentLocation.Chat, + canUseTools: true + }; + + const ref = testObject.acquireOrCreate(props, 'ChatModelStoreTest#create'); + ref.dispose(); + + assert.deepStrictEqual(testObject.getReferenceDebugSnapshot(), { + totalModels: 1, + totalReferences: 0, + models: [{ + sessionResource: uri, + title: '', + createdBy: 'ChatModelStoreTest#create', + initialLocation: ChatAgentLocation.Chat, + isImported: false, + willKeepAlive: true, + hasPendingEdits: false, + pendingDisposal: true, + referenceCount: 0, + holders: [] + }] + }); + + willDisposePromises[0].complete(); + await testObject.waitForModelDisposals(); + }); + + test('resurrection preserves debug tracking', async () => { + const uri = URI.parse('test://session'); + const props: IStartSessionProps = { + sessionResource: uri, + location: ChatAgentLocation.Chat, + canUseTools: true + }; + + const ref1 = testObject.acquireOrCreate(props, 'OriginalCreator'); + ref1.dispose(); + + // Model is pending disposal — re-acquire before disposal completes + const ref2 = testObject.acquireOrCreate(props, 'Rescuer'); + + // Complete the old disposal — should NOT wipe the model or tracking + willDisposePromises[0].complete(); + await testObject.waitForModelDisposals(); + + assert.deepStrictEqual(testObject.getReferenceDebugSnapshot(), { + totalModels: 1, + totalReferences: 1, + models: [{ + sessionResource: uri, + title: '', + createdBy: 'OriginalCreator', + initialLocation: ChatAgentLocation.Chat, + isImported: false, + willKeepAlive: true, + hasPendingEdits: false, + pendingDisposal: false, + referenceCount: 1, + holders: [{ holder: 'Rescuer', count: 1 }] + }] + }); + + ref2.dispose(); + willDisposePromises[1].complete(); + await testObject.waitForModelDisposals(); + }); }); From 1c1dc9f684da82d75e56bb8b4a62e428985558f9 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 31 Mar 2026 11:41:03 -0700 Subject: [PATCH 05/16] Fix "Select {0}" in workspace picker (#306829) --- .eslint-plugin-local/code-no-unexternalized-strings.ts | 6 ++++++ .../sessions/contrib/chat/browser/sessionWorkspacePicker.ts | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.eslint-plugin-local/code-no-unexternalized-strings.ts b/.eslint-plugin-local/code-no-unexternalized-strings.ts index a7065cb2a0d..c181152ec2a 100644 --- a/.eslint-plugin-local/code-no-unexternalized-strings.ts +++ b/.eslint-plugin-local/code-no-unexternalized-strings.ts @@ -62,6 +62,9 @@ export default new class NoUnexternalizedStrings implements eslint.Rule.RuleModu doubleQuotedStringLiterals.delete(keyNode); key = keyNode.value; + } else if (keyNode.type === AST_NODE_TYPES.TemplateLiteral && keyNode.expressions.length === 0 && keyNode.quasis.length === 1) { + key = keyNode.quasis[0].value.cooked ?? undefined; + } else if (keyNode.type === AST_NODE_TYPES.ObjectExpression) { for (const property of keyNode.properties) { if (property.type === AST_NODE_TYPES.Property && !property.computed) { @@ -70,6 +73,9 @@ export default new class NoUnexternalizedStrings implements eslint.Rule.RuleModu doubleQuotedStringLiterals.delete(property.value); key = property.value.value; break; + } else if (property.value.type === AST_NODE_TYPES.TemplateLiteral && property.value.expressions.length === 0 && property.value.quasis.length === 1) { + key = property.value.quasis[0].value.cooked ?? undefined; + break; } } } diff --git a/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts b/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts index f610613f237..8c5fc0d81ba 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts @@ -367,7 +367,7 @@ export class WorkspacePicker extends Disposable { const remoteStatus = remoteProvider?.connectionStatus?.get(); const actionItems = actions.map(({ action, index }, ci) => toAction({ id: `workspacePicker.browse.${index}`, - label: localize(`workspacePicker.browse`, "{0}...", action.label), + label: localize(`workspacePicker.browseAction`, "{0}...", action.label), tooltip: ci === 0 ? provider.label : '', enabled: remoteStatus !== RemoteAgentHostConnectionStatus.Disconnected && remoteStatus !== RemoteAgentHostConnectionStatus.Connecting, run: () => this._executeBrowseAction(index), @@ -382,7 +382,7 @@ export class WorkspacePicker extends Disposable { items.push({ kind: ActionListItemKind.Action, - label: localize('workspacePicker.browse', "Select..."), + label: localize('workspacePicker.browseSelect', "Select..."), group: { title: '', icon: Codicon.folderOpened }, item: {}, submenuActions, @@ -392,7 +392,7 @@ export class WorkspacePicker extends Disposable { const action = allBrowseActions[i]; items.push({ kind: ActionListItemKind.Action, - label: localize(`workspacePicker.browse`, "Select {0}...", action.label), + label: localize(`workspacePicker.browseSelectAction`, "Select {0}...", action.label), group: { title: '', icon: action.icon }, item: { browseActionIndex: i }, }); From 213b27d7c3ed1ee61367b9ce8356f900e4b23ace Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:41:40 -0700 Subject: [PATCH 06/16] make sure to cancel current request on restore checkpoints (#306901) --- .../contrib/chat/browser/chatEditing/chatEditingActions.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 709c7664063..c0d6a47f9fe 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -495,6 +495,8 @@ async function restoreSnapshotWithConfirmationByRequestId(accessor: ServicesAcce await configurationService.updateValue('chat.editing.confirmEditRequestRemoval', false); } + await chatService.cancelCurrentRequestForSession(sessionResource, 'restoreCheckpoint'); + // Restore the snapshot to what it was before the request(s) that we deleted const snapshotRequestId = chatRequests[itemIndex].id; await session.restoreSnapshot(snapshotRequestId, undefined); From 78f4c2471eb4e2124609e85771899fafae13b36f Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Tue, 31 Mar 2026 19:49:07 +0100 Subject: [PATCH 07/16] Sessions: Refactor AI Customization Management styles (#306873) * Refactor padding and line-height in AI Customization Management styles for improved layout Co-authored-by: Copilot * Update src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: mrleemurray Co-authored-by: Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Michael Lively <12552271+Yoyokrazy@users.noreply.github.com> --- .../aiCustomization/media/aiCustomizationManagement.css | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css index 4c6fd79b107..8e74be7abe1 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -159,7 +159,7 @@ .ai-customization-management-editor .content-inner { height: 100%; - padding: 8px 12px; + padding: 10px; box-sizing: border-box; } @@ -552,7 +552,6 @@ .ai-customization-list-widget .section-footer .section-footer-description { font-size: 13px; color: var(--vscode-descriptionForeground); - line-height: 1.5; margin: 0 0 8px 0; } @@ -960,8 +959,7 @@ display: flex; align-items: center; gap: 12px; - padding: 8px 4px 12px 4px; - border-bottom: 1px solid var(--vscode-widget-border); + padding: 4px 0; flex-shrink: 0; } From dc93e83a60e4ee10f3637b8e7d538f3e3cf2a6c8 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:19:41 -0700 Subject: [PATCH 08/16] show 'ran subagent' when finishing subagents without descriptions (#306903) --- .../chatSubagentContentPart.ts | 35 +++++++++---- .../chatSubagentContentPart.test.ts | 49 +++++++++++++++++++ 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts index ae1b4f352f0..fc2b58ab793 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts @@ -91,6 +91,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen // Model name used by this subagent for hover tooltip private modelName: string | undefined; + private _isDefaultDescription: boolean; private readonly _hoverDisposable = this._register(new MutableDisposable()); // Confirmation auto-expand tracking @@ -114,18 +115,20 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen /** * Extracts subagent info (description, agentName, prompt) from a tool invocation. */ - private static extractSubagentInfo(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): { description: string; agentName: string | undefined; prompt: string | undefined; modelName: string | undefined } { + private static extractSubagentInfo(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): { description: string; isDefaultDescription: boolean; agentName: string | undefined; prompt: string | undefined; modelName: string | undefined } { const defaultDescription = localize('chat.subagent.defaultDescription', 'Running subagent'); // Only parent subagent tools contain the full subagent info if (!ChatSubagentContentPart.isParentSubagentTool(toolInvocation)) { - return { description: defaultDescription, agentName: undefined, prompt: undefined, modelName: undefined }; + return { description: defaultDescription, isDefaultDescription: true, agentName: undefined, prompt: undefined, modelName: undefined }; } // Check toolSpecificData first (works for both live and serialized) if (toolInvocation.toolSpecificData?.kind === 'subagent') { + const hasDescription = !!toolInvocation.toolSpecificData.description; return { description: toolInvocation.toolSpecificData.description ?? defaultDescription, + isDefaultDescription: !hasDescription, agentName: toolInvocation.toolSpecificData.agentName, prompt: toolInvocation.toolSpecificData.prompt, modelName: toolInvocation.toolSpecificData.modelName, @@ -138,15 +141,17 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen const params = state.type !== IChatToolInvocation.StateKind.Streaming ? state.parameters as IRunSubagentToolInputParams | undefined : undefined; + const hasDescription = !!params?.description; return { description: params?.description ?? defaultDescription, + isDefaultDescription: !hasDescription, agentName: params?.agentName, prompt: params?.prompt, modelName: undefined, }; } - return { description: defaultDescription, agentName: undefined, prompt: undefined, modelName: undefined }; + return { description: defaultDescription, isDefaultDescription: true, agentName: undefined, prompt: undefined, modelName: undefined }; } constructor( @@ -164,7 +169,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen @IConfigurationService configurationService: IConfigurationService, ) { // Extract description, agentName, and prompt from toolInvocation - const { description, agentName, prompt, modelName } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation); + const { description, isDefaultDescription, agentName, prompt, modelName } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation); // Build title: "AgentName: description" or "Subagent: description" const prefix = agentName || localize('chat.subagent.prefix', 'Subagent'); @@ -172,6 +177,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen super(initialTitle, context, undefined, hoverService, configurationService); this.description = description; + this._isDefaultDescription = isDefaultDescription; this.agentName = agentName; this.prompt = prompt; this.modelName = modelName; @@ -354,6 +360,10 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen if (this._collapseButton) { this._collapseButton.icon = Codicon.check; } + + if (this._isDefaultDescription) { + this.description = localize('chat.subagent.completedDefaultDescription', 'Ran subagent'); + } this.finalizeTitle(); // Collapse when done this.setExpanded(false); @@ -516,10 +526,16 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen this.renderResultText(textParts.join('\n')); } - // Update model name from toolSpecificData (set during invoke()) - if (toolInvocation.toolSpecificData?.kind === 'subagent' && toolInvocation.toolSpecificData.modelName) { - this.modelName = toolInvocation.toolSpecificData.modelName; - this.updateHover(); + // Update description and model name from toolSpecificData (set during invoke()) + if (toolInvocation.toolSpecificData?.kind === 'subagent') { + if (toolInvocation.toolSpecificData.description) { + this.description = toolInvocation.toolSpecificData.description; + this._isDefaultDescription = false; + } + if (toolInvocation.toolSpecificData.modelName) { + this.modelName = toolInvocation.toolSpecificData.modelName; + this.updateHover(); + } } // Mark as inactive when the tool completes @@ -527,8 +543,9 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen } else if (wasStreaming && state.type !== IChatToolInvocation.StateKind.Streaming) { wasStreaming = false; // Update things that change when tool is done streaming - const { description, agentName, prompt, modelName } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation); + const { description, isDefaultDescription, agentName, prompt, modelName } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation); this.description = description; + this._isDefaultDescription = isDefaultDescription; this.agentName = agentName; this.prompt = prompt; if (modelName) { diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts index c4ac5b4df57..991baa6f303 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts @@ -411,6 +411,55 @@ suite('ChatSubagentContentPart', () => { assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should be collapsed after markAsInactive'); }); + test('markAsInactive should change default description to past tense', () => { + const toolInvocation = createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + // no description — should use the default "Running subagent" + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Before marking inactive, title should show "Running subagent" + const button = getCollapseButton(part); + assert.ok(button, 'Should have collapse button'); + const labelBefore = getCollapseButtonLabel(button); + const textBefore = labelBefore?.textContent ?? button.textContent ?? ''; + assert.ok(textBefore.includes('Running subagent'), 'Title should show "Running subagent" before completion'); + + part.markAsInactive(); + + // After marking inactive, title should show "Ran subagent" + const labelAfter = getCollapseButtonLabel(button); + const textAfter = labelAfter?.textContent ?? button.textContent ?? ''; + assert.ok(textAfter.includes('Ran subagent'), 'Title should show "Ran subagent" after completion'); + assert.ok(!textAfter.includes('Running subagent'), 'Title should no longer show "Running subagent"'); + }); + + test('markAsInactive should keep custom description unchanged', () => { + const toolInvocation = createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Searching the codebase', + agentName: 'Explorer', + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + part.markAsInactive(); + + // After marking inactive, title should still show the custom description + const button = getCollapseButton(part); + assert.ok(button, 'Should have collapse button'); + const label = getCollapseButtonLabel(button); + const text = label?.textContent ?? button.textContent ?? ''; + assert.ok(text.includes('Searching the codebase'), 'Title should keep custom description after completion'); + }); + test('finalizeTitle should update button icon to check', () => { // Enable the showCheckmarks setting so the check icon is visible const configService = instantiationService.get(IConfigurationService) as TestConfigurationService; From ef8b671c0620709d1753a9b0b6372d0c5fa6cec4 Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Tue, 31 Mar 2026 20:28:47 +0100 Subject: [PATCH 09/16] Sessions: Update agent feedback widget styles for improved clarity and consistency (#306858) * style: update agent feedback widget styles for code review and adjust border radius * style: adjust agent feedback widget colors for better visibility and consistency * style: update agent feedback widget suggestion styles for improved layout and visual clarity * style: enhance suggestion border for code review items with color-mix for better visibility * style: update agent feedback widget suggestion header for improved clarity and consistency Co-authored-by: Copilot * style: refactor suggestion rendering and improve CSS for consistency and clarity Co-authored-by: Copilot * Update src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: mrleemurray Co-authored-by: Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Michael Lively <12552271+Yoyokrazy@users.noreply.github.com> --- .../agentFeedbackEditorWidgetContribution.ts | 12 +++---- .../media/agentFeedbackEditorWidget.css | 32 +++++++------------ 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts index 7c4c463f391..08d73ad6532 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts @@ -286,19 +286,17 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid private _renderSuggestion(comment: ISessionEditorComment): HTMLElement { const suggestionNode = $('div.agent-feedback-widget-suggestion'); - const title = $('div.agent-feedback-widget-suggestion-title'); - title.textContent = nls.localize('suggestedChange', "Suggested Change"); - suggestionNode.appendChild(title); for (const edit of comment.suggestion?.edits ?? []) { const editNode = $('div.agent-feedback-widget-suggestion-edit'); - const rangeLabel = $('div.agent-feedback-widget-suggestion-range'); + + const header = $('div.agent-feedback-widget-suggestion-header'); if (edit.range.startLineNumber === edit.range.endLineNumber) { - rangeLabel.textContent = nls.localize('suggestionLineNumber', "Line {0}", edit.range.startLineNumber); + header.textContent = nls.localize('suggestedChangeLine', "Suggested Change \u2022 Line {0}", edit.range.startLineNumber); } else { - rangeLabel.textContent = nls.localize('suggestionLineRange', "Lines {0}-{1}", edit.range.startLineNumber, edit.range.endLineNumber); + header.textContent = nls.localize('suggestedChangeLines', "Suggested Change \u2022 Lines {0}-{1}", edit.range.startLineNumber, edit.range.endLineNumber); } - editNode.appendChild(rangeLabel); + editNode.appendChild(header); const newText = $('pre.agent-feedback-widget-suggestion-text'); newText.textContent = edit.newText; diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css index 22db201e709..f9e5985f954 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css @@ -85,6 +85,7 @@ /* Title */ .agent-feedback-widget-title { font-weight: 500; + line-height: 12px; color: var(--vscode-foreground); white-space: nowrap; overflow: hidden; @@ -104,6 +105,7 @@ justify-content: center; width: 22px; height: 22px; + min-width: 22px; border-radius: var(--vscode-cornerRadius-medium); cursor: pointer; color: var(--vscode-foreground); @@ -154,14 +156,6 @@ color: var(--vscode-list-activeSelectionForeground); } -.agent-feedback-widget-item-codeReview { - box-shadow: inset 2px 0 0 var(--vscode-editorWarning-foreground); -} - -.agent-feedback-widget-item-prReview { - box-shadow: inset 2px 0 0 var(--vscode-editorInfo-foreground); -} - .agent-feedback-widget-item-header { display: flex; align-items: center; @@ -195,7 +189,7 @@ display: inline-flex; align-items: center; padding: 1px 6px; - border-radius: 999px; + border-radius: 4px; font-size: 10px; font-weight: 600; letter-spacing: 0.2px; @@ -204,12 +198,12 @@ } .agent-feedback-widget-item-codeReview .agent-feedback-widget-item-type { - background: color-mix(in srgb, var(--vscode-editorWarning-foreground) 22%, transparent); + background: color-mix(in srgb, var(--vscode-editorWarning-foreground) 22%, var(--vscode-editorWidget-background)); color: var(--vscode-editorWarning-foreground); } .agent-feedback-widget-item-prReview .agent-feedback-widget-item-type { - background: color-mix(in srgb, var(--vscode-editorInfo-foreground) 22%, transparent); + background: color-mix(in srgb, var(--vscode-editorInfo-foreground) 22%, var(--vscode-editorWidget-background)); color: var(--vscode-editorInfo-foreground); } @@ -244,21 +238,19 @@ display: flex; flex-direction: column; gap: 6px; - padding: 8px; - border-radius: 6px; - background: color-mix(in srgb, var(--vscode-editorWidget-border, var(--vscode-widget-border)) 12%, transparent); + margin-top: 8px; + padding: 0px 8px 4px 12px; } .agent-feedback-widget-item-codeReview .agent-feedback-widget-suggestion { - background: color-mix(in srgb, var(--vscode-editorWarning-foreground) 10%, transparent); + border-left: 1px solid color-mix(in srgb, var(--vscode-editorWarning-foreground) 50%, transparent); } .agent-feedback-widget-item-prReview .agent-feedback-widget-suggestion { - background: color-mix(in srgb, var(--vscode-editorInfo-foreground) 10%, transparent); + border-left: 1px solid color-mix(in srgb, var(--vscode-editorInfo-foreground) 50%, transparent); } -.agent-feedback-widget-suggestion-title, -.agent-feedback-widget-suggestion-range { +.agent-feedback-widget-suggestion-header { font-size: 10px; font-weight: 600; text-transform: uppercase; @@ -278,10 +270,10 @@ border-radius: 4px; overflow-x: auto; white-space: pre-wrap; - font-family: monospace; + font-family: var(--monaco-monospace-font); font-size: 11px; line-height: 1.45; - background: color-mix(in srgb, var(--vscode-editor-background) 65%, transparent); + background: var(--vscode-editorWidget-background); } /* Gutter decoration for range indicator on hover */ From c27cf4f84a6e7c0c571de52ca5d7a0ae706f08d9 Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Tue, 31 Mar 2026 20:34:19 +0100 Subject: [PATCH 10/16] Sessions: Adjust header height and refine button styles (#306902) Adjust header height and padding in CIStatusWidget; refine button styles in changes view Co-authored-by: mrleemurray --- src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts | 2 +- .../sessions/contrib/changes/browser/media/changesView.css | 2 +- .../contrib/changes/browser/media/ciStatusWidget.css | 5 ++--- .../contrib/sessions/browser/media/sessionsViewPane.css | 4 ++++ 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts b/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts index d9f7aecdff7..fbbb10f9052 100644 --- a/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts +++ b/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts @@ -148,7 +148,7 @@ class CICheckListRenderer implements IListRenderer Date: Tue, 31 Mar 2026 15:35:13 -0400 Subject: [PATCH 11/16] Reduce request to entitlement endpoints (#306914) --- src/vs/base/common/defaultAccount.ts | 1 + .../accounts/browser/defaultAccount.ts | 70 +++++++++++-------- 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/src/vs/base/common/defaultAccount.ts b/src/vs/base/common/defaultAccount.ts index 3c53a698622..40b2d70a318 100644 --- a/src/vs/base/common/defaultAccount.ts +++ b/src/vs/base/common/defaultAccount.ts @@ -25,6 +25,7 @@ export interface ILegacyQuotaSnapshotData { export interface IEntitlementsData extends ILegacyQuotaSnapshotData { readonly access_type_sku: string; + readonly chat_enabled: boolean; readonly assigned_date: string; readonly can_signup_for_limited: boolean; readonly copilot_plan: string; diff --git a/src/vs/workbench/services/accounts/browser/defaultAccount.ts b/src/vs/workbench/services/accounts/browser/defaultAccount.ts index 7262e899d67..766c5160e6d 100644 --- a/src/vs/workbench/services/accounts/browser/defaultAccount.ts +++ b/src/vs/workbench/services/accounts/browser/defaultAccount.ts @@ -202,6 +202,7 @@ export class DefaultAccountService extends Disposable implements IDefaultAccount interface IAccountPolicyData { readonly accountId: string; readonly policyData: IPolicyData; + readonly entitlementsFetchedAt?: number; readonly tokenEntitlementsFetchedAt?: number; readonly mcpRegistryDataFetchedAt?: number; } @@ -379,7 +380,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid this._register(this.hostService.onDidChangeFocus(focused => { if (focused) { - this.refetchDefaultAccount(true); + this.refetchDefaultAccount(); } })); } @@ -396,7 +397,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid return this.defaultAccount; } - private async refetchDefaultAccount(useExistingEntitlements?: boolean): Promise { + private async refetchDefaultAccount(): Promise { if (this.accountDataPollScheduler.isScheduled()) { this.accountDataPollScheduler.cancel(); } @@ -406,16 +407,16 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid return; } this.logService.debug('[DefaultAccount] Refetching default account'); - await this.updateDefaultAccount(useExistingEntitlements); + await this.updateDefaultAccount(); } - private async updateDefaultAccount(useExistingEntitlements?: boolean): Promise { - await this.updateThrottler.trigger(() => this.doUpdateDefaultAccount(useExistingEntitlements)); + private async updateDefaultAccount(): Promise { + await this.updateThrottler.trigger(() => this.doUpdateDefaultAccount()); } - private async doUpdateDefaultAccount(useExistingEntitlements: boolean = false): Promise { + private async doUpdateDefaultAccount(): Promise { try { - const defaultAccount = await this.fetchDefaultAccount(useExistingEntitlements); + const defaultAccount = await this.fetchDefaultAccount(); this.setDefaultAccount(defaultAccount); this.scheduleAccountDataPoll(); } catch (error) { @@ -423,7 +424,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid } } - private async fetchDefaultAccount(useExistingEntitlements: boolean): Promise { + private async fetchDefaultAccount(): Promise { const defaultAccountProvider = this.getDefaultAccountAuthenticationProvider(); this.logService.debug('[DefaultAccount] Default account provider ID:', defaultAccountProvider.id); @@ -433,7 +434,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid return null; } - return await this.getDefaultAccountForAuthenticationProvider(defaultAccountProvider, useExistingEntitlements); + return await this.getDefaultAccountForAuthenticationProvider(defaultAccountProvider); } private setDefaultAccount(account: IDefaultAccountData | null): void { @@ -510,7 +511,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid return result; } - private async getDefaultAccountForAuthenticationProvider(authenticationProvider: IDefaultAccountAuthenticationProvider, useExistingEntitlements: boolean): Promise { + private async getDefaultAccountForAuthenticationProvider(authenticationProvider: IDefaultAccountAuthenticationProvider): Promise { try { this.logService.debug('[DefaultAccount] Getting Default Account from authenticated sessions for provider:', authenticationProvider.id); const sessions = await this.findMatchingProviderSession(authenticationProvider.id, this.defaultAccountConfig.authenticationProvider.scopes); @@ -520,29 +521,27 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid return null; } - return this.getDefaultAccountFromAuthenticatedSessions(authenticationProvider, sessions, useExistingEntitlements); + return this.getDefaultAccountFromAuthenticatedSessions(authenticationProvider, sessions); } catch (error) { this.logService.error('[DefaultAccount] Failed to get default account for provider:', authenticationProvider.id, getErrorMessage(error)); return null; } } - private async getDefaultAccountFromAuthenticatedSessions(authenticationProvider: IDefaultAccountAuthenticationProvider, sessions: AuthenticationSession[], useExistingEntitlements: boolean): Promise { + private async getDefaultAccountFromAuthenticatedSessions(authenticationProvider: IDefaultAccountAuthenticationProvider, sessions: AuthenticationSession[]): Promise { try { const accountId = sessions[0].account.id; - const existingEntitlementsData = this._defaultAccount?.accountId === accountId ? this._defaultAccount?.defaultAccount.entitlementsData : undefined; const accountPolicyData = this._policyData?.accountId === accountId ? this._policyData : undefined; - const [entitlementsData, tokenEntitlementsResult] = await Promise.all([ - useExistingEntitlements && existingEntitlementsData ? existingEntitlementsData : this.getEntitlements(sessions), - this.getTokenEntitlements(sessions, accountPolicyData), - ]); + const entitlementsResult = await this.getEntitlements(sessions, accountPolicyData); + const entitlementsData = entitlementsResult?.data; + const entitlementsFetchedAt = entitlementsResult?.fetchedAt; + const tokenEntitlementsResult = entitlementsData?.chat_enabled ? await this.getTokenEntitlements(sessions, accountPolicyData) : undefined; - let tokenEntitlementsFetchedAt: number | undefined; + const tokenEntitlementsFetchedAt: number | undefined = tokenEntitlementsResult?.fetchedAt; let mcpRegistryDataFetchedAt: number | undefined; let policyData: Mutable | undefined = accountPolicyData?.policyData ? { ...accountPolicyData.policyData } : undefined; - if (tokenEntitlementsResult) { - tokenEntitlementsFetchedAt = tokenEntitlementsResult.fetchedAt; + if (tokenEntitlementsResult?.data) { const tokenEntitlementsData = tokenEntitlementsResult.data; policyData = policyData ?? {}; policyData.chat_agent_enabled = tokenEntitlementsData.policyData.chat_agent_enabled; @@ -567,11 +566,14 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid entitlementsData, }; this.logService.debug('[DefaultAccount] Successfully created default account for provider:', authenticationProvider.id); + const accountPolicyResult: IAccountPolicyData | null = policyData || entitlementsFetchedAt + ? { accountId, policyData: policyData ?? {}, entitlementsFetchedAt, tokenEntitlementsFetchedAt, mcpRegistryDataFetchedAt } + : null; return { defaultAccount, accountId, - policyData: policyData ? { accountId, policyData, tokenEntitlementsFetchedAt, mcpRegistryDataFetchedAt } : null, - copilotTokenInfo: tokenEntitlementsResult?.data.copilotTokenInfo ?? null, + policyData: accountPolicyResult, + copilotTokenInfo: tokenEntitlementsResult?.data?.copilotTokenInfo ?? null, }; } catch (error) { this.logService.error('[DefaultAccount] Failed to create default account for provider:', authenticationProvider.id, getErrorMessage(error)); @@ -627,13 +629,13 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid return expectedScopes.every(scope => scopes.includes(scope)); } - private async getTokenEntitlements(sessions: AuthenticationSession[], accountPolicyData: IAccountPolicyData | undefined): Promise<{ data: { policyData: Partial; copilotTokenInfo: ICopilotTokenInfo }; fetchedAt: number } | undefined> { + private async getTokenEntitlements(sessions: AuthenticationSession[], accountPolicyData: IAccountPolicyData | undefined): Promise<{ data: { policyData: Partial; copilotTokenInfo: ICopilotTokenInfo } | undefined; fetchedAt: number }> { if (accountPolicyData?.tokenEntitlementsFetchedAt && !this.isDataStale(accountPolicyData.tokenEntitlementsFetchedAt)) { this.logService.debug('[DefaultAccount] Using last fetched token entitlements data'); return { data: { policyData: accountPolicyData.policyData, copilotTokenInfo: this._copilotTokenInfo ?? {} }, fetchedAt: accountPolicyData.tokenEntitlementsFetchedAt }; } const data = await this.requestTokenEntitlements(sessions); - return data ? { data, fetchedAt: Date.now() } : undefined; + return { data, fetchedAt: Date.now() }; } private async requestTokenEntitlements(sessions: AuthenticationSession[]): Promise<{ policyData: Partial; copilotTokenInfo: ICopilotTokenInfo } | undefined> { @@ -680,37 +682,45 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid return undefined; } - private async getEntitlements(sessions: AuthenticationSession[]): Promise { + private async getEntitlements(sessions: AuthenticationSession[], accountPolicyData: IAccountPolicyData | undefined): Promise<{ data: IEntitlementsData | undefined | null; fetchedAt: number | undefined }> { + const accountId = sessions[0].account.id; + const existingData = this._defaultAccount?.accountId === accountId ? this._defaultAccount?.defaultAccount.entitlementsData : undefined; + if (existingData && accountPolicyData?.entitlementsFetchedAt && !this.isDataStale(accountPolicyData.entitlementsFetchedAt)) { + this.logService.debug('[DefaultAccount] Using last fetched entitlements data'); + return { data: existingData, fetchedAt: accountPolicyData.entitlementsFetchedAt }; + } + const entitlementUrl = this.getEntitlementUrl(); if (!entitlementUrl) { this.logService.debug('[DefaultAccount] No chat entitlements URL found'); - return undefined; + return { data: undefined, fetchedAt: undefined }; } this.logService.debug('[DefaultAccount] Fetching entitlements from:', entitlementUrl); const response = await this.request(entitlementUrl, 'GET', undefined, sessions, CancellationToken.None, 'defaultAccount.entitlements'); if (!response) { - return undefined; + return { data: undefined, fetchedAt: Date.now() }; } if (response.res.statusCode && response.res.statusCode !== 200) { this.logService.trace(`[DefaultAccount] unexpected status code ${response.res.statusCode} while fetching entitlements`); - return ( + const data = ( response.res.statusCode === 401 || // oauth token being unavailable (expired/revoked) response.res.statusCode === 404 // missing scopes/permissions, service pretends the endpoint doesn't exist ) ? null : undefined; + return { data, fetchedAt: Date.now() }; } try { const data = await asJson(response); if (data) { - return data; + return { data, fetchedAt: Date.now() }; } this.logService.error('[DefaultAccount] Failed to fetch entitlements', 'No data returned'); } catch (error) { this.logService.error('[DefaultAccount] Failed to fetch entitlements', getErrorMessage(error)); } - return undefined; + return { data: undefined, fetchedAt: Date.now() }; } private async getMcpRegistryProvider(sessions: AuthenticationSession[], accountPolicyData: IAccountPolicyData | undefined): Promise<{ data: IMcpRegistryProvider | null; fetchedAt: number } | undefined> { From 17faef0d3c25db663ff888a7aac0093444e6c727 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 31 Mar 2026 22:02:26 +0200 Subject: [PATCH 12/16] Sessions: Update naming (fix #306461) (#306922) * Sessions: Update naming (fix #306461) * . --- .agents/skills/launch/SKILL.md | 10 +++---- .github/agents/sessions.md | 8 +++--- .github/instructions/sessions.instructions.md | 6 ++-- .github/skills/agent-sessions-layout/SKILL.md | 10 +++---- .github/skills/sessions/SKILL.md | 28 +++++++++---------- .vscode/launch.json | 2 +- .vscode/tasks.json | 2 +- src/vs/code/electron-main/app.ts | 24 ++++++++-------- src/vs/code/node/cli.ts | 6 ++-- .../test/node/testRemoteAgentHost.sh | 2 +- src/vs/platform/environment/common/argv.ts | 2 +- src/vs/platform/environment/node/argv.ts | 2 +- .../launch/electron-main/launchMainService.ts | 6 ++-- src/vs/platform/native/common/native.ts | 2 +- .../electron-main/nativeHostMainService.ts | 4 +-- .../platform/windows/electron-main/windows.ts | 2 +- .../electron-main/windowsMainService.ts | 6 ++-- src/vs/sessions/AI_CUSTOMIZATIONS.md | 4 +-- src/vs/sessions/LAYOUT.md | 2 +- src/vs/sessions/README.md | 2 +- src/vs/sessions/SESSIONS_PROVIDER.md | 2 +- src/vs/sessions/browser/workbench.ts | 2 +- src/vs/sessions/common/categories.ts | 2 +- .../browser/account.contribution.ts | 4 +-- .../test/browser/accountWidget.fixture.ts | 2 +- .../contrib/chat/browser/promptsService.ts | 2 +- .../chat/browser/runScriptCustomTaskWidget.ts | 2 +- .../chat/common/builtinPromptsStorage.ts | 2 +- .../welcome/browser/welcome.contribution.ts | 10 +++---- .../sessions/copilot-customizations-spec.md | 2 +- src/vs/workbench/common/contextkeys.ts | 2 +- src/vs/workbench/common/views.ts | 4 +-- .../contrib/chat/browser/chatTipCatalog.ts | 10 +++---- .../agentSessions/agentSessionsActions.ts | 8 +++--- .../electron-browser/chat.contribution.ts | 4 +-- .../electron-browser/workbenchTestServices.ts | 2 +- 36 files changed, 95 insertions(+), 95 deletions(-) diff --git a/.agents/skills/launch/SKILL.md b/.agents/skills/launch/SKILL.md index 20f207907ac..2b564d812bb 100644 --- a/.agents/skills/launch/SKILL.md +++ b/.agents/skills/launch/SKILL.md @@ -111,19 +111,19 @@ agent-browser snapshot -i - Code OSS uses the default user data directory. Unlike VS Code Insiders, you don't typically need `--user-data-dir` since there's usually only one Code OSS instance running. - If you see "Sent env to running instance. Terminating..." it means Code OSS is already running and forwarded your args to the existing instance. Quit Code OSS and relaunch with the flag, or use `--user-data-dir=/tmp/code-oss-debug` to force a new instance. -## Launching the Sessions App (Agent Sessions Window) +## Launching the Agents App (Agents Window) -The Sessions app is a separate workbench mode launched with the `--sessions` flag. It uses a dedicated user data directory to avoid conflicts with the main Code OSS instance. +The Agents app is a separate workbench mode launched with the `--agents` flag. It uses a dedicated user data directory to avoid conflicts with the main Code OSS instance. ```bash cd # the root of your VS Code checkout -./scripts/code.sh --sessions --remote-debugging-port=9224 +./scripts/code.sh --agents --remote-debugging-port=9224 ``` Wait for the window to fully initialize, then connect: ```bash -# Wait for Sessions app to start, retry until connected +# Wait for Agents app to start, retry until connected for i in 1 2 3 4 5; do agent-browser connect 9224 2>/dev/null && break || sleep 3; done # Verify you're connected to the right target (not about:blank) @@ -132,7 +132,7 @@ agent-browser snapshot -i ``` **Tips:** -- The `--sessions` flag launches the Agent Sessions workbench instead of the standard VS Code workbench. +- The `--agents` flag launches the Agents workbench instead of the standard VS Code workbench. - Set `VSCODE_SKIP_PRELAUNCH=1` to skip the compile step if you've already built. ## Launching VS Code Extensions for Debugging diff --git a/.github/agents/sessions.md b/.github/agents/sessions.md index 1bd1d3986c3..19bd7cb3c18 100644 --- a/.github/agents/sessions.md +++ b/.github/agents/sessions.md @@ -1,15 +1,15 @@ --- -name: Sessions Window Developer -description: Specialist in developing the Agent Sessions Window +name: Agents Window Developer +description: Specialist in developing the Agents Window --- # Role and Objective -You are a developer working on the 'sessions window'. Your goal is to make changes to the sessions window (`src/vs/sessions`), minimally editing outside of that directory. +You are a developer working on the 'agents window'. Your goal is to make changes to the agents window (`src/vs/sessions`), minimally editing outside of that directory. # Instructions 1. **Always read the `sessions` skill first.** This is your primary source of truth for the sessions architecture. - Invoke `skill: "sessions"`. 2. Focus your work on `src/vs/sessions/`. -3. Avoid making changes to core VS Code files (`src/vs/workbench/`, `src/vs/platform/`, etc.) unless absolutely necessary for the sessions window functionality. +3. Avoid making changes to core VS Code files (`src/vs/workbench/`, `src/vs/platform/`, etc.) unless absolutely necessary for the agents window functionality. diff --git a/.github/instructions/sessions.instructions.md b/.github/instructions/sessions.instructions.md index dc3f187e96c..ef9dd0066c7 100644 --- a/.github/instructions/sessions.instructions.md +++ b/.github/instructions/sessions.instructions.md @@ -1,11 +1,11 @@ --- -description: Architecture documentation for the Agent Sessions window — a sessions-first app built as a new top-level layer alongside vs/workbench. Covers layout, parts, chat widget, contributions, entry points, and development guidelines. Use when working in `src/vs/sessions` +description: Architecture documentation for the Agents window — an agents-first app built as a new top-level layer alongside vs/workbench. Covers layout, parts, chat widget, contributions, entry points, and development guidelines. Use when working in `src/vs/sessions` applyTo: src/vs/sessions/** --- -# Agent Sessions Window +# Agents Window -The Agent Sessions window is a **standalone application** built as a new top-level layer (`vs/sessions`) in the VS Code architecture. It provides a sessions-first experience optimized for agent workflows — a simplified, fixed-layout workbench where chat is the primary interaction surface and editors appear as modal overlays. +The Agents window is a **standalone application** built as a new top-level layer (`vs/sessions`) in the VS Code architecture. It provides an agents-first experience optimized for agent workflows — a simplified, fixed-layout workbench where chat is the primary interaction surface and editors appear as modal overlays. When working on files under `src/vs/sessions/`, use these skills for detailed guidance: diff --git a/.github/skills/agent-sessions-layout/SKILL.md b/.github/skills/agent-sessions-layout/SKILL.md index af4f03a3f60..c9bd30c5824 100644 --- a/.github/skills/agent-sessions-layout/SKILL.md +++ b/.github/skills/agent-sessions-layout/SKILL.md @@ -1,13 +1,13 @@ --- name: agent-sessions-layout -description: Agent Sessions workbench layout — covers the fixed layout structure, grid configuration, part visibility, editor modal, titlebar, sidebar footer, and implementation requirements. Use when implementing features or fixing issues in the Agent Sessions workbench layout. +description: Agents workbench layout — covers the fixed layout structure, grid configuration, part visibility, editor modal, titlebar, sidebar footer, and implementation requirements. Use when implementing features or fixing issues in the Agents workbench layout. --- -When working on the Agent Sessions workbench layout, always follow these guidelines: +When working on the Agents workbench layout, always follow these guidelines: ## 1. Read the Specification First -The authoritative specification for the Agent Sessions layout lives at: +The authoritative specification for the Agents layout lives at: **`src/vs/sessions/LAYOUT.md`** @@ -55,7 +55,7 @@ When proposing or implementing changes, follow these rules from the spec: |------|---------| | `sessions/LAYOUT.md` | Authoritative layout specification | | `sessions/browser/workbench.ts` | Main layout implementation (`Workbench` class) | -| `sessions/browser/menus.ts` | Agent sessions menu IDs (`Menus` export) | +| `sessions/browser/menus.ts` | Agents menu IDs (`Menus` export) | | `sessions/browser/layoutActions.ts` | Layout actions (toggle sidebar, panel, secondary sidebar) | | `sessions/browser/paneCompositePartService.ts` | `AgenticPaneCompositePartService` | | `sessions/browser/media/style.css` | Layout-specific styles | @@ -67,7 +67,7 @@ When proposing or implementing changes, follow these rules from the spec: | `sessions/browser/parts/panelPart.ts` | Panel part | | `sessions/browser/parts/projectBarPart.ts` | Project Bar part (folder entries, icon customization) | | `sessions/contrib/configuration/browser/configuration.contribution.ts` | Sets `workbench.editor.useModal` to `'all'` for modal editor overlay | -| `sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts` | Title bar widget and session picker | +| `sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts` | Title bar widget and agent picker | | `sessions/contrib/chat/browser/runScriptAction.ts` | Run script split button for titlebar | | `sessions/contrib/accountMenu/browser/account.contribution.ts` | Account widget for sidebar footer | | `sessions/electron-browser/parts/titlebarPart.ts` | Desktop (Electron) titlebar part | diff --git a/.github/skills/sessions/SKILL.md b/.github/skills/sessions/SKILL.md index e39957e5f66..87669ba6a19 100644 --- a/.github/skills/sessions/SKILL.md +++ b/.github/skills/sessions/SKILL.md @@ -1,9 +1,9 @@ --- name: sessions -description: Agent Sessions window architecture — covers the sessions-first app, layering, folder structure, chat widget, menus, contributions, entry points, and development guidelines. Use when implementing features or fixing issues in the Agent Sessions window. +description: Agents window architecture — covers the agents-first app, layering, folder structure, chat widget, menus, contributions, entry points, and development guidelines. Use when implementing features or fixing issues in the Agents window. --- -When working on the Agent Sessions window (`src/vs/sessions/`), always follow these guidelines: +When working on the Agents window (`src/vs/sessions/`), always follow these guidelines: ## 1. Read the Specification Documents First @@ -41,13 +41,13 @@ vs/sessions ← Agent Sessions window (this layer) ### 2.3 How It Differs from VS Code -| Aspect | VS Code Workbench | Agent Sessions Window | +| Aspect | VS Code Workbench | Agents Window | |--------|-------------------|----------------------| | Layout | Configurable part positions | Fixed layout, no settings customization | | Chrome | Activity bar, status bar, banner | Simplified — none of these | | Primary UX | Editor-centric | Chat-first (Chat Bar is a primary part) | | Editors | In the grid layout | Modal overlay above the workbench | -| Titlebar | Menubar, editor actions, layout controls | Session picker, run script, toggle sidebar/panel | +| Titlebar | Menubar, editor actions, layout controls | Agent picker, run script, toggle sidebar/panel | | Navigation | Activity bar with viewlets | Sidebar (views) + sidebar footer (account) | | Entry point | `vs/workbench` workbench class | `vs/sessions/browser/workbench.ts` `Workbench` class | @@ -154,7 +154,7 @@ The main editor part is hidden (`display:none`). All editors open via `MODAL_GRO ## 5. Chat Widget -The Agent Sessions chat experience is built around `AgentSessionsChatWidget` — a wrapper around the core `ChatWidget` that adds: +The Agents chat experience is built around `AgentSessionsChatWidget` — a wrapper around the core `ChatWidget` that adds: - **Deferred session creation** — the UI is interactive before any session resource exists; sessions are created on first message send - **Target configuration** — observable state tracking which agent provider (Local, Cloud) is selected @@ -172,17 +172,17 @@ Read `browser/widget/AGENTS_CHAT_WIDGET.md` for the full architecture. ## 6. Menus -The agent sessions window uses **its own menu IDs** defined in `browser/menus.ts` via the `Menus` export. **Never use shared `MenuId.*` constants** from `vs/platform/actions` for agent sessions UI — use the `Menus.*` equivalents instead. +The agents window uses **its own menu IDs** defined in `browser/menus.ts` via the `Menus` export. **Never use shared `MenuId.*` constants** from `vs/platform/actions` for agents window UI — use the `Menus.*` equivalents instead. | Menu ID | Purpose | |---------|---------| | `Menus.ChatBarTitle` | Chat bar title actions | -| `Menus.CommandCenter` | Center toolbar with session picker widget | +| `Menus.CommandCenter` | Center toolbar with agent picker widget | | `Menus.CommandCenterCenter` | Center section of command center | | `Menus.TitleBarContext` | Titlebar context menu | | `Menus.TitleBarLeftLayout` | Left layout toolbar | -| `Menus.TitleBarSessionTitle` | Session title in titlebar | -| `Menus.TitleBarSessionMenu` | Session menu in titlebar | +| `Menus.TitleBarSessionTitle` | Agent title in titlebar | +| `Menus.TitleBarSessionMenu` | Agent menu in titlebar | | `Menus.TitleBarRightLayout` | Right layout toolbar | | `Menus.PanelTitle` | Panel title bar actions | | `Menus.SidebarTitle` | Sidebar title bar actions | @@ -201,7 +201,7 @@ Defined in `common/contextkeys.ts`: | `activeChatBar` | `string` | ID of the active chat bar panel | | `chatBarFocus` | `boolean` | Whether chat bar has keyboard focus | | `chatBarVisible` | `boolean` | Whether chat bar is visible | -| `sessionsWelcomeVisible` | `boolean` | Whether the sessions welcome overlay is visible | +| `sessionsWelcomeVisible` | `boolean` | Whether the agents welcome overlay is visible | ## 8. Contributions Feature contributions live under `contrib//browser/` and are registered via imports in `sessions.desktop.main.ts` (desktop) or `sessions.common.main.ts` (browser-compatible). @@ -210,8 +210,8 @@ Feature contributions live under `contrib//browser/` and are regist | Contribution | Location | Purpose | |-------------|----------|---------| -| **Sessions View** | `contrib/sessions/browser/` | Sessions list in sidebar, session picker, active session service | -| **Title Bar Widget** | `contrib/sessions/browser/sessionsTitleBarWidget.ts` | Session picker in titlebar center | +| **Sessions View** | `contrib/sessions/browser/` | Agents list in sidebar, agent picker, active session service | +| **Title Bar Widget** | `contrib/sessions/browser/sessionsTitleBarWidget.ts` | Agent picker in titlebar center | | **Account Widget** | `contrib/accountMenu/browser/` | Account button in sidebar footer | | **Chat Actions** | `contrib/chat/browser/` | Chat actions (run script, branch, prompts, customizations debug log) | | **Changes View** | `contrib/changesView/browser/` | File changes in auxiliary bar | @@ -229,7 +229,7 @@ Feature contributions live under `contrib//browser/` and are regist ### 8.2 Service Overrides -The agent sessions window registers its own implementations for: +The agents window registers its own implementations for: - `IPaneCompositePartService` → `AgenticPaneCompositePartService` (creates agent-specific parts) - `IPromptsService` → `AgenticPromptsService` (scopes prompt discovery to active session worktree) @@ -241,7 +241,7 @@ Service overrides also live under `services/`: ### 8.3 `WindowVisibility.Sessions` -Views and contributions that should only appear in the agent sessions window (not in regular VS Code) use `WindowVisibility.Sessions` in their registration. +Views and contributions that should only appear in the agents window (not in regular VS Code) use `WindowVisibility.Sessions` in their registration. ## 9. Entry Points diff --git a/.vscode/launch.json b/.vscode/launch.json index 24c1abde456..9ad9ef5e1c4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -316,7 +316,7 @@ // for general runtime freezes: https://github.com/microsoft/vscode/issues/127861#issuecomment-904144910 "--disable-features=CalculateNativeWinOcclusion", "--disable-extension=vscode.vscode-api-tests", - "--sessions" + "--agents" ], "userDataDir": "${userHome}/.vscode-oss-sessions-dev", "webRoot": "${workspaceFolder}", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e0b2a048d9a..2748b2d1216 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -235,7 +235,7 @@ "command": ".\\scripts\\code.bat" }, "args": [ - "--sessions", + "--agents", "--user-data-dir=${userHome}/.vscode-oss-sessions-dev", "--extensions-dir=${userHome}/.vscode-oss-sessions-dev/extensions" ], diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index d0f85d850bd..7ea93f3824d 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -412,8 +412,8 @@ export class CodeApplication extends Disposable { // Mac only event: open new window when we get activated if (!hasVisibleWindows) { - if ((process as INodeProcess).isEmbeddedApp || (this.environmentMainService.args['sessions'] && this.productService.quality !== 'stable')) { - await this.windowsMainService?.openSessionsWindow({ context: OpenContext.DOCK }); + if ((process as INodeProcess).isEmbeddedApp || (this.environmentMainService.args['agents'] && this.productService.quality !== 'stable')) { + await this.windowsMainService?.openAgentsWindow({ context: OpenContext.DOCK }); } else { await this.windowsMainService?.openEmptyWindow({ context: OpenContext.DOCK }); } @@ -753,9 +753,9 @@ export class CodeApplication extends Disposable { const windowOpenable = this.getWindowOpenableFromProtocolUrl(protocolUrl.uri); if (windowOpenable) { - // Sessions app: skip all window openables (file/folder/workspace) + // Agents app: skip all window openables (file/folder/workspace) if ((process as INodeProcess).isEmbeddedApp) { - this.logService.trace('app#resolveInitialProtocolUrls() sessions app skipping window openable:', protocolUrl.uri.toString(true)); + this.logService.trace('app#resolveInitialProtocolUrls() agents app skipping window openable:', protocolUrl.uri.toString(true)); continue; } @@ -904,19 +904,19 @@ export class CodeApplication extends Disposable { private async handleProtocolUrl(windowsMainService: IWindowsMainService, dialogMainService: IDialogMainService, urlService: IURLService, uri: URI, options?: IOpenURLOptions): Promise { this.logService.trace('app#handleProtocolUrl():', uri.toString(true), options); - // Sessions app: ensure the sessions window is open, then let other handlers process the URL. + // Agents app: ensure the agents window is open, then let other handlers process the URL. if ((process as INodeProcess).isEmbeddedApp) { - this.logService.trace('app#handleProtocolUrl() sessions app handling protocol URL:', uri.toString(true)); + this.logService.trace('app#handleProtocolUrl() agents app handling protocol URL:', uri.toString(true)); // Skip window openables (file/folder/workspace) for security const windowOpenable = this.getWindowOpenableFromProtocolUrl(uri); if (windowOpenable) { - this.logService.trace('app#handleProtocolUrl() sessions app skipping window openable:', uri.toString(true)); + this.logService.trace('app#handleProtocolUrl() agents app skipping window openable:', uri.toString(true)); return true; } - // Ensure sessions window is open to receive the URL - const windows = await windowsMainService.openSessionsWindow({ context: OpenContext.LINK, contextWindowId: undefined }); + // Ensure agents window is open to receive the URL + const windows = await windowsMainService.openAgentsWindow({ context: OpenContext.LINK, contextWindowId: undefined }); const window = windows.at(0); window?.focus(); await window?.ready(); @@ -1358,9 +1358,9 @@ export class CodeApplication extends Disposable { const context = isLaunchedFromCli(process.env) ? OpenContext.CLI : OpenContext.DESKTOP; const args = this.environmentMainService.args; - // Handle sessions window first based on context - if ((process as INodeProcess).isEmbeddedApp || (args['sessions'] && this.productService.quality !== 'stable')) { - return windowsMainService.openSessionsWindow({ context, contextWindowId: undefined }); + // Handle agents window first based on context + if ((process as INodeProcess).isEmbeddedApp || (args['agents'] && this.productService.quality !== 'stable')) { + return windowsMainService.openAgentsWindow({ context, contextWindowId: undefined }); } // Then check for windows from protocol links to open diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 5f50659ff46..d3a89db8cb0 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -500,9 +500,9 @@ export async function main(argv: string[]): Promise { // focusing issues when the new instance only sends data to a previous instance and then closes. const spawnArgs = ['-n', '-g']; - // Figure out the app to launch: with --sessions we try to launch the embedded app + // Figure out the app to launch: with --agents we try to launch the embedded app let appToLaunch = process.execPath; - if (args.sessions) { + if (args.agents) { // process.execPath is e.g. /Applications/Code.app/Contents/MacOS/Electron // Embedded app is at /Applications/Code.app/Contents/Applications/.app const contentsPath = dirname(dirname(process.execPath)); @@ -512,7 +512,7 @@ export async function main(argv: string[]): Promise { const embeddedApp = files.find(file => file.endsWith('.app')); if (embeddedApp) { appToLaunch = join(applicationsPath, embeddedApp); - argv = argv.filter(arg => arg !== '--sessions'); + argv = argv.filter(arg => arg !== '--agents'); } } catch (error) { /* may not exist on disk */ diff --git a/src/vs/platform/agentHost/test/node/testRemoteAgentHost.sh b/src/vs/platform/agentHost/test/node/testRemoteAgentHost.sh index c56157e3540..e6808f7e96f 100755 --- a/src/vs/platform/agentHost/test/node/testRemoteAgentHost.sh +++ b/src/vs/platform/agentHost/test/node/testRemoteAgentHost.sh @@ -221,7 +221,7 @@ echo "=== Step 3: Launching Sessions app ===" >&2 cd "$ROOT" # Unset ELECTRON_RUN_AS_NODE to ensure the app launches as Electron, not Node. VSCODE_SKIP_PRELAUNCH=1 ELECTRON_RUN_AS_NODE= ./scripts/code.sh \ - --sessions \ + --agents \ --skip-sessions-welcome \ --remote-debugging-port="$CDP_PORT" \ --user-data-dir="$USERDATA_DIR" \ diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index b391d4e6a91..69d15c95307 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -54,7 +54,7 @@ export interface NativeParsedArgs { goto?: boolean; 'new-window'?: boolean; 'reuse-window'?: boolean; - 'sessions'?: boolean; + 'agents'?: boolean; locale?: string; 'user-data-dir'?: string; 'prof-startup'?: boolean; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index dcb2b33a1f2..0a78893bfc1 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -108,7 +108,7 @@ export const OPTIONS: OptionDescriptions> = { 'goto': { type: 'boolean', cat: 'o', alias: 'g', args: 'file:line[:character]', description: localize('goto', "Open a file at the path on the specified line and character position.") }, 'new-window': { type: 'boolean', cat: 'o', alias: 'n', description: localize('newWindow', "Force to open a new window.") }, 'reuse-window': { type: 'boolean', cat: 'o', alias: 'r', description: localize('reuseWindow', "Force to open a file or folder in an already opened window.") }, - 'sessions': { type: 'boolean', cat: 'o', description: localize('sessions', "Opens the sessions window.") }, + 'agents': { type: 'boolean', cat: 'o', deprecates: ['sessions'], description: localize('agents', "Opens the agents window.") }, 'wait': { type: 'boolean', cat: 'o', alias: 'w', description: localize('wait', "Wait for the files to be closed before returning.") }, 'waitMarkerFilePath': { type: 'string' }, 'locale': { type: 'string', cat: 'o', args: 'locale', description: localize('locale', "The locale to use (e.g. en-US or zh-TW).") }, diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 274600742e4..bce49e9de48 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -145,9 +145,9 @@ export class LaunchMainService implements ILaunchMainService { await this.windowsMainService.openExtensionDevelopmentHostWindow(args.extensionDevelopmentPath, baseConfig); } - // Sessions window - else if (args['sessions'] && this.productService.quality !== 'stable') { - usedWindows = await this.windowsMainService.openSessionsWindow({ context, contextWindowId: undefined }); + // Agents window + else if (args['agents'] && this.productService.quality !== 'stable') { + usedWindows = await this.windowsMainService.openAgentsWindow({ context, contextWindowId: undefined }); } // Start without file/folder arguments diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index aa73a7c63c4..dbcd0f1dfd1 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -129,7 +129,7 @@ export interface ICommonNativeHostService { openWindow(options?: IOpenEmptyWindowOptions): Promise; openWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise; - openSessionsWindow(): Promise; + openAgentsWindow(): Promise; isFullScreen(options?: INativeHostOptions): Promise; toggleFullScreen(options?: INativeHostOptions): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 3edc2ef195d..22cdfbe2fa4 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -304,8 +304,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain }, options); } - async openSessionsWindow(windowId: number | undefined): Promise { - await this.windowsMainService.openSessionsWindow({ + async openAgentsWindow(windowId: number | undefined): Promise { + await this.windowsMainService.openAgentsWindow({ context: OpenContext.API, contextWindowId: windowId, }); diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 75bcee0c678..c4d37c7cc97 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -41,7 +41,7 @@ export interface IWindowsMainService { openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): Promise; openExistingWindow(window: ICodeWindow, openConfig: IOpenConfiguration): void; - openSessionsWindow(openConfig: IBaseOpenConfiguration): Promise; + openAgentsWindow(openConfig: IBaseOpenConfiguration): Promise; sendToFocused(channel: string, ...args: unknown[]): void; sendToOpeningWindow(channel: string, ...args: unknown[]): void; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index ec4b29a101b..404a61a6736 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -292,12 +292,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic this.handleChatRequest(openConfig, [window]); } - async openSessionsWindow(openConfig: IBaseOpenConfiguration): Promise { - this.logService.trace('windowsManager#openSessionsWindow'); + async openAgentsWindow(openConfig: IBaseOpenConfiguration): Promise { + this.logService.trace('windowsManager#openAgentsWindow'); const agentSessionsWorkspaceUri = this.environmentMainService.agentSessionsWorkspace; if (!agentSessionsWorkspaceUri) { - throw new Error('Sessions workspace is not configured'); + throw new Error('Agents workspace is not configured'); } // Ensure the workspace file exists diff --git a/src/vs/sessions/AI_CUSTOMIZATIONS.md b/src/vs/sessions/AI_CUSTOMIZATIONS.md index 2a7bddb5a7a..0edfdbbbf78 100644 --- a/src/vs/sessions/AI_CUSTOMIZATIONS.md +++ b/src/vs/sessions/AI_CUSTOMIZATIONS.md @@ -32,7 +32,7 @@ src/vs/workbench/contrib/chat/common/ └── customizationHarnessService.ts # ICustomizationHarnessService + ISectionOverride + helpers ``` -The tree view and overview live in `vs/sessions` (sessions window only): +The tree view and overview live in `vs/sessions` (agent sessions window only): ``` src/vs/sessions/contrib/aiCustomizationTreeView/browser/ @@ -61,7 +61,7 @@ src/vs/sessions/contrib/sessions/browser/ The `IAICustomizationWorkspaceService` interface controls per-window behavior: -| Property / Method | Core VS Code | Sessions Window | +| Property / Method | Core VS Code | Agent Sessions Window | |----------|-------------|----------| | `managementSections` | All sections except Models | All sections except Models | | `getStorageSourceFilter(type)` | Delegates to `ICustomizationHarnessService` | Delegates to `ICustomizationHarnessService` | diff --git a/src/vs/sessions/LAYOUT.md b/src/vs/sessions/LAYOUT.md index 65d9f05cb3e..494b6e0059b 100644 --- a/src/vs/sessions/LAYOUT.md +++ b/src/vs/sessions/LAYOUT.md @@ -199,7 +199,7 @@ When the setting is `'all'`: The setting `workbench.editor.useModal` is an enum with three values: - `'off'`: Editors never open in a modal overlay - `'some'`: Certain editors (e.g. Settings, Keyboard Shortcuts) may open in a modal overlay when requested via `MODAL_GROUP` -- `'all'`: All editors open in a modal overlay (used by sessions window) +- `'all'`: All editors open in a modal overlay (used by agent sessions window) --- diff --git a/src/vs/sessions/README.md b/src/vs/sessions/README.md index 9e8d73a6e36..5903e497d3a 100644 --- a/src/vs/sessions/README.md +++ b/src/vs/sessions/README.md @@ -114,7 +114,7 @@ See [LAYOUT.md](LAYOUT.md) for the detailed layout specification. ## Sessions Provider Architecture -The sessions window uses an extensible provider model to manage sessions. Instead of hardcoding session type logic (CLI, Cloud, Agent Host) throughout the codebase, all session behavior is encapsulated in **sessions providers** that register with a central registry. +The agent sessions window uses an extensible provider model to manage sessions. Instead of hardcoding session type logic (CLI, Cloud, Agent Host) throughout the codebase, all session behavior is encapsulated in **sessions providers** that register with a central registry. ### Overview Diagram diff --git a/src/vs/sessions/SESSIONS_PROVIDER.md b/src/vs/sessions/SESSIONS_PROVIDER.md index 1db3330f1ae..de8969d629c 100644 --- a/src/vs/sessions/SESSIONS_PROVIDER.md +++ b/src/vs/sessions/SESSIONS_PROVIDER.md @@ -2,7 +2,7 @@ ## Overview -The Sessions Provider architecture introduces an **extensible provider model** for managing agent sessions in the Sessions window. Instead of hardcoding session types and backends, multiple providers register with a central registry (`ISessionsProvidersService`), which aggregates sessions from all providers and routes actions to the correct one. +The Sessions Provider architecture introduces an **extensible provider model** for managing agent sessions in the Agent Sessions window. Instead of hardcoding session types and backends, multiple providers register with a central registry (`ISessionsProvidersService`), which aggregates sessions from all providers and routes actions to the correct one. This design allows new compute environments (remote agent hosts, cloud backends, third-party agents) to plug in without modifying core session management code. diff --git a/src/vs/sessions/browser/workbench.ts b/src/vs/sessions/browser/workbench.ts index 3ebbfbf7da0..291f1ac9789 100644 --- a/src/vs/sessions/browser/workbench.ts +++ b/src/vs/sessions/browser/workbench.ts @@ -896,7 +896,7 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { !this.partVisibility.panel ? LayoutClasses.PANEL_HIDDEN : undefined, !this.partVisibility.auxiliaryBar ? LayoutClasses.AUXILIARYBAR_HIDDEN : undefined, !this.partVisibility.chatBar ? LayoutClasses.CHATBAR_HIDDEN : undefined, - LayoutClasses.STATUSBAR_HIDDEN, // sessions window never has a status bar + LayoutClasses.STATUSBAR_HIDDEN, // agents window never has a status bar this.mainWindowFullscreen ? LayoutClasses.FULLSCREEN : undefined ]); } diff --git a/src/vs/sessions/common/categories.ts b/src/vs/sessions/common/categories.ts index 6b5ac08a433..4645c05dc61 100644 --- a/src/vs/sessions/common/categories.ts +++ b/src/vs/sessions/common/categories.ts @@ -6,5 +6,5 @@ import { localize2 } from '../../nls.js'; export const SessionsCategories = Object.freeze({ - Sessions: localize2('sessions', "Sessions"), + Sessions: localize2('agents', "Agents"), }); diff --git a/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts b/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts index c4d28d53385..70c8b3793e8 100644 --- a/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts +++ b/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts @@ -263,7 +263,7 @@ export class AccountWidget extends ActionViewItem { if (state.type === StateType.AvailableForDownload && state.canInstall === false) { const { confirmed } = await this.dialogService.confirm({ message: localize('updateFromVSCode.title', "Update from VS Code"), - detail: localize('updateFromVSCode.detail', "This will close the Sessions app and open VS Code so you can install the update.\n\nLaunch Sessions again after the update is complete."), + detail: localize('updateFromVSCode.detail', "This will close the Agents app and open VS Code so you can install the update.\n\nLaunch Agents again after the update is complete."), primaryButton: localize('updateFromVSCode.open', "Close and Open VS Code"), }); if (confirmed) { @@ -311,7 +311,7 @@ class AccountWidgetContribution extends Disposable implements IWorkbenchContribu constructor() { super({ id: sessionsAccountWidgetAction, - title: localize2('sessionsAccountWidget', 'Sessions Account'), + title: localize2('agentsAccountWidget', 'Agents Account'), menu: { id: Menus.SidebarFooter, group: 'navigation', diff --git a/src/vs/sessions/contrib/accountMenu/test/browser/accountWidget.fixture.ts b/src/vs/sessions/contrib/accountMenu/test/browser/accountWidget.fixture.ts index 293c26b7f91..53601b4d7de 100644 --- a/src/vs/sessions/contrib/accountMenu/test/browser/accountWidget.fixture.ts +++ b/src/vs/sessions/contrib/accountMenu/test/browser/accountWidget.fixture.ts @@ -80,7 +80,7 @@ function renderAccountWidget(ctx: ComponentFixtureContext, state: State, account additionalServices: registerWorkbenchServices, }); - const action = ctx.disposableStore.add(new Action('sessions.action.accountWidget', 'Sessions Account')); + const action = ctx.disposableStore.add(new Action('sessions.action.accountWidget', 'Agents Account')); const contextMenuService = instantiationService.get(IContextMenuService); const menuService = instantiationService.get(IMenuService); const contextKeyService = instantiationService.get(IContextKeyService); diff --git a/src/vs/sessions/contrib/chat/browser/promptsService.ts b/src/vs/sessions/contrib/chat/browser/promptsService.ts index b77212599ea..13e2ef24d76 100644 --- a/src/vs/sessions/contrib/chat/browser/promptsService.ts +++ b/src/vs/sessions/contrib/chat/browser/promptsService.ts @@ -25,7 +25,7 @@ import { IUserDataProfileService } from '../../../../workbench/services/userData import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; -/** URI root for built-in skills bundled with the Sessions app. */ +/** URI root for built-in skills bundled with the Agents app. */ export const BUILTIN_SKILLS_URI = FileAccess.asFileUri('vs/sessions/skills'); export class AgenticPromptsService extends PromptsService { diff --git a/src/vs/sessions/contrib/chat/browser/runScriptCustomTaskWidget.ts b/src/vs/sessions/contrib/chat/browser/runScriptCustomTaskWidget.ts index 08e4c451613..dd6a9f74779 100644 --- a/src/vs/sessions/contrib/chat/browser/runScriptCustomTaskWidget.ts +++ b/src/vs/sessions/contrib/chat/browser/runScriptCustomTaskWidget.ts @@ -222,7 +222,7 @@ export class RunScriptCustomTaskWidget extends Disposable { private _getSubmitLabel(): string { if (this._isAddExistingTask) { - return localize('confirmAddToSessions', "Add to Sessions Window"); + return localize('confirmAddToAgents', "Add to Agents Window"); } if (!this._isExistingTask) { return localize('confirmAddTask', "Add Task"); diff --git a/src/vs/sessions/contrib/chat/common/builtinPromptsStorage.ts b/src/vs/sessions/contrib/chat/common/builtinPromptsStorage.ts index a4eb5afd410..d6b0fb72fd0 100644 --- a/src/vs/sessions/contrib/chat/common/builtinPromptsStorage.ts +++ b/src/vs/sessions/contrib/chat/common/builtinPromptsStorage.ts @@ -12,7 +12,7 @@ export type { AICustomizationPromptsStorage } from '../../../../workbench/contri export { BUILTIN_STORAGE } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; /** - * Prompt path for built-in prompts bundled with the Sessions app. + * Prompt path for built-in prompts bundled with the Agents app. */ export interface IBuiltinPromptPath { readonly uri: URI; diff --git a/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts b/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts index bdf5cb8a0e4..cb73c3044e6 100644 --- a/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts +++ b/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts @@ -48,7 +48,7 @@ class SessionsWelcomeOverlay extends Disposable { this.overlay = append(container, $('.sessions-welcome-overlay')); this.overlay.setAttribute('role', 'dialog'); this.overlay.setAttribute('aria-modal', 'true'); - this.overlay.setAttribute('aria-label', localize('welcomeOverlay.aria', "Sign in to use Sessions")); + this.overlay.setAttribute('aria-label', localize('welcomeOverlay.aria', "Sign in to use Agents")); this._register(toDisposable(() => this.overlay.remove())); const card = append(this.overlay, $('.sessions-welcome-card')); @@ -57,7 +57,7 @@ class SessionsWelcomeOverlay extends Disposable { const header = append(card, $('.sessions-welcome-header')); const iconEl = append(header, $('span.sessions-welcome-icon')); iconEl.appendChild(renderIcon(Codicon.agent)); - append(header, $('h2', undefined, localize('welcomeTitle', "Sign in to use Sessions"))); + append(header, $('h2', undefined, localize('welcomeTitle', "Sign in to use Agents"))); append(header, $('p.sessions-welcome-subtitle', undefined, localize('welcomeSubtitle', "Agent-powered development"))); // Action area @@ -93,8 +93,8 @@ class SessionsWelcomeOverlay extends Disposable { const success = await this.commandService.executeCommand(CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID, { dialogIcon: Codicon.agent, dialogTitle: this.chatEntitlementService.anonymous ? - localize('sessions.startUsingSessions', "Start using Sessions") : - localize('sessions.signinRequired', "Sign in to use Sessions"), + localize('agents.startUsingAgents', "Start using Agents") : + localize('agents.signinRequired', "Sign in to use Agents"), }); if (success) { @@ -259,7 +259,7 @@ registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.resetSessionsWelcome', - title: localize2('resetSessionsWelcome', "Reset Sessions Welcome"), + title: localize2('resetSessionsWelcome', "Reset Agents Welcome"), category: Categories.Developer, f1: true, }); diff --git a/src/vs/sessions/copilot-customizations-spec.md b/src/vs/sessions/copilot-customizations-spec.md index 2b392966180..4f92903ef7b 100644 --- a/src/vs/sessions/copilot-customizations-spec.md +++ b/src/vs/sessions/copilot-customizations-spec.md @@ -4,7 +4,7 @@ > > **Source:** `github/copilot-agent-runtime` codebase as of 2026-02-25. -> Some information has been removed by the human compiling this spec, scoping to what is deemed most relevant for the sessions window implementation. For the full details, see the source code (for maintainers likely checked out side-by-side). +> Some information has been removed by the human compiling this spec, scoping to what is deemed most relevant for the agent sessions window implementation. For the full details, see the source code (for maintainers likely checked out side-by-side). --- diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index f7e31e40bff..73e207b85d3 100644 --- a/src/vs/workbench/common/contextkeys.ts +++ b/src/vs/workbench/common/contextkeys.ts @@ -33,7 +33,7 @@ export const RemoteNameContext = new RawContextKey('remoteName', '', loc export const VirtualWorkspaceContext = new RawContextKey('virtualWorkspace', '', localize('virtualWorkspace', "The scheme of the current workspace is from a virtual file system or an empty string.")); export const TemporaryWorkspaceContext = new RawContextKey('temporaryWorkspace', false, localize('temporaryWorkspace', "The scheme of the current workspace is from a temporary file system.")); -export const IsSessionsWindowContext = new RawContextKey('isSessionsWindow', false, localize('isSessionsWindow', "Whether the current window is a sessions window.")); +export const IsSessionsWindowContext = new RawContextKey('isSessionsWindow', false, localize('isSessionsWindow', "Whether the current window is a agent sessions window.")); export const HasWebFileSystemAccess = new RawContextKey('hasWebFileSystemAccess', false, true); // Support for FileSystemAccess web APIs (https://wicg.github.io/file-system-access) diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index b439e870035..cdd58916539 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -69,11 +69,11 @@ export const enum WindowVisibility { */ Editor = 1, /** - * Visible only in sessions window + * Visible only in agent sessions window */ Sessions = 2, /** - * Visible in both editor and sessions windows + * Visible in both editor and agent sessions windows */ Both = 3, } diff --git a/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts b/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts index 0134980d2c0..d86957ebb8c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts @@ -413,13 +413,13 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ excludeWhenToolsInvoked: ['listDebugEvents'], }, { - id: 'tip.openSessionsWindow', + id: 'tip.openAgentsWindow', tier: ChatTipTier.Qol, buildMessage() { return new MarkdownString( localize( - 'tip.openSessionsWindow', - "Try the [Sessions Window](command:workbench.action.openSessionsWindow \"Open Sessions Window\") to run multiple agents simultaneously and manage your coding sessions." + 'tip.openAgentsWindow', + "Try the [Agents Application](command:workbench.action.openAgentsWindow \"Open Agents Application\") to run multiple agents simultaneously and manage your coding sessions." ) ); }, @@ -428,7 +428,7 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ IsSessionsWindowContext.negate(), ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), ), - excludeWhenCommandsExecuted: ['workbench.action.openSessionsWindow'], - dismissWhenCommandsClicked: ['workbench.action.openSessionsWindow'], + excludeWhenCommandsExecuted: ['workbench.action.openAgentsWindow'], + dismissWhenCommandsClicked: ['workbench.action.openAgentsWindow'], }, ]; diff --git a/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts index 25bed8ecd2e..ee9d0eb3206 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts @@ -18,11 +18,11 @@ import { isMacintosh, isWindows } from '../../../../../base/common/platform.js'; import { IWorkbenchEnvironmentService } from '../../../../services/environment/common/environmentService.js'; import { Schemas } from '../../../../../base/common/network.js'; -export class OpenSessionsWindowAction extends Action2 { +export class OpenAgentsWindowAction extends Action2 { constructor() { super({ - id: 'workbench.action.openSessionsWindow', - title: localize2('openSessionsWindow', "Open Sessions Window"), + id: 'workbench.action.openAgentsWindow', + title: localize2('openAgentsWindow', "Open Agents Application"), category: CHAT_CATEGORY, precondition: ContextKeyExpr.and(ProductQualityContext.notEqualsTo('stable'), ChatEntitlementContextKeys.Setup.hidden.negate(), IsSessionsWindowContext.negate()), f1: true, @@ -50,7 +50,7 @@ export class OpenSessionsWindowAction extends Action2 { await openerService.open(URI.from({ scheme, authority: Schemas.file }), { openExternal: true }); } else { const nativeHostService = accessor.get(INativeHostService); - await nativeHostService.openSessionsWindow(); + await nativeHostService.openAgentsWindow(); } } } diff --git a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts index 6fddb201f36..bb636f37e61 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts @@ -42,7 +42,7 @@ import { registerChatDeveloperActions } from './actions/chatDeveloperActions.js' import { registerChatExportZipAction } from './actions/chatExportZip.js'; import { HoldToVoiceChatInChatViewAction, InlineVoiceChatAction, KeywordActivationContribution, QuickVoiceChatAction, ReadChatResponseAloud, StartVoiceChatAction, StopListeningAction, StopListeningAndSubmitAction, StopReadAloud, StopReadChatItemAloud, VoiceChatInChatViewAction } from './actions/voiceChatActions.js'; import { NativeBuiltinToolsContribution } from './builtInTools/tools.js'; -import { OpenSessionsWindowAction } from './agentSessions/agentSessionsActions.js'; +import { OpenAgentsWindowAction } from './agentSessions/agentSessionsActions.js'; class ChatCommandLineHandler extends Disposable { @@ -217,7 +217,7 @@ class ChatLifecycleHandler extends Disposable { } } -registerAction2(OpenSessionsWindowAction); +registerAction2(OpenAgentsWindowAction); registerAction2(StartVoiceChatAction); registerAction2(VoiceChatInChatViewAction); diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 74ce76f240d..62e4132a1a7 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -103,7 +103,7 @@ export class TestNativeHostService implements INativeHostService { throw new Error('Method not implemented.'); } - async openSessionsWindow(): Promise { } + async openAgentsWindow(): Promise { } async toggleFullScreen(): Promise { } async isMaximized(): Promise { return true; } From 79a97517fbde6ece504bfa4bb8575b867a8635bf Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 31 Mar 2026 16:09:02 -0400 Subject: [PATCH 13/16] Add `send_to_terminal` tool for sending commands to background terminals (#306875) --- .../browser/runInTerminalHelpers.ts | 17 ++ .../terminal.chatAgentTools.contribution.ts | 5 + .../browser/tools/runInTerminalTool.ts | 11 +- .../browser/tools/sendToTerminalTool.ts | 94 ++++++++++ .../chatAgentTools/browser/tools/toolIds.ts | 1 + .../test/browser/sendToTerminalTool.test.ts | 164 ++++++++++++++++++ 6 files changed, 286 insertions(+), 6 deletions(-) create mode 100644 src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/sendToTerminalTool.ts create mode 100644 src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/sendToTerminalTool.test.ts diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts index 4edc69a5fad..da4024062f4 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts @@ -85,6 +85,23 @@ export function normalizeTerminalCommandForDisplay(commandLine: string): string return commandLine.replace(/\\(["'\/])/g, '$1'); } +/** + * Builds a single-line display string for a terminal command, suitable for UI messages. + * Normalizes escape artifacts, collapses newlines to spaces, and truncates to 80 characters. + */ +export function buildCommandDisplayText(command: string): string { + const normalized = normalizeTerminalCommandForDisplay(command).replace(/\r\n|\r|\n/g, ' '); + return normalized.length > 80 ? normalized.substring(0, 77) + '...' : normalized; +} + +/** + * Normalizes a terminal command for execution by collapsing newlines to spaces. + * This prevents multi-line input from being sent as multiple commands via sendText. + */ +export function normalizeCommandForExecution(command: string): string { + return command.replace(/\r\n|\r|\n/g, ' ').trim(); +} + export function generateAutoApproveActions(commandLine: string, subCommands: string[], autoApproveResult: { subCommandResults: ICommandApprovalResultWithReason[]; commandLineResult: ICommandApprovalResultWithReason }): ToolConfirmationAction[] { const actions: ToolConfirmationAction[] = []; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts index eec2f8c5d81..7798303a537 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts @@ -24,6 +24,7 @@ import { AwaitTerminalTool, AwaitTerminalToolData } from './tools/awaitTerminalT import { GetTerminalLastCommandTool, GetTerminalLastCommandToolData } from './tools/getTerminalLastCommandTool.js'; import { KillTerminalTool, KillTerminalToolData } from './tools/killTerminalTool.js'; import { GetTerminalOutputTool, GetTerminalOutputToolData } from './tools/getTerminalOutputTool.js'; +import { SendToTerminalTool, SendToTerminalToolData } from './tools/sendToTerminalTool.js'; import { GetTerminalSelectionTool, GetTerminalSelectionToolData } from './tools/getTerminalSelectionTool.js'; import { ConfirmTerminalCommandTool, ConfirmTerminalCommandToolData } from './tools/runInTerminalConfirmationTool.js'; import { RunInTerminalTool, createRunInTerminalToolData } from './tools/runInTerminalTool.js'; @@ -105,6 +106,10 @@ export class ChatAgentToolsContribution extends Disposable implements IWorkbench this._register(_toolsService.registerTool(KillTerminalToolData, killTerminalTool)); this._register(_toolsService.executeToolSet.addTool(KillTerminalToolData)); + const sendToTerminalTool = _instantiationService.createInstance(SendToTerminalTool); + this._register(_toolsService.registerTool(SendToTerminalToolData, sendToTerminalTool)); + this._register(_toolsService.executeToolSet.addTool(SendToTerminalToolData)); + this._registerRunInTerminalTool(); const getTerminalSelectionTool = _instantiationService.createInstance(GetTerminalSelectionTool); 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 3ab326afae7..0df6fa55214 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -38,7 +38,7 @@ import type { ITerminalExecuteStrategy, ITerminalExecuteStrategyResult } from '. import { NoneExecuteStrategy } from '../executeStrategy/noneExecuteStrategy.js'; import { RichExecuteStrategy } from '../executeStrategy/richExecuteStrategy.js'; import { getOutput } from '../outputHelpers.js'; -import { extractCdPrefix, isFish, isPowerShell, isWindowsPowerShell, isZsh, normalizeTerminalCommandForDisplay } from '../runInTerminalHelpers.js'; +import { buildCommandDisplayText, extractCdPrefix, isFish, isPowerShell, isWindowsPowerShell, isZsh, normalizeTerminalCommandForDisplay } from '../runInTerminalHelpers.js'; import type { ICommandLinePresenter } from './commandLinePresenter/commandLinePresenter.js'; import { NodeCommandLinePresenter } from './commandLinePresenter/nodeCommandLinePresenter.js'; import { PythonCommandLinePresenter } from './commandLinePresenter/pythonCommandLinePresenter.js'; @@ -108,6 +108,7 @@ function createPowerShellModelDescription(shell: string, isSandboxEnabled: boole 'Background Processes:', '- For long-running tasks (e.g., servers), set isBackground=true', '- Returns a terminal ID for checking status and runtime later', + `- Use ${TerminalToolId.SendToTerminal} to send commands to a background terminal`, '- Use Start-Job for background PowerShell jobs', ]; @@ -184,7 +185,8 @@ Program Execution: Background Processes: - For long-running tasks (e.g., servers), set isBackground=true -- Returns a terminal ID for checking status and runtime later`]; +- Returns a terminal ID for checking status and runtime later +- Use ${TerminalToolId.SendToTerminal} to send commands to a background terminal`]; if (isSandboxEnabled) { parts.push(createSandboxLines(networkDomains).join('\n')); @@ -528,10 +530,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { async handleToolStream(context: IToolInvocationStreamContext, _token: CancellationToken): Promise { const partialInput = context.rawInput as Partial | undefined; if (partialInput && typeof partialInput === 'object' && partialInput.command) { - const normalizedCommand = normalizeTerminalCommandForDisplay(partialInput.command).replace(/\r\n|\r|\n/g, ' '); - const truncatedCommand = normalizedCommand.length > 80 - ? normalizedCommand.substring(0, 77) + '...' - : normalizedCommand; + const truncatedCommand = buildCommandDisplayText(partialInput.command); const invocationMessage = partialInput.isBackground ? new MarkdownString(localize('runInTerminal.streaming.background', "Running `{0}` in background", truncatedCommand)) : new MarkdownString(localize('runInTerminal.streaming', "Running `{0}`", truncatedCommand)); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/sendToTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/sendToTerminalTool.ts new file mode 100644 index 00000000000..40cc4260bb4 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/sendToTerminalTool.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { localize } from '../../../../../../nls.js'; +import { ToolDataSource, type CountTokensCallback, type IPreparedToolInvocation, type IToolData, type IToolImpl, type IToolInvocation, type IToolInvocationPreparationContext, type IToolResult, type ToolProgress } from '../../../../chat/common/tools/languageModelToolsService.js'; +import { buildCommandDisplayText, normalizeCommandForExecution } from '../runInTerminalHelpers.js'; +import { RunInTerminalTool } from './runInTerminalTool.js'; +import { TerminalToolId } from './toolIds.js'; + +export const SendToTerminalToolData: IToolData = { + id: TerminalToolId.SendToTerminal, + toolReferenceName: 'sendToTerminal', + displayName: localize('sendToTerminalTool.displayName', 'Send to Terminal'), + modelDescription: `Send a command to an existing background terminal that was started with ${TerminalToolId.RunInTerminal}. Use this to send commands to long-running terminal sessions. The ID must be the exact opaque value returned by ${TerminalToolId.RunInTerminal}. After sending, use ${TerminalToolId.GetTerminalOutput} to check for updated output.`, + icon: Codicon.terminal, + source: ToolDataSource.Internal, + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: `The ID of the background terminal to send a command to (returned by ${TerminalToolId.RunInTerminal}).`, + pattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$' + }, + command: { + type: 'string', + description: 'The command to send to the terminal. The text will be sent followed by Enter to execute it.' + }, + }, + required: [ + 'id', + 'command', + ] + } +}; + +export interface ISendToTerminalInputParams { + id: string; + command: string; +} + +export class SendToTerminalTool extends Disposable implements IToolImpl { + async prepareToolInvocation(context: IToolInvocationPreparationContext, _token: CancellationToken): Promise { + const args = context.parameters as ISendToTerminalInputParams; + const displayCommand = buildCommandDisplayText(args.command); + + const invocationMessage = new MarkdownString(); + invocationMessage.appendText(localize('send.progressive', "Sending {0} to terminal", displayCommand)); + + const pastTenseMessage = new MarkdownString(); + pastTenseMessage.appendText(localize('send.past', "Sent {0} to terminal", displayCommand)); + + const confirmationMessage = new MarkdownString(); + confirmationMessage.appendText(localize('send.confirm.message', "Run {0} in background terminal {1}", displayCommand, args.id)); + + return { + invocationMessage, + pastTenseMessage, + confirmationMessages: { + title: localize('send.confirm.title', "Send to Terminal"), + message: confirmationMessage, + }, + }; + } + + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, _token: CancellationToken): Promise { + const args = invocation.parameters as ISendToTerminalInputParams; + + const execution = RunInTerminalTool.getExecution(args.id); + if (!execution) { + return { + content: [{ + kind: 'text', + value: `Error: No active terminal execution found with ID ${args.id}. The terminal may have already been killed or the ID is invalid. The ID must be the exact value returned by ${TerminalToolId.RunInTerminal}.` + }] + }; + } + + await execution.instance.sendText(normalizeCommandForExecution(args.command), true); + + return { + content: [{ + kind: 'text', + value: `Successfully sent command to terminal ${args.id}. Use ${TerminalToolId.GetTerminalOutput} to check for updated output.` + }] + }; + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/toolIds.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/toolIds.ts index adab9944e2c..cc2cfa752f9 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/toolIds.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/toolIds.ts @@ -5,6 +5,7 @@ export const enum TerminalToolId { RunInTerminal = 'run_in_terminal', + SendToTerminal = 'send_to_terminal', AwaitTerminal = 'await_terminal', GetTerminalOutput = 'get_terminal_output', KillTerminal = 'kill_terminal', diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/sendToTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/sendToTerminalTool.test.ts new file mode 100644 index 00000000000..ba957aa9cb7 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/sendToTerminalTool.test.ts @@ -0,0 +1,164 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import type { IMarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { SendToTerminalTool, SendToTerminalToolData } from '../../browser/tools/sendToTerminalTool.js'; +import { RunInTerminalTool, type IActiveTerminalExecution } from '../../browser/tools/runInTerminalTool.js'; +import type { IToolInvocation, IToolInvocationPreparationContext } from '../../../../chat/common/tools/languageModelToolsService.js'; +import type { ITerminalExecuteStrategyResult } from '../../browser/executeStrategy/executeStrategy.js'; +import type { ITerminalInstance } from '../../../../terminal/browser/terminal.js'; + +suite('SendToTerminalTool', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + const UNKNOWN_TERMINAL_ID = '123e4567-e89b-12d3-a456-426614174000'; + const KNOWN_TERMINAL_ID = '123e4567-e89b-12d3-a456-426614174001'; + let tool: SendToTerminalTool; + let originalGetExecution: typeof RunInTerminalTool.getExecution; + + setup(() => { + tool = store.add(new SendToTerminalTool()); + originalGetExecution = RunInTerminalTool.getExecution; + }); + + teardown(() => { + RunInTerminalTool.getExecution = originalGetExecution; + }); + + function createInvocation(id: string, command: string): IToolInvocation { + return { + parameters: { id, command }, + callId: 'test-call', + context: { sessionId: 'test-session' }, + toolId: 'send_to_terminal', + tokenBudget: 1000, + isComplete: () => false, + isCancellationRequested: false, + } as unknown as IToolInvocation; + } + + function createMockExecution(output: string): IActiveTerminalExecution & { sentTexts: { text: string; shouldExecute: boolean }[] } { + const sentTexts: { text: string; shouldExecute: boolean }[] = []; + return { + completionPromise: Promise.resolve({ output } as ITerminalExecuteStrategyResult), + instance: { + sendText: async (text: string, shouldExecute: boolean) => { + sentTexts.push({ text, shouldExecute }); + }, + } as unknown as ITerminalInstance, + getOutput: () => output, + sentTexts, + }; + } + + test('tool description documents terminal IDs and use cases', () => { + const idProperty = SendToTerminalToolData.inputSchema?.properties?.id as { description?: string; pattern?: string } | undefined; + assert.ok(SendToTerminalToolData.modelDescription.includes('existing background terminal')); + assert.ok(idProperty?.pattern?.includes('[0-9a-fA-F]{8}')); + }); + + test('returns error for unknown terminal id', async () => { + RunInTerminalTool.getExecution = () => undefined; + + const result = await tool.invoke( + createInvocation(UNKNOWN_TERMINAL_ID, 'ls'), + async () => 0, + { report: () => { } }, + CancellationToken.None, + ); + + assert.strictEqual(result.content.length, 1); + assert.strictEqual(result.content[0].kind, 'text'); + const value = (result.content[0] as { value: string }).value; + assert.ok(value.includes('No active terminal execution found')); + assert.ok(value.includes(UNKNOWN_TERMINAL_ID)); + }); + + test('sends command to terminal and returns acknowledgment', async () => { + const mockExecution = createMockExecution('$ ls\nfile1.txt\nfile2.txt'); + RunInTerminalTool.getExecution = () => mockExecution; + + const result = await tool.invoke( + createInvocation(KNOWN_TERMINAL_ID, 'ls'), + async () => 0, + { report: () => { } }, + CancellationToken.None, + ); + + assert.strictEqual(result.content.length, 1); + assert.strictEqual(result.content[0].kind, 'text'); + const value = (result.content[0] as { value: string }).value; + assert.ok(value.includes('Successfully sent command')); + assert.ok(value.includes(KNOWN_TERMINAL_ID)); + assert.ok(value.includes('get_terminal_output'), 'should direct agent to use get_terminal_output'); + + // Verify sendText was called with shouldExecute=true + assert.strictEqual(mockExecution.sentTexts.length, 1); + assert.strictEqual(mockExecution.sentTexts[0].text, 'ls'); + assert.strictEqual(mockExecution.sentTexts[0].shouldExecute, true); + }); + + test('sends multi-word command correctly', async () => { + const mockExecution = createMockExecution('output'); + RunInTerminalTool.getExecution = () => mockExecution; + + await tool.invoke( + createInvocation(KNOWN_TERMINAL_ID, 'echo hello world'), + async () => 0, + { report: () => { } }, + CancellationToken.None, + ); + + assert.strictEqual(mockExecution.sentTexts.length, 1); + assert.strictEqual(mockExecution.sentTexts[0].text, 'echo hello world'); + assert.strictEqual(mockExecution.sentTexts[0].shouldExecute, true); + }); + + function createPreparationContext(id: string, command: string): IToolInvocationPreparationContext { + return { + parameters: { id, command }, + toolCallId: 'test-call', + } as unknown as IToolInvocationPreparationContext; + } + + test('prepareToolInvocation shows command in messages', async () => { + const prepared = await tool.prepareToolInvocation( + createPreparationContext(KNOWN_TERMINAL_ID, 'ls -la'), + CancellationToken.None, + ); + + assert.ok(prepared); + assert.ok(prepared.invocationMessage); + assert.ok(prepared.pastTenseMessage); + assert.ok(prepared.confirmationMessages); + assert.ok(prepared.confirmationMessages.title); + assert.ok(prepared.confirmationMessages.message); + }); + + test('prepareToolInvocation truncates long commands', async () => { + const longCommand = 'a'.repeat(100); + const prepared = await tool.prepareToolInvocation( + createPreparationContext(KNOWN_TERMINAL_ID, longCommand), + CancellationToken.None, + ); + + assert.ok(prepared); + const message = prepared.invocationMessage as IMarkdownString; + assert.ok(message.value.includes('...')); + }); + + test('prepareToolInvocation normalizes newlines in command', async () => { + const prepared = await tool.prepareToolInvocation( + createPreparationContext(KNOWN_TERMINAL_ID, 'echo hello\necho world'), + CancellationToken.None, + ); + + assert.ok(prepared); + const message = prepared.invocationMessage as IMarkdownString; + assert.ok(!message.value.includes('\n'), 'newlines should be collapsed to spaces'); + }); +}); From 20e37ff1887a1a6a73591554f11d29a843416a0a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 31 Mar 2026 22:32:08 +0200 Subject: [PATCH 14/16] sessions - more renames (#306937) --- .vscode/tasks.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 2748b2d1216..8e634a658e4 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -228,7 +228,7 @@ "problemMatcher": [] }, { - "label": "Run Dev Sessions", + "label": "Run Dev Agents", "type": "shell", "command": "./scripts/code.sh", "windows": { @@ -248,8 +248,8 @@ "problemMatcher": [] }, { - "label": "Run and Compile Sessions - OSS", - "dependsOn": ["Transpile Client", "Run Dev Sessions"], + "label": "Run and Compile Agents - OSS", + "dependsOn": ["Transpile Client", "Run Dev Agents"], "dependsOrder": "sequence", "inSessions": true, "problemMatcher": [] From 54493e6c24ce2826ab35361275ea52c7c7bc392d Mon Sep 17 00:00:00 2001 From: "vs-code-engineering[bot]" <122617954+vs-code-engineering[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 20:40:31 +0000 Subject: [PATCH 15/16] Update distro commit (main) (#306927) * Update distro commit to 1992012c * Update distro commit to 084798d4 --------- Co-authored-by: vs-code-engineering[bot] <122617954+vs-code-engineering[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c530f4c90a1..6b3e88bdee9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.115.0", - "distro": "fdfcc35f4a498ffc0fae5966393153e96672dc89", + "distro": "084798d47e4cc4c713def402d4a1110fb619116d", "author": { "name": "Microsoft Corporation" }, From 26bd211592b9962910556850dc339af54342f8e8 Mon Sep 17 00:00:00 2001 From: Aaron Munger <2019016+amunger@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:47:44 -0700 Subject: [PATCH 16/16] track disposable (#306933) --- .../notebook/browser/contrib/multicursor/notebookMulticursor.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts b/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts index 48532bfcff7..28a9ee9abdf 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts @@ -273,8 +273,10 @@ export class NotebookMultiCursorController extends Disposable implements INotebo const textModelRef = await this.textModelService.createModelReference(cell.cellViewModel.uri); const textModel = textModelRef.object.textEditorModel; if (!textModel) { + textModelRef.dispose(); return undefined; } + this.cursorsDisposables.add(textModelRef); const cursorSimpleModel = this.constructCursorSimpleModel(cell.cellViewModel); const converter = this.constructCoordinatesConverter();