From dcd1e301fcb4094cf55abe4b90a3af31c2f32dd4 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:06:08 -0700 Subject: [PATCH 01/13] Remove support for searchable option groups This no longer appears to be used --- .../chat/browser/extensionToolbarPickers.ts | 7 +- .../api/browser/mainThreadChatSessions.ts | 8 +- .../workbench/api/common/extHost.protocol.ts | 1 - .../api/common/extHostChatSessions.ts | 30 +-- .../browser/mainThreadChatSessions.test.ts | 2 - .../searchableOptionPickerActionItem.ts | 233 ------------------ .../browser/widget/input/chatInputPart.ts | 11 +- .../chat/common/chatSessionsService.ts | 2 - .../vscode.proposed.chatSessionsProvider.d.ts | 16 -- 9 files changed, 10 insertions(+), 300 deletions(-) delete mode 100644 src/vs/workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.ts diff --git a/src/vs/sessions/contrib/chat/browser/extensionToolbarPickers.ts b/src/vs/sessions/contrib/chat/browser/extensionToolbarPickers.ts index 6ed3fe1115573..f1ce006a38f63 100644 --- a/src/vs/sessions/contrib/chat/browser/extensionToolbarPickers.ts +++ b/src/vs/sessions/contrib/chat/browser/extensionToolbarPickers.ts @@ -11,7 +11,6 @@ import { autorun } from '../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { ChatSessionPickerActionItem, IChatSessionPickerDelegate } from '../../../../workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.js'; -import { SearchableOptionPickerActionItem } from '../../../../workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.js'; import { IChatSessionProviderOptionItem } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { ISessionOptionGroup } from './newSession.js'; import { RemoteNewSession } from '../../copilotChatSessions/browser/copilotChatSessionsProvider.js'; @@ -26,7 +25,7 @@ import { ISessionsProvidersService } from '../../sessions/browser/sessionsProvid export class ExtensionToolbarPickers extends Disposable { private _container: HTMLElement | undefined; - private readonly _pickerWidgets = new Map(); + private readonly _pickerWidgets = new Map(); private readonly _pickerDisposables = this._register(new DisposableStore()); private readonly _optionEmitters = new Map>(); private readonly _optionContextKeys = new Map>(); @@ -84,7 +83,7 @@ export class ExtensionToolbarPickers extends Disposable { const toolbarOptions = session.getOtherOptionGroups(); const visibleGroups = toolbarOptions.filter(option => { const group = option.group; - return group.items.length > 0 || (group.commands || []).length > 0 || !!group.searchable; + return group.items.length > 0 || (group.commands || []).length > 0; }); if (visibleGroups.length === 0) { @@ -136,7 +135,7 @@ export class ExtensionToolbarPickers extends Disposable { const action = toAction({ id: optionGroup.id, label: optionGroup.name, run: () => { } }); const widget = this.instantiationService.createInstance( - optionGroup.searchable ? SearchableOptionPickerActionItem : ChatSessionPickerActionItem, + ChatSessionPickerActionItem, action, initialState, itemDelegate, undefined ); diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index 928fa28198e18..e58f71b3a631d 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -887,13 +887,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat private _refreshProviderOptions(handle: number, chatSessionScheme: string): void { this._proxy.$provideChatSessionProviderOptions(handle, CancellationToken.None).then(options => { if (options?.optionGroups && options.optionGroups.length) { - const groupsWithCallbacks = options.optionGroups.map(group => ({ - ...group, - onSearch: group.searchable ? async (query: string, token: CancellationToken) => { - return await this._proxy.$invokeOptionGroupSearch(handle, group.id, query, token); - } : undefined, - })); - this._chatSessionsService.setOptionGroupsForSessionType(chatSessionScheme, handle, groupsWithCallbacks); + this._chatSessionsService.setOptionGroupsForSessionType(chatSessionScheme, handle, [...options.optionGroups]); } if (options?.newSessionOptions) { this._chatSessionsService.setNewSessionOptionsForSessionType(chatSessionScheme, ChatSessionOptionsMap.fromRecord(options.newSessionOptions)); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 83b68a5ec4c83..4a54529634d5b 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3674,7 +3674,6 @@ export interface ExtHostChatSessionsShape { $disposeChatSessionContent(providerHandle: number, sessionResource: UriComponents): Promise; $invokeChatSessionRequestHandler(providerHandle: number, sessionResource: UriComponents, request: IChatAgentRequest, history: any[], token: CancellationToken): Promise; $provideChatSessionProviderOptions(providerHandle: number, token: CancellationToken): Promise; - $invokeOptionGroupSearch(providerHandle: number, optionGroupId: string, query: string, token: CancellationToken): Promise; $provideHandleOptionsChange(providerHandle: number, sessionResource: UriComponents, updates: Record, token: CancellationToken): Promise; $forkChatSession(providerHandle: number, sessionResource: UriComponents, request: IChatSessionRequestHistoryItemDto | undefined, token: CancellationToken): Promise>; } diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index a640e9b5ce745..1b31cd97f8d3e 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -326,10 +326,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio * Map of uri -> chat sessions infos */ private readonly _extHostChatSessions = new ResourceMap<{ readonly sessionObj: ExtHostChatSession; readonly disposeCts: CancellationTokenSource }>(); - /** - * Store option groups with onSearch callbacks per provider handle - */ - private readonly _providerOptionGroups = new Map(); + constructor( private readonly commands: ExtHostCommands, @@ -610,9 +607,6 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio return; } const { optionGroups, newSessionOptions } = result; - if (optionGroups) { - this._providerOptionGroups.set(handle, optionGroups); - } return { optionGroups, newSessionOptions, @@ -801,28 +795,6 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio }; } - async $invokeOptionGroupSearch(providerHandle: number, optionGroupId: string, query: string, token: CancellationToken): Promise { - const optionGroups = this._providerOptionGroups.get(providerHandle); - if (!optionGroups) { - this._logService.warn(`No option groups found for provider handle ${providerHandle}`); - return []; - } - - const group = optionGroups.find((g: vscode.ChatSessionProviderOptionGroup) => g.id === optionGroupId); - if (!group || !group.onSearch) { - this._logService.warn(`No onSearch callback found for option group ${optionGroupId}`); - return []; - } - - try { - const results = await group.onSearch(query, token); - return results ?? []; - } catch (error) { - this._logService.error(`Error calling onSearch for option group ${optionGroupId}:`, error); - return []; - } - } - async $refreshChatSessionItems(handle: number, token: CancellationToken): Promise { const controllerData = this._chatSessionItemControllers.get(handle); if (!controllerData) { diff --git a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts index 5e9504d111f80..6cf5b4a51067c 100644 --- a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts @@ -66,7 +66,6 @@ suite('ObservableChatSession', function () { $provideChatSessionContent: sinon.stub(), $provideChatSessionProviderOptions: sinon.stub<[providerHandle: number, token: CancellationToken], Promise>().resolves(undefined), $provideHandleOptionsChange: sinon.stub(), - $invokeOptionGroupSearch: sinon.stub().resolves([]), $interruptChatSessionActiveResponse: sinon.stub(), $invokeChatSessionRequestHandler: sinon.stub(), $disposeChatSessionContent: sinon.stub(), @@ -516,7 +515,6 @@ suite('MainThreadChatSessions', function () { $provideChatSessionContent: sinon.stub(), $provideChatSessionProviderOptions: sinon.stub<[providerHandle: number, token: CancellationToken], Promise>().resolves(undefined), $provideHandleOptionsChange: sinon.stub(), - $invokeOptionGroupSearch: sinon.stub().resolves([]), $interruptChatSessionActiveResponse: sinon.stub(), $invokeChatSessionRequestHandler: sinon.stub(), $disposeChatSessionContent: sinon.stub(), diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.ts deleted file mode 100644 index 45145ec7ec30f..0000000000000 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.ts +++ /dev/null @@ -1,233 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import './media/chatSessionPickerActionItem.css'; -import { IAction } from '../../../../../base/common/actions.js'; -import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; -import { Delayer } from '../../../../../base/common/async.js'; -import * as dom from '../../../../../base/browser/dom.js'; -import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js'; -import { IActionWidgetDropdownAction } from '../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; -import { IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem } from '../../common/chatSessionsService.js'; -import { DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { renderLabelWithIcons, renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { localize } from '../../../../../nls.js'; -import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { ChatSessionPickerActionItem, IChatSessionPickerDelegate } from './chatSessionPickerActionItem.js'; -import { ILogService } from '../../../../../platform/log/common/log.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; -import { IChatInputPickerOptions } from '../widget/input/chatInputPickerActionItem.js'; - -interface ISearchableOptionQuickPickItem extends IQuickPickItem { - readonly optionItem: IChatSessionProviderOptionItem; -} - -function isSearchableOptionQuickPickItem(item: IQuickPickItem | undefined): item is ISearchableOptionQuickPickItem { - return !!item && typeof (item as ISearchableOptionQuickPickItem).optionItem === 'object'; -} - -/** - * Action view item for searchable option groups with QuickPick. - * Used when an option group has `searchable: true` (e.g., repository selection). - * Shows an inline dropdown with items + "See more..." option that opens a searchable QuickPick. - */ -export class SearchableOptionPickerActionItem extends ChatSessionPickerActionItem { - private static readonly SEE_MORE_ID = '__see_more__'; - - constructor( - action: IAction, - initialState: { group: IChatSessionProviderOptionGroup; item: IChatSessionProviderOptionItem | undefined }, - delegate: IChatSessionPickerDelegate, - pickerOptions: IChatInputPickerOptions | undefined, - @IActionWidgetService actionWidgetService: IActionWidgetService, - @IContextKeyService contextKeyService: IContextKeyService, - @IKeybindingService keybindingService: IKeybindingService, - @IQuickInputService private readonly quickInputService: IQuickInputService, - @ILogService private readonly logService: ILogService, - @ICommandService commandService: ICommandService, - @ITelemetryService telemetryService: ITelemetryService, - ) { - super(action, initialState, delegate, pickerOptions, actionWidgetService, contextKeyService, keybindingService, commandService, telemetryService); - } - - protected override getDropdownActions(): IActionWidgetDropdownAction[] { - // If locked, show the current option only - const currentOption = this.delegate.getCurrentOption(); - if (currentOption?.locked) { - return [this.createLockedOptionAction(currentOption)]; - } - - const optionGroup = this.delegate.getOptionGroup(); - if (!optionGroup) { - return []; - } - - // Build actions from items - const actions: IActionWidgetDropdownAction[] = optionGroup.items.map(optionItem => { - const isCurrent = optionItem.id === currentOption?.id; - return { - id: optionItem.id, - enabled: !optionItem.locked, - icon: optionItem.icon, - checked: isCurrent, - class: undefined, - description: optionItem.description, - tooltip: optionItem.description ?? optionItem.name, - label: optionItem.name, - run: () => { - this.delegate.setOption(optionItem); - } - }; - }); - - // Add "See more..." action if onSearch is available - if (optionGroup.onSearch) { - actions.push({ - id: SearchableOptionPickerActionItem.SEE_MORE_ID, - enabled: true, - checked: false, - class: 'searchable-picker-see-more', - description: undefined, - tooltip: localize('seeMore.tooltip', "Search for more options"), - label: localize('seeMore', "See more..."), - run: () => { - this.showSearchableQuickPick(optionGroup); - } - } satisfies IActionWidgetDropdownAction); - } - - return actions; - } - - protected override renderLabel(element: HTMLElement): IDisposable | null { - const domChildren = []; - const optionGroup = this.delegate.getOptionGroup(); - - element.classList.add('chat-session-option-picker'); - - if (optionGroup?.icon) { - domChildren.push(renderIcon(optionGroup.icon)); - } - - // Label - const label = this.currentOption?.name ?? optionGroup?.name ?? localize('selectOption', "Select..."); - domChildren.push(dom.$('span.chat-session-option-label', undefined, label)); - - domChildren.push(...renderLabelWithIcons(`$(chevron-down)`)); - - dom.reset(element, ...domChildren); - this.setAriaLabelAttributes(element); - return null; - } - - protected override getContainerClass(): string { - return 'chat-searchable-option-picker-item'; - } - - /** - * Shows the full searchable QuickPick with all items (initial + search results) - * Called when user clicks "See more..." from the dropdown - */ - private async showSearchableQuickPick(optionGroup: IChatSessionProviderOptionGroup): Promise { - if (optionGroup.onSearch) { - const disposables = new DisposableStore(); - const quickPick = this.quickInputService.createQuickPick(); - disposables.add(quickPick); - quickPick.placeholder = optionGroup.description ?? localize('selectOption.placeholder', "Select {0}", optionGroup.name); - quickPick.matchOnDescription = true; - quickPick.matchOnDetail = true; - quickPick.matchOnLabelMode = 'fuzzy'; - quickPick.ignoreFocusOut = true; - quickPick.busy = true; - quickPick.show(); - - // Debounced search state - let currentSearchCts: CancellationTokenSource | undefined; - const searchDelayer = disposables.add(new Delayer(300)); - - const performSearch = async (query: string) => { - // Cancel previous search - currentSearchCts?.cancel(); - currentSearchCts?.dispose(); - currentSearchCts = new CancellationTokenSource(); - const token = currentSearchCts.token; - - quickPick.busy = true; - try { - const items = await optionGroup.onSearch!(query, token); - if (!token.isCancellationRequested) { - quickPick.items = items.map(item => this.createQuickPickItem(item)); - } - } catch (error) { - if (!token.isCancellationRequested) { - this.logService.error('Error fetching searchable option items:', error); - } - } finally { - if (!token.isCancellationRequested) { - quickPick.busy = false; - } - } - }; - - // Initial search with empty query - await performSearch(''); - - // Listen for value changes and perform debounced search - disposables.add(quickPick.onDidChangeValue(value => { - searchDelayer.trigger(() => performSearch(value)); - })); - - - // Handle selection - return new Promise((resolve) => { - disposables.add(quickPick.onDidAccept(() => { - const pick = quickPick.selectedItems[0]; - if (isSearchableOptionQuickPickItem(pick)) { - const selectedItem = pick.optionItem; - if (!selectedItem.locked) { - this.delegate.setOption(selectedItem); - } - } - quickPick.hide(); - })); - - disposables.add(quickPick.onDidHide(() => { - currentSearchCts?.cancel(); - currentSearchCts?.dispose(); - disposables.dispose(); - resolve(); - })); - }); - } - } - - private createQuickPickItem( - item: IChatSessionProviderOptionItem, - ): ISearchableOptionQuickPickItem { - const iconClass = item.icon ? ThemeIcon.asClassName(item.icon) : undefined; - - return { - label: item.name, - description: item.description, - iconClass, - disabled: item.locked, - optionItem: item, - }; - } - - /** - * Opens the picker programmatically. - */ - override show(): void { - const optionGroup = this.delegate.getOptionGroup(); - if (optionGroup) { - this.showSearchableQuickPick(optionGroup); - } - } -} diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 08a4ed41ac98b..9a65ec306f09a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -108,7 +108,6 @@ import { IChatWidget, IChatWidgetViewModelChangeEvent, ISessionTypePickerDelegat import { ChatEditingShowChangesAction, ViewAllSessionChangesAction, ViewPreviousEditsAction } from '../../chatEditing/chatEditingActions.js'; import { resizeImage } from '../../chatImageUtils.js'; import { ChatSessionPickerActionItem, IChatSessionPickerDelegate } from '../../chatSessions/chatSessionPickerActionItem.js'; -import { SearchableOptionPickerActionItem } from '../../chatSessions/searchableOptionPickerActionItem.js'; import { IChatContextService } from '../../contextContrib/chatContextService.js'; import { IDisposableReference } from '../chatContentParts/chatCollections.js'; import { ChatQuestionCarouselPart, IChatQuestionCarouselOptions } from '../chatContentParts/chatQuestionCarouselPart.js'; @@ -387,7 +386,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private permissionWidget: PermissionPickerActionItem | undefined; private sessionTargetWidget: SessionTypePickerActionItem | undefined; private delegationWidget: DelegationSessionPickerActionItem | undefined; - private readonly chatSessionPickerWidgets = this._register(new DisposableMap()); + private readonly chatSessionPickerWidgets = this._register(new DisposableMap()); private chatSessionPickerContainer: HTMLElement | undefined; private _lastSessionPickerAction: MenuItemAction | undefined; private _lastSessionPickerOptions: IChatInputPickerOptions | undefined; @@ -852,7 +851,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge /** * Create picker widgets for all option groups available for the current session type. */ - private createChatSessionPickerWidgets(action: MenuItemAction, pickerOptions?: IChatInputPickerOptions): (ChatSessionPickerActionItem | SearchableOptionPickerActionItem)[] { + private createChatSessionPickerWidgets(action: MenuItemAction, pickerOptions?: IChatInputPickerOptions): ChatSessionPickerActionItem[] { this._lastSessionPickerAction = action; this._lastSessionPickerOptions = pickerOptions; @@ -864,7 +863,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const { visibleGroupIds, optionGroups, effectiveSessionType } = result; this.chatSessionPickerWidgets.clearAndDisposeAll(); - const widgets: (ChatSessionPickerActionItem | SearchableOptionPickerActionItem)[] = []; + const widgets: ChatSessionPickerActionItem[] = []; for (const optionGroup of optionGroups) { if (!visibleGroupIds.has(optionGroup.id)) { continue; @@ -901,7 +900,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } }; - const widget = this.instantiationService.createInstance(optionGroup.searchable ? SearchableOptionPickerActionItem : ChatSessionPickerActionItem, action, initialState, itemDelegate, pickerOptions); + const widget = this.instantiationService.createInstance(ChatSessionPickerActionItem, action, initialState, itemDelegate, pickerOptions); this.chatSessionPickerWidgets.set(optionGroup.id, widget); widgets.push(widget); } @@ -3337,7 +3336,7 @@ function getLastPosition(model: ITextModel): IPosition { const chatInputEditorContainerSelector = '.interactive-input-editor'; setupSimpleEditorSelectionStyling(chatInputEditorContainerSelector); -type ChatSessionPickerWidget = ChatSessionPickerActionItem | SearchableOptionPickerActionItem; +type ChatSessionPickerWidget = ChatSessionPickerActionItem; class ChatSessionPickersContainerActionItem extends ActionViewItem { constructor( diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index 365586b3f51f5..f6f48f10ea892 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -52,8 +52,6 @@ export interface IChatSessionProviderOptionGroup { readonly name: string; readonly description?: string; readonly items: readonly IChatSessionProviderOptionItem[]; - readonly searchable?: boolean; - readonly onSearch?: (query: string, token: CancellationToken) => Thenable; /** * A context key expression that controls visibility of this option group picker. * When specified, the picker is only visible when the expression evaluates to true. diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index 96adb30967185..8a26c1a2b3a27 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -629,27 +629,11 @@ declare module 'vscode' { */ readonly when?: string; - /** - * When true, displays a searchable QuickPick with a "See more..." option. - * Recommended for option groups with additional async items (e.g., repositories). - */ - readonly searchable?: boolean; - /** * An icon for the option group shown in UI. */ readonly icon?: ThemeIcon; - /** - * Handler for dynamic search when `searchable` is true. - * Called when the user types in the searchable QuickPick or clicks "See more..." to load additional items. - * - * @param query The search query entered by the user. Empty string for initial load. - * @param token A cancellation token. - * @returns Additional items to display in the searchable QuickPick. - */ - readonly onSearch?: (query: string, token: CancellationToken) => Thenable; - /** * Optional commands. * From 5ead6aec425f43e2f057f88bee42759d2745e377 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:31:21 -0700 Subject: [PATCH 02/13] Merge fixes --- src/vs/workbench/api/browser/mainThreadChatSessions.ts | 8 +------- src/vs/workbench/api/common/extHostChatSessions.ts | 8 -------- .../workbench/contrib/chat/common/chatSessionsService.ts | 2 +- 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index 11eed0c4d3d8c..2317c79d8a61d 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -644,13 +644,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat } private _applyOptionGroups(handle: number, chatSessionType: string, optionGroups: readonly IChatSessionProviderOptionGroup[]): void { - const groupsWithCallbacks = optionGroups.map(group => ({ - ...group, - onSearch: group.searchable ? async (query: string, token: CancellationToken) => { - return await this._proxy.$invokeOptionGroupSearch(handle, group.id, query, token); - } : undefined, - })); - this._chatSessionsService.setOptionGroupsForSessionType(chatSessionType, handle, groupsWithCallbacks); + this._chatSessionsService.setOptionGroupsForSessionType(chatSessionType, handle, optionGroups); } private getController(handle: number): MainThreadChatSessionItemController { diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index b3d0bb8b9d412..3aec612e55708 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -782,14 +782,6 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio return undefined; } - private _getControllerForContentProviderHandle(handle: number) { - const entry = this._chatSessionContentProviders.get(handle); - if (!entry) { - return undefined; - } - return this.getChatSessionItemController(entry.chatSessionScheme); - } - private _createInputStateFromOptions( groups: readonly vscode.ChatSessionProviderOptionGroup[], sessionOptions?: ReadonlyArray<{ optionId: string; value: string }>, diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index e046147eede78..0a4b15bcf42ea 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -424,7 +424,7 @@ export interface IChatSessionsService { readonly onDidChangeOptionGroups: Event; getOptionGroupsForSessionType(chatSessionType: string): IChatSessionProviderOptionGroup[] | undefined; - setOptionGroupsForSessionType(chatSessionType: string, handle: number, optionGroups?: IChatSessionProviderOptionGroup[]): void; + setOptionGroupsForSessionType(chatSessionType: string, handle: number, optionGroups?: readonly IChatSessionProviderOptionGroup[]): void; /** * Get the default options for new sessions of this type, derived from option groups' From c63abfc1f097fb620687da92be3d967a8c232b9e Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 1 Apr 2026 23:01:49 -0700 Subject: [PATCH 03/13] Fire onDidChange for chat input state changes For #288457 Workaround until we can get core tracking input states properly --- .../workbench/api/common/extHostChatSessions.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index a572fbedda787..c6ea18a16e678 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -343,6 +343,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio readonly extension: IExtensionDescription; readonly disposable: DisposableStore; readonly onDidChangeChatSessionItemStateEmitter: Emitter; + readonly inputStates: Set; optionGroups?: readonly vscode.ChatSessionProviderOptionGroup[]; }>(); @@ -405,7 +406,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio throw new Error('Not implemented for providers'); }, createChatSessionInputState: (_options: vscode.ChatSessionProviderOptionItem[]) => { - return new ChatSessionInputStateImpl([]); + throw new Error('Not implemented for providers'); }, onDidChangeChatSessionItemState: onDidChangeChatSessionItemStateEmitter.event, newChatSessionItemHandler: undefined, @@ -418,7 +419,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio }, }; - this._chatSessionItemControllers.set(controllerHandle, { chatSessionType: chatSessionType, controller, extension, disposable: disposables, onDidChangeChatSessionItemStateEmitter }); + this._chatSessionItemControllers.set(controllerHandle, { chatSessionType: chatSessionType, controller, extension, disposable: disposables, onDidChangeChatSessionItemStateEmitter, inputStates: new Set() }); this._proxy.$registerChatSessionItemController(controllerHandle, chatSessionType); if (provider.onDidChangeChatSessionItems) { @@ -456,6 +457,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio let forkHandler: vscode.ChatSessionItemController['forkHandler']; let provideChatSessionInputStateHandler: vscode.ChatSessionItemController['getChatSessionInputState']; const onDidChangeChatSessionItemStateEmitter = disposables.add(new Emitter()); + const inputStates = new Set(); const collection = new ChatSessionItemCollectionImpl(controllerHandle, this._proxy); @@ -514,6 +516,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio })); void this._proxy.$updateChatSessionInputState(controllerHandle, serializableGroups); }); + inputStates.add(inputState); return inputState; }, dispose: () => { @@ -522,7 +525,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio }, }); - this._chatSessionItemControllers.set(controllerHandle, { controller, extension, disposable: disposables, chatSessionType: id, onDidChangeChatSessionItemStateEmitter }); + this._chatSessionItemControllers.set(controllerHandle, { controller, extension, disposable: disposables, chatSessionType: id, onDidChangeChatSessionItemStateEmitter, inputStates }); // Register the controller with the main thread this._proxy.$registerChatSessionItemController(controllerHandle, id); @@ -664,6 +667,13 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio } catch (error) { this._logService.error(`Error calling provideHandleOptionsChange for handle ${handle}, sessionResource ${sessionResource}:`, error); } + + // Temporary workaround: input state changes for one resource are propagated to all + // input states for the same resource type until we can make this session-specific. + const controllerData = this.getChatSessionItemController(sessionResource.scheme); + for (const inputState of controllerData?.inputStates ?? []) { + inputState._fireDidChange(); + } } async $provideChatSessionProviderOptions(handle: number, token: CancellationToken): Promise { From b2099b82ac1236b287d39cd8217094755ad2cb2c Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Wed, 1 Apr 2026 23:19:36 -0700 Subject: [PATCH 04/13] fix #307276 (#307294) * fix #307276. * :lipstick: --- .../chatExternalPathConfirmation.ts | 6 +- .../chatExternalPathConfirmation.test.ts | 204 ++++++++++++++++++ 2 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/common/tools/builtinTools/chatExternalPathConfirmation.test.ts diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/chatExternalPathConfirmation.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/chatExternalPathConfirmation.ts index ee75a3bc056f5..526fb48384ece 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/chatExternalPathConfirmation.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/chatExternalPathConfirmation.ts @@ -193,15 +193,13 @@ export class ChatExternalPathConfirmationContribution implements ILanguageModelT select: async () => { const gitRootUri = await findGitRoot(pathUri); gitRootCache.set(pathUri, gitRootUri ?? null); - if (!gitRootUri) { - return false; - } let folders = allowlist.get(sessionResource); if (!folders) { folders = new ResourceSet(); allowlist.set(sessionResource, folders); } - folders.add(gitRootUri); + // If we found the git root, allow the entire repo; otherwise fall back to just this folder + folders.add(gitRootUri ?? folderUri); return true; } }); diff --git a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/chatExternalPathConfirmation.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/chatExternalPathConfirmation.test.ts new file mode 100644 index 0000000000000..a046a5302bb76 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/chatExternalPathConfirmation.test.ts @@ -0,0 +1,204 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { ILabelService } from '../../../../../../../platform/label/common/label.js'; +import { ToolConfirmKind } from '../../../../common/chatService/chatService.js'; +import { ChatExternalPathConfirmationContribution, IExternalPathInfo } from '../../../../common/tools/builtinTools/chatExternalPathConfirmation.js'; +import { ILanguageModelToolConfirmationRef } from '../../../../common/tools/languageModelToolsConfirmationService.js'; + +suite('ChatExternalPathConfirmationContribution', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + const sessionResource = URI.parse('vscode-chat-session:/session/1'); + const source = { type: 'internal' as const, label: 'test' }; + const mockLabelService = { getUriLabel: (uri: URI) => uri.fsPath } as ILabelService; + + function createRef(filePath: string, isDirectory = false): ILanguageModelToolConfirmationRef { + return { + toolId: 'copilot_readFile', + source, + parameters: isDirectory ? { path: filePath } : { filePath }, + chatSessionResource: sessionResource, + }; + } + + function createContribution(findGitRoot?: (pathUri: URI) => Promise): ChatExternalPathConfirmationContribution { + const getPathInfo = (ref: ILanguageModelToolConfirmationRef): IExternalPathInfo | undefined => { + const params = ref.parameters as { filePath?: string; path?: string }; + if (params?.filePath) { + return { path: params.filePath, isDirectory: false }; + } + if (params?.path) { + return { path: params.path, isDirectory: true }; + } + return undefined; + }; + + const contribution = new ChatExternalPathConfirmationContribution( + getPathInfo, + mockLabelService, + findGitRoot, + ); + disposables.add(contribution); + return contribution; + } + + test('getPreConfirmAction returns undefined with no allowlist entries', () => { + const contribution = createContribution(); + const ref = createRef('/external/repo/src/file.ts'); + const result = contribution.getPreConfirmAction(ref); + assert.strictEqual(result, undefined); + }); + + test('allow folder in session works', async () => { + const contribution = createContribution(); + const ref = createRef('/external/repo/src/file.ts'); + + const actions = contribution.getPreConfirmActions(ref); + assert.ok(actions.length >= 1); + const folderAction = actions[0]; + assert.ok(folderAction.label.includes('folder')); + + const shouldConfirm = await folderAction.select(); + assert.strictEqual(shouldConfirm, true); + + // Same folder should now be auto-approved + const result = contribution.getPreConfirmAction(ref); + assert.deepStrictEqual(result, { type: ToolConfirmKind.UserAction }); + }); + + test('allow repo in session - first time resolves git root', async () => { + const gitRootUri = URI.file('/external/repo'); + const contribution = createContribution(async () => gitRootUri); + + const ref = createRef('/external/repo/src/file.ts'); + + const actions = contribution.getPreConfirmActions(ref); + // Should have "allow folder" and "allow repo" actions + assert.strictEqual(actions.length, 2); + const repoAction = actions[1]; + assert.ok(repoAction.label.includes('repository')); + + const shouldConfirm = await repoAction.select(); + assert.strictEqual(shouldConfirm, true); + + // File in the same repo should now be auto-approved + const ref2 = createRef('/external/repo/src/other.ts'); + const result = contribution.getPreConfirmAction(ref2); + assert.deepStrictEqual(result, { type: ToolConfirmKind.UserAction }); + }); + + test('allow repo in session - cached git root', async () => { + const gitRootUri = URI.file('/external/repo'); + const contribution = createContribution(async () => gitRootUri); + + const ref = createRef('/external/repo/src/file.ts'); + + // First call - resolves git root + const actions1 = contribution.getPreConfirmActions(ref); + const repoAction1 = actions1[1]; + await repoAction1.select(); + + // Second call with same path - should use cached git root + const actions2 = contribution.getPreConfirmActions(ref); + assert.strictEqual(actions2.length, 2); + const repoAction2 = actions2[1]; + assert.ok(repoAction2.detail!.includes(gitRootUri.fsPath)); + + const shouldConfirm = await repoAction2.select(); + assert.strictEqual(shouldConfirm, true); + }); + + test('allow repo in session - git root not found falls back to folder', async () => { + const contribution = createContribution(async () => undefined); + + const ref = createRef('/not-in-repo/file.ts'); + + const actions = contribution.getPreConfirmActions(ref); + assert.strictEqual(actions.length, 2); + const repoAction = actions[1]; + + // Should still confirm (falls back to allowing the folder) + const shouldConfirm = await repoAction.select(); + assert.strictEqual(shouldConfirm, true); + + // The containing folder should be auto-approved + const result = contribution.getPreConfirmAction(ref); + assert.deepStrictEqual(result, { type: ToolConfirmKind.UserAction }); + }); + + test('allow repo in session - hides option after git root not found', async () => { + const contribution = createContribution(async () => undefined); + + const ref = createRef('/not-in-repo/file.ts'); + + // First call - resolve returns undefined, caches null + const actions1 = contribution.getPreConfirmActions(ref); + assert.strictEqual(actions1.length, 2); + await actions1[1].select(); + + // Second call - should not show repo option (cached === null) + const actions2 = contribution.getPreConfirmActions(ref); + assert.strictEqual(actions2.length, 1); + }); + + test('allow repo in session - different files in same repo', async () => { + const gitRootUri = URI.file('/external/repo'); + const contribution = createContribution(async () => gitRootUri); + + const ref1 = createRef('/external/repo/src/a.ts'); + const ref2 = createRef('/external/repo/lib/b.ts'); + const ref3 = createRef('/external/repo/deep/nested/c.ts'); + + // Allow repo via first file + const actions = contribution.getPreConfirmActions(ref1); + await actions[1].select(); + + // All files in the repo should be auto-approved + assert.deepStrictEqual(contribution.getPreConfirmAction(ref1), { type: ToolConfirmKind.UserAction }); + assert.deepStrictEqual(contribution.getPreConfirmAction(ref2), { type: ToolConfirmKind.UserAction }); + assert.deepStrictEqual(contribution.getPreConfirmAction(ref3), { type: ToolConfirmKind.UserAction }); + + // File outside the repo should NOT be auto-approved + const refOutside = createRef('/other/place/file.ts'); + assert.strictEqual(contribution.getPreConfirmAction(refOutside), undefined); + }); + + test('session allowlist is per-session', async () => { + const gitRootUri = URI.file('/external/repo'); + const contribution = createContribution(async () => gitRootUri); + + const ref = createRef('/external/repo/src/file.ts'); + const actions = contribution.getPreConfirmActions(ref); + await actions[1].select(); + + // Same file, different session + const refOtherSession: ILanguageModelToolConfirmationRef = { + toolId: 'copilot_readFile', + source, + parameters: { filePath: '/external/repo/src/file.ts' }, + chatSessionResource: URI.parse('vscode-chat-session:/session/2'), + }; + assert.strictEqual(contribution.getPreConfirmAction(refOtherSession), undefined); + }); + + test('reset clears all allowlists', async () => { + const gitRootUri = URI.file('/external/repo'); + const contribution = createContribution(async () => gitRootUri); + + const ref = createRef('/external/repo/src/file.ts'); + const actions = contribution.getPreConfirmActions(ref); + await actions[1].select(); + + assert.deepStrictEqual(contribution.getPreConfirmAction(ref), { type: ToolConfirmKind.UserAction }); + + contribution.reset(); + + assert.strictEqual(contribution.getPreConfirmAction(ref), undefined); + }); +}); From 8b5453f39126d4559b65e6fffce0746f16d464f3 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 1 Apr 2026 23:32:48 -0700 Subject: [PATCH 05/13] Update src/vs/workbench/api/common/extHostChatSessions.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/vs/workbench/api/common/extHostChatSessions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index c6ea18a16e678..8e50d6206cd2d 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -405,7 +405,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio createChatSessionItem: (_resource: vscode.Uri, _label: string) => { throw new Error('Not implemented for providers'); }, - createChatSessionInputState: (_options: vscode.ChatSessionProviderOptionItem[]) => { + createChatSessionInputState: (_options: vscode.ChatSessionProviderOptionGroup[]) => { throw new Error('Not implemented for providers'); }, onDidChangeChatSessionItemState: onDidChangeChatSessionItemStateEmitter.event, From a3c51e91008dae00b557f8a8b3c9b6f459d6556b Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 1 Apr 2026 23:34:06 -0700 Subject: [PATCH 06/13] Revert provider change for now --- src/vs/workbench/api/common/extHostChatSessions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index 8e50d6206cd2d..e3df9db42f7ac 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -406,7 +406,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio throw new Error('Not implemented for providers'); }, createChatSessionInputState: (_options: vscode.ChatSessionProviderOptionGroup[]) => { - throw new Error('Not implemented for providers'); + return new ChatSessionInputStateImpl([]); }, onDidChangeChatSessionItemState: onDidChangeChatSessionItemStateEmitter.event, newChatSessionItemHandler: undefined, From 9f901c648a85624364ddf9c17eba47d3bb0df34c Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Thu, 2 Apr 2026 00:45:25 -0700 Subject: [PATCH 07/13] Update UI fixes (#307318) --- .../browser/media/updateTitleBarEntry.css | 3 ++- .../update/browser/updateTitleBarEntry.ts | 21 ++++++++++++---- .../contrib/update/browser/updateTooltip.ts | 24 +++++++------------ 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/update/browser/media/updateTitleBarEntry.css b/src/vs/workbench/contrib/update/browser/media/updateTitleBarEntry.css index 266a0a4484895..5ae71bac4789b 100644 --- a/src/vs/workbench/contrib/update/browser/media/updateTitleBarEntry.css +++ b/src/vs/workbench/contrib/update/browser/media/updateTitleBarEntry.css @@ -8,8 +8,9 @@ align-items: center; border-radius: var(--vscode-cornerRadius-medium); white-space: nowrap; - padding: 0px 12px; + padding: 0px 8px; height: 22px; + box-sizing: border-box; background-color: transparent; border: 1px solid transparent; } diff --git a/src/vs/workbench/contrib/update/browser/updateTitleBarEntry.ts b/src/vs/workbench/contrib/update/browser/updateTitleBarEntry.ts index d6c9fdaf0ce19..09da755a08058 100644 --- a/src/vs/workbench/contrib/update/browser/updateTitleBarEntry.ts +++ b/src/vs/workbench/contrib/update/browser/updateTitleBarEntry.ts @@ -309,36 +309,47 @@ export class UpdateTitleBarEntry extends BaseActionViewItem { this.content.style.removeProperty('--update-progress'); const label = dom.append(this.content, dom.$('.indicator-label')); - label.textContent = localize('updateIndicator.update', "Update"); - switch (state.type) { case StateType.Disabled: + label.textContent = localize('updateIndicator.update', "Update"); this.content.classList.add('update-disabled'); break; case StateType.CheckingForUpdates: - case StateType.Overwriting: + label.textContent = localize('updateIndicator.checking', "Checking..."); this.renderProgressState(this.content); break; - case StateType.Restarting: - label.textContent = localize('updateIndicator.restarting', "Restarting"); + case StateType.Overwriting: + label.textContent = localize('updateIndicator.overwriting', "Updating..."); this.renderProgressState(this.content); break; case StateType.AvailableForDownload: case StateType.Downloaded: case StateType.Ready: + label.textContent = localize('updateIndicator.update', "Update"); this.content.classList.add('prominent'); break; case StateType.Downloading: + label.textContent = localize('updateIndicator.downloading', "Downloading..."); this.renderProgressState(this.content, computeProgressPercent(state.downloadedBytes, state.totalBytes)); break; case StateType.Updating: + label.textContent = localize('updateIndicator.installing', "Installing..."); this.renderProgressState(this.content, computeProgressPercent(state.currentProgress, state.maxProgress)); break; + + case StateType.Restarting: + label.textContent = localize('updateIndicator.restarting', "Restarting..."); + this.renderProgressState(this.content); + break; + + default: + label.textContent = localize('updateIndicator.update', "Update"); + break; } } diff --git a/src/vs/workbench/contrib/update/browser/updateTooltip.ts b/src/vs/workbench/contrib/update/browser/updateTooltip.ts index e08c8fd237ecd..b64a06871d4c2 100644 --- a/src/vs/workbench/contrib/update/browser/updateTooltip.ts +++ b/src/vs/workbench/contrib/update/browser/updateTooltip.ts @@ -4,8 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../base/browser/dom.js'; -import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; -import { toAction } from '../../../../base/common/actions.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; @@ -15,7 +13,7 @@ import { localize } from '../../../../nls.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IHoverService, nativeHoverDelegate } from '../../../../platform/hover/browser/hover.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IMarkdownRendererService, openLinkFromMarkdown } from '../../../../platform/markdown/browser/markdownRenderer.js'; import { IMeteredConnectionService } from '../../../../platform/meteredConnection/common/meteredConnection.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; @@ -88,14 +86,6 @@ export class UpdateTooltip extends Disposable { const header = dom.append(this.domNode, dom.$('.header')); this.titleNode = dom.append(header, dom.$('.title')); - const actionBar = this._register(new ActionBar(header, { hoverDelegate: nativeHoverDelegate })); - actionBar.push(toAction({ - id: 'update.openSettings', - label: localize('updateTooltip.settingsTooltip', "Update Settings"), - class: ThemeIcon.asClassName(Codicon.gear), - run: () => this.runCommandAndClose('workbench.action.openSettings', '@id:update*'), - }), { icon: true, label: false }); - // Product info section this.productInfoNode = dom.append(this.domNode, dom.$('.product-info')); @@ -142,7 +132,7 @@ export class UpdateTooltip extends Disposable { this.buttonBar = dom.append(this.domNode, dom.$('.button-bar')); this.releaseNotesButton = dom.append(this.buttonBar, dom.$('button.release-notes-button')) as HTMLButtonElement; - this.releaseNotesButton.textContent = localize('updateTooltip.viewReleaseNotes', "View Release Notes"); + this.releaseNotesButton.textContent = localize('updateTooltip.viewReleaseNotes', "Release Notes"); this._register(dom.addDisposableListener(this.releaseNotesButton, 'click', () => { if (this.releaseNotesVersion) { this.runCommandAndClose(ShowCurrentReleaseNotesActionId, this.releaseNotesVersion); @@ -374,8 +364,12 @@ export class UpdateTooltip extends Disposable { } private renderReady({ update }: Ready) { - this.renderTitleAndInfo(localize('updateTooltip.updateInstalledTitle', "Update Installed"), update); - this.renderActionButton(localize('updateTooltip.restartButton', "Restart"), 'update.restart'); + if (this.configurationService.getValue('update.mode') === 'manual') { + this.renderTitleAndInfo(localize('updateTooltip.updateInstalledTitle', "Update Installed"), update); + this.renderActionButton(localize('updateTooltip.restartButton', "Restart"), 'update.restart'); + } else { + this.renderTitleAndInfo(localize('updateTooltip.restartToUpdateTitle', "Restart to Update"), update); + } } private renderOverwriting({ update }: Overwriting) { @@ -461,6 +455,7 @@ export class UpdateTooltip extends Disposable { // Release notes button this.releaseNotesVersion = version ?? this.productService.version; this.releaseNotesButton.style.display = this.releaseNotesVersion ? '' : 'none'; + this.releaseNotesButton.style.marginRight = this.releaseNotesVersion ? 'auto' : ''; this.buttonBar.style.display = this.releaseNotesVersion ? '' : 'none'; } @@ -468,7 +463,6 @@ export class UpdateTooltip extends Disposable { this.actionButton.textContent = label; this.actionButton.dataset.commandId = commandId; this.actionButton.style.display = ''; - this.releaseNotesButton.style.marginRight = 'auto'; } private renderMessage(message: string, icon?: ThemeIcon) { From 8773343b45f326d5018c0abe9ab1f7532afa6e95 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 2 Apr 2026 00:45:38 -0700 Subject: [PATCH 08/13] agentHost: fix ssh always using stable CLI (#307328) Co-authored-by: Copilot --- .../node/sshRemoteAgentHostService.ts | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/vs/platform/agentHost/node/sshRemoteAgentHostService.ts b/src/vs/platform/agentHost/node/sshRemoteAgentHostService.ts index ee6881ce49a5b..443bd8c2a1e86 100644 --- a/src/vs/platform/agentHost/node/sshRemoteAgentHostService.ts +++ b/src/vs/platform/agentHost/node/sshRemoteAgentHostService.ts @@ -13,6 +13,7 @@ import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; import { localize } from '../../../nls.js'; import { ILogService } from '../../log/common/log.js'; +import { IProductService } from '../../product/common/productService.js'; import { ISSHRemoteAgentHostMainService, SSHAuthMethod, @@ -50,8 +51,13 @@ interface SSHClient { const LOG_PREFIX = '[SSHRemoteAgentHost]'; /** Install location for the VS Code CLI on the remote machine. */ -const REMOTE_CLI_DIR = '~/.vscode-cli'; -const REMOTE_CLI_BIN = `${REMOTE_CLI_DIR}/code`; +function getRemoteCLIDir(quality: string): string { + return quality === 'stable' || !quality ? '~/.vscode-cli' : `~/.vscode-cli-${quality}`; +} +function getRemoteCLIBin(quality: string): string { + const binaryName = quality === 'stable' ? 'code' : 'code-insiders'; + return `${getRemoteCLIDir(quality)}/${binaryName}`; +} /** Escape a string for use as a single shell argument (single-quote wrapping). */ function shellEscape(s: string): string { @@ -135,10 +141,11 @@ function redactToken(text: string): string { function startRemoteAgentHost( client: SSHClient, logService: ILogService, + quality: string, commandOverride?: string, ): Promise<{ port: number; connectionToken: string | undefined; stream: SSHChannel }> { return new Promise((resolve, reject) => { - const baseCmd = commandOverride ?? `${REMOTE_CLI_BIN} agent-host --port 0 --accept-server-license-terms`; + const baseCmd = commandOverride ?? `${getRemoteCLIBin(quality)} agent-host --port 0 --accept-server-license-terms`; // Wrap in a login shell so the agent host process inherits the // user's PATH and environment from ~/.bash_profile / ~/.bashrc // (ssh2 exec runs a non-interactive non-login shell by default). @@ -338,6 +345,7 @@ export class SSHRemoteAgentHostMainService extends Disposable implements ISSHRem constructor( @ILogService private readonly _logService: ILogService, + @IProductService private readonly _productService: IProductService, ) { super(); } @@ -393,7 +401,7 @@ export class SSHRemoteAgentHostMainService extends Disposable implements ISSHRem // 4. Start agent-host and capture port/token reportProgress(localize('sshProgressStartingAgent', "Starting remote agent host...")); - const { port: remotePort, connectionToken, stream: agentStream } = await startRemoteAgentHost(sshClient, this._logService, config.remoteAgentHostCommand); + const { port: remotePort, connectionToken, stream: agentStream } = await startRemoteAgentHost(sshClient, this._logService, this._quality, config.remoteAgentHostCommand); // 5. Connect to remote agent host via WebSocket relay (no local TCP port) reportProgress(localize('sshProgressForwarding', "Connecting to remote agent host...")); @@ -627,21 +635,26 @@ export class SSHRemoteAgentHostMainService extends Disposable implements ISSHRem }); } + private get _quality(): string { + return this._productService.quality || 'insider'; + } + private async _ensureCLIInstalled(client: SSHClient, platform: { os: string; arch: string }, reportProgress: (message: string) => void): Promise { - const { code } = await sshExec(client, `${REMOTE_CLI_BIN} --version`, { ignoreExitCode: true }); + const cliDir = getRemoteCLIDir(this._quality); + const cliBin = getRemoteCLIBin(this._quality); + const { code } = await sshExec(client, `${cliBin} --version`, { ignoreExitCode: true }); if (code === 0) { this._logService.info(`${LOG_PREFIX} VS Code CLI already installed on remote`); return; } reportProgress(localize('sshProgressDownloadingCLI', "Installing VS Code CLI on remote...")); - const quality = 'stable'; - const url = buildCLIDownloadUrl(platform.os, platform.arch, quality); + const url = buildCLIDownloadUrl(platform.os, platform.arch, this._quality); const installCmd = [ - `mkdir -p ${REMOTE_CLI_DIR}`, - `curl -fsSL '${url}' | tar xz -C ${REMOTE_CLI_DIR}`, - `chmod +x ${REMOTE_CLI_BIN}`, + `mkdir -p ${cliDir}`, + `curl -fsSL '${url}' | tar xz -C ${cliDir}`, + `chmod +x ${cliBin}`, ].join(' && '); await sshExec(client, installCmd); From 7f7a02bda9e3ec4b398a6bca6baf59e2f1b4813c Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 2 Apr 2026 07:52:08 +0000 Subject: [PATCH 09/13] Sessions - add context key to track uncommitted changes (#307346) * Sessions - add context key to track uncommitted changes * Sessions - show "Mark as Done" when there are no outgoing changes * Pull request feedback * Tweak context keys to avoid flickering --- .../contrib/changes/browser/changesView.ts | 25 +++++++++++++++++-- .../browser/views/sessionsViewActions.ts | 12 +++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/vs/sessions/contrib/changes/browser/changesView.ts b/src/vs/sessions/contrib/changes/browser/changesView.ts index 61ecb4ebd42b3..238f7e8bbd020 100644 --- a/src/vs/sessions/contrib/changes/browser/changesView.ts +++ b/src/vs/sessions/contrib/changes/browser/changesView.ts @@ -115,6 +115,7 @@ const hasPullRequestContextKey = new RawContextKey('sessions.hasPullReq const hasOpenPullRequestContextKey = new RawContextKey('sessions.hasOpenPullRequest', false); const hasIncomingChangesContextKey = new RawContextKey('sessions.hasIncomingChanges', false); const hasOutgoingChangesContextKey = new RawContextKey('sessions.hasOutgoingChanges', false); +const hasUncommittedChangesContextKey = new RawContextKey('sessions.hasUncommittedChanges', true); // --- List Item @@ -912,6 +913,10 @@ export class ChangesViewPane extends ViewPane { const outgoingChangesObs = derived(reader => { const repository = this.viewModel.activeSessionRepositoryObs.read(reader); const repositoryState = repository?.state.read(reader); + if (!repositoryState) { + return 0; + } + return repositoryState?.HEAD?.ahead ?? 0; }); @@ -920,6 +925,19 @@ export class ChangesViewPane extends ViewPane { return outgoingChanges > 0; })); + this.renderDisposables.add(bindContextKey(hasUncommittedChangesContextKey, this.scopedContextKeyService, reader => { + const repository = this.viewModel.activeSessionRepositoryObs.read(reader); + const repositoryState = repository?.state.read(reader); + if (!repositoryState) { + return true; + } + + return (repositoryState?.mergeChanges.length ?? 0) > 0 || + (repositoryState?.indexChanges.length ?? 0) > 0 || + (repositoryState?.workingTreeChanges.length ?? 0) > 0 || + (repositoryState?.untrackedChanges.length ?? 0) > 0; + })); + const scopedServiceCollection = new ServiceCollection([IContextKeyService, this.scopedContextKeyService]); const scopedInstantiationService = this.instantiationService.createChild(scopedServiceCollection); this.renderDisposables.add(scopedInstantiationService); @@ -965,7 +983,10 @@ export class ChangesViewPane extends ViewPane { ? { args: [sessionResource, this.agentSessionsService.getSession(sessionResource)?.metadata] } : { shouldForwardArgs: true }, buttonConfigProvider: (action) => { - if (action.id === 'github.copilot.chat.createPullRequestCopilotCLIAgentSession.updatePR') { + if ( + action.id === 'github.copilot.sessions.sync' || + action.id === 'github.copilot.chat.createPullRequestCopilotCLIAgentSession.updatePR' + ) { const customLabel = outgoingChanges > 0 ? `${action.label} ${outgoingChanges}↑` : action.label; @@ -995,7 +1016,7 @@ export class ChangesViewPane extends ViewPane { action.id === 'github.copilot.chat.checkoutPullRequestReroute' || action.id === 'pr.checkoutFromChat' || action.id === 'github.copilot.sessions.initializeRepository' || - action.id === 'github.copilot.sessions.commitChanges' || + action.id === 'github.copilot.sessions.commit' || action.id === 'agentSession.markAsDone' ) { return { showIcon: true, showLabel: true, isSecondary: false }; diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts index c37cdf36a5b22..c37ed9ac42ed5 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts @@ -684,8 +684,16 @@ registerAction2(class MarkSessionAsDoneAction extends Action2 { order: 1, when: ContextKeyExpr.and( IsSessionsWindowContext, - ContextKeyExpr.equals('sessions.hasPullRequest', true), - ContextKeyExpr.equals('sessions.hasOpenPullRequest', false), + ContextKeyExpr.or( + ContextKeyExpr.and( + ContextKeyExpr.equals('sessions.hasPullRequest', false), + ContextKeyExpr.equals('sessions.hasOutgoingChanges', false), + ), + ContextKeyExpr.and( + ContextKeyExpr.equals('sessions.hasPullRequest', true), + ContextKeyExpr.equals('sessions.hasOpenPullRequest', false), + ) + ) ) }] }); From a230337699073152d72ac71d559b7120bb65f514 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 2 Apr 2026 09:57:47 +0200 Subject: [PATCH 10/13] sessions - more renames (#307343) --- extensions/github-authentication/src/common/env.ts | 6 +++--- extensions/microsoft-authentication/src/common/env.ts | 6 +++--- .../electron-browser/agentSessions/agentSessionsActions.ts | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/extensions/github-authentication/src/common/env.ts b/extensions/github-authentication/src/common/env.ts index 5456fb864ee4c..56cad4beb339a 100644 --- a/extensions/github-authentication/src/common/env.ts +++ b/extensions/github-authentication/src/common/env.ts @@ -9,9 +9,9 @@ const VALID_DESKTOP_CALLBACK_SCHEMES = [ 'vscode', 'vscode-insiders', 'vscode-exploration', - 'vscode-sessions', - 'vscode-sessions-insiders', - 'vscode-sessions-exploration', + 'vscode-agents', + 'vscode-agents-insiders', + 'vscode-agents-exploration', // On Windows, some browsers don't seem to redirect back to OSS properly. // As a result, you get stuck in the auth flow. We exclude this from the // list until we can figure out a way to fix this behavior in browsers. diff --git a/extensions/microsoft-authentication/src/common/env.ts b/extensions/microsoft-authentication/src/common/env.ts index b63c94195a9e9..1c8505503ada2 100644 --- a/extensions/microsoft-authentication/src/common/env.ts +++ b/extensions/microsoft-authentication/src/common/env.ts @@ -10,9 +10,9 @@ const VALID_DESKTOP_CALLBACK_SCHEMES = [ 'vscode', 'vscode-insiders', 'vscode-exploration', - 'vscode-sessions', - 'vscode-sessions-insiders', - 'vscode-sessions-exploration', + 'vscode-agents', + 'vscode-agents-insiders', + 'vscode-agents-exploration', // On Windows, some browsers don't seem to redirect back to OSS properly. // As a result, you get stuck in the auth flow. We exclude this from the // list until we can figure out a way to fix this behavior in browsers. 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 ee9d0eb3206df..f371c6474d1aa 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts @@ -42,10 +42,10 @@ export class OpenAgentsWindowAction extends Action2 { if (environmentService.isBuilt && (isMacintosh || isWindows)) { const scheme = productService.quality === 'stable' - ? 'vscode-sessions' + ? 'vscode-agents' : productService.quality === 'exploration' - ? 'vscode-sessions-exploration' - : 'vscode-sessions-insiders'; + ? 'vscode-agents-exploration' + : 'vscode-agents-insiders'; await openerService.open(URI.from({ scheme, authority: Schemas.file }), { openExternal: true }); } else { From b955f7ce8c698d24e12209d18f3c5adeff018a54 Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Thu, 2 Apr 2026 01:02:15 -0700 Subject: [PATCH 11/13] Add General Purpose agent support behind experiment (#306871) * Add General Purpose agent support behind experiment Add a built-in 'General Purpose' agent to the runSubagent tool, gated behind the 'chat.generalPurposeAgent' experiment treatment: - Add GeneralPurposeAgentName constant - Make agentName required and route undefined/GP names to built-in agent - Render GP agent in automatic instructions agents block - Clean up duplicate DI injection in RunSubagentTool - Add unit tests for GP agent paths * Address PR feedback: fix Event cast, add experiment Emitter, try/catch, deterministic tests - Replace unsafe 'configEvent as Event' cast with dedicated Emitter (fixes tsgo typecheck CI failure) - Fire onDidUpdateToolData when experiment resolution changes the value - Add try/catch around getTreatment in computeAutomaticInstructions - Replace flaky setTimeout(0) in tests with Event.toPromise(onDidUpdateToolData) * Fix merge regression: restore fullLength in toolReferences range calculation The merge conflict resolution incorrectly replaced fullLength with name.length + 1 for toolReference OffsetRange calculation, and removed fullLength from variableReferences test expectations. Restore the original behavior from main. * Apply critical review fixes - Add error handler to _resolveExperiment() preventing unhandled promise rejections when getTreatment fails - Decouple GP agent from SubagentToolCustomAgents config gate so experiment works independently of custom agents setting - Fix redundant parens on Event listener arrow function - Add test for GP agent rendering without custom agents config * Remove unrelated promptsServiceImpl refactor from PR Reset promptsServiceImpl.ts and promptsService.test.ts back to main. These files contained an unrelated refactor (method renames, type simplifications) that was accidentally carried over during the port from PR #295494. * Fix GP agent description: inherit parent tools, not 'all tools' --- .../contrib/chat/browser/chat.contribution.ts | 9 ++ .../contrib/chat/common/constants.ts | 7 + .../computeAutomaticInstructions.ts | 21 ++- .../tools/builtinTools/runSubagentTool.ts | 93 ++++++++---- .../computeAutomaticInstructions.test.ts | 83 ++++++++++- .../builtinTools/runSubagentTool.test.ts | 141 +++++++++++++++--- 6 files changed, 297 insertions(+), 57 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 06e553b252893..690322fdd1170 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -1359,6 +1359,15 @@ configurationRegistry.registerConfiguration({ mode: 'auto' } }, + [ChatConfiguration.GeneralPurposeAgentEnabled]: { + type: 'boolean', + description: nls.localize('chat.generalPurposeAgent.enabled', "Controls whether the built-in General Purpose agent is available as a subagent."), + default: false, + tags: ['experimental', 'advanced'], + experiment: { + mode: 'auto' + } + }, [ChatConfiguration.SubagentsAllowInvocationsFromSubagents]: { type: 'boolean', description: nls.localize('chat.subagents.allowInvocationsFromSubagents', "Allow subagents to invoke subagents."), diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index eeba510b72b6e..0f8c8034f1b12 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -48,6 +48,7 @@ export enum ChatConfiguration { ChatViewProgressBadgeEnabled = 'chat.viewProgressBadge.enabled', ChatContextUsageEnabled = 'chat.contextUsage.enabled', SubagentToolCustomAgents = 'chat.customAgentInSubagent.enabled', + GeneralPurposeAgentEnabled = 'chat.generalPurposeAgent.enabled', SubagentsAllowInvocationsFromSubagents = 'chat.subagents.allowInvocationsFromSubagents', ShowCodeBlockProgressAnimation = 'chat.agent.codeBlockProgress', RestoreLastPanelSession = 'chat.restoreLastPanelSession', @@ -196,3 +197,9 @@ export const ChatEditorTitleMaxLength = 30; export const CHAT_TERMINAL_OUTPUT_MAX_PREVIEW_LINES = 1000; export const CONTEXT_MODELS_EDITOR = new RawContextKey('inModelsEditor', false); export const CONTEXT_MODELS_SEARCH_FOCUS = new RawContextKey('inModelsSearch', false); + +/** + * The built-in general-purpose agent name. When the model uses this name, + * the subagent inherits the parent's system prompt, model, and tools. + */ +export const GeneralPurposeAgentName = 'General Purpose'; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index 4849423cd9607..1b6fed663b4c6 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -27,7 +27,7 @@ import { ParsedPromptFile } from './promptFileParser.js'; import { AgentInstructionFileType, IAgentSkill, ICustomAgent, IInstructionFile, IPromptsService } from './service/promptsService.js'; import { AGENT_DEBUG_LOG_ENABLED_SETTING, AGENT_DEBUG_LOG_FILE_LOGGING_ENABLED_SETTING, TROUBLESHOOT_SKILL_PATH } from './promptTypes.js'; import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js'; -import { ChatConfiguration, ChatModeKind } from '../constants.js'; +import { ChatConfiguration, ChatModeKind, GeneralPurposeAgentName } from '../constants.js'; import { UserSelectedTools } from '../participants/chatAgents.js'; import { hash } from '../../../../../base/common/hash.js'; import { IAgentPlugin, IAgentPluginService } from '../plugins/agentPluginService.js'; @@ -431,7 +431,10 @@ export class ComputeAutomaticInstructions { entries.push('', '', ''); // add trailing newline } } - if (runSubagentTool && this._configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents)) { + if (runSubagentTool) { + const generalPurposeAgentEnabled = !!this._configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled); + + const customAgentsEnabled = !!this._configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents); const canUseAgent = (() => { if (!this._enabledSubagents || this._enabledSubagents.includes('*')) { return (agent: ICustomAgent) => agent.visibility.agentInvocable; @@ -440,12 +443,22 @@ export class ComputeAutomaticInstructions { return (agent: ICustomAgent) => subagents.includes(agent.name); } })(); - const agents = await this._promptsService.getCustomAgents(token); - if (agents.length > 0) { + const agents = customAgentsEnabled ? await this._promptsService.getCustomAgents(token) : []; + + if (generalPurposeAgentEnabled || agents.length > 0) { entries.push(''); entries.push('Here is a list of agents that can be used when running a subagent.'); entries.push('Each agent has optionally a description with the agent\'s purpose and expertise. When asked to run a subagent, choose the most appropriate agent from this list.'); entries.push(`Use the ${runSubagentTool.variable} tool with the agent name to run the subagent.`); + + if (generalPurposeAgentEnabled) { + // Built-in General Purpose agent, always available when experiment is on + entries.push(''); + entries.push(`${GeneralPurposeAgentName}`); + entries.push(`Full-capability agent for complex multi-step tasks requiring high-quality reasoning. Has access to the same tools and capabilities as the current agent and inherits the parent agent's model and system prompt. Use for tasks that don't fit a more specialized agent.`); + entries.push(''); + } + for (const agent of agents) { if (canUseAgent(agent)) { entries.push(''); diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts index 92b43a4993632..f953732e3414f 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -5,20 +5,20 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { Event } from '../../../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../../../base/common/event.js'; import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { IJSONSchema, IJSONSchemaMap } from '../../../../../../base/common/jsonSchema.js'; import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { generateUuid } from '../../../../../../base/common/uuid.js'; import { localize } from '../../../../../../nls.js'; -import { IConfigurationChangeEvent, IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { IProductService } from '../../../../../../platform/product/common/productService.js'; import { ChatRequestVariableSet } from '../../attachments/chatVariableEntries.js'; import { IChatProgress, IChatService } from '../../chatService/chatService.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, GeneralPurposeAgentName } from '../../constants.js'; import { ILanguageModelsService } from '../../languageModels.js'; import { ChatModel, IChatRequestModeInstructions } from '../../model/chatModel.js'; import { IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../../participants/chatAgents.js'; @@ -50,7 +50,8 @@ const BaseModelDescription = `Launch a new agent to handle complex, multi-step t - When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. - Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you. - The agent's outputs should generally be trusted -- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user\'s intent`; +- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user\'s intent +- If the user asks for a certain agent, you MUST provide that EXACT agent name (case-sensitive) to invoke that specific agent.`; export interface IRunSubagentToolInputParams { prompt: string; @@ -64,7 +65,8 @@ export class RunSubagentTool extends Disposable implements IToolImpl { static readonly Id = 'runSubagent'; - readonly onDidUpdateToolData: Event; + private readonly _onDidUpdateToolData = this._register(new Emitter()); + readonly onDidUpdateToolData: Event = this._onDidUpdateToolData.event; /** Hack to port data between prepare/invoke */ private readonly _resolvedModels = new Map(); @@ -78,42 +80,54 @@ export class RunSubagentTool extends Disposable implements IToolImpl { @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, @ILogService private readonly logService: ILogService, - @ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService, @IConfigurationService private readonly configurationService: IConfigurationService, @IPromptsService private readonly promptsService: IPromptsService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IProductService private readonly productService: IProductService, ) { super(); - this.onDidUpdateToolData = Event.filter(this.configurationService.onDidChangeConfiguration, e => - e.affectsConfiguration(ChatConfiguration.SubagentToolCustomAgents) - ); + + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => + e.affectsConfiguration(ChatConfiguration.SubagentToolCustomAgents) || + e.affectsConfiguration(ChatConfiguration.GeneralPurposeAgentEnabled) + )(() => this._onDidUpdateToolData.fire())); } getToolData(): IToolData { - let modelDescription = BaseModelDescription; - const inputSchema: IJSONSchema & { properties: IJSONSchemaMap } = { - type: 'object', - properties: { - prompt: { - type: 'string', - description: 'A detailed description of the task for the agent to perform' - }, - description: { - type: 'string', - description: 'A short (3-5 word) description of the task' - } + const modelDescription = BaseModelDescription; + const generalPurposeAgentEnabled = this.configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled); + const customAgentsEnabled = this.configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents); + + const properties: IJSONSchemaMap = { + prompt: { + type: 'string', + description: 'A detailed description of the task for the agent to perform' }, - required: ['prompt', 'description'] + description: { + type: 'string', + description: 'A short (3-5 word) description of the task' + } }; - if (this.configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents)) { - inputSchema.properties.agentName = { + if (customAgentsEnabled || generalPurposeAgentEnabled) { + properties.agentName = { type: 'string', - description: 'Optional name of a specific agent to invoke. If not provided, uses the current agent.' + description: generalPurposeAgentEnabled + ? 'Name of the agent to invoke.' + : 'Optional name of a specific agent to invoke. If not provided, uses the current agent.' }; - modelDescription += `\n- If the user asks for a certain agent, you MUST provide that EXACT agent name (case-sensitive) to invoke that specific agent.`; } + + const required: string[] = ['prompt', 'description']; + if (generalPurposeAgentEnabled) { + required.push('agentName'); + } + + const inputSchema: IJSONSchema & { properties: IJSONSchemaMap } = { + type: 'object', + properties, + required + }; const runSubagentToolData: IToolData = { id: RunSubagentTool.Id, toolReferenceName: VSCodeToolReference.runSubagent, @@ -161,8 +175,14 @@ export class RunSubagentTool extends Disposable implements IToolImpl { let resolvedModelName: string | undefined; const subAgentName = args.agentName; - if (subAgentName) { - subagent = await this.getSubAgentByName(subAgentName); + // Defensive: model may omit agentName despite schema requiring it + const gpEnabled = this.configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled); + const customAgentsEnabled = this.configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents); + const isGeneralPurpose = gpEnabled && (!subAgentName || subAgentName === GeneralPurposeAgentName); + const effectiveSubAgentName = isGeneralPurpose ? GeneralPurposeAgentName : subAgentName; + + if (subAgentName && !isGeneralPurpose) { + subagent = customAgentsEnabled ? await this.getSubAgentByName(subAgentName) : undefined; if (subagent) { // Check the pre-resolved model cache from prepareToolInvocation const cached = this._resolvedModels.get(invocation.callId); @@ -195,12 +215,15 @@ export class RunSubagentTool extends Disposable implements IToolImpl { modeInstructions = instructions && { name: subAgentName, content: instructions.content, - toolReferences: this.toolsService.toToolReferences(instructions.toolReferences), + toolReferences: this.languageModelToolsService.toToolReferences(instructions.toolReferences), metadata: instructions.metadata, isBuiltin: isBuiltinAgent(subagent.source, subagent.uri, this.productService), }; } else { - throw new Error(`Requested agent '${subAgentName}' not found. Try again with the correct agent name, or omit the agentName to use the current agent.`); + this._resolvedModels.delete(invocation.callId); + const baseHint = ' Try again with the correct agent name, or omit agentName to use the current agent.'; + const gpHint = gpEnabled ? ` Additionally, you can use '${GeneralPurposeAgentName}' for a full-capability agent.` : ''; + throw new Error(`Requested agent '${subAgentName}' not found.${baseHint}${gpHint}`); } } else { // No subagent name - clean up any cached entry and resolve model name from main model @@ -312,7 +335,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { variables: { variables: variableSet.asArray() }, location: ChatAgentLocation.Chat, subAgentInvocationId: subAgentInvocationId, - subAgentName: subAgentName, + subAgentName: effectiveSubAgentName, userSelectedModelId: modeModelId, modelConfiguration: modeModelId ? this.languageModelsService.getModelConfiguration(modeModelId) : undefined, userSelectedTools: modeTools, @@ -433,7 +456,11 @@ export class RunSubagentTool extends Disposable implements IToolImpl { async prepareToolInvocation(context: IToolInvocationPreparationContext, _token: CancellationToken): Promise { const args = context.parameters as IRunSubagentToolInputParams; - const subagent = args.agentName ? await this.getSubAgentByName(args.agentName) : undefined; + // Defensive: model may omit agentName despite schema requiring it + const gpEnabled = this.configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled); + const customAgentsEnabled = this.configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents); + const isGeneralPurpose = gpEnabled && (!args.agentName || args.agentName === GeneralPurposeAgentName); + const subagent = (args.agentName && !isGeneralPurpose && customAgentsEnabled) ? await this.getSubAgentByName(args.agentName) : undefined; // Resolve the model early and cache it for invoke() const resolved = this.resolveSubagentModel(subagent, context.modelId); @@ -444,7 +471,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { toolSpecificData: { kind: 'subagent', description: args.description, - agentName: subagent?.name, + agentName: isGeneralPurpose ? GeneralPurposeAgentName : (subagent?.name ?? args.agentName), prompt: args.prompt, modelName: resolved.resolvedModelName, }, diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts index af0c6acb7cc0d..7c6aa3fd2ea58 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts @@ -45,7 +45,7 @@ import { ILanguageModelToolsService } from '../../../common/tools/languageModelT import { IRemoteAgentService } from '../../../../../../workbench/services/remote/common/remoteAgentService.js'; import { basename } from '../../../../../../base/common/resources.js'; import { match } from '../../../../../../base/common/glob.js'; -import { ChatModeKind } from '../../../common/constants.js'; +import { ChatModeKind, GeneralPurposeAgentName } from '../../../common/constants.js'; import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { MockContextKeyService } from '../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { IAgentPlugin, IAgentPluginService } from '../../../common/plugins/agentPluginService.js'; @@ -1505,6 +1505,87 @@ suite('ComputeAutomaticInstructions', () => { assert.equal(xmlContents(agents[2], 'name')[0], `test-agent-5`); }); + test('should include General Purpose agent first when experiment is enabled', async () => { + const rootFolderName = 'gp-agents-list-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + testConfigService.setUserConfiguration('chat.customAgentInSubagent.enabled', true); + + testConfigService.setUserConfiguration('chat.generalPurposeAgent.enabled', true); + + testConfigService.setUserConfiguration(PromptsConfig.AGENTS_LOCATION_KEY, { + [AGENTS_SOURCE_FOLDER]: true, + }); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/agents/test-agent-1.agent.md`, + contents: [ + '---', + 'description: \'Test agent 1\'', + '---', + 'Test agent content', + ] + }, + ]); + + const contextComputer = instaService.createInstance( + ComputeAutomaticInstructions, + ChatModeKind.Agent, + { 'vscode_runSubagent': true }, + ['*'], + ); + const variables = new ChatRequestVariableSet(); + + await contextComputer.collect(variables, CancellationToken.None); + + const textVariables = variables.asArray().filter(v => isPromptTextVariableEntry(v)); + assert.equal(textVariables.length, 1, 'There should be one text variable for agents list'); + + const agentsList = xmlContents(textVariables[0].value, 'agents'); + assert.equal(agentsList.length, 1, 'There should be one agents list'); + + const agents = xmlContents(agentsList[0], 'agent'); + assert.equal(agents.length, 2, 'There should be two agents (General Purpose + 1 custom)'); + + // First agent should always be the built-in General Purpose agent + assert.equal(xmlContents(agents[0], 'name')[0], GeneralPurposeAgentName); + + assert.equal(xmlContents(agents[1], 'name')[0], 'test-agent-1'); + assert.equal(xmlContents(agents[1], 'description')[0], 'Test agent 1'); + }); + + test('should include General Purpose agent even without custom agents config', async () => { + workspaceContextService.setWorkspace(testWorkspace(URI.file('/gp-only-test'))); + + // Explicitly do NOT set chat.customAgentInSubagent.enabled + + testConfigService.setUserConfiguration('chat.generalPurposeAgent.enabled', true); + + const contextComputer = instaService.createInstance( + ComputeAutomaticInstructions, + ChatModeKind.Agent, + { 'vscode_runSubagent': true }, + ['*'], + ); + const variables = new ChatRequestVariableSet(); + + await contextComputer.collect(variables, CancellationToken.None); + + const textVariables = variables.asArray().filter(v => isPromptTextVariableEntry(v)); + assert.equal(textVariables.length, 1, 'There should be one text variable for agents list'); + + const agentsList = xmlContents(textVariables[0].value, 'agents'); + assert.equal(agentsList.length, 1, 'There should be one agents list'); + + const agents = xmlContents(agentsList[0], 'agent'); + assert.equal(agents.length, 1, 'There should be only the GP agent'); + assert.equal(xmlContents(agents[0], 'name')[0], GeneralPurposeAgentName); + }); + test('should include skills list when readFile tool available', async () => { const rootFolderName = 'skills-list-test'; const rootFolder = `/${rootFolderName}`; diff --git a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts index fdc8346147587..19fb1f155f8b3 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts @@ -22,7 +22,7 @@ import { MockPromptsService } from '../../promptSyntax/service/mockPromptsServic import { ExtensionIdentifier } from '../../../../../../../platform/extensions/common/extensions.js'; import { IToolInvocation, ToolProgress } from '../../../../common/tools/languageModelToolsService.js'; import { IChatModel } from '../../../../common/model/chatModel.js'; -import { ChatConfiguration } from '../../../../common/constants.js'; +import { ChatConfiguration, GeneralPurposeAgentName } from '../../../../common/constants.js'; suite('RunSubagentTool', () => { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -50,7 +50,6 @@ suite('RunSubagentTool', () => { suite('prepareToolInvocation', () => { test('returns correct toolSpecificData', async () => { const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); - const configService = new TestConfigurationService(); const promptsService = new MockPromptsService(); const customMode: ICustomAgent = { @@ -71,8 +70,7 @@ suite('RunSubagentTool', () => { mockToolsService, {} as ILanguageModelsService, new NullLogService(), - mockToolsService, - configService, + new TestConfigurationService(), promptsService, {} as IInstantiationService, {} as IProductService, @@ -101,12 +99,124 @@ suite('RunSubagentTool', () => { modelName: undefined, }); }); + + function createToolWithGP(opts?: { customAgents?: ICustomAgent[] }) { + const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); + const promptsService = new MockPromptsService(); + if (opts?.customAgents) { + promptsService.setCustomModes(opts.customAgents); + } + + const tool = testDisposables.add(new RunSubagentTool( + {} as IChatAgentService, + {} as IChatService, + mockToolsService, + {} as ILanguageModelsService, + new NullLogService(), + new TestConfigurationService({ [ChatConfiguration.GeneralPurposeAgentEnabled]: true }), + promptsService, + {} as IInstantiationService, + {} as IProductService, + )); + return tool; + } + + async function createToolWithGPReady(opts?: { customAgents?: ICustomAgent[] }) { + return createToolWithGP(opts); + } + + test('treats undefined agentName as General Purpose when experiment is enabled', async () => { + const tool = await createToolWithGPReady(); + + const result = await tool.prepareToolInvocation( + { + parameters: { prompt: 'Test prompt', description: 'Test task', agentName: undefined }, + toolCallId: 'test-call-undef', + chatSessionResource: URI.parse('test://session'), + }, + CancellationToken.None + ); + + assert.ok(result); + assert.deepStrictEqual(result.toolSpecificData, { + kind: 'subagent', + description: 'Test task', + agentName: GeneralPurposeAgentName, + prompt: 'Test prompt', + modelName: undefined, + }); + }); + + test('treats empty string agentName as General Purpose when experiment is enabled', async () => { + const tool = await createToolWithGPReady(); + + const result = await tool.prepareToolInvocation( + { + parameters: { prompt: 'Test prompt', description: 'Test task', agentName: '' }, + toolCallId: 'test-call-empty', + chatSessionResource: URI.parse('test://session'), + }, + CancellationToken.None + ); + + assert.ok(result); + assert.deepStrictEqual(result.toolSpecificData, { + kind: 'subagent', + description: 'Test task', + agentName: GeneralPurposeAgentName, + prompt: 'Test prompt', + modelName: undefined, + }); + }); + + test('treats explicit General Purpose agentName as GP path', async () => { + const tool = await createToolWithGPReady(); + + const result = await tool.prepareToolInvocation( + { + parameters: { prompt: 'Test prompt', description: 'Test task', agentName: GeneralPurposeAgentName }, + toolCallId: 'test-call-gp', + chatSessionResource: URI.parse('test://session'), + }, + CancellationToken.None + ); + + assert.ok(result); + assert.deepStrictEqual(result.toolSpecificData, { + kind: 'subagent', + description: 'Test task', + agentName: GeneralPurposeAgentName, + prompt: 'Test prompt', + modelName: undefined, + }); + }); + + test('passes through unknown agentName when experiment is enabled', async () => { + const tool = await createToolWithGPReady(); + + const result = await tool.prepareToolInvocation( + { + parameters: { prompt: 'Test prompt', description: 'Test task', agentName: 'NonExistentAgent' }, + toolCallId: 'test-call-unknown', + chatSessionResource: URI.parse('test://session'), + }, + CancellationToken.None + ); + + assert.ok(result); + assert.deepStrictEqual(result.toolSpecificData, { + kind: 'subagent', + description: 'Test task', + agentName: 'NonExistentAgent', + prompt: 'Test prompt', + modelName: undefined, + }); + }); }); suite('getToolData', () => { test('returns basic tool data', () => { const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); - const configService = new TestConfigurationService(); const promptsService = new MockPromptsService(); const tool = testDisposables.add(new RunSubagentTool( @@ -115,8 +225,7 @@ suite('RunSubagentTool', () => { mockToolsService, {} as ILanguageModelsService, new NullLogService(), - mockToolsService, - configService, + new TestConfigurationService(), promptsService, {} as IInstantiationService, {} as IProductService, @@ -128,14 +237,12 @@ suite('RunSubagentTool', () => { assert.ok(toolData.inputSchema); assert.ok(toolData.inputSchema.properties?.prompt); assert.ok(toolData.inputSchema.properties?.description); + assert.strictEqual(toolData.inputSchema.properties?.agentName, undefined, 'agentName should not be in schema when neither GP nor custom agents is enabled'); assert.deepStrictEqual(toolData.inputSchema.required, ['prompt', 'description']); }); - test('includes agentName property when SubagentToolCustomAgents is enabled', () => { + test('marks agentName as required when GP experiment is enabled', async () => { const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); - const configService = new TestConfigurationService({ - 'chat.customAgentInSubagent.enabled': true, - }); const promptsService = new MockPromptsService(); const tool = testDisposables.add(new RunSubagentTool( @@ -144,16 +251,15 @@ suite('RunSubagentTool', () => { mockToolsService, {} as ILanguageModelsService, new NullLogService(), - mockToolsService, - configService, + new TestConfigurationService({ [ChatConfiguration.GeneralPurposeAgentEnabled]: true }), promptsService, {} as IInstantiationService, {} as IProductService, )); const toolData = tool.getToolData(); - - assert.ok(toolData.inputSchema?.properties?.agentName, 'agentName should be in schema when custom agents enabled'); + assert.ok(toolData.inputSchema?.properties?.agentName); + assert.deepStrictEqual(toolData.inputSchema.required, ['prompt', 'description', 'agentName']); }); }); @@ -244,7 +350,6 @@ suite('RunSubagentTool', () => { customAgents?: ICustomAgent[]; }) { const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); - const configService = new TestConfigurationService(); const promptsService = new MockPromptsService(); if (opts.customAgents) { promptsService.setCustomModes(opts.customAgents); @@ -265,8 +370,7 @@ suite('RunSubagentTool', () => { mockToolsService, mockLanguageModelsService as ILanguageModelsService, new NullLogService(), - mockToolsService, - configService, + new TestConfigurationService({ [ChatConfiguration.SubagentToolCustomAgents]: true }), promptsService, {} as IInstantiationService, {} as IProductService, @@ -542,7 +646,6 @@ suite('RunSubagentTool', () => { mockToolsService, {} as ILanguageModelsService, new NullLogService(), - mockToolsService, configService, promptsService, mockInstantiationService as IInstantiationService, From ffdcf820f5e96f6c85f7aabef6c2df05ffa7ad8c Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 2 Apr 2026 09:00:46 +0000 Subject: [PATCH 12/13] Sessions - improve feedback/comments rendering (#307362) * Sessions - improve feedback/comments rendering * Pull request feedback --- .../contrib/changes/browser/changesView.ts | 82 +++++++++++-------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/src/vs/sessions/contrib/changes/browser/changesView.ts b/src/vs/sessions/contrib/changes/browser/changesView.ts index 238f7e8bbd020..11dd2b9c92b8b 100644 --- a/src/vs/sessions/contrib/changes/browser/changesView.ts +++ b/src/vs/sessions/contrib/changes/browser/changesView.ts @@ -130,8 +130,6 @@ interface IChangesFileItem { readonly changeType: ChangeType; readonly linesAdded: number; readonly linesRemoved: number; - readonly reviewCommentCount: number; - readonly agentFeedbackCount: number; } interface IChangesRootItem { @@ -244,9 +242,7 @@ function toChangesFileItem(changes: GitDiffChange[], modifiedRef: string | undef isDeletion, changeType: isDeletion ? 'deleted' : isAddition ? 'added' : 'modified', linesAdded: change.insertions, - linesRemoved: change.deletions, - reviewCommentCount: 0, - agentFeedbackCount: 0, + linesRemoved: change.deletions } satisfies IChangesFileItem; }); } @@ -753,8 +749,6 @@ export class ChangesViewPane extends ViewPane { // Convert session file changes to list items (cloud/background sessions) const sessionFilesObs = derived(reader => { - const reviewCommentCountByFile = this.viewModel.activeSessionReviewCommentCountByFileObs.read(reader); - const agentFeedbackCountByFile = this.viewModel.activeSessionAgentFeedbackCountByFileObs.read(reader); const changes = [...this.viewModel.activeSessionChangesObs.read(reader)]; return changes.map((entry): IChangesFileItem => { @@ -771,9 +765,7 @@ export class ChangesViewPane extends ViewPane { isDeletion, changeType: isDeletion ? 'deleted' : isAddition ? 'added' : 'modified', linesAdded: entry.insertions, - linesRemoved: entry.deletions, - reviewCommentCount: reviewCommentCountByFile.get(uri.fsPath) ?? 0, - agentFeedbackCount: agentFeedbackCountByFile.get(uri.fsPath) ?? 0, + linesRemoved: entry.deletions }; }); }); @@ -1517,7 +1509,6 @@ class ChangesTreeDelegate implements IListVirtualDelegate { interface IChangesTreeTemplate { readonly label: IResourceLabel; - readonly templateDisposables: DisposableStore; readonly toolbar: MenuWorkbenchToolBar | undefined; readonly contextKeyService: IContextKeyService | undefined; readonly reviewCommentsBadge: HTMLElement; @@ -1526,6 +1517,8 @@ interface IChangesTreeTemplate { readonly addedSpan: HTMLElement; readonly removedSpan: HTMLElement; readonly lineCountsContainer: HTMLElement; + readonly elementDisposables: DisposableStore; + readonly templateDisposables: DisposableStore; } class ChangesTreeRenderer implements ICompressibleTreeRenderer { @@ -1582,7 +1575,7 @@ class ChangesTreeRenderer implements ICompressibleTreeRenderer, _index: number, templateData: IChangesTreeTemplate): void { @@ -1651,29 +1644,41 @@ class ChangesTreeRenderer implements ICompressibleTreeRenderer 0) { - templateData.reviewCommentsBadge.style.display = ''; - templateData.reviewCommentsBadge.className = 'changes-review-comments-badge'; - templateData.reviewCommentsBadge.replaceChildren( - dom.$('.codicon.codicon-comment-unresolved'), - dom.$('span', undefined, `${data.reviewCommentCount}`) - ); - } else { - templateData.reviewCommentsBadge.style.display = 'none'; - templateData.reviewCommentsBadge.replaceChildren(); - } + // Review comments + templateData.elementDisposables.add(autorun(reader => { + const reviewCommentByFile = this.viewModel.activeSessionReviewCommentCountByFileObs.read(reader); + const reviewCommentCount = reviewCommentByFile?.get(data.uri.fsPath) ?? 0; + + if (reviewCommentCount > 0) { + templateData.reviewCommentsBadge.style.display = ''; + templateData.reviewCommentsBadge.className = 'changes-review-comments-badge'; + templateData.reviewCommentsBadge.replaceChildren( + dom.$('.codicon.codicon-comment-unresolved'), + dom.$('span', undefined, `${reviewCommentCount}`) + ); + } else { + templateData.reviewCommentsBadge.style.display = 'none'; + templateData.reviewCommentsBadge.replaceChildren(); + } + })); - if (data.agentFeedbackCount > 0) { - templateData.agentFeedbackBadge.style.display = ''; - templateData.agentFeedbackBadge.className = 'changes-agent-feedback-badge'; - templateData.agentFeedbackBadge.replaceChildren( - dom.$('.codicon.codicon-comment'), - dom.$('span', undefined, `${data.agentFeedbackCount}`) - ); - } else { - templateData.agentFeedbackBadge.style.display = 'none'; - templateData.agentFeedbackBadge.replaceChildren(); - } + // Agent feedback + templateData.elementDisposables.add(autorun(reader => { + const agentFeedbackByFile = this.viewModel.activeSessionAgentFeedbackCountByFileObs.read(reader); + const agentFeedbackCount = agentFeedbackByFile?.get(data.uri.fsPath) ?? 0; + + if (agentFeedbackCount > 0) { + templateData.agentFeedbackBadge.style.display = ''; + templateData.agentFeedbackBadge.className = 'changes-agent-feedback-badge'; + templateData.agentFeedbackBadge.replaceChildren( + dom.$('.codicon.codicon-comment'), + dom.$('span', undefined, `${agentFeedbackCount}`) + ); + } else { + templateData.agentFeedbackBadge.style.display = 'none'; + templateData.agentFeedbackBadge.replaceChildren(); + } + })); const badge = templateData.decorationBadge; badge.className = 'changes-decoration-badge'; @@ -1756,7 +1761,16 @@ class ChangesTreeRenderer implements ICompressibleTreeRenderer, _index: number, templateData: IChangesTreeTemplate): void { + templateData.elementDisposables.clear(); + } + + disposeCompressedElements(_element: ITreeNode, void>, _index: number, templateData: IChangesTreeTemplate): void { + templateData.elementDisposables.clear(); + } + disposeTemplate(templateData: IChangesTreeTemplate): void { + templateData.elementDisposables.dispose(); templateData.templateDisposables.dispose(); } } From f7b7a34d28a1e0a4d20dfdfad040032c4aaa8d9c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 2 Apr 2026 11:03:12 +0200 Subject: [PATCH 13/13] chat - fix migration of completed key (#307345) --- .../workbench/services/chat/common/chatEntitlementService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/services/chat/common/chatEntitlementService.ts b/src/vs/workbench/services/chat/common/chatEntitlementService.ts index 5075750b9dfd0..961247310655d 100644 --- a/src/vs/workbench/services/chat/common/chatEntitlementService.ts +++ b/src/vs/workbench/services/chat/common/chatEntitlementService.ts @@ -1094,8 +1094,9 @@ export class ChatEntitlementContext extends Disposable { const migrated = this.storageService.getBoolean(ChatEntitlementContext.CHAT_ENTITLEMENT_CONTEXT_MIGRATED_STORAGE_KEY, StorageScope.PROFILE) === true; if (!migrated) { this.storageService.store(ChatEntitlementContext.CHAT_ENTITLEMENT_CONTEXT_MIGRATED_STORAGE_KEY, true, StorageScope.PROFILE, StorageTarget.MACHINE); - if (this._state.installed) { + if (this._state.installed && !this._state.completed) { this._state.completed = true; // treat installation signal as completed signal once + this.storageService.store(ChatEntitlementContext.CHAT_ENTITLEMENT_CONTEXT_STORAGE_KEY, this._state, StorageScope.PROFILE, StorageTarget.MACHINE); } }