From 40676033ac2d41497781792b1633f5ca57997f7d Mon Sep 17 00:00:00 2001 From: Isidor Date: Fri, 20 Feb 2026 11:00:10 +0100 Subject: [PATCH 01/17] chat: use manage settings link for admin-unavailable models --- .../browser/widget/input/chatModelPicker.ts | 11 +++++-- .../widget/input/chatModelPicker.test.ts | 29 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts index d612c7aa052a1..1b34b783b8661 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts @@ -129,6 +129,7 @@ export function buildModelPickerItems( updateStateType: StateType, onSelect: (model: ILanguageModelChatMetadataAndIdentifier) => void, upgradePlanUrl: string | undefined, + manageSettingsUrl: string | undefined, commandService: ICommandService, chatEntitlementService: IChatEntitlementService, ): IActionListItem[] { @@ -259,7 +260,7 @@ export function buildModelPickerItems( if (item.kind === 'available') { items.push(createModelItem(createModelAction(item.model, selectedModelId, onSelect), item.model)); } else { - items.push(createUnavailableModelItem(item.entry, item.reason, upgradePlanUrl, updateStateType)); + items.push(createUnavailableModelItem(item.entry, item.reason, upgradePlanUrl, manageSettingsUrl, updateStateType)); } } } @@ -301,7 +302,7 @@ export function buildModelPickerItems( for (const model of otherModels) { const entry = controlModels[model.metadata.id] ?? controlModels[model.identifier]; if (entry?.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { - items.push(createUnavailableModelItem(entry, 'update', upgradePlanUrl, updateStateType, ModelPickerSection.Other)); + items.push(createUnavailableModelItem(entry, 'update', upgradePlanUrl, manageSettingsUrl, updateStateType, ModelPickerSection.Other)); } else { items.push(createModelItem(createModelAction(model, selectedModelId, onSelect, ModelPickerSection.Other), model)); } @@ -376,6 +377,7 @@ function createUnavailableModelItem( entry: IModelControlEntry, reason: 'upgrade' | 'update' | 'admin', upgradePlanUrl: string | undefined, + manageSettingsUrl: string | undefined, updateStateType: StateType, section?: string, ): IActionListItem { @@ -390,7 +392,9 @@ function createUnavailableModelItem( description = localize('chat.modelPicker.updateDescription', "Update VS Code"); icon = Codicon.warning; } else { - description = localize('chat.modelPicker.adminDescription', "Contact admin"); + description = manageSettingsUrl + ? new MarkdownString(localize('chat.modelPicker.adminLink', "[Contact your admin]({0})", manageSettingsUrl), { isTrusted: true }) + : localize('chat.modelPicker.adminDescription', "Contact your admin"); icon = Codicon.warning; } @@ -550,6 +554,7 @@ export class ModelPickerWidget extends Disposable { this._updateService.state.type, onSelect, this._productService.defaultChatAgent?.upgradePlanUrl, + this._productService.defaultChatAgent?.manageSettingsUrl, this._commandService, this._entitlementService ); diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts index 7e8c1835c3d3f..b1a245f6fe506 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts @@ -6,6 +6,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { IStringDictionary } from '../../../../../../../base/common/collections.js'; +import { MarkdownString } from '../../../../../../../base/common/htmlContent.js'; import { ActionListItemKind, IActionListItem } from '../../../../../../../platform/actionWidget/browser/actionList.js'; import { IActionWidgetDropdownAction } from '../../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'; @@ -71,6 +72,7 @@ function callBuild( currentVSCodeVersion?: string; updateStateType?: StateType; upgradePlanUrl?: string; + manageSettingsUrl?: string; } = {}, ): IActionListItem[] { const onSelect = () => { }; @@ -84,6 +86,7 @@ function callBuild( opts.updateStateType ?? StateType.Idle, onSelect, opts.upgradePlanUrl, + opts.manageSettingsUrl, stubCommandService, stubChatEntitlementService as IChatEntitlementService, ); @@ -439,6 +442,7 @@ suite('buildModelPickerItems', () => { StateType.Idle, onSelect, undefined, + undefined, stubCommandService, stubChatEntitlementService as IChatEntitlementService, ); @@ -508,4 +512,29 @@ suite('buildModelPickerItems', () => { // Then Other Models toggle assert.ok(actions[3].isSectionToggle); }); + + test('admin unavailable model shows manage settings link in description', () => { + const auto = createAutoModel(); + const items = buildModelPickerItems( + [auto], + undefined, + ['missing-model'], + { 'missing-model': { id: 'missing-model', label: 'Missing Model' } }, + true, + '1.100.0', + StateType.Idle, + () => { }, + undefined, + 'https://aka.ms/github-copilot-settings', + stubCommandService, + stubChatEntitlementService as IChatEntitlementService, + ); + + const adminItem = getActionItems(items).find(a => a.label === 'Missing Model'); + assert.ok(adminItem); + assert.strictEqual(adminItem.disabled, true); + const description = adminItem.description; + assert.ok(description instanceof MarkdownString); + assert.ok(description.value.includes('https://aka.ms/github-copilot-settings')); + }); }); From d1f34d5d725d6d6e3e40bcc1efabe54513d3f40a Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 20 Feb 2026 12:04:34 +0100 Subject: [PATCH 02/17] refactor: remove redundant 'id' property from model control entries in chat model picker (#296476) --- .../browser/widget/input/chatModelPicker.ts | 25 ++++++++-------- .../contrib/chat/common/languageModels.ts | 5 ++-- .../widget/input/chatModelPicker.test.ts | 30 +++++++++---------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts index 929c68cecafa8..bfa1de6229ee1 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts @@ -184,7 +184,7 @@ export function buildModelPickerItems( // --- 2. Promoted section (selected + recently used + featured) --- type PromotedItem = | { kind: 'available'; model: ILanguageModelChatMetadataAndIdentifier } - | { kind: 'unavailable'; entry: IModelControlEntry; reason: 'upgrade' | 'update' | 'admin' }; + | { kind: 'unavailable'; id: string; entry: IModelControlEntry; reason: 'upgrade' | 'update' | 'admin' }; const promotedItems: PromotedItem[] = []; @@ -198,7 +198,7 @@ export function buildModelPickerItems( markPlaced(model.identifier, model.metadata.id); const entry = controlModels[model.metadata.id]; if (entry?.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { - promotedItems.push({ kind: 'unavailable', entry, reason: 'update' }); + promotedItems.push({ kind: 'unavailable', id: model.metadata.id, entry, reason: 'update' }); } else { promotedItems.push({ kind: 'available', model }); } @@ -208,7 +208,7 @@ export function buildModelPickerItems( const entry = controlModels[id]; if (entry) { markPlaced(id); - promotedItems.push({ kind: 'unavailable', entry, reason: getUnavailableReason(entry) }); + promotedItems.push({ kind: 'unavailable', id, entry, reason: getUnavailableReason(entry) }); return true; } } @@ -226,21 +226,21 @@ export function buildModelPickerItems( } // Featured models from control manifest - for (const entry of Object.values(controlModels)) { - if (!entry.featured || placed.has(entry.id)) { + for (const [entryId, entry] of Object.entries(controlModels)) { + if (!entry.featured || placed.has(entryId)) { continue; } - const model = resolveModel(entry.id); + const model = resolveModel(entryId); if (model && !placed.has(model.identifier)) { markPlaced(model.identifier, model.metadata.id); if (entry.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { - promotedItems.push({ kind: 'unavailable', entry, reason: 'update' }); + promotedItems.push({ kind: 'unavailable', id: entryId, entry, reason: 'update' }); } else { promotedItems.push({ kind: 'available', model }); } } else if (!model) { - markPlaced(entry.id); - promotedItems.push({ kind: 'unavailable', entry, reason: getUnavailableReason(entry) }); + markPlaced(entryId); + promotedItems.push({ kind: 'unavailable', id: entryId, entry, reason: getUnavailableReason(entry) }); } } @@ -259,7 +259,7 @@ export function buildModelPickerItems( if (item.kind === 'available') { items.push(createModelItem(createModelAction(item.model, selectedModelId, onSelect), item.model)); } else { - items.push(createUnavailableModelItem(item.entry, item.reason, upgradePlanUrl, updateStateType)); + items.push(createUnavailableModelItem(item.id, item.entry, item.reason, upgradePlanUrl, updateStateType)); } } } @@ -301,7 +301,7 @@ export function buildModelPickerItems( for (const model of otherModels) { const entry = controlModels[model.metadata.id] ?? controlModels[model.identifier]; if (entry?.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { - items.push(createUnavailableModelItem(entry, 'update', upgradePlanUrl, updateStateType, ModelPickerSection.Other)); + items.push(createUnavailableModelItem(model.metadata.id, entry, 'update', upgradePlanUrl, updateStateType, ModelPickerSection.Other)); } else { items.push(createModelItem(createModelAction(model, selectedModelId, onSelect, ModelPickerSection.Other), model)); } @@ -373,6 +373,7 @@ export function buildModelPickerItems( } function createUnavailableModelItem( + id: string, entry: IModelControlEntry, reason: 'upgrade' | 'update' | 'admin', upgradePlanUrl: string | undefined, @@ -406,7 +407,7 @@ function createUnavailableModelItem( return { item: { - id: entry.id, + id, enabled: false, checked: false, class: undefined, diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 0842bfccc8da8..80310409aa06b 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -402,7 +402,6 @@ export interface ILanguageModelsService { } export interface IModelControlEntry { - readonly id: string; readonly label: string; readonly featured?: boolean; readonly minVSCodeVersion?: string; @@ -1429,7 +1428,7 @@ export class LanguageModelsService implements ILanguageModelsService { if (!entry || !isObject(entry) || typeof entry.id !== 'string') { continue; } - free[entry.id] = { id: entry.id, label: entry.label, featured: entry.featured }; + free[entry.id] = { label: entry.label, featured: entry.featured }; } } @@ -1439,7 +1438,7 @@ export class LanguageModelsService implements ILanguageModelsService { if (!entry || !isObject(entry) || typeof entry.id !== 'string') { continue; } - paid[entry.id] = { id: entry.id, label: entry.label, featured: entry.featured, minVSCodeVersion: entry.minVSCodeVersion }; + paid[entry.id] = { label: entry.label, featured: entry.featured, minVSCodeVersion: entry.minVSCodeVersion }; } } diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts index 7e8c1835c3d3f..c1076807a7304 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts @@ -138,7 +138,7 @@ suite('buildModelPickerItems', () => { const items = callBuild([auto, modelA], { selectedModelId: modelA.identifier, controlModels: { - 'gpt-4o': { id: 'gpt-4o', label: 'GPT-4o', minVSCodeVersion: '2.0.0' }, + 'gpt-4o': { label: 'GPT-4o', minVSCodeVersion: '2.0.0' }, }, currentVSCodeVersion: '1.90.0', }); @@ -169,7 +169,7 @@ suite('buildModelPickerItems', () => { const items = callBuild([auto], { recentModelIds: ['missing-model'], controlModels: { - 'missing-model': { id: 'missing-model', label: 'Missing Model' }, + 'missing-model': { label: 'Missing Model' }, }, isProUser: false, }); @@ -184,7 +184,7 @@ suite('buildModelPickerItems', () => { const items = callBuild([auto], { recentModelIds: ['missing-model'], controlModels: { - 'missing-model': { id: 'missing-model', label: 'Missing Model', minVSCodeVersion: '2.0.0' }, + 'missing-model': { label: 'Missing Model', minVSCodeVersion: '2.0.0' }, }, isProUser: true, currentVSCodeVersion: '1.90.0', @@ -200,7 +200,7 @@ suite('buildModelPickerItems', () => { const items = callBuild([auto], { recentModelIds: ['missing-model'], controlModels: { - 'missing-model': { id: 'missing-model', label: 'Missing Model' }, + 'missing-model': { label: 'Missing Model' }, }, isProUser: true, }); @@ -216,7 +216,7 @@ suite('buildModelPickerItems', () => { const modelB = createModel('claude', 'Claude'); const items = callBuild([auto, modelA, modelB], { controlModels: { - 'gpt-4o': { id: 'gpt-4o', label: 'GPT-4o', featured: true }, + 'gpt-4o': { label: 'GPT-4o', featured: true }, }, }); const actions = getActionItems(items); @@ -229,7 +229,7 @@ suite('buildModelPickerItems', () => { const auto = createAutoModel(); const items = callBuild([auto], { controlModels: { - 'premium-model': { id: 'premium-model', label: 'Premium Model', featured: true }, + 'premium-model': { label: 'Premium Model', featured: true }, }, isProUser: false, }); @@ -243,7 +243,7 @@ suite('buildModelPickerItems', () => { const auto = createAutoModel(); const items = callBuild([auto], { controlModels: { - 'premium-model': { id: 'premium-model', label: 'Premium Model', featured: true }, + 'premium-model': { label: 'Premium Model', featured: true }, }, isProUser: true, }); @@ -258,7 +258,7 @@ suite('buildModelPickerItems', () => { const modelA = createModel('gpt-4o', 'GPT-4o'); const items = callBuild([auto, modelA], { controlModels: { - 'gpt-4o': { id: 'gpt-4o', label: 'GPT-4o', featured: true, minVSCodeVersion: '2.0.0' }, + 'gpt-4o': { label: 'GPT-4o', featured: true, minVSCodeVersion: '2.0.0' }, }, currentVSCodeVersion: '1.90.0', }); @@ -274,7 +274,7 @@ suite('buildModelPickerItems', () => { const modelB = createModel('claude', 'Claude'); const items = callBuild([auto, modelA, modelB], { controlModels: { - 'gpt-4o': { id: 'gpt-4o', label: 'GPT-4o', featured: false }, + 'gpt-4o': { label: 'GPT-4o', featured: false }, }, }); // With no selected, no recent, and no featured, both models should be in Other @@ -308,7 +308,7 @@ suite('buildModelPickerItems', () => { const items = callBuild([auto, modelA], { recentModelIds: [modelA.identifier, 'missing-model'], controlModels: { - 'missing-model': { id: 'missing-model', label: 'Missing Model' }, + 'missing-model': { label: 'Missing Model' }, }, isProUser: false, }); @@ -356,7 +356,7 @@ suite('buildModelPickerItems', () => { const modelA = createModel('gpt-4o', 'GPT-4o'); const items = callBuild([auto, modelA], { controlModels: { - 'gpt-4o': { id: 'gpt-4o', label: 'GPT-4o', minVSCodeVersion: '2.0.0' }, + 'gpt-4o': { label: 'GPT-4o', minVSCodeVersion: '2.0.0' }, }, currentVSCodeVersion: '1.90.0', }); @@ -375,8 +375,8 @@ suite('buildModelPickerItems', () => { selectedModelId: modelA.identifier, recentModelIds: [modelA.identifier, modelB.identifier], controlModels: { - 'gpt-4o': { id: 'gpt-4o', label: 'GPT-4o', featured: true }, - 'claude': { id: 'claude', label: 'Claude', featured: true }, + 'gpt-4o': { label: 'GPT-4o', featured: true }, + 'claude': { label: 'Claude', featured: true }, }, }); const labels = getActionLabels(items).filter(l => l !== 'Other Models' && !l.includes('Manage Models')); @@ -391,7 +391,7 @@ suite('buildModelPickerItems', () => { selectedModelId: auto.identifier, recentModelIds: [auto.identifier], controlModels: { - 'auto': { id: 'auto', label: 'Auto', featured: true }, + 'auto': { label: 'Auto', featured: true }, }, }); const autoItems = getActionItems(items).filter(a => a.label === 'Auto'); @@ -497,7 +497,7 @@ suite('buildModelPickerItems', () => { const items = callBuild([auto, modelA, modelB, modelC, modelD], { recentModelIds: [modelC.identifier], controlModels: { - 'alpha': { id: 'alpha', label: 'Alpha', featured: true }, + 'alpha': { label: 'Alpha', featured: true }, }, }); const actions = getActionItems(items); From 643712dfd0e4e2475135dac39a2b4cfa7421723b Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 20 Feb 2026 12:04:52 +0100 Subject: [PATCH 03/17] clean up (#296490) --- .../contrib/chat/browser/widget/input/chatModelPicker.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts index bfa1de6229ee1..c7d217fa11655 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts @@ -420,7 +420,6 @@ function createUnavailableModelItem( label: entry.label, description, disabled: true, - group: { title: '' }, hideIcon: false, section, hover: { content: hoverContent }, From efa65806368190b48cb672fa41537b34c9e681dd Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 20 Feb 2026 12:05:19 +0100 Subject: [PATCH 04/17] chat - add `product.overrides.json` to `git.worktreeIncludeFiles` (#296485) --- .vscode/settings.json | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index f65efbf06ee50..5cd09294f0168 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -100,10 +100,10 @@ // --- TypeScript --- "typescript.experimental.useTsgo": true, "typescript.tsdk": "node_modules/typescript/lib", - "typescript.preferences.importModuleSpecifier": "relative", - "typescript.preferences.quoteStyle": "single", + "js/ts.preferences.importModuleSpecifier": "relative", + "js/ts.preferences.quoteStyle": "single", "typescript.tsc.autoDetect": "off", - "typescript.preferences.autoImportFileExcludePatterns": [ + "js/ts.preferences.autoImportFileExcludePatterns": [ "@xterm/xterm", "@xterm/headless", "node-pty", @@ -144,6 +144,9 @@ "ts": "warning", "eslint": "warning" }, + "git.worktreeIncludeFiles": [ + "product.overrides.json" + ], // --- GitHub --- "githubPullRequests.experimental.createView": true, "githubPullRequests.assignCreated": "${user}", @@ -199,12 +202,11 @@ "sash" ], // --- Workbench --- - // "application.experimental.rendererProfiling": true, // https://github.com/microsoft/vscode/issues/265654 "editor.aiStats.enabled": true, // Team selfhosting on ai stats "azureMcp.enabledServices": [ "kusto" // Needed for kusto tool in data.prompt.md ], "azureMcp.serverMode": "all", "azureMcp.readOnly": true, - "debug.breakpointsView.presentation": "tree" + "debug.breakpointsView.presentation": "tree", } From 3d96e621ef837b6e9afabddb7ebda232367b8810 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 20 Feb 2026 12:06:11 +0100 Subject: [PATCH 05/17] sessions - tweak customization sidebar styling (#296491) --- .../browser/aiCustomizationManagementEditor.ts | 2 +- .../browser/media/aiCustomizationManagement.css | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts b/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts index caaf09e7ca242..7d2e3de1face5 100644 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts +++ b/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts @@ -247,7 +247,7 @@ export class AICustomizationManagementEditor extends EditorPane { layout: (width, _, height) => { this.sidebarContainer.style.width = `${width}px`; if (height !== undefined) { - const listHeight = height - 24; + const listHeight = height - 8; this.sectionsList.layout(listHeight, width); } }, diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/media/aiCustomizationManagement.css b/src/vs/sessions/contrib/aiCustomizationManagement/browser/media/aiCustomizationManagement.css index 4954a2a3fd979..a307cfa98dfce 100644 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/media/aiCustomizationManagement.css +++ b/src/vs/sessions/contrib/aiCustomizationManagement/browser/media/aiCustomizationManagement.css @@ -9,7 +9,6 @@ flex-direction: column; height: 100%; overflow: hidden; - border-top: 1px solid var(--vscode-panel-border); } /* Sidebar */ @@ -17,12 +16,12 @@ background-color: var(--vscode-sideBar-background); height: 100%; overflow: hidden; - border-right: 1px solid var(--vscode-panel-border); } .ai-customization-management-editor .sidebar-content { height: 100%; - padding: 12px 0 12px 4px; + padding: 4px; + box-sizing: border-box; display: flex; flex-direction: column; } @@ -41,7 +40,6 @@ padding: 4px 8px; gap: 10px; cursor: pointer; - margin: 1px 6px; border-radius: 4px; transition: background-color 0.1s ease, opacity 0.1s ease; } From 75fd74f8cb8dbe70038cd504edaf0e73d114c48a Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:16:20 +0100 Subject: [PATCH 06/17] GitService - add the ability to get list of references (#296486) * Initial implementation * GitService - add the ability to get list of references * Pull request feedback * More types cleanup --- .../browser/mainThreadGitExtensionService.ts | 34 ++++++-- .../workbench/api/common/extHost.protocol.ts | 21 +++++ .../api/common/extHostGitExtensionService.ts | 78 +++++++++++++++++-- .../contrib/git/browser/gitService.ts | 53 ++++++++++--- .../contrib/git/common/gitService.ts | 49 ++++++++---- 5 files changed, 199 insertions(+), 36 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts b/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts index 30a68e7b3c5e8..c40bf42242590 100644 --- a/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts @@ -3,24 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from '../../../base/common/cancellation.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { URI } from '../../../base/common/uri.js'; -import { IGitExtensionService, IGitService } from '../../contrib/git/common/gitService.js'; +import { IGitExtensionDelegate, IGitService, GitRef, GitRefQuery, GitRefType } from '../../contrib/git/common/gitService.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; -import { ExtHostContext, ExtHostGitExtensionShape, MainContext, MainThreadGitExtensionShape } from '../common/extHost.protocol.js'; +import { ExtHostContext, ExtHostGitExtensionShape, GitRefTypeDto, MainContext, MainThreadGitExtensionShape } from '../common/extHost.protocol.js'; + +function toGitRefType(type: GitRefTypeDto): GitRefType { + switch (type) { + case GitRefTypeDto.Head: return GitRefType.Head; + case GitRefTypeDto.RemoteHead: return GitRefType.RemoteHead; + case GitRefTypeDto.Tag: return GitRefType.Tag; + default: throw new Error(`Unknown GitRefType: ${type}`); + } +} @extHostNamedCustomer(MainContext.MainThreadGitExtension) -export class MainThreadGitExtensionService extends Disposable implements MainThreadGitExtensionShape, IGitExtensionService { +export class MainThreadGitExtensionService extends Disposable implements MainThreadGitExtensionShape, IGitExtensionDelegate { private readonly _proxy: ExtHostGitExtensionShape; constructor( extHostContext: IExtHostContext, - @IGitService private readonly gitService: IGitService, + @IGitService gitService: IGitService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostGitExtension); - gitService.setDelegate(this); + this._register(gitService.setDelegate(this)); } async openRepository(uri: URI): Promise { @@ -28,8 +38,16 @@ export class MainThreadGitExtensionService extends Disposable implements MainThr return result ? URI.revive(result) : undefined; } - override dispose(): void { - this.gitService.clearDelegate(); - super.dispose(); + async getRefs(root: URI, query: GitRefQuery, token?: CancellationToken): Promise { + const result = await this._proxy.$getRefs(root, query, token); + + if (token?.isCancellationRequested) { + return []; + } + + return result.map(ref => ({ + ...ref, + type: toGitRefType(ref.type) + } satisfies GitRef)); } } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d10a973d8bc00..22792484d36b8 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3456,8 +3456,29 @@ export interface ExtHostChatSessionsShape { $provideHandleOptionsChange(providerHandle: number, sessionResource: UriComponents, updates: ReadonlyArray, token: CancellationToken): Promise; } +export interface GitRefQueryDto { + readonly contains?: string; + readonly count?: number; + readonly pattern?: string | string[]; + readonly sort?: 'alphabetically' | 'committerdate' | 'creatordate'; +} + +export enum GitRefTypeDto { + Head, + RemoteHead, + Tag +} + +export interface GitRefDto { + readonly id: string; + readonly name: string; + readonly type: GitRefTypeDto; + readonly revision: string; +} + export interface ExtHostGitExtensionShape { $openRepository(root: UriComponents): Promise; + $getRefs(root: UriComponents, query: GitRefQueryDto, token?: CancellationToken): Promise; } // --- proxy identifiers diff --git a/src/vs/workbench/api/common/extHostGitExtensionService.ts b/src/vs/workbench/api/common/extHostGitExtensionService.ts index 47276d420b8d6..b02931d8e805e 100644 --- a/src/vs/workbench/api/common/extHostGitExtensionService.ts +++ b/src/vs/workbench/api/common/extHostGitExtensionService.ts @@ -10,12 +10,46 @@ import { ExtensionIdentifier } from '../../../platform/extensions/common/extensi import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; import { IExtHostExtensionService } from './extHostExtensionService.js'; import { IExtHostRpcService } from './extHostRpcService.js'; -import { ExtHostGitExtensionShape } from './extHost.protocol.js'; +import { ExtHostGitExtensionShape, GitRefDto, GitRefQueryDto, GitRefTypeDto } from './extHost.protocol.js'; const GIT_EXTENSION_ID = 'vscode.git'; +function toGitRefTypeDto(type: GitRefType): GitRefTypeDto { + switch (type) { + case GitRefType.Head: return GitRefTypeDto.Head; + case GitRefType.RemoteHead: return GitRefTypeDto.RemoteHead; + case GitRefType.Tag: return GitRefTypeDto.Tag; + default: throw new Error(`Unknown GitRefType: ${type}`); + } +} + +interface Repository { + readonly rootUri: vscode.Uri; + getRefs(query: GitRefQuery, token?: vscode.CancellationToken): Promise; +} + +interface GitRef { + type: GitRefType; + name?: string; + commit?: string; + remote?: string; +} + +const enum GitRefType { + Head, + RemoteHead, + Tag +} + +interface GitRefQuery { + readonly contains?: string; + readonly count?: number; + readonly pattern?: string | string[]; + readonly sort?: 'alphabetically' | 'committerdate' | 'creatordate'; +} + interface GitExtensionAPI { - openRepository(root: vscode.Uri): Promise<{ readonly rootUri: vscode.Uri } | null>; + openRepository(root: vscode.Uri): Promise; } interface GitExtension { @@ -41,8 +75,6 @@ export class ExtHostGitExtensionService extends Disposable implements IExtHostGi super(); } - // --- Called by the main thread via RPC (ExtHostGitShape) --- - async $openRepository(uri: UriComponents): Promise { const api = await this._ensureGitApi(); if (!api) { @@ -53,7 +85,43 @@ export class ExtHostGitExtensionService extends Disposable implements IExtHostGi return repository?.rootUri; } - // --- Private helpers --- + async $getRefs(uri: UriComponents, query: GitRefQueryDto, token?: vscode.CancellationToken): Promise { + const api = await this._ensureGitApi(); + if (!api) { + return []; + } + + const repository = await api.openRepository(URI.revive(uri)); + if (!repository) { + return []; + } + + try { + const refs = await repository.getRefs(query, token); + const result: (GitRefDto | undefined)[] = refs.map(ref => { + if (!ref.name || !ref.commit) { + return undefined; + } + + const id = ref.type === GitRefType.Head + ? `refs/heads/${ref.name}` + : ref.type === GitRefType.RemoteHead + ? `refs/remotes/${ref.remote}/${ref.name}` + : `refs/tags/${ref.name}`; + + return { + id, + name: ref.name, + type: toGitRefTypeDto(ref.type), + revision: ref.commit + } satisfies GitRefDto; + }); + + return result.filter(ref => !!ref); + } catch { + return []; + } + } private async _ensureGitApi(): Promise { if (this._gitApi) { diff --git a/src/vs/workbench/contrib/git/browser/gitService.ts b/src/vs/workbench/contrib/git/browser/gitService.ts index c298ec69e2a76..c697b9cef97a2 100644 --- a/src/vs/workbench/contrib/git/browser/gitService.ts +++ b/src/vs/workbench/contrib/git/browser/gitService.ts @@ -3,29 +3,62 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../base/common/map.js'; +import { observableValue } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; -import { IGitService, IGitExtensionService } from '../common/gitService.js'; +import { IGitService, IGitExtensionDelegate, GitRef, GitRefQuery, IGitRepository } from '../common/gitService.js'; export class GitService extends Disposable implements IGitService { declare readonly _serviceBrand: undefined; - private _delegate: IGitExtensionService | undefined; + private _delegate: IGitExtensionDelegate | undefined; - setDelegate(delegate: IGitExtensionService): void { - this._delegate = delegate; + private readonly _repositories = new ResourceMap(); + get repositories(): Iterable { + return this._repositories.values(); } - clearDelegate(): void { - this._delegate = undefined; + readonly isInitialized = observableValue(this, false); + + setDelegate(delegate: IGitExtensionDelegate): IDisposable { + this._delegate = delegate; + this.isInitialized.set(true, undefined); + + return toDisposable(() => { + this._delegate = undefined; + this._repositories.clear(); + this.isInitialized.set(false, undefined); + }); } - async openRepository(root: URI): Promise { + async openRepository(uri: URI): Promise { if (!this._delegate) { return undefined; } - const result = await this._delegate.openRepository(root); - return result ? URI.revive(result) : undefined; + const root = await this._delegate.openRepository(uri); + if (!root) { + return undefined; + } + + const rootUri = URI.revive(root); + let repository = this._repositories.get(rootUri); + if (repository) { + return repository; + } + + repository = new GitRepository(this._delegate, rootUri); + this._repositories.set(rootUri, repository); + return repository; + } +} + +export class GitRepository implements IGitRepository { + constructor(private readonly delegate: IGitExtensionDelegate, readonly rootUri: URI) { } + + async getRefs(query: GitRefQuery, token?: CancellationToken): Promise { + return this.delegate.getRefs(this.rootUri, query, token); } } diff --git a/src/vs/workbench/contrib/git/common/gitService.ts b/src/vs/workbench/contrib/git/common/gitService.ts index 3092386866947..5f1c3807ee8bc 100644 --- a/src/vs/workbench/contrib/git/common/gitService.ts +++ b/src/vs/workbench/contrib/git/common/gitService.ts @@ -3,15 +3,39 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { IObservable } from '../../../../base/common/observable.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -/** - * Delegate interface that bridges to the git extension running - * in the extension host. Set by MainThreadGit when an extension - * host connects. - */ -export interface IGitExtensionService { +export enum GitRefType { + Head, + RemoteHead, + Tag +} + +export interface GitRef { + readonly type: GitRefType; + readonly name?: string; + readonly commit?: string; + readonly remote?: string; +} + +export interface GitRefQuery { + readonly contains?: string; + readonly count?: number; + readonly pattern?: string | string[]; + readonly sort?: 'alphabetically' | 'committerdate' | 'creatordate'; +} + +export interface IGitRepository { + readonly rootUri: URI; + getRefs(query: GitRefQuery, token?: CancellationToken): Promise; +} + +export interface IGitExtensionDelegate { + getRefs(uri: UriComponents, query?: GitRefQuery, token?: CancellationToken): Promise; openRepository(uri: UriComponents): Promise; } @@ -20,12 +44,11 @@ export const IGitService = createDecorator('gitService'); export interface IGitService { readonly _serviceBrand: undefined; - setDelegate(delegate: IGitExtensionService): void; - clearDelegate(): void; + readonly isInitialized: IObservable; + + readonly repositories: Iterable; + + setDelegate(delegate: IGitExtensionDelegate): IDisposable; - /** - * Open a git repository at the given URI. - * @returns The repository root URI or `undefined` if the repository could not be opened. - */ - openRepository(uri: URI): Promise; + openRepository(uri: URI): Promise; } From 7a0b83f270038305afab4f7e2a23493152c834df Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 20 Feb 2026 13:13:55 +0100 Subject: [PATCH 07/17] remove id (#296497) --- src/vs/workbench/contrib/chat/common/languageModels.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 80310409aa06b..9089427aeed7e 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -506,8 +506,8 @@ interface IChatControlResponse { readonly version: number; readonly restrictedChatParticipants: { [name: string]: string[] }; readonly models?: { - readonly free?: Record; - readonly paid?: Record; + readonly free?: Record; + readonly paid?: Record; }; } @@ -1425,7 +1425,7 @@ export class LanguageModelsService implements ILanguageModelsService { if (response?.free) { const freeEntries = Array.isArray(response.free) ? response.free : Object.values(response.free); for (const entry of freeEntries) { - if (!entry || !isObject(entry) || typeof entry.id !== 'string') { + if (!entry || !isObject(entry)) { continue; } free[entry.id] = { label: entry.label, featured: entry.featured }; @@ -1435,7 +1435,7 @@ export class LanguageModelsService implements ILanguageModelsService { if (response?.paid) { const paidEntries = Array.isArray(response.paid) ? response.paid : Object.values(response.paid); for (const entry of paidEntries) { - if (!entry || !isObject(entry) || typeof entry.id !== 'string') { + if (!entry || !isObject(entry)) { continue; } paid[entry.id] = { label: entry.label, featured: entry.featured, minVSCodeVersion: entry.minVSCodeVersion }; From 7f4996ff8429f7bb527ebaa0f4a89f6b42e8cf9a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 20 Feb 2026 15:15:06 +0100 Subject: [PATCH 08/17] sessions - separate protocol handler (#296514) --- build/gulpfile.vscode.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 31c31f58406ab..bc34262af5c1e 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -389,7 +389,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d const isInsiderOrExploration = quality === 'insider' || quality === 'exploration'; const embedded = isInsiderOrExploration - ? (product as typeof product & { embedded?: { nameShort: string; nameLong: string; applicationName: string; dataFolderName: string; darwinBundleIdentifier: string } }).embedded + ? (product as typeof product & { embedded?: { nameShort: string; nameLong: string; applicationName: string; dataFolderName: string; darwinBundleIdentifier: string; urlProtocol: string } }).embedded : undefined; const packageSubJsonStream = isInsiderOrExploration @@ -409,6 +409,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d json.applicationName = embedded.applicationName; json.dataFolderName = embedded.dataFolderName; json.darwinBundleIdentifier = embedded.darwinBundleIdentifier; + json.urlProtocol = embedded.urlProtocol; return json; })) .pipe(rename('product.sub.json')) From 5ed0d0daa7f6c5b48cf0f585ce7eaea71ca9280e Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 20 Feb 2026 15:25:59 +0100 Subject: [PATCH 09/17] add logging (#296518) --- .../contrib/chat/common/languageModels.ts | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 9089427aeed7e..c4804f662aa2f 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -1488,16 +1488,34 @@ export class LanguageModelsService implements ILanguageModelsService { } private async _fetchChatControlData(): Promise { - const context = await this._requestService.request({ type: 'GET', url: this._chatControlUrl! }, CancellationToken.None); + this._logService.trace('[LM] Fetching chat control data from', this._chatControlUrl); + + let context; + try { + context = await this._requestService.request({ type: 'GET', url: this._chatControlUrl! }, CancellationToken.None); + } catch (err) { + this._logService.warn('[LM] Failed to request chat control data', getErrorMessage(err)); + return; + } if (context.res.statusCode !== 200) { - throw new Error('Could not get chat control data.'); + this._logService.warn(`[LM] Chat control data request failed with status ${context.res.statusCode}`); + return; } - const result = await asJson(context); + let result: IChatControlResponse | null; + try { + result = await asJson(context); + } catch (err) { + this._logService.warn('[LM] Failed to parse chat control response', getErrorMessage(err)); + return; + } + + this._logService.trace('[LM] Received chat control response', result ? Object.keys(result) : 'null'); if (!result || result.version !== 1) { - throw new Error('Unexpected chat control response.'); + this._logService.warn('[LM] Unexpected chat control response version', result?.version); + return; } // Update restricted chat participants @@ -1507,6 +1525,7 @@ export class LanguageModelsService implements ILanguageModelsService { // Update models control manifest if (result.models) { + this._logService.trace('[LM] Updating models control manifest', { freeCount: Object.keys(result.models.free ?? {}).length, paidCount: Object.keys(result.models.paid ?? {}).length }); this._setModelsControlManifest(result.models); this._storageService.store(CHAT_MODELS_CONTROL_STORAGE_KEY, JSON.stringify(result.models), StorageScope.APPLICATION, StorageTarget.MACHINE); } From 9529b11320af47517fcc0973bdff3e8c5b2197ee Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:32:33 +0100 Subject: [PATCH 10/17] Git - handle multiple extension host process (#296510) * Move the waitForState, and add a sequencer * Git - handle multiple extension host process * Improve open repository method --- .../browser/mainThreadGitExtensionService.ts | 15 ++++- .../workbench/api/common/extHost.protocol.ts | 1 + .../api/common/extHostGitExtensionService.ts | 5 ++ .../contrib/git/browser/gitService.ts | 60 ++++++++++++------- .../contrib/git/common/gitService.ts | 3 - 5 files changed, 58 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts b/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts index c40bf42242590..99d5b415b922e 100644 --- a/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts @@ -25,12 +25,23 @@ export class MainThreadGitExtensionService extends Disposable implements MainThr constructor( extHostContext: IExtHostContext, - @IGitService gitService: IGitService, + @IGitService private readonly gitService: IGitService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostGitExtension); - this._register(gitService.setDelegate(this)); + this._initializeDelegate(); + } + + private async _initializeDelegate(): Promise { + // Check whether the vscode.git extension is available in the extension host + // process before setting the delegate. The delegate should only be set once, + // for the extension host process that runs the vscode.git extension + const isExtensionAvailable = await this._proxy.$isGitExtensionAvailable(); + + if (isExtensionAvailable && !this._store.isDisposed) { + this._register(this.gitService.setDelegate(this)); + } } async openRepository(uri: URI): Promise { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 22792484d36b8..116d0b9f1fade 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3477,6 +3477,7 @@ export interface GitRefDto { } export interface ExtHostGitExtensionShape { + $isGitExtensionAvailable(): Promise; $openRepository(root: UriComponents): Promise; $getRefs(root: UriComponents, query: GitRefQueryDto, token?: CancellationToken): Promise; } diff --git a/src/vs/workbench/api/common/extHostGitExtensionService.ts b/src/vs/workbench/api/common/extHostGitExtensionService.ts index b02931d8e805e..91e267f62405f 100644 --- a/src/vs/workbench/api/common/extHostGitExtensionService.ts +++ b/src/vs/workbench/api/common/extHostGitExtensionService.ts @@ -75,6 +75,11 @@ export class ExtHostGitExtensionService extends Disposable implements IExtHostGi super(); } + async $isGitExtensionAvailable(): Promise { + const registry = await this._extHostExtensionService.getExtensionRegistry(); + return !!registry.getExtensionDescription(GIT_EXTENSION_ID); + } + async $openRepository(uri: UriComponents): Promise { const api = await this._ensureGitApi(); if (!api) { diff --git a/src/vs/workbench/contrib/git/browser/gitService.ts b/src/vs/workbench/contrib/git/browser/gitService.ts index c697b9cef97a2..8b6cab3183580 100644 --- a/src/vs/workbench/contrib/git/browser/gitService.ts +++ b/src/vs/workbench/contrib/git/browser/gitService.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Sequencer } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { BugIndicatingError } from '../../../../base/common/errors.js'; import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../base/common/map.js'; -import { observableValue } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { IGitService, IGitExtensionDelegate, GitRef, GitRefQuery, IGitRepository } from '../common/gitService.js'; @@ -14,44 +15,61 @@ export class GitService extends Disposable implements IGitService { declare readonly _serviceBrand: undefined; private _delegate: IGitExtensionDelegate | undefined; + private readonly _openRepositorySequencer = new Sequencer(); private readonly _repositories = new ResourceMap(); get repositories(): Iterable { return this._repositories.values(); } - readonly isInitialized = observableValue(this, false); - setDelegate(delegate: IGitExtensionDelegate): IDisposable { + // The delegate can only be set once, since the vscode.git + // extension can only run in one extension host process per + // window. + if (this._delegate) { + throw new BugIndicatingError('GitService delegate is already set.'); + } + this._delegate = delegate; - this.isInitialized.set(true, undefined); return toDisposable(() => { - this._delegate = undefined; this._repositories.clear(); - this.isInitialized.set(false, undefined); + this._delegate = undefined; }); } async openRepository(uri: URI): Promise { - if (!this._delegate) { - return undefined; - } + return this._openRepositorySequencer.queue(async () => { + if (!this._delegate) { + return undefined; + } - const root = await this._delegate.openRepository(uri); - if (!root) { - return undefined; - } + // Check whether we have an opened repository for the uri + let repository = this._repositories.get(uri); + if (repository) { + return repository; + } - const rootUri = URI.revive(root); - let repository = this._repositories.get(rootUri); - if (repository) { - return repository; - } + // Open the repository to get the repository root + const root = await this._delegate.openRepository(uri); + if (!root) { + return undefined; + } + + const rootUri = URI.revive(root); + + // Check whether we have an opened repository for the root + repository = this._repositories.get(rootUri); + if (repository) { + return repository; + } - repository = new GitRepository(this._delegate, rootUri); - this._repositories.set(rootUri, repository); - return repository; + // Create a new repository + repository = new GitRepository(this._delegate, rootUri); + this._repositories.set(rootUri, repository); + + return repository; + }); } } diff --git a/src/vs/workbench/contrib/git/common/gitService.ts b/src/vs/workbench/contrib/git/common/gitService.ts index 5f1c3807ee8bc..89f76d083cf00 100644 --- a/src/vs/workbench/contrib/git/common/gitService.ts +++ b/src/vs/workbench/contrib/git/common/gitService.ts @@ -5,7 +5,6 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable } from '../../../../base/common/observable.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; @@ -44,8 +43,6 @@ export const IGitService = createDecorator('gitService'); export interface IGitService { readonly _serviceBrand: undefined; - readonly isInitialized: IObservable; - readonly repositories: Iterable; setDelegate(delegate: IGitExtensionDelegate): IDisposable; From f71c31e85225d31860b273d414eb9935edfdea43 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 20 Feb 2026 06:34:06 -0800 Subject: [PATCH 11/17] Show stop button when question is active --- .../contrib/chat/browser/actions/chatExecuteActions.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 1fac629812d06..a9e08c350c5db 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -181,7 +181,11 @@ abstract class SubmitAction extends Action2 { } } -const requestInProgressOrPendingToolCall = ContextKeyExpr.or(ChatContextKeys.requestInProgress, ChatContextKeys.Editing.hasToolConfirmation); +const requestInProgressOrPendingToolCall = ContextKeyExpr.or( + ChatContextKeys.requestInProgress, + ChatContextKeys.Editing.hasToolConfirmation, + ChatContextKeys.Editing.hasQuestionCarousel, +); const whenNotInProgress = ChatContextKeys.requestInProgress.negate(); export class ChatSubmitAction extends SubmitAction { From da432668a4627f6cbf97c37aae86051cbc968519 Mon Sep 17 00:00:00 2001 From: Isidor Date: Fri, 20 Feb 2026 15:40:22 +0100 Subject: [PATCH 12/17] fix hygeien --- .../chat/test/browser/widget/input/chatModelPicker.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts index 703ba80839c3e..704754ad90818 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts @@ -519,7 +519,7 @@ suite('buildModelPickerItems', () => { [auto], undefined, ['missing-model'], - { 'missing-model': { id: 'missing-model', label: 'Missing Model' } }, + { 'missing-model': { label: 'Missing Model' } }, true, '1.100.0', StateType.Idle, From 7a0848c8173d78561f4bb3d97ace48419541ab8e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 20 Feb 2026 16:09:15 +0100 Subject: [PATCH 13/17] :up: distro (#296526) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fb6ac1043c878..df095bf4486f3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.110.0", - "distro": "85914d5a600261a53306190177be48aa8f0cdfb4", + "distro": "bd187e4508a244500eb533c56e5cccb6801a699c", "author": { "name": "Microsoft Corporation" }, @@ -245,4 +245,4 @@ "optionalDependencies": { "windows-foreground-love": "0.6.1" } -} +} \ No newline at end of file From de687420b5dcee51641ebe5527557ee1c7a78e75 Mon Sep 17 00:00:00 2001 From: Isidor Date: Fri, 20 Feb 2026 16:38:24 +0100 Subject: [PATCH 14/17] refactor: remove upgradePlanUrl from model picker functions and tests --- .../chat/browser/widget/input/chatModelPicker.ts | 15 ++++----------- .../browser/widget/input/chatModelPicker.test.ts | 4 ---- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts index cc98c13f05678..db11cf80294f7 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts @@ -128,7 +128,6 @@ export function buildModelPickerItems( currentVSCodeVersion: string, updateStateType: StateType, onSelect: (model: ILanguageModelChatMetadataAndIdentifier) => void, - upgradePlanUrl: string | undefined, manageSettingsUrl: string | undefined, commandService: ICommandService, chatEntitlementService: IChatEntitlementService, @@ -260,7 +259,7 @@ export function buildModelPickerItems( if (item.kind === 'available') { items.push(createModelItem(createModelAction(item.model, selectedModelId, onSelect), item.model)); } else { - items.push(createUnavailableModelItem(item.id, item.entry, item.reason, upgradePlanUrl, manageSettingsUrl, updateStateType)); + items.push(createUnavailableModelItem(item.id, item.entry, item.reason, manageSettingsUrl, updateStateType)); } } } @@ -302,7 +301,7 @@ export function buildModelPickerItems( for (const model of otherModels) { const entry = controlModels[model.metadata.id] ?? controlModels[model.identifier]; if (entry?.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { - items.push(createUnavailableModelItem(model.metadata.id, entry, 'update', upgradePlanUrl, manageSettingsUrl, updateStateType, ModelPickerSection.Other)); + items.push(createUnavailableModelItem(model.metadata.id, entry, 'update', manageSettingsUrl, updateStateType, ModelPickerSection.Other)); } else { items.push(createModelItem(createModelAction(model, selectedModelId, onSelect, ModelPickerSection.Other), model)); } @@ -377,7 +376,6 @@ function createUnavailableModelItem( id: string, entry: IModelControlEntry, reason: 'upgrade' | 'update' | 'admin', - upgradePlanUrl: string | undefined, manageSettingsUrl: string | undefined, updateStateType: StateType, section?: string, @@ -385,9 +383,7 @@ function createUnavailableModelItem( let description: string | MarkdownString | undefined; if (reason === 'upgrade') { - description = upgradePlanUrl - ? new MarkdownString(localize('chat.modelPicker.upgradeLink', "[Upgrade your plan]({0})", upgradePlanUrl), { isTrusted: true }) - : localize('chat.modelPicker.upgrade', "Upgrade"); + description = new MarkdownString(localize('chat.modelPicker.upgradeLink', "[Upgrade your plan](command:workbench.action.chat.upgradePlan)"), { isTrusted: true }); } else if (reason === 'update') { description = localize('chat.modelPicker.updateDescription', "Update VS Code"); } else { @@ -399,9 +395,7 @@ function createUnavailableModelItem( let hoverContent: MarkdownString; if (reason === 'upgrade') { hoverContent = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); - hoverContent.appendMarkdown(upgradePlanUrl - ? localize('chat.modelPicker.upgradeHover', "This model requires a paid plan. [Upgrade]({0}) to access it.", upgradePlanUrl) - : localize('chat.modelPicker.upgradeHoverNoLink', "This model requires a paid plan.")); + hoverContent.appendMarkdown(localize('chat.modelPicker.upgradeHover', "[Upgrade your plan](command:workbench.action.chat.upgradePlan) to use this model.")); } else if (reason === 'update') { hoverContent = getUpdateHoverContent(updateStateType); } else { @@ -550,7 +544,6 @@ export class ModelPickerWidget extends Disposable { this._productService.version, this._updateService.state.type, onSelect, - this._productService.defaultChatAgent?.upgradePlanUrl, this._productService.defaultChatAgent?.manageSettingsUrl, this._commandService, this._entitlementService diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts index 704754ad90818..b63815fa125d5 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts @@ -71,7 +71,6 @@ function callBuild( isProUser?: boolean; currentVSCodeVersion?: string; updateStateType?: StateType; - upgradePlanUrl?: string; manageSettingsUrl?: string; } = {}, ): IActionListItem[] { @@ -85,7 +84,6 @@ function callBuild( opts.currentVSCodeVersion ?? '1.100.0', opts.updateStateType ?? StateType.Idle, onSelect, - opts.upgradePlanUrl, opts.manageSettingsUrl, stubCommandService, stubChatEntitlementService as IChatEntitlementService, @@ -442,7 +440,6 @@ suite('buildModelPickerItems', () => { StateType.Idle, onSelect, undefined, - undefined, stubCommandService, stubChatEntitlementService as IChatEntitlementService, ); @@ -524,7 +521,6 @@ suite('buildModelPickerItems', () => { '1.100.0', StateType.Idle, () => { }, - undefined, 'https://aka.ms/github-copilot-settings', stubCommandService, stubChatEntitlementService as IChatEntitlementService, From df22d31761882c5c9ff97ba85d4f1d66bb46ad6d Mon Sep 17 00:00:00 2001 From: Isidor Date: Fri, 20 Feb 2026 16:56:06 +0100 Subject: [PATCH 15/17] remove ugly native hover --- .../contrib/chat/browser/widget/input/chatModelPicker.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts index db11cf80294f7..8e46f1fd327ab 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts @@ -383,7 +383,7 @@ function createUnavailableModelItem( let description: string | MarkdownString | undefined; if (reason === 'upgrade') { - description = new MarkdownString(localize('chat.modelPicker.upgradeLink', "[Upgrade your plan](command:workbench.action.chat.upgradePlan)"), { isTrusted: true }); + description = new MarkdownString(localize('chat.modelPicker.upgradeLink', "[Upgrade your plan](command:workbench.action.chat.upgradePlan \" \")"), { isTrusted: true }); } else if (reason === 'update') { description = localize('chat.modelPicker.updateDescription', "Update VS Code"); } else { @@ -395,7 +395,7 @@ function createUnavailableModelItem( let hoverContent: MarkdownString; if (reason === 'upgrade') { hoverContent = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); - hoverContent.appendMarkdown(localize('chat.modelPicker.upgradeHover', "[Upgrade your plan](command:workbench.action.chat.upgradePlan) to use this model.")); + hoverContent.appendMarkdown(localize('chat.modelPicker.upgradeHover', "[Upgrade your plan](command:workbench.action.chat.upgradePlan \" \") to use this model.")); } else if (reason === 'update') { hoverContent = getUpdateHoverContent(updateStateType); } else { From da7774420ac7ddee28d378c9eba3176dfcf125ea Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 20 Feb 2026 16:57:27 +0100 Subject: [PATCH 16/17] Fix #296523 (#296530) --- .../browser/widget/input/chatModelPicker.ts | 4 +-- .../contrib/chat/common/languageModels.ts | 26 ++++++++++++++-- .../widget/input/chatModelPicker.test.ts | 30 +++++++++---------- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts index cc98c13f05678..26a018615c089 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts @@ -207,7 +207,7 @@ export function buildModelPickerItems( } if (!model) { const entry = controlModels[id]; - if (entry) { + if (entry && !entry.exists) { markPlaced(id); promotedItems.push({ kind: 'unavailable', id, entry, reason: getUnavailableReason(entry) }); return true; @@ -239,7 +239,7 @@ export function buildModelPickerItems( } else { promotedItems.push({ kind: 'available', model }); } - } else if (!model) { + } else if (!model && !entry.exists) { markPlaced(entryId); promotedItems.push({ kind: 'unavailable', id: entryId, entry, reason: getUnavailableReason(entry) }); } diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index c4804f662aa2f..803c331fb67ec 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -405,6 +405,7 @@ export interface IModelControlEntry { readonly label: string; readonly featured?: boolean; readonly minVSCodeVersion?: string; + readonly exists: boolean; } export interface IModelsControlManifest { @@ -541,6 +542,7 @@ export class LanguageModelsService implements ILanguageModelsService { readonly onDidChangeModelsControlManifest = this._onDidChangeModelsControlManifest.event; private _modelsControlManifest: IModelsControlManifest = { free: {}, paid: {} }; + private _modelsControlRawResponse: IChatControlResponse['models'] | undefined; private _chatControlUrl: string | undefined; private _chatControlDisposed = false; @@ -565,7 +567,10 @@ export class LanguageModelsService implements ILanguageModelsService { this._initChatControlData(); this._store.add(this._storageService.onDidChangeValue(StorageScope.PROFILE, CHAT_MODEL_PICKER_PREFERENCES_STORAGE_KEY, this._store)(() => this._onDidChangeModelPickerPreferences())); - this._store.add(this.onDidChangeLanguageModels(() => this._hasUserSelectableModels.set(this._modelCache.size > 0 && Array.from(this._modelCache.values()).some(model => model.isUserSelectable)))); + this._store.add(this.onDidChangeLanguageModels(() => { + this._hasUserSelectableModels.set(this._modelCache.size > 0 && Array.from(this._modelCache.values()).some(model => model.isUserSelectable)); + this._refreshModelsControlManifest(); + })); this._store.add(this._languageModelsConfigurationService.onDidChangeLanguageModelGroups(changedGroups => this._onDidChangeLanguageModelGroups(changedGroups))); this._store.add(languageModelChatProviderExtensionPoint.setHandler((extensions, { added, removed }) => { @@ -1419,6 +1424,12 @@ export class LanguageModelsService implements ILanguageModelsService { } private _setModelsControlManifest(response: IChatControlResponse['models']): void { + this._modelsControlRawResponse = response; + this._refreshModelsControlManifest(); + } + + private _refreshModelsControlManifest(): void { + const response = this._modelsControlRawResponse; const free: IStringDictionary = {}; const paid: IStringDictionary = {}; @@ -1428,7 +1439,7 @@ export class LanguageModelsService implements ILanguageModelsService { if (!entry || !isObject(entry)) { continue; } - free[entry.id] = { label: entry.label, featured: entry.featured }; + free[entry.id] = { label: entry.label, featured: entry.featured, exists: this._modelExistsInCache(entry.id) }; } } @@ -1438,7 +1449,7 @@ export class LanguageModelsService implements ILanguageModelsService { if (!entry || !isObject(entry)) { continue; } - paid[entry.id] = { label: entry.label, featured: entry.featured, minVSCodeVersion: entry.minVSCodeVersion }; + paid[entry.id] = { label: entry.label, featured: entry.featured, minVSCodeVersion: entry.minVSCodeVersion, exists: this._modelExistsInCache(entry.id) }; } } @@ -1446,6 +1457,15 @@ export class LanguageModelsService implements ILanguageModelsService { this._onDidChangeModelsControlManifest.fire(this._modelsControlManifest); } + private _modelExistsInCache(metadataId: string): boolean { + for (const model of this._modelCache.values()) { + if (model.id === metadataId) { + return true; + } + } + return false; + } + //#region Chat control data private _initChatControlData(): void { diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts index 704754ad90818..2739b01c44ed4 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts @@ -141,7 +141,7 @@ suite('buildModelPickerItems', () => { const items = callBuild([auto, modelA], { selectedModelId: modelA.identifier, controlModels: { - 'gpt-4o': { label: 'GPT-4o', minVSCodeVersion: '2.0.0' }, + 'gpt-4o': { label: 'GPT-4o', minVSCodeVersion: '2.0.0', exists: true }, }, currentVSCodeVersion: '1.90.0', }); @@ -172,7 +172,7 @@ suite('buildModelPickerItems', () => { const items = callBuild([auto], { recentModelIds: ['missing-model'], controlModels: { - 'missing-model': { label: 'Missing Model' }, + 'missing-model': { label: 'Missing Model', exists: false }, }, isProUser: false, }); @@ -187,7 +187,7 @@ suite('buildModelPickerItems', () => { const items = callBuild([auto], { recentModelIds: ['missing-model'], controlModels: { - 'missing-model': { label: 'Missing Model', minVSCodeVersion: '2.0.0' }, + 'missing-model': { label: 'Missing Model', minVSCodeVersion: '2.0.0', exists: false }, }, isProUser: true, currentVSCodeVersion: '1.90.0', @@ -203,7 +203,7 @@ suite('buildModelPickerItems', () => { const items = callBuild([auto], { recentModelIds: ['missing-model'], controlModels: { - 'missing-model': { label: 'Missing Model' }, + 'missing-model': { label: 'Missing Model', exists: false }, }, isProUser: true, }); @@ -219,7 +219,7 @@ suite('buildModelPickerItems', () => { const modelB = createModel('claude', 'Claude'); const items = callBuild([auto, modelA, modelB], { controlModels: { - 'gpt-4o': { label: 'GPT-4o', featured: true }, + 'gpt-4o': { label: 'GPT-4o', featured: true, exists: true }, }, }); const actions = getActionItems(items); @@ -232,7 +232,7 @@ suite('buildModelPickerItems', () => { const auto = createAutoModel(); const items = callBuild([auto], { controlModels: { - 'premium-model': { label: 'Premium Model', featured: true }, + 'premium-model': { label: 'Premium Model', featured: true, exists: false }, }, isProUser: false, }); @@ -246,7 +246,7 @@ suite('buildModelPickerItems', () => { const auto = createAutoModel(); const items = callBuild([auto], { controlModels: { - 'premium-model': { label: 'Premium Model', featured: true }, + 'premium-model': { label: 'Premium Model', featured: true, exists: false }, }, isProUser: true, }); @@ -261,7 +261,7 @@ suite('buildModelPickerItems', () => { const modelA = createModel('gpt-4o', 'GPT-4o'); const items = callBuild([auto, modelA], { controlModels: { - 'gpt-4o': { label: 'GPT-4o', featured: true, minVSCodeVersion: '2.0.0' }, + 'gpt-4o': { label: 'GPT-4o', featured: true, minVSCodeVersion: '2.0.0', exists: true }, }, currentVSCodeVersion: '1.90.0', }); @@ -277,7 +277,7 @@ suite('buildModelPickerItems', () => { const modelB = createModel('claude', 'Claude'); const items = callBuild([auto, modelA, modelB], { controlModels: { - 'gpt-4o': { label: 'GPT-4o', featured: false }, + 'gpt-4o': { label: 'GPT-4o', featured: false, exists: true }, }, }); // With no selected, no recent, and no featured, both models should be in Other @@ -311,7 +311,7 @@ suite('buildModelPickerItems', () => { const items = callBuild([auto, modelA], { recentModelIds: [modelA.identifier, 'missing-model'], controlModels: { - 'missing-model': { label: 'Missing Model' }, + 'missing-model': { label: 'Missing Model', exists: false }, }, isProUser: false, }); @@ -359,7 +359,7 @@ suite('buildModelPickerItems', () => { const modelA = createModel('gpt-4o', 'GPT-4o'); const items = callBuild([auto, modelA], { controlModels: { - 'gpt-4o': { label: 'GPT-4o', minVSCodeVersion: '2.0.0' }, + 'gpt-4o': { label: 'GPT-4o', minVSCodeVersion: '2.0.0', exists: true }, }, currentVSCodeVersion: '1.90.0', }); @@ -378,8 +378,8 @@ suite('buildModelPickerItems', () => { selectedModelId: modelA.identifier, recentModelIds: [modelA.identifier, modelB.identifier], controlModels: { - 'gpt-4o': { label: 'GPT-4o', featured: true }, - 'claude': { label: 'Claude', featured: true }, + 'gpt-4o': { label: 'GPT-4o', featured: true, exists: true }, + 'claude': { label: 'Claude', featured: true, exists: true }, }, }); const labels = getActionLabels(items).filter(l => l !== 'Other Models' && !l.includes('Manage Models')); @@ -394,7 +394,7 @@ suite('buildModelPickerItems', () => { selectedModelId: auto.identifier, recentModelIds: [auto.identifier], controlModels: { - 'auto': { label: 'Auto', featured: true }, + 'auto': { label: 'Auto', featured: true, exists: true }, }, }); const autoItems = getActionItems(items).filter(a => a.label === 'Auto'); @@ -501,7 +501,7 @@ suite('buildModelPickerItems', () => { const items = callBuild([auto, modelA, modelB, modelC, modelD], { recentModelIds: [modelC.identifier], controlModels: { - 'alpha': { label: 'Alpha', featured: true }, + 'alpha': { label: 'Alpha', featured: true, exists: true }, }, }); const actions = getActionItems(items); From 0432ab0df687ff80b1340d39962a161279477bf4 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 20 Feb 2026 10:02:43 -0600 Subject: [PATCH 17/17] if user has unselected options, respect that (#296387) fixes #292032 --- .../chatQuestionCarouselPart.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts index d1284a8d1e601..20aa1bdcd56b8 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts @@ -1054,20 +1054,10 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const freeformTextarea = this._freeformTextareas.get(question.id); const freeformValue = freeformTextarea?.value !== '' ? freeformTextarea?.value : undefined; - // Only include defaults if nothing selected AND no freeform input - let finalSelectedValues = selectedValues; - if (selectedValues.length === 0 && !freeformValue && question.defaultValue !== undefined) { - const defaultIds = Array.isArray(question.defaultValue) - ? question.defaultValue - : [question.defaultValue]; - const defaultValues = question.options - ?.filter(opt => defaultIds.includes(opt.id)) - .map(opt => opt.value); - finalSelectedValues = defaultValues?.filter(v => v !== undefined) || []; - } - - if (freeformValue || finalSelectedValues.length > 0) { - return { selectedValues: finalSelectedValues, freeformValue }; + // Return whatever was selected - defaults are applied at render time when + // checkboxes are initially checked, so empty selection means user unchecked all + if (freeformValue || selectedValues.length > 0) { + return { selectedValues, freeformValue }; } return undefined; }