From cd09c911526bcdbfa9376013dd28fc1a3e383fd6 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Tue, 27 Jan 2026 17:11:32 -0800 Subject: [PATCH 001/152] fix: enhance subagent tool result with metadata and default name --- package.json | 2 +- .../workbench/api/common/extHostTypeConverters.ts | 10 +++++++--- .../common/tools/builtinTools/runSubagentTool.ts | 15 +++++++++++++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 977853c6f02..3120a157741 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "watch-extensions": "npm run gulp watch-extensions watch-extension-media", "watch-extensionsd": "deemon npm run watch-extensions", "kill-watch-extensionsd": "deemon --kill npm run watch-extensions", - "precommit": "node build/hygiene.ts", + "precommit": "node --experimental-strip-types build/hygiene.ts", "gulp": "node --max-old-space-size=8192 ./node_modules/gulp/bin/gulp.js", "electron": "node build/lib/electron.ts", "7z": "7z", diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index d14c61952d1..c5ddf191f01 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -3717,8 +3717,8 @@ export namespace LanguageModelToolSource { } export namespace LanguageModelToolResult { - export function to(result: IToolResult): vscode.LanguageModelToolResult { - return new types.LanguageModelToolResult(result.content.map(item => { + export function to(result: IToolResult): vscode.ExtendedLanguageModelToolResult { + const toolResult = new types.LanguageModelToolResult(result.content.map(item => { if (item.kind === 'text') { return new types.LanguageModelTextPart(item.value, item.audience); } else if (item.kind === 'data') { @@ -3726,7 +3726,11 @@ export namespace LanguageModelToolResult { } else { return new types.LanguageModelPromptTsxPart(item.value); } - })); + })) as vscode.ExtendedLanguageModelToolResult; + if (result.toolMetadata !== undefined) { + toolResult.toolMetadata = result.toolMetadata; + } + return toolResult; } export function from(result: vscode.ExtendedLanguageModelToolResult2, extension: IExtensionDescription): Dto | SerializableObjectWithBuffers> { 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 e9245caf859..7fdab8e2ae3 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -240,7 +240,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { variables: { variables: variableSet.asArray() }, location: ChatAgentLocation.Chat, subAgentInvocationId: invocation.callId, - subAgentName: args.agentName, + subAgentName: args.agentName ?? 'subagent', userSelectedModelId: modeModelId, userSelectedTools: modeTools, modeInstructions, @@ -267,7 +267,18 @@ export class RunSubagentTool extends Disposable implements IToolImpl { invocation.toolSpecificData.result = resultText; } - return createToolSimpleTextResult(resultText); + // Return result with toolMetadata containing subAgentInvocationId for trajectory tracking + return { + content: [{ + kind: 'text', + value: resultText + }], + toolMetadata: { + subAgentInvocationId, + description: args.description, + agentName: agentRequest.subAgentName, + } + }; } catch (error) { const errorMessage = `Error invoking subagent: ${error instanceof Error ? error.message : 'Unknown error'}`; From 7b0aa3bfa0ad937a1100043db22d1d0b59128310 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 28 Jan 2026 15:48:03 +0100 Subject: [PATCH 002/152] fix #290688 (#291294) --- .../common/extensionGalleryService.ts | 163 ++++++++++++++---- 1 file changed, 131 insertions(+), 32 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index e74641e055e..001e5d65b15 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -54,7 +54,7 @@ export interface IRawGalleryExtensionVersion { readonly assetUri: string; readonly fallbackAssetUri: string; readonly files: IRawGalleryExtensionFile[]; - readonly properties?: IRawGalleryExtensionProperty[]; + properties?: IRawGalleryExtensionProperty[]; readonly targetPlatform?: string; } @@ -348,6 +348,11 @@ function getEngine(version: IRawGalleryExtensionVersion): string { return (values.length > 0 && values[0].value) || ''; } +function setEngine(version: IRawGalleryExtensionVersion, engine: string): void { + version.properties = version.properties ?? []; + version.properties.push({ key: PropertyType.Engine, value: engine }); +} + function isPreReleaseVersion(version: IRawGalleryExtensionVersion): boolean { const values = version.properties ? version.properties.filter(p => p.key === PropertyType.PreRelease) : []; return values.length > 0 && values[0].value === 'true'; @@ -829,11 +834,24 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle return 'NOT_FOUND'; } - const targetPlatform = options.targetPlatform ?? CURRENT_TARGET_PLATFORM; const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension); - const rawGalleryExtensionVersion = await this.getValidRawGalleryExtensionVersion( + const rawGalleryExtensionVersion = await this.getValidRawGalleryExtensionVersionFromLatestVersions(rawGalleryExtension, rawGalleryExtension.versions, extensionInfo, options, allTargetPlatforms); + + if (!rawGalleryExtensionVersion) { + return 'NOT_COMPATIBLE'; + } + + return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, extensionGalleryManifest, this.productService); + } + + private async getValidRawGalleryExtensionVersionFromLatestVersions(rawGalleryExtension: IRawGalleryExtension, latestVersions: IRawGalleryExtensionVersion[], extensionInfo: IExtensionInfo, options: IExtensionQueryOptions, allTargetPlatforms: TargetPlatform[]): Promise { + const targetPlatform = options.targetPlatform ?? CURRENT_TARGET_PLATFORM; + const latestExtensionVersionsForTargetPlatform = filterLatestExtensionVersionsForTargetPlatform(latestVersions, targetPlatform, allTargetPlatforms); + + // First, find a valid version matching the requested type (pre-release or release) + const result = await this.getValidRawGalleryExtensionVersion( rawGalleryExtension, - filterLatestExtensionVersionsForTargetPlatform(rawGalleryExtension.versions, targetPlatform, allTargetPlatforms), + latestExtensionVersionsForTargetPlatform, { targetPlatform, compatible: !!options.compatible, @@ -841,14 +859,63 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle version: this.productService.version, date: this.productService.date }, - version: extensionInfo.preRelease ? VersionKind.Latest : VersionKind.Release + version: extensionInfo.preRelease ? VersionKind.Prerelease : VersionKind.Release }, allTargetPlatforms); - if (rawGalleryExtensionVersion) { - return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, extensionGalleryManifest, this.productService); + // For release version requests, simply return the found release version + if (!extensionInfo.preRelease) { + return result; } - return 'NOT_COMPATIBLE'; + // For pre-release version requests, we need to consider both pre-release and release versions + const prereleaseVersion = result; + const releaseVersion = await this.getValidRawGalleryExtensionVersion( + rawGalleryExtension, + latestExtensionVersionsForTargetPlatform, + { + targetPlatform, + compatible: !!options.compatible, + productVersion: options.productVersion ?? { + version: this.productService.version, + date: this.productService.date + }, + version: VersionKind.Release + }, allTargetPlatforms); + + // When both versions exist, return whichever has the higher version number + if (prereleaseVersion && releaseVersion) { + return semver.gt(releaseVersion.version, prereleaseVersion.version) ? releaseVersion : prereleaseVersion; + } + + // Special handling for compatible version requests + if (options.compatible) { + // If we have a compatible release version, check if it's better than any pre-release + if (releaseVersion) { + // Check if there exists any pre-release version (ignoring compatibility) + const anyPrereleaseVersion = await this.getValidRawGalleryExtensionVersion( + rawGalleryExtension, + latestExtensionVersionsForTargetPlatform, + { + targetPlatform, + compatible: false, + productVersion: options.productVersion ?? { + version: this.productService.version, + date: this.productService.date + }, + version: VersionKind.Prerelease + }, allTargetPlatforms); + + // If no pre-release exists or the release version is greater, prefer the compatible release + // This ensures users get a stable compatible version when pre-releases aren't newer or compatible + if (!anyPrereleaseVersion || semver.gt(releaseVersion.version, anyPrereleaseVersion.version)) { + return releaseVersion; + } + } + return prereleaseVersion; + } + + // Return pre-release if available, otherwise release, otherwise null + return prereleaseVersion ?? releaseVersion ?? null; } async getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { @@ -962,40 +1029,54 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle private async isEngineValid(extensionId: string, version: string, engine: string | undefined, manifestAsset: IGalleryExtensionAsset | null, productVersion: IProductVersion): Promise { if (!engine) { - if (!manifestAsset) { - this.logService.error(`Missing engine and manifest asset for the extension ${extensionId} with version ${version}`); - return false; - } try { - type GalleryServiceEngineFallbackClassification = { - owner: 'sandy081'; - comment: 'Fallback request when engine is not found in properties of an extension version'; - extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension name' }; - extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'version' }; - }; - type GalleryServiceEngineFallbackEvent = { - extension: string; - extensionVersion: string; - }; - this.telemetryService.publicLog2('galleryService:engineFallback', { extension: extensionId, extensionVersion: version }); - - const headers = { 'Accept-Encoding': 'gzip' }; - const context = await this.getAsset(extensionId, manifestAsset, AssetType.Manifest, version, { headers }); - const manifest = await asJson(context); - if (!manifest) { - this.logService.error(`Manifest was not found for the extension ${extensionId} with version ${version}`); - return false; - } - engine = manifest.engines.vscode; + engine = await this.getEngine(extensionId, version, manifestAsset); } catch (error) { this.logService.error(`Error while getting the engine for the version ${version}.`, getErrorMessage(error)); return false; } } + if (!engine) { + this.logService.error(`Missing engine for the extension ${extensionId} with version ${version}`); + return false; + } + return isEngineValid(engine, productVersion.version, productVersion.date); } + private async getEngine(extensionId: string, version: string, manifestAsset: IGalleryExtensionAsset | null): Promise { + if (!manifestAsset) { + this.logService.error(`Missing engine and manifest asset for the extension ${extensionId} with version ${version}`); + return undefined; + } + try { + type GalleryServiceEngineFallbackClassification = { + owner: 'sandy081'; + comment: 'Fallback request when engine is not found in properties of an extension version'; + extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension name' }; + extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'version' }; + }; + type GalleryServiceEngineFallbackEvent = { + extension: string; + extensionVersion: string; + }; + this.telemetryService.publicLog2('galleryService:engineFallback', { extension: extensionId, extensionVersion: version }); + + const headers = { 'Accept-Encoding': 'gzip' }; + const context = await this.getAsset(extensionId, manifestAsset, AssetType.Manifest, version, { headers }); + const manifest = await asJson(context); + if (!manifest) { + this.logService.error(`Manifest was not found for the extension ${extensionId} with version ${version}`); + return undefined; + } + return manifest.engines.vscode; + } catch (error) { + this.logService.error(`Error while getting the engine for the version ${version}.`, getErrorMessage(error)); + return undefined; + } + } + async query(options: IQueryOptions, token: CancellationToken): Promise> { const extensionGalleryManifest = await this.extensionGalleryManifestService.getExtensionGalleryManifest(); @@ -1211,6 +1292,9 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle for (let index = 0; index < rawGalleryExtensionVersions.length; index++) { const rawGalleryExtensionVersion = rawGalleryExtensionVersions[index]; + if (criteria.compatible) { + await this.setEngineIfNotExists(extensionIdentifier.id, rawGalleryExtensionVersion); + } if (await this.isValidVersion( { id: extensionIdentifier.id, @@ -1243,6 +1327,21 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle return rawGalleryExtension.versions[0]; } + private async setEngineIfNotExists(extensionId: string, rawGalleryExtensionVersion: IRawGalleryExtensionVersion): Promise { + if (getEngine(rawGalleryExtensionVersion)) { + return; + } + + try { + const engine = await this.getEngine(extensionId, rawGalleryExtensionVersion.version, getVersionAsset(rawGalleryExtensionVersion, AssetType.Manifest)); + if (engine) { + setEngine(rawGalleryExtensionVersion, engine); + } + } catch (error) { + this.logService.error(`Error while getting the engine for the version ${rawGalleryExtensionVersion.version}.`, getErrorMessage(error)); + } + } + private async queryRawGalleryExtensions(query: Query, extensionGalleryManifest: IExtensionGalleryManifest, token: CancellationToken): Promise { const extensionsQueryApi = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, ExtensionGalleryResourceType.ExtensionQueryService); From c721687d3696e9669e97a762786ab94fd4dcf09d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 28 Jan 2026 16:01:49 +0100 Subject: [PATCH 003/152] layout - reduce scope of `workbench.secondarySideBar.forceMaximized` to only apply when editors are closed (#291293) * layout - reduce scope of `workbench.secondarySideBar.forceMaximized` to only apply when editors are closed We later need to revisit how a better layout would be for focus on sessions, but the current implementation has too many bugs. * Update src/vs/workbench/browser/workbench.contribution.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * . --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/vs/workbench/browser/layout.ts | 11 +---------- src/vs/workbench/browser/workbench.contribution.ts | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index af8ce70f5ad..fc7da781979 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -354,12 +354,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } }; - // Maybe maximize auxiliary bar when no editors, sidebar hidden, and panel hidden + // Maybe maximize auxiliary bar when no editors are visible const maybeMaximizeAuxiliaryBar = () => { if ( this.mainPartEditorService.visibleEditors.length === 0 && - !this.isVisible(Parts.SIDEBAR_PART) && - !this.isVisible(Parts.PANEL_PART) && this.configurationService.getValue(WorkbenchLayoutSettings.AUXILIARYBAR_FORCE_MAXIMIZED) === true ) { this.setAuxiliaryBarMaximized(true); @@ -383,13 +381,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi })); this._register(this.editorGroupService.mainPart.onDidActivateGroup(showEditorIfHidden)); - // Maybe maximize auxiliary bar when sidebar or panel hides - this._register(this.onDidChangePartVisibility(({ partId, visible }) => { - if (!visible && (partId === Parts.SIDEBAR_PART || partId === Parts.PANEL_PART)) { - maybeMaximizeAuxiliaryBar(); - } - })); - // Revalidate center layout when active editor changes: diff editor quits centered mode this._register(this.mainPartEditorService.onDidActiveEditorChange(() => this.centerMainEditorLayout(this.stateModel.getRuntimeValue(LayoutStateKeys.MAIN_EDITOR_CENTERED)))); }); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index a1e90803d5a..5ea32fb1a11 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -589,7 +589,7 @@ const registry = Registry.as(ConfigurationExtensions.Con 'type': 'boolean', 'default': false, tags: ['experimental'], - 'description': localize('secondarySideBarForceMaximized', "Controls whether the secondary side bar is enforced to always show maximized unless other parts or editors are showing."), + 'description': localize('secondarySideBarForceMaximized', "Controls whether the secondary side bar is enforced to always show maximized on startup and when there are no open editors, in layouts that support a maximized secondary side bar."), }, 'workbench.secondarySideBar.showLabels': { 'type': 'boolean', From 921274364e982a1a81e06b7dc8b605a7fc73ec53 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 28 Jan 2026 10:48:01 -0500 Subject: [PATCH 004/152] Revert "fix: memory leak in abstract task service" (#291306) Revert "fix: memory leak in abstract task service (#289863)" This reverts commit d50d60023986a899797acfa426f8866846fe1a16. --- .../tasks/browser/abstractTaskService.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 48986a69454..96ce5ef97fd 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -8,8 +8,8 @@ import { IStringDictionary } from '../../../../base/common/collections.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import * as glob from '../../../../base/common/glob.js'; import * as json from '../../../../base/common/json.js'; -import { Disposable, DisposableMap, DisposableStore, dispose, IDisposable, IReference, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { LRUCache, ResourceMap, Touch } from '../../../../base/common/map.js'; +import { Disposable, DisposableStore, dispose, IDisposable, IReference, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { LRUCache, Touch } from '../../../../base/common/map.js'; import * as Objects from '../../../../base/common/objects.js'; import { ValidationState, ValidationStatus } from '../../../../base/common/parsers.js'; import * as Platform from '../../../../base/common/platform.js'; @@ -106,14 +106,13 @@ export namespace ConfigureTaskAction { export type TaskQuickPickEntryType = (IQuickPickItem & { task: Task }) | (IQuickPickItem & { folder: IWorkspaceFolder }) | (IQuickPickItem & { settingType: string }); -class ProblemReporter extends Disposable implements TaskConfig.IProblemReporter { +class ProblemReporter implements TaskConfig.IProblemReporter { private _validationStatus: ValidationStatus; - private readonly _onDidError: Emitter = this._register(new Emitter()); + private readonly _onDidError: Emitter = new Emitter(); public readonly onDidError: Event = this._onDidError.event; constructor(private _outputChannel: IOutputChannel) { - super(); this._validationStatus = new ValidationStatus(); } @@ -259,7 +258,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private _activatedTaskProviders: Set = new Set(); private readonly notification = this._register(new MutableDisposable()); - private readonly _workspaceTaskDisposables: DisposableMap = this._register(new DisposableMap(new ResourceMap())); constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -2645,10 +2643,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } await ProblemMatcherRegistry.onReady(); const taskSystemInfo: ITaskSystemInfo | undefined = this._getTaskSystemInfo(workspaceFolder.uri.scheme); - const store = new DisposableStore(); - this._workspaceTaskDisposables.set(workspaceFolder.uri, store); - const problemReporter = store.add(new ProblemReporter(this._outputChannel)); - store.add(problemReporter.onDidError(error => this._showOutput(runSource, undefined, error))); + const problemReporter = new ProblemReporter(this._outputChannel); + this._register(problemReporter.onDidError(error => this._showOutput(runSource, undefined, error))); const parseResult = TaskConfig.parse(workspaceFolder, undefined, taskSystemInfo ? taskSystemInfo.platform : Platform.platform, workspaceFolderConfiguration.config, problemReporter, TaskConfig.TaskConfigSource.TasksJson, this._contextKeyService); let hasErrors = false; if (!parseResult.validationStatus.isOK() && (parseResult.validationStatus.state !== ValidationState.Info)) { @@ -2671,7 +2667,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this._logService.warn('Custom workspace tasks are not supported.'); } return { workspaceFolder, set: { tasks: this._jsonTasksSupported ? parseResult.custom : [] }, configurations: customizedTasks, hasErrors }; - } private _testParseExternalConfig(config: TaskConfig.IExternalTaskRunnerConfiguration | undefined, location: string): { config: TaskConfig.IExternalTaskRunnerConfiguration | undefined; hasParseErrors: boolean } { From c157f884d4ca5d4df525d807310551de8e06bb44 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 28 Jan 2026 17:01:10 +0100 Subject: [PATCH 005/152] fix incorrect telemetry sending for inline completions that are never shown to the user --- .../browser/model/provideInlineCompletions.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts index 1748629085f..84c14a69639 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts @@ -380,7 +380,7 @@ export class InlineSuggestData { public async reportInlineEditShown(commandService: ICommandService, updatedInsertText: string, viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData, editKind: InlineSuggestionEditKind | undefined, timeWhenShown: number): Promise { this.updateShownDuration(viewKind); - if (this._didShow) { + if (this._didShow || this._didReportEndOfLife) { return; } this.addPerformanceMarker('shown'); @@ -429,6 +429,12 @@ export class InlineSuggestData { reason = this._lastSetEndOfLifeReason ?? { kind: InlineCompletionEndOfLifeReasonKind.Ignored, userTypingDisagreed: false, supersededBy: undefined }; } + // A suggestion can only be "rejected" if it was actually shown to the user. + // If the suggestion was never shown, downgrade to "ignored". + if (reason.kind === InlineCompletionEndOfLifeReasonKind.Rejected && !this._didShow) { + reason = { kind: InlineCompletionEndOfLifeReasonKind.Ignored, userTypingDisagreed: false, supersededBy: undefined }; + } + if (reason.kind === InlineCompletionEndOfLifeReasonKind.Rejected && this.source.provider.handleRejection) { this.source.provider.handleRejection(this.source.inlineSuggestions, this.sourceInlineCompletion); } From 1b505efacc8ddaf9a2591dbf3030c7ef215042c7 Mon Sep 17 00:00:00 2001 From: Robo Date: Thu, 29 Jan 2026 01:15:37 +0900 Subject: [PATCH 006/152] chore: update dmg icon vertical alignment (#291313) --- build/darwin/create-dmg.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/darwin/create-dmg.ts b/build/darwin/create-dmg.ts index 6bea7e76d5f..dcfb8001a8e 100644 --- a/build/darwin/create-dmg.ts +++ b/build/darwin/create-dmg.ts @@ -106,7 +106,7 @@ async function main(buildDir?: string, outDir?: string): Promise { 'text-size': 12, window: { position: { x: 100, y: 400 }, - size: { width: 480, height: 320 } + size: { width: 480, height: 352 } }, contents: [ { From 38ccea399ca0ce76f833b4b2d2cf2747394c67bf Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 28 Jan 2026 17:28:50 +0100 Subject: [PATCH 007/152] chat - introduce setting to show view badge or not (#291314) --- .../contrib/chat/browser/chat.contribution.ts | 5 +++ .../widgetHosts/viewPane/chatViewPane.ts | 33 +++++++++++++++++++ .../contrib/chat/common/constants.ts | 1 + 3 files changed, 39 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index ae0d92d901a..c364d06164e 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -440,6 +440,11 @@ configurationRegistry.registerConfiguration({ default: true, description: nls.localize('chat.viewTitle.enabled', "Show the title of the chat above the chat in the chat view."), }, + [ChatConfiguration.ChatViewProgressBadgeEnabled]: { + type: 'boolean', + default: false, + description: nls.localize('chat.viewProgressBadge.enabled', "Show a progress badge on the chat view when an agent session is in progress that is opened in that view."), + }, [ChatConfiguration.NotifyWindowOnResponseReceived]: { type: 'boolean', default: true, diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 3c8e240f4f5..b8265052b4e 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -55,6 +55,7 @@ import { IWorkbenchLayoutService, LayoutSettings, Position } from '../../../../. import { AgentSessionsViewerOrientation, AgentSessionsViewerPosition } from '../../agentSessions/agentSessions.js'; import { IProgressService } from '../../../../../../platform/progress/common/progress.js'; import { ChatViewId } from '../../chat.js'; +import { IActivityService, ProgressBadge } from '../../../../../services/activity/common/activity.js'; import { disposableTimeout } from '../../../../../../base/common/async.js'; import { AgentSessionsFilter, AgentSessionsGrouping } from '../../agentSessions/agentSessionsFilter.js'; import { IAgentSessionsService } from '../../agentSessions/agentSessionsService.js'; @@ -89,6 +90,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private restoringSession: Promise | undefined; private readonly modelRef = this._register(new MutableDisposable()); + private readonly activityBadge = this._register(new MutableDisposable()); + constructor( options: IViewPaneOptions, @IKeybindingService keybindingService: IKeybindingService, @@ -112,6 +115,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, @ICommandService private readonly commandService: ICommandService, + @IActivityService private readonly activityService: IActivityService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -607,6 +611,35 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this.relayout(); } })); + + // Show progress badge when the current session is in progress + const progressBadgeDisposables = this._register(new MutableDisposable()); + const updateProgressBadge = () => { + progressBadgeDisposables.value = new DisposableStore(); + + if (!this.configurationService.getValue(ChatConfiguration.ChatViewProgressBadgeEnabled)) { + this.activityBadge.clear(); + return; + } + + const model = chatWidget.viewModel?.model; + if (model) { + progressBadgeDisposables.value.add(autorun(reader => { + if (model.requestInProgress.read(reader)) { + this.activityBadge.value = this.activityService.showViewActivity(this.id, { + badge: new ProgressBadge(() => localize('sessionInProgress', "Agent Session in Progress")) + }); + } else { + this.activityBadge.clear(); + } + })); + } else { + this.activityBadge.clear(); + } + }; + this._register(chatWidget.onDidChangeViewModel(() => updateProgressBadge())); + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ChatConfiguration.ChatViewProgressBadgeEnabled))(() => updateProgressBadge())); + updateProgressBadge(); } private setupContextMenu(parent: HTMLElement): void { diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 62e5f123429..0315ae9ca5e 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -38,6 +38,7 @@ export enum ChatConfiguration { ChatViewSessionsGrouping = 'chat.viewSessions.grouping', ChatViewSessionsOrientation = 'chat.viewSessions.orientation', ChatViewTitleEnabled = 'chat.viewTitle.enabled', + ChatViewProgressBadgeEnabled = 'chat.viewProgressBadge.enabled', SubagentToolCustomAgents = 'chat.customAgentInSubagent.enabled', ShowCodeBlockProgressAnimation = 'chat.agent.codeBlockProgress', RestoreLastPanelSession = 'chat.restoreLastPanelSession', From b6d264afe96157866bc1653b43b15791f6ccf445 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 28 Jan 2026 17:29:26 +0100 Subject: [PATCH 008/152] Improve initial instruction/prompt/agent/skill file contents (#291318) --- .../promptSyntax/newPromptFileActions.ts | 56 +++++++++---------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts index 71f6f880de3..4aa9eea127e 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts @@ -24,9 +24,8 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { CHAT_CATEGORY } from '../actions/chatActions.js'; import { askForPromptFileName } from './pickers/askForPromptName.js'; import { askForPromptSourceFolder } from './pickers/askForPromptSourceFolder.js'; -import { IChatModeService } from '../../common/chatModes.js'; import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; -import { SKILL_FILENAME } from '../../common/promptSyntax/config/promptFileLocations.js'; +import { getCleanPromptName, SKILL_FILENAME } from '../../common/promptSyntax/config/promptFileLocations.js'; class AbstractNewPromptFileAction extends Action2 { @@ -57,7 +56,6 @@ class AbstractNewPromptFileAction extends Action2 { const editorService = accessor.get(IEditorService); const fileService = accessor.get(IFileService); const instaService = accessor.get(IInstantiationService); - const chatModeService = accessor.get(IChatModeService); const selectedFolder = await instaService.invokeFunction(askForPromptSourceFolder, this.type); if (!selectedFolder) { @@ -68,7 +66,6 @@ class AbstractNewPromptFileAction extends Action2 { if (!fileName) { return; } - // create the prompt file await fileService.createFolder(selectedFolder.uri); @@ -78,11 +75,13 @@ class AbstractNewPromptFileAction extends Action2 { await openerService.open(promptUri); + const cleanName = getCleanPromptName(promptUri); + const editor = getCodeEditor(editorService.activeTextEditorControl); if (editor && editor.hasModel() && isEqual(editor.getModel().uri, promptUri)) { SnippetController2.get(editor)?.apply([{ range: editor.getModel().getFullModelRange(), - template: getDefaultContentSnippet(this.type, chatModeService), + template: getDefaultContentSnippet(this.type, cleanName), }]); } @@ -140,51 +139,47 @@ class AbstractNewPromptFileAction extends Action2 { } } -function getDefaultContentSnippet(promptType: PromptsType, chatModeService: IChatModeService): string { - const agents = chatModeService.getModes(); - const agentNames = agents.builtin.map(agent => agent.name.get()).join(',') + (agents.custom.length ? (',' + agents.custom.map(agent => agent.name.get()).join(',')) : ''); +function getDefaultContentSnippet(promptType: PromptsType, name: string | undefined): string { switch (promptType) { case PromptsType.prompt: return [ `---`, - `agent: \${1|${agentNames}|}`, + `name: ${name ?? '${1:prompt-name}'}`, + `description: \${2:Describe when to use this prompt}`, `---`, - `\${2:Define the task to achieve, including specific requirements, constraints, and success criteria.}`, + `\${3:Define the prompt content here. You can include instructions, examples, and any other relevant information to guide the AI's responses.}`, ].join('\n'); case PromptsType.instructions: return [ `---`, - `applyTo: '\${1|**,**/*.ts|}'`, + `description: \${1:Describe when these instructions should be loaded}`, + `# applyTo: '\${1|**,**/*.ts|}' # when provided, instructions will automatically be added to the request context when the pattern matches an attached file`, `---`, `\${2:Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.}`, ].join('\n'); case PromptsType.agent: return [ `---`, - `description: '\${1:Describe what this custom agent does and when to use it.}'`, - `tools: []`, + `name: ${name ?? '${1:agent-name}'}`, + `description: \${2:Describe what this custom agent does and when to use it.}`, + `argument-hint: \${3:The inputs this agent expects, e.g., "a task to implement" or "a question to answer".}`, + `# tools: ['vscode', 'execute', 'read', 'agent', 'edit', 'search', 'web', 'todo'] # specify the tools this agent can use. if not set, all enabled tools are allowed`, `---`, - `\${2:Define what this custom agent accomplishes for the user, when to use it, and the edges it won't cross. Specify its ideal inputs/outputs, the tools it may call, and how it reports progress or asks for help.}`, + `\${4:Define what this custom agent does, including its behavior, capabilities, and any specific instructions for its operation.}`, + ].join('\n'); + case PromptsType.skill: + return [ + `---`, + `name: ${name ?? '${1:skill-name}'}`, + `description: \${2:Describe what this skill does and when to use it. Include keywords that help agents identify relevant tasks.}`, + `---`, + `\${3:Define the functionality provided by this skill, including detailed instructions and examples}`, ].join('\n'); default: throw new Error(`Unsupported prompt type: ${promptType}`); } } -/** - * Generates the content snippet for a skill file with the name pre-populated. - * Per agentskills.io/specification, the name field must match the parent directory name. - */ -function getSkillContentSnippet(skillName: string): string { - return [ - `---`, - `name: ${skillName}`, - `description: '\${1:Describe what this skill does and when to use it. Include keywords that help agents identify relevant tasks.}'`, - `---`, - ``, - `\${2:Provide detailed instructions for the agent. Include step-by-step guidance, examples, and edge cases.}`, - ].join('\n'); -} export const NEW_PROMPT_COMMAND_ID = 'workbench.command.new.prompt'; @@ -287,7 +282,7 @@ class NewSkillFileAction extends Action2 { if (editor && editor.hasModel() && isEqual(editor.getModel().uri, skillFileUri)) { SnippetController2.get(editor)?.apply([{ range: editor.getModel().getFullModelRange(), - template: getSkillContentSnippet(trimmedName), + template: getDefaultContentSnippet(PromptsType.skill, trimmedName), }]); } } @@ -309,7 +304,6 @@ class NewUntitledPromptFileAction extends Action2 { public override async run(accessor: ServicesAccessor) { const editorService = accessor.get(IEditorService); - const chatModeService = accessor.get(IChatModeService); const languageId = getLanguageIdForPromptsType(PromptsType.prompt); @@ -326,7 +320,7 @@ class NewUntitledPromptFileAction extends Action2 { if (editor && editor.hasModel()) { SnippetController2.get(editor)?.apply([{ range: editor.getModel().getFullModelRange(), - template: getDefaultContentSnippet(type, chatModeService), + template: getDefaultContentSnippet(type, undefined), }]); } From dab807f670916ee1c34612b816f80f31ddaabff7 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:29:50 +0100 Subject: [PATCH 009/152] Git - expose more options to apply a patch (#291309) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Git - expose more options to apply a patch * 💄 Reorder options --- extensions/git/src/api/api1.ts | 7 +++++-- extensions/git/src/api/git.d.ts | 1 + extensions/git/src/git.ts | 14 +++++++++++--- extensions/git/src/repository.ts | 4 ++-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index 4c865974639..4932b07d5d4 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -100,8 +100,11 @@ export class ApiRepository implements Repository { filterEvent(this.#repository.onDidRunOperation, e => e.operation.kind === OperationKind.Checkout || e.operation.kind === OperationKind.CheckoutTracking), () => null); } - apply(patch: string, reverse?: boolean): Promise { - return this.#repository.apply(patch, reverse); + apply(patch: string, reverse?: boolean): Promise; + apply(patch: string, options?: { allowEmpty?: boolean; reverse?: boolean; threeWay?: boolean }): Promise; + apply(patch: string, reverseOrOptions?: boolean | { allowEmpty?: boolean; reverse?: boolean; threeWay?: boolean }): Promise { + const options = typeof reverseOrOptions === 'boolean' ? { reverse: reverseOrOptions } : reverseOrOptions; + return this.#repository.apply(patch, options); } getConfigs(): Promise<{ key: string; value: string }[]> { diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index b43eaaa6184..8b28d3cb48a 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -256,6 +256,7 @@ export interface Repository { clean(paths: string[]): Promise; apply(patch: string, reverse?: boolean): Promise; + apply(patch: string, options?: { allowEmpty?: boolean; reverse?: boolean; threeWay?: boolean; }): Promise; diff(cached?: boolean): Promise; diffWithHEAD(): Promise; diffWithHEAD(path: string): Promise; diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index fb431257d19..a4bd92c9812 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1679,11 +1679,19 @@ export class Repository { } } - async apply(patch: string, reverse?: boolean): Promise { + async apply(patch: string, options?: { reverse?: boolean; threeWay?: boolean; allowEmpty?: boolean }): Promise { const args = ['apply', patch]; - if (reverse) { - args.push('-R'); + if (options?.allowEmpty) { + args.push('--allow-empty'); + } + + if (options?.reverse) { + args.push('--reverse'); + } + + if (options?.threeWay) { + args.push('--3way'); } try { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 862223a2fca..7ae8527b101 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -2387,8 +2387,8 @@ export class Repository implements Disposable { return this.run(Operation.Show, () => this.repository.detectObjectType(object)); } - async apply(patch: string, reverse?: boolean): Promise { - return await this.run(Operation.Apply, () => this.repository.apply(patch, reverse)); + async apply(patch: string, options?: { allowEmpty?: boolean; reverse?: boolean; threeWay?: boolean }): Promise { + return await this.run(Operation.Apply, () => this.repository.apply(patch, options)); } async getStashes(): Promise { From 654064886c5e47c3bb96c90a8afad3cf82009c04 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 28 Jan 2026 17:43:43 +0100 Subject: [PATCH 010/152] fix hover position --- .../actionWidget/browser/actionList.ts | 5 +- .../browser/widget/input/chatInputPart.ts | 70 ++++++++++++++++++- .../widget/input/chatInputPickerActionItem.ts | 3 + .../delegationSessionPickerActionItem.ts | 2 +- .../widget/input/modePickerActionItem.ts | 4 +- .../widget/input/modelPickerActionItem.ts | 8 +-- .../input/sessionTargetPickerActionItem.ts | 2 +- 7 files changed, 82 insertions(+), 12 deletions(-) diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 5ce46fadff9..3c12b59418e 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -24,7 +24,7 @@ import { ILayoutService } from '../../layout/browser/layoutService.js'; import { IHoverService } from '../../hover/browser/hover.js'; import { MarkdownString } from '../../../base/common/htmlContent.js'; import { HoverPosition } from '../../../base/browser/ui/hover/hoverWidget.js'; -import { IHoverWidget } from '../../../base/browser/ui/hover/hover.js'; +import { IHoverPositionOptions, IHoverWidget } from '../../../base/browser/ui/hover/hover.js'; export const acceptSelectedActionCommand = 'acceptSelectedCodeAction'; export const previewSelectedActionCommand = 'previewSelectedCodeAction'; @@ -44,6 +44,8 @@ export interface IActionListItemHover { * Content to display in the hover. */ readonly content?: string; + + readonly position?: IHoverPositionOptions; } export interface IActionListItem { @@ -479,6 +481,7 @@ export class ActionList extends Disposable { position: { hoverPosition: HoverPosition.LEFT, forcePosition: false, + ...element.hover.position, }, appearance: { showPointer: true, 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 bb1c3c0f14c..3bcee9ee00b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -12,6 +12,7 @@ import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions } from '../. import * as aria from '../../../../../../base/browser/ui/aria/aria.js'; import { ButtonWithIcon } from '../../../../../../base/browser/ui/button/button.js'; import { createInstantHoverDelegate } from '../../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { HoverPosition } from '../../../../../../base/browser/ui/hover/hoverWidget.js'; import { renderLabelWithIcons } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { IAction } from '../../../../../../base/common/actions.js'; import { equals as arraysEqual } from '../../../../../../base/common/arrays.js'; @@ -70,6 +71,8 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../../../ import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; import { ISharedWebContentExtractorService } from '../../../../../../platform/webContentExtractor/common/webContentExtractor.js'; import { IWorkspaceContextService, WorkbenchState } from '../../../../../../platform/workspace/common/workspace.js'; +import { IWorkbenchLayoutService, Position } from '../../../../../services/layout/browser/layoutService.js'; +import { IViewDescriptorService, ViewContainerLocation } from '../../../../../common/views.js'; import { ResourceLabels } from '../../../../../browser/labels.js'; import { IWorkbenchAssignmentService } from '../../../../../services/assignment/common/assignmentService.js'; import { IChatEntitlementService } from '../../../../../services/chat/common/chatEntitlementService.js'; @@ -100,7 +103,7 @@ import { ChatAttachmentModel } from '../../attachments/chatAttachmentModel.js'; import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, SCMHistoryItemChangeRangeAttachmentWidget, TerminalCommandAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../../attachments/chatAttachmentWidgets.js'; import { ChatImplicitContexts } from '../../attachments/chatImplicitContext.js'; import { ImplicitContextAttachmentWidget } from '../../attachments/implicitContextAttachment.js'; -import { IChatWidget, ISessionTypePickerDelegate, isIChatResourceViewContext, IWorkspacePickerDelegate } from '../../chat.js'; +import { IChatWidget, ISessionTypePickerDelegate, isIChatResourceViewContext, isIChatViewViewContext, IWorkspacePickerDelegate } from '../../chat.js'; import { ChatEditingShowChangesAction, ViewAllSessionChangesAction, ViewPreviousEditsAction } from '../../chatEditing/chatEditingActions.js'; import { resizeImage } from '../../chatImageUtils.js'; import { ChatSessionPickerActionItem, IChatSessionPickerDelegate } from '../../chatSessions/chatSessionPickerActionItem.js'; @@ -164,6 +167,18 @@ export interface IWorkingSetEntry { uri: URI; } +export const enum ChatWidgetLocation { + SidebarLeft = 'sidebarLeft', + SidebarRight = 'sidebarRight', + Panel = 'panel', + Editor = 'editor', +} + +export interface IChatWidgetLocationInfo { + readonly location: ChatWidgetLocation; + readonly isMaximized: boolean; +} + const emptyInputState = observableMemento({ defaultValue: undefined, key: 'chat.untitledInputState', @@ -466,6 +481,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge @IChatContextService private readonly chatContextService: IChatContextService, @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, ) { super(); @@ -1869,7 +1886,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge })); const hoverDelegate = this._register(createInstantHoverDelegate()); - const chatSessionPosition = isIChatResourceViewContext(widget.viewContext) ? 'editor' : 'sidebar'; + + const { location, isMaximized } = this.getWidgetLocationInfo(widget); + const pickerOptions: IChatInputPickerOptions = { getOverflowAnchor: () => this.inputActionsToolbar.getElement(), actionContext: { widget }, @@ -1877,6 +1896,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._inputEditor.onDidLayoutChange, (l?: EditorLayoutInfo) => (l?.width ?? this._inputEditor.getLayoutInfo().width) < 650 /* This is a magical number based on testing*/ ).recomputeInitiallyAndOnChange(this._store), + hoverPosition: { + forcePosition: true, + hoverPosition: location === ChatWidgetLocation.SidebarRight && !isMaximized ? HoverPosition.LEFT : HoverPosition.RIGHT + }, }; this._register(dom.addStandardDisposableListener(toolbarsContainer, dom.EventType.CLICK, e => this.inputEditor.focus())); @@ -1942,7 +1965,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge }; const isWelcomeViewMode = !!this.options.sessionTypePickerDelegate?.setActiveSessionProvider; const Picker = (action.id === OpenSessionTargetPickerAction.ID || isWelcomeViewMode) ? SessionTypePickerActionItem : DelegationSessionPickerActionItem; - return this.sessionTargetWidget = this.instantiationService.createInstance(Picker, action, chatSessionPosition, delegate, pickerOptions); + return this.sessionTargetWidget = this.instantiationService.createInstance(Picker, action, location === ChatWidgetLocation.Editor ? 'editor' : 'sidebar', delegate, pickerOptions); } else if (action.id === OpenWorkspacePickerAction.ID && action instanceof MenuItemAction) { if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY && this.options.workspacePickerDelegate) { return this.instantiationService.createInstance(WorkspacePickerActionItem, action, this.options.workspacePickerDelegate, pickerOptions); @@ -2710,6 +2733,47 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge sideToolbarWidth: inputSideToolbarWidth > 0 ? inputSideToolbarWidth + 4 /*gap*/ : 0, }; } + + /** + * Gets the location of the chat widget and whether that location is maximized. + */ + private getWidgetLocationInfo(widget: IChatWidget): IChatWidgetLocationInfo { + // Editor context (quick chat, inline chat, etc.) + if (isIChatResourceViewContext(widget.viewContext)) { + return { location: ChatWidgetLocation.Editor, isMaximized: false }; + } + + // View context - determine actual location from view descriptor service + if (isIChatViewViewContext(widget.viewContext)) { + const viewLocation = this.viewDescriptorService.getViewLocationById(widget.viewContext.viewId); + const sideBarPosition = this.layoutService.getSideBarPosition(); + + switch (viewLocation) { + case ViewContainerLocation.Panel: + return { + location: ChatWidgetLocation.Panel, + isMaximized: this.layoutService.isPanelMaximized(), + }; + case ViewContainerLocation.AuxiliaryBar: + // AuxiliaryBar is on the opposite side of the primary sidebar + return { + location: sideBarPosition === Position.LEFT ? ChatWidgetLocation.SidebarRight : ChatWidgetLocation.SidebarLeft, + isMaximized: this.layoutService.isAuxiliaryBarMaximized(), + }; + case ViewContainerLocation.Sidebar: + default: + // Primary sidebar follows its configured position + // Note: Primary sidebar cannot be maximized, so always false + return { + location: sideBarPosition === Position.LEFT ? ChatWidgetLocation.SidebarLeft : ChatWidgetLocation.SidebarRight, + isMaximized: false, + }; + } + } + + // Fallback for unknown contexts + return { location: ChatWidgetLocation.Editor, isMaximized: false }; + } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.ts index e223350c4c7..1377aa52607 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { getActiveWindow } from '../../../../../../base/browser/dom.js'; +import { IHoverPositionOptions } from '../../../../../../base/browser/ui/hover/hover.js'; import { IAction } from '../../../../../../base/common/actions.js'; import { autorun, IObservable } from '../../../../../../base/common/observable.js'; import { ActionWidgetDropdownActionViewItem } from '../../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js'; @@ -24,6 +25,8 @@ export interface IChatInputPickerOptions { readonly actionContext?: IChatExecuteActionContext; readonly onlyShowIconsForDefaultActions: IObservable; + + readonly hoverPosition?: IHoverPositionOptions; } /** diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts index d30bc4402cd..a51b59a4548 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts @@ -91,7 +91,7 @@ export class DelegationSessionPickerActionItem extends SessionTypePickerActionIt class: undefined, label: localize('chat.newChatSession', "New Chat Session"), tooltip: '', - hover: { content: '' }, + hover: { content: '', position: this.pickerOptions.hoverPosition }, checked: false, icon: Codicon.plus, enabled: true, diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index 381f97bfe4d..42293b45786 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -115,7 +115,7 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { enabled: !isDisabledViaPolicy, checked: !isDisabledViaPolicy && currentMode.id === mode.id, tooltip: '', - hover: { content: tooltip }, + hover: { content: tooltip, position: this.pickerOptions.hoverPosition }, toolbarActions, run: async () => { if (isDisabledViaPolicy) { @@ -138,7 +138,7 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { return { ...makeAction(mode, currentMode), tooltip: '', - hover: { content: mode.description.get() ?? chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, mode.kind)?.description ?? action.tooltip }, + hover: { content: mode.description.get() ?? chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, mode.kind)?.description ?? action.tooltip, position: this.pickerOptions.hoverPosition }, icon: mode.icon.get() ?? (isModeConsideredBuiltIn(mode, this._productService) ? builtinDefaultIcon : undefined), category: agentModeDisabledViaPolicy ? policyDisabledCategory : customCategory }; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts index 5ca9a98a56f..4b419fca28c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts @@ -44,7 +44,7 @@ type ChatModelChangeEvent = { }; -function modelDelegateToWidgetActionsProvider(delegate: IModelPickerDelegate, telemetryService: ITelemetryService): IActionWidgetDropdownActionProvider { +function modelDelegateToWidgetActionsProvider(delegate: IModelPickerDelegate, telemetryService: ITelemetryService, pickerOptions: IChatInputPickerOptions): IActionWidgetDropdownActionProvider { return { getActions: () => { const models = delegate.getModels(); @@ -59,7 +59,7 @@ function modelDelegateToWidgetActionsProvider(delegate: IModelPickerDelegate, te description: localize('chat.modelPicker.auto.detail', "Best for your request based on capacity and performance."), tooltip: localize('chat.modelPicker.auto', "Auto"), label: localize('chat.modelPicker.auto', "Auto"), - hover: { content: localize('chat.modelPicker.auto.description', "Automatically selects the best model for your task based on context and complexity.") }, + hover: { content: localize('chat.modelPicker.auto.description', "Automatically selects the best model for your task based on context and complexity."), position: pickerOptions.hoverPosition }, run: () => { } } satisfies IActionWidgetDropdownAction]; } @@ -74,7 +74,7 @@ function modelDelegateToWidgetActionsProvider(delegate: IModelPickerDelegate, te class: undefined, description: model.metadata.multiplier ?? model.metadata.detail, tooltip: hoverContent ? '' : model.metadata.name, - hover: hoverContent ? { content: hoverContent } : undefined, + hover: hoverContent ? { content: hoverContent, position: pickerOptions.hoverPosition } : undefined, label: model.metadata.name, run: () => { const previousModel = delegate.currentModel.get(); @@ -167,7 +167,7 @@ export class ModelPickerActionItem extends ChatInputPickerActionViewItem { }; const modelPickerActionWidgetOptions: Omit = { - actionProvider: modelDelegateToWidgetActionsProvider(delegate, telemetryService), + actionProvider: modelDelegateToWidgetActionsProvider(delegate, telemetryService, pickerOptions), actionBarActionProvider: getModelPickerActionBarActionProvider(commandService, chatEntitlementService, productService), reporter: { name: 'ChatModelPicker', includeOptions: true }, }; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts index ffc6e0cb22f..69721f5bd5b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts @@ -74,7 +74,7 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { category: this._getSessionCategory(sessionTypeItem), description: this._getSessionDescription(sessionTypeItem), tooltip: '', - hover: { content: sessionTypeItem.hoverDescription }, + hover: { content: sessionTypeItem.hoverDescription, position: this.pickerOptions.hoverPosition }, run: async () => { this._run(sessionTypeItem); }, From 5446e393268c5f376b3f1ec2a3750be4de09b753 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 28 Jan 2026 17:51:07 +0100 Subject: [PATCH 011/152] session type picker also for sessions that cannot be delegated. --- .../contrib/chat/browser/actions/chatExecuteActions.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index b78c0a34125..829c553fbdf 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -491,7 +491,7 @@ export class OpenSessionTargetPickerAction extends Action2 { tooltip: localize('setSessionTarget', "Set Session Target"), category: CHAT_CATEGORY, f1: false, - precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.hasCanDelegateProviders, ContextKeyExpr.or(ChatContextKeys.chatSessionIsEmpty, ChatContextKeys.inAgentSessionsWelcome)), + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or(ChatContextKeys.chatSessionIsEmpty, ChatContextKeys.inAgentSessionsWelcome)), menu: [ { id: MenuId.ChatInput, @@ -500,8 +500,7 @@ export class OpenSessionTargetPickerAction extends Action2 { ChatContextKeys.enabled, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), ChatContextKeys.inQuickChat.negate(), - ChatContextKeys.hasCanDelegateProviders, - ContextKeyExpr.or(ChatContextKeys.chatSessionIsEmpty, ChatContextKeys.hasCanDelegateProviders.negate())), + ChatContextKeys.chatSessionIsEmpty), group: 'navigation', }, ] @@ -527,7 +526,7 @@ export class OpenDelegationPickerAction extends Action2 { tooltip: localize('delegateSession', "Delegate Session"), category: CHAT_CATEGORY, f1: false, - precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.hasCanDelegateProviders, ChatContextKeys.chatSessionIsEmpty.negate()), + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.chatSessionIsEmpty.negate()), menu: [ { id: MenuId.ChatInput, @@ -536,8 +535,7 @@ export class OpenDelegationPickerAction extends Action2 { ChatContextKeys.enabled, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), ChatContextKeys.inQuickChat.negate(), - ChatContextKeys.hasCanDelegateProviders, - ContextKeyExpr.and(ChatContextKeys.chatSessionIsEmpty.negate(), ChatContextKeys.hasCanDelegateProviders)), + ChatContextKeys.chatSessionIsEmpty.negate()), group: 'navigation', }, ] From d308743726689a685bc6634bb884a0df53938005 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 28 Jan 2026 08:56:51 -0800 Subject: [PATCH 012/152] fix: add cancellation token support to ChatAgentResponseStream (#291327) --- src/vs/workbench/api/common/extHostChatAgents2.ts | 11 ++++++----- src/vs/workbench/api/common/extHostChatSessions.ts | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index cdcdb1580ea..bb35634e6f2 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -5,7 +5,7 @@ import type * as vscode from 'vscode'; import { coalesce } from '../../../base/common/arrays.js'; -import { DeferredPromise, timeout } from '../../../base/common/async.js'; +import { DeferredPromise, raceCancellation, timeout } from '../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js'; import { toErrorMessage } from '../../../base/common/errorMessage.js'; import { Emitter } from '../../../base/common/event.js'; @@ -51,7 +51,8 @@ export class ChatAgentResponseStream { private readonly _proxy: IChatAgentProgressShape, private readonly _commandsConverter: CommandsConverter, private readonly _sessionDisposables: DisposableStore, - private readonly _pendingCarouselResolvers: Map | undefined>>> + private readonly _pendingCarouselResolvers: Map | undefined>>>, + private readonly _token: CancellationToken ) { } close() { @@ -330,8 +331,8 @@ export class ChatAgentResponseStream { _report(dto); - // Wait for the user to submit answers - return deferred.p; + // Wait for the user to submit answers, but respect cancellation + return raceCancellation(deferred.p, that._token); }, beginToolInvocation(toolCallId, toolName, streamData) { throwIfDone(this.beginToolInvocation); @@ -688,7 +689,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS this._sessionDisposables.set(request.sessionResource, sessionDisposables); } - stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._commands.converter, sessionDisposables, this._pendingCarouselResolvers); + stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._commands.converter, sessionDisposables, this._pendingCarouselResolvers, token); const model = await this.getModelForRequest(request, agent.extension); const tools = await this.getToolsForRequest(agent.extension, request.userSelectedTools, model.id, token); diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index 4d5183a4251..182e9495974 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -233,7 +233,7 @@ class ExtHostChatSession { public readonly commandsConverter: CommandsConverter, public readonly sessionDisposables: DisposableStore ) { - this._stream = new ChatAgentResponseStream(extension, request, proxy, commandsConverter, sessionDisposables, this._pendingCarouselResolvers); + this._stream = new ChatAgentResponseStream(extension, request, proxy, commandsConverter, sessionDisposables, this._pendingCarouselResolvers, CancellationToken.None); } get activeResponseStream() { @@ -241,7 +241,7 @@ class ExtHostChatSession { } getActiveRequestStream(request: IChatAgentRequest) { - return new ChatAgentResponseStream(this.extension, request, this.proxy, this.commandsConverter, this.sessionDisposables, this._pendingCarouselResolvers); + return new ChatAgentResponseStream(this.extension, request, this.proxy, this.commandsConverter, this.sessionDisposables, this._pendingCarouselResolvers, CancellationToken.None); } } From 09a6795b2593c5999cd83f2ae146e25dcc2dfca0 Mon Sep 17 00:00:00 2001 From: Robo Date: Thu, 29 Jan 2026 01:58:34 +0900 Subject: [PATCH 013/152] fix: path to appx package in Add-AppxProvisionedPackage command (#291300) * fix: path to appx package in Add-AppxProvisionedPackage command * fix: removing appx package in system setup --- build/win32/code.iss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/win32/code.iss b/build/win32/code.iss index 6b39799ec73..650384d4dc1 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -1564,7 +1564,7 @@ begin #if "user" == InstallTarget ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Add-AppxPackage -Path ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx\{#AppxPackage}') + ''' -ExternalLocation ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); #else - ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Add-AppxPackage -Stage ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx\{#AppxPackage}') + ''' -ExternalLocation ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx') + '''; Add-AppxProvisionedPackage -Online -SkipLicense -PackagePath ''' + ExpandConstant('{app}\appx\{#AppxPackage}') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); + ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Add-AppxPackage -Stage ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx\{#AppxPackage}') + ''' -ExternalLocation ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx') + '''; Add-AppxProvisionedPackage -Online -SkipLicense -PackagePath ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx\{#AppxPackage}') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); #endif Log('Add-AppxPackage complete.'); end; @@ -1589,7 +1589,7 @@ begin #if "user" == InstallTarget ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Remove-AppxPackage -Package ''' + AppxPackageFullname + ''''), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); #else - ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('$packages = Get-AppxPackage ''' + AppxPackageFullname + '''; foreach ($package in $packages) { Remove-AppxProvisionedPackage -PackageName $package.PackageFullName -Online }; foreach ($package in $packages) { Remove-AppxPackage -Package $package.PackageFullName -AllUsers }'), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); + ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('$packages = Get-AppxPackage ''' + ExpandConstant('{#AppxPackageName}') + '''; foreach ($package in $packages) { Remove-AppxProvisionedPackage -PackageName $package.PackageFullName -Online }; foreach ($package in $packages) { Remove-AppxPackage -Package $package.PackageFullName -AllUsers }'), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); #endif Log('Remove-AppxPackage for current appx installation complete.'); end; From ff54e5cbb1e1e34873d2bc423314976a2d8b41b6 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 28 Jan 2026 09:05:11 -0800 Subject: [PATCH 014/152] chat: fix handling of old session states (#291185) Guard old sessions from Insiders that may have Pending or NeedsInput states, converting them to Complete state for consistency. This ensures proper handling of sessions migrated from pre-PR #288161 versions. - Added check to convert Pending/NeedsInput states to Complete state - Preserves backward compatibility with old session data - Prevents state display issues in migrated sessions (Commit message generated by Copilot) --- .../contrib/chat/common/chatService/chatServiceImpl.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 17e8088fb7f..d133ab69419 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -380,6 +380,7 @@ export class ChatService extends Disposable implements IChatService { .filter(entry => !this._sessionModels.has(LocalChatSessionUri.forSession(entry.sessionId)) && entry.initialLocation === ChatAgentLocation.Chat && !entry.isEmpty) .map((entry): IChatDetail => { const sessionResource = LocalChatSessionUri.forSession(entry.sessionId); + const lastResponseState = entry.lastResponseState ?? ResponseModelState.Complete; return ({ ...entry, sessionResource, @@ -391,7 +392,8 @@ export class ChatService extends Disposable implements IChatService { }, isActive: this._sessionModels.has(sessionResource), // TODO@roblourens- missing for old data- normalize inside the store - lastResponseState: entry.lastResponseState ?? ResponseModelState.Complete, + // TODO@connor4312: the check here guards old sessions from Insiders pre PR #288161 and it can be safely removed after a transition period. + lastResponseState: lastResponseState === ResponseModelState.Pending || lastResponseState === ResponseModelState.NeedsInput ? ResponseModelState.Complete : lastResponseState, }); }); } From 69a9b7235d9813cca8da9dc4648d602b30457223 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:06:06 -0800 Subject: [PATCH 015/152] feat(chat): add model switching capability during handoff --- src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts | 4 ++++ .../contrib/chat/common/promptSyntax/promptFileParser.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 04effea806c..430d6cf6d6c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -1208,6 +1208,10 @@ export class ChatWidget extends Disposable implements IChatWidget { } else if (handoff.agent) { // Regular handoff to specified agent this._switchToAgentByName(handoff.agent); + // Switch to the specified model if provided + if (handoff.model) { + this.input.switchModelByQualifiedName(handoff.model); + } // Insert the handoff prompt into the input this.input.setValue(promptToUse, false); this.input.focus(); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts index 995d6d7c332..c69a453afe2 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts @@ -325,6 +325,7 @@ export interface IHandOff { readonly prompt: string; readonly send?: boolean; readonly showContinueOn?: boolean; // treated exactly like send (optional boolean) + readonly model?: readonly string[]; // qualified model name(s) to switch to (e.g., "GPT-4o (copilot)") } export interface IHeaderAttribute { From 69d1718f97fb119ba67d867eae663086598ba1fb Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 28 Jan 2026 18:10:37 +0100 Subject: [PATCH 016/152] agent sessions - harden read/unread tracking (#291308) --- .../agentSessions/agentSessionsModel.ts | 96 +++++++++++++++---- .../experiments/agentTitleBarStatusWidget.ts | 2 +- .../agentSessionViewModel.test.ts | 37 ++++--- .../test/browser/workbenchTestServices.ts | 26 +++++ 4 files changed, 133 insertions(+), 28 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index 7af226c8a31..4fcf0bbdef0 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -17,12 +17,14 @@ import { URI, UriComponents } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService, LogLevel } from '../../../../../platform/log/common/log.js'; +import { IProductService } from '../../../../../platform/product/common/productService.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; import { Extensions, IOutputChannelRegistry, IOutputService } from '../../../../services/output/common/output.js'; import { ChatSessionStatus as AgentSessionStatus, IChatSessionFileChange, IChatSessionFileChange2, IChatSessionItem, IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js'; +import { IChatWidgetService } from '../chat.js'; import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName } from './agentSessions.js'; //#region Interfaces, Types @@ -392,6 +394,8 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode @ILifecycleService private readonly lifecycleService: ILifecycleService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService private readonly storageService: IStorageService, + @IProductService private readonly productService: IProductService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService ) { super(); @@ -413,8 +417,9 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode )); this.logger.logAllStatsIfTrace('Loaded cached sessions'); - this.registerListeners(); + this.runMarkAllReadMigrationOnce(); // TODO@bpasero remove this in the future + this.registerListeners(); } private registerListeners(): void { @@ -563,11 +568,6 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode //#region States - // In order to reduce the amount of sessions showing as unread, we maintain - // a certain cut off date that we consider good, given the issues we fixed - // around unread tracking. This is ~1 week before we ship 1.109 stable. - private static readonly READ_STATE_INITIAL_DATE = Date.UTC(2026, 0 /* January */, 28); - private readonly sessionStates: ResourceMap; private isArchived(session: IInternalAgentSessionData): boolean { @@ -599,20 +599,84 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode return true; // archived sessions are always read } - const readDate = this.sessionStates.get(session.resource)?.read; + const readDate = this.sessionStates.get(session.resource)?.read ?? 0; - return (readDate ?? AgentSessionsModel.READ_STATE_INITIAL_DATE) >= (session.timing.lastRequestEnded ?? session.timing.lastRequestStarted ?? session.timing.created); - } - - private setRead(session: IInternalAgentSessionData, read: boolean): void { - if (read === this.isRead(session)) { - return; // no change + // Install a heuristic to reduce false positives: a user might observe + // the output of a session and quickly click on another session before + // it is finished. Strictly speaking the session is unread, but we + // allow a certain threshold of time to count as read to accommodate. + if (readDate >= this.sessionTimeForReadStateTracking(session) - 2000) { + return true; } - const state = this.sessionStates.get(session.resource) ?? { archived: false, read: 0 }; - this.sessionStates.set(session.resource, { ...state, read: read ? Date.now() : 0 }); + // Never consider a session as unread if its connected to a widget + return !!this.chatWidgetService.getWidgetBySessionResource(session.resource); + } - this._onDidChangeSessions.fire(); + private sessionTimeForReadStateTracking(session: IInternalAgentSessionData): number { + return session.timing.lastRequestEnded ?? session.timing.lastRequestStarted ?? session.timing.created; + } + + private setRead(session: IInternalAgentSessionData, read: boolean, skipEvent?: boolean): void { + const state = this.sessionStates.get(session.resource) ?? { archived: false, read: 0 }; + + let newRead: number; + if (read) { + newRead = Math.max(Date.now(), this.sessionTimeForReadStateTracking(session)); + + if (state.read >= newRead) { + return; // already read with a sufficient timestamp + } + } else { + newRead = 0; + if (state.read === 0) { + return; // already unread + } + } + + this.sessionStates.set(session.resource, { ...state, read: newRead }); + + if (!skipEvent) { + this._onDidChangeSessions.fire(); + } + } + + private static readonly MARK_ALL_READ_MIGRATION_KEY = 'agentSessions.markAllReadMigration'; + private static readonly MARK_ALL_READ_MIGRATION_VERSION = 1; + + private migrationCompleted = false; + + private runMarkAllReadMigrationOnce(): void { + if (this.migrationCompleted) { + return; + } + + const storedVersion = this.storageService.getNumber(AgentSessionsModel.MARK_ALL_READ_MIGRATION_KEY, StorageScope.WORKSPACE, 0); + if (storedVersion >= AgentSessionsModel.MARK_ALL_READ_MIGRATION_VERSION) { + this.migrationCompleted = true; + return; // migration already completed for this version + } + + this.logger.logIfTrace(`Running mark-all-read migration from version ${storedVersion} to ${AgentSessionsModel.MARK_ALL_READ_MIGRATION_VERSION}`); + + const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000); + for (const session of this._sessions.values()) { + if (!this.isRead(session)) { + let markRead = true; + if (this.productService.quality === 'stable' && this.sessionTimeForReadStateTracking(session) >= sevenDaysAgo) { + markRead = false; // for stable, preserve state for up to 1 week ago + } + + if (markRead) { + this.setRead(session, true, true /* skipEvent */); + } + } + } + + // Store the migration version + this.storageService.store(AgentSessionsModel.MARK_ALL_READ_MIGRATION_KEY, AgentSessionsModel.MARK_ALL_READ_MIGRATION_VERSION, StorageScope.WORKSPACE, StorageTarget.MACHINE); + + this.migrationCompleted = true; } //#endregion diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts index 0de17dcf02b..9ec4f162d4f 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts @@ -336,7 +336,7 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { // Active sessions include both InProgress and NeedsInput const activeSessions = filteredSessions.filter(s => isSessionInProgressStatus(s.status) && !s.isArchived()); - const unreadSessions = filteredSessions.filter(s => !s.isRead() && !this.chatWidgetService.getWidgetBySessionResource(s.resource)); + const unreadSessions = filteredSessions.filter(s => !s.isRead()); // Sessions that need user input/attention (subset of active) const attentionNeededSessions = filteredSessions.filter(s => s.status === AgentSessionStatus.NeedsInput && !this.chatWidgetService.getWidgetBySessionResource(s.resource)); diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts index e9d0189ab9c..7c6cd0d2395 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts @@ -24,7 +24,7 @@ import { TestInstantiationService } from '../../../../../../platform/instantiati import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../../../browser/agentSessions/agentSessions.js'; -suite('Agent Sessions', () => { +suite('AgentSessions', () => { suite('AgentSessionsViewModel', () => { @@ -1400,6 +1400,9 @@ suite('Agent Sessions', () => { instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + // Set migration key to indicate migration has already happened + const storageService = instantiationService.get(IStorageService); + storageService.store('agentSessions.markAllReadMigration', 1, StorageScope.WORKSPACE, StorageTarget.MACHINE); }); teardown(() => { @@ -1410,11 +1413,22 @@ suite('Agent Sessions', () => { test('should mark session as read and unread', async () => { return runWithFakedTimers({}, async () => { + // Create session with timing after READ_STATE_INITIAL_DATE so it can be marked unread + const futureSessionTiming: IChatSessionItem['timing'] = { + created: Date.UTC(2026, 1 /* February */, 1), + lastRequestStarted: Date.UTC(2026, 1 /* February */, 1), + lastRequestEnded: Date.UTC(2026, 1 /* February */, 2), + }; + const provider: IChatSessionItemProvider = { chatSessionType: 'test-type', onDidChangeChatSessionItems: Event.None, provideChatSessionItems: async () => [ - makeSimpleSessionItem('session-1'), + { + resource: URI.parse('test://session-1'), + label: 'Session 1', + timing: futureSessionTiming, + }, ] }; @@ -1520,7 +1534,7 @@ suite('Agent Sessions', () => { test('should consider sessions before initial date as read by default', async () => { return runWithFakedTimers({}, async () => { - // Session with timing before the READ_STATE_INITIAL_DATE (January 28, 2026) + // Without migration, all sessions are unread by default const oldSessionTiming: IChatSessionItem['timing'] = { created: Date.UTC(2025, 10 /* November */, 1), lastRequestStarted: Date.UTC(2025, 10 /* November */, 1), @@ -1545,8 +1559,8 @@ suite('Agent Sessions', () => { await viewModel.resolve(undefined); const session = viewModel.sessions[0]; - // Sessions before the initial date should be considered read - assert.strictEqual(session.isRead(), true); + // Sessions are unread by default (migration already happened in setup) + assert.strictEqual(session.isRead(), false); }); }); @@ -1616,7 +1630,7 @@ suite('Agent Sessions', () => { test('should use startTime for read state comparison when endTime is not available', async () => { return runWithFakedTimers({}, async () => { - // Session with only startTime before initial date + // Session with only startTime const sessionTiming: IChatSessionItem['timing'] = { created: Date.UTC(2025, 10 /* November */, 1), lastRequestStarted: Date.UTC(2025, 10 /* November */, 1), @@ -1641,8 +1655,8 @@ suite('Agent Sessions', () => { await viewModel.resolve(undefined); const session = viewModel.sessions[0]; - // Should use startTime (November 1) which is before the initial date - assert.strictEqual(session.isRead(), true); + // Sessions are unread by default + assert.strictEqual(session.isRead(), false); }); }); @@ -1777,7 +1791,7 @@ suite('Agent Sessions', () => { test('should not fire onDidChangeSessions when archiving an already read session', async () => { return runWithFakedTimers({}, async () => { - // Session with timing before the READ_STATE_INITIAL_DATE (already read) + // Session with timing const oldSessionTiming: IChatSessionItem['timing'] = { created: Date.UTC(2025, 10 /* November */, 1), lastRequestStarted: Date.UTC(2025, 10 /* November */, 1), @@ -1802,7 +1816,8 @@ suite('Agent Sessions', () => { await viewModel.resolve(undefined); const session = viewModel.sessions[0]; - // Session before the initial date should be read + // Mark session as read first + session.setRead(true); assert.strictEqual(session.isRead(), true); let changeEventCount = 0; @@ -1813,7 +1828,7 @@ suite('Agent Sessions', () => { // Archive the session session.setArchived(true); - // Should fire once (for archived state change only, not for read since already read) + // Should fire only once for archived state change since session is already read assert.strictEqual(changeEventCount, 1); }); }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index a99db7e8532..9e8feee8336 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -130,6 +130,9 @@ import { SideBySideEditorInput } from '../../common/editor/sideBySideEditorInput import { TextResourceEditorInput } from '../../common/editor/textResourceEditorInput.js'; import { IPaneComposite } from '../../common/panecomposite.js'; import { IView, IViewDescriptor, ViewContainer, ViewContainerLocation } from '../../common/views.js'; +import { IChatWidget, IChatWidgetService } from '../../contrib/chat/browser/chat.js'; +import { IChatEditorOptions } from '../../contrib/chat/browser/widgetHosts/editor/chatEditor.js'; +import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { FileEditorInput } from '../../contrib/files/browser/editors/fileEditorInput.js'; import { TextFileEditor } from '../../contrib/files/browser/editors/textFileEditor.js'; import { FILE_EDITOR_INPUT_ID } from '../../contrib/files/common/files.js'; @@ -374,6 +377,7 @@ export function workbenchInstantiationService( instantiationService.stub(IHoverService, NullHoverService); instantiationService.stub(IChatEntitlementService, new TestChatEntitlementService()); instantiationService.stub(IMarkdownRendererService, instantiationService.createInstance(MarkdownRendererService)); + instantiationService.stub(IChatWidgetService, instantiationService.createInstance(TestChatWidgetService)); return instantiationService; } @@ -2107,3 +2111,25 @@ export class TestContextMenuService implements IContextMenuService { throw new Error('Method not implemented.'); } } + +export class TestChatWidgetService implements IChatWidgetService { + + _serviceBrand: undefined; + + lastFocusedWidget: IChatWidget | undefined; + + onDidAddWidget = Event.None; + onDidBackgroundSession = Event.None; + + async reveal(widget: IChatWidget, preserveFocus?: boolean): Promise { return false; } + async revealWidget(preserveFocus?: boolean): Promise { return undefined; } + getAllWidgets(): ReadonlyArray { return []; } + getWidgetByInputUri(uri: URI): IChatWidget | undefined { return undefined; } + openSession(sessionResource: URI): Promise; + openSession(sessionResource: URI, target?: PreferredGroup, options?: IChatEditorOptions): Promise; + openSession(sessionResource: URI): Promise; + async openSession(sessionResource: unknown, target?: unknown, options?: unknown): Promise { return undefined; } + getWidgetBySessionResource(sessionResource: URI): IChatWidget | undefined { return undefined; } + getWidgetsByLocations(location: ChatAgentLocation): ReadonlyArray { return []; } + register(newWidget: IChatWidget): IDisposable { return Disposable.None; } +} From 33cbf6ba58c616df6c6c79ac9a83735be76c5d50 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 28 Jan 2026 18:18:38 +0100 Subject: [PATCH 017/152] No intellisense docs for custom agent properties (#291330) --- .../promptHeaderAutocompletion.ts | 3 +- .../languageProviders/promptHovers.ts | 99 +++++-------------- .../languageProviders/promptValidator.ts | 63 ++++++++++++ 3 files changed, 88 insertions(+), 77 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts index 2b5997e514e..1acb5024350 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -16,7 +16,7 @@ import { getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js'; import { IPromptsService } from '../service/promptsService.js'; import { Iterable } from '../../../../../../base/common/iterator.js'; import { IHeaderAttribute, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; -import { getValidAttributeNames, isGithubTarget, knownGithubCopilotTools } from './promptValidator.js'; +import { getAttributeDescription, getValidAttributeNames, isGithubTarget, knownGithubCopilotTools } from './promptValidator.js'; import { localize } from '../../../../../../nls.js'; export class PromptHeaderAutocompletion implements CompletionItemProvider { @@ -127,6 +127,7 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { for (const attribute of attributesToPropose) { const item: CompletionItem = { label: attribute, + documentation: getAttributeDescription(attribute, promptType), kind: CompletionItemKind.Property, insertText: getInsertText(attribute), insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts index 8613d2513fb..f92dafd9228 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts @@ -16,7 +16,7 @@ import { IChatModeService, isBuiltinChatMode } from '../../chatModes.js'; import { getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js'; import { IPromptsService } from '../service/promptsService.js'; import { IHeaderAttribute, PromptBody, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; -import { isGithubTarget } from './promptValidator.js'; +import { getAttributeDescription, isGithubTarget } from './promptValidator.js'; export class PromptHoverProvider implements HoverProvider { /** @@ -68,80 +68,27 @@ export class PromptHoverProvider implements HoverProvider { } private async provideHeaderHover(position: Position, promptType: PromptsType, header: PromptHeader): Promise { - switch (promptType) { - case PromptsType.instructions: - for (const attribute of header.attributes) { - if (attribute.range.containsPosition(position)) { - switch (attribute.key) { - case PromptHeaderAttributes.name: - return this.createHover(localize('promptHeader.instructions.name', 'The name of the instruction file as shown in the UI. If not set, the name is derived from the file name.'), attribute.range); - case PromptHeaderAttributes.description: - return this.createHover(localize('promptHeader.instructions.description', 'The description of the instruction file. It can be used to provide additional context or information about the instructions and is passed to the language model as part of the prompt.'), attribute.range); - case PromptHeaderAttributes.applyTo: - return this.createHover(localize('promptHeader.instructions.applyToRange', 'One or more glob pattern (separated by comma) that describe for which files the instructions apply to. Based on these patterns, the file is automatically included in the prompt, when the context contains a file that matches one or more of these patterns. Use `**` when you want this file to always be added.\nExample: `**/*.ts`, `**/*.js`, `client/**`'), attribute.range); - } + for (const attribute of header.attributes) { + if (attribute.range.containsPosition(position)) { + const description = getAttributeDescription(attribute.key, promptType); + if (description) { + switch (attribute.key) { + case PromptHeaderAttributes.model: + return this.getModelHover(attribute, position, description, promptType === PromptsType.agent && isGithubTarget(promptType, header.target)); + case PromptHeaderAttributes.tools: + return this.getToolHover(attribute, position, description); + case PromptHeaderAttributes.agent: + case PromptHeaderAttributes.mode: + return this.getAgentHover(attribute, position, description); + case PromptHeaderAttributes.handOffs: + return this.getHandsOffHover(attribute, position, promptType === PromptsType.agent && isGithubTarget(promptType, header.target)); + case PromptHeaderAttributes.infer: + return this.createHover(description + '\n\n' + localize('promptHeader.attribute.infer.hover', '- `all`, `true`: Available in the agent picker and can be used as a subagent.\n- `user`, `false`: Only available in the agent picker.\n- `agent`: Only available as a subagent (not shown in picker).\n- `hidden`: Not available in the picker nor as a subagent.'), attribute.range); + default: + return this.createHover(description, attribute.range); } } - break; - case PromptsType.skill: - for (const attribute of header.attributes) { - if (attribute.range.containsPosition(position)) { - switch (attribute.key) { - case PromptHeaderAttributes.name: - return this.createHover(localize('promptHeader.skill.name', 'The name of the skill.'), attribute.range); - case PromptHeaderAttributes.description: - return this.createHover(localize('promptHeader.skill.description', 'The description of the skill. The description is added to every request and will be used by the agent to decide when to load the skill.'), attribute.range); - } - } - } - break; - case PromptsType.agent: - for (const attribute of header.attributes) { - if (attribute.range.containsPosition(position)) { - switch (attribute.key) { - case PromptHeaderAttributes.name: - return this.createHover(localize('promptHeader.agent.name', 'The name of the agent as shown in the UI.'), attribute.range); - case PromptHeaderAttributes.description: - return this.createHover(localize('promptHeader.agent.description', 'The description of the custom agent, what it does and when to use it.'), attribute.range); - case PromptHeaderAttributes.argumentHint: - return this.createHover(localize('promptHeader.agent.argumentHint', 'The argument-hint describes what inputs the custom agent expects or supports.'), attribute.range); - case PromptHeaderAttributes.model: - return this.getModelHover(attribute, position, localize('promptHeader.agent.model', 'Specify the model that runs this custom agent. Can also be a list of models. The first available model will be used.'), isGithubTarget(promptType, header.target)); - case PromptHeaderAttributes.tools: - return this.getToolHover(attribute, position, localize('promptHeader.agent.tools', 'The set of tools that the custom agent has access to.')); - case PromptHeaderAttributes.handOffs: - return this.getHandsOffHover(attribute, position, isGithubTarget(promptType, header.target)); - case PromptHeaderAttributes.target: - return this.createHover(localize('promptHeader.agent.target', 'The target to which the header attributes like tools apply to. Possible values are `github-copilot` and `vscode`.'), attribute.range); - case PromptHeaderAttributes.infer: - return this.createHover(localize('promptHeader.agent.infer', 'Controls visibility of the agent.\n\n- `all`, `true`: Available in the agent picker and can be used as a subagent.\n- `user`, `false`: Only available in the agent picker.\n- `agent`: Only available as a subagent (not shown in picker).\n- `hidden`: Not available in the picker nor as a subagent.'), attribute.range); - case PromptHeaderAttributes.agents: - return this.createHover(localize('promptHeader.agent.agents', 'One or more agents that this agent can use as subagents. Use \'*\' to specify all available agents.'), attribute.range); - } - } - } - break; - case PromptsType.prompt: - for (const attribute of header.attributes) { - if (attribute.range.containsPosition(position)) { - switch (attribute.key) { - case PromptHeaderAttributes.name: - return this.createHover(localize('promptHeader.prompt.name', 'The name of the prompt. This is also the name of the slash command that will run this prompt.'), attribute.range); - case PromptHeaderAttributes.description: - return this.createHover(localize('promptHeader.prompt.description', 'The description of the reusable prompt, what it does and when to use it.'), attribute.range); - case PromptHeaderAttributes.argumentHint: - return this.createHover(localize('promptHeader.prompt.argumentHint', 'The argument-hint describes what inputs the prompt expects or supports.'), attribute.range); - case PromptHeaderAttributes.model: - return this.getModelHover(attribute, position, localize('promptHeader.prompt.model', 'The model to use in this prompt. Can also be a list of models. The first available model will be used.'), false); - case PromptHeaderAttributes.tools: - return this.getToolHover(attribute, position, localize('promptHeader.prompt.tools', 'The tools to use in this prompt.')); - case PromptHeaderAttributes.agent: - case PromptHeaderAttributes.mode: - return this.getAgentHover(attribute, position); - } - } - } - break; + } } return undefined; } @@ -221,7 +168,7 @@ export class PromptHoverProvider implements HoverProvider { return this.createHover(baseMessage, node.range); } - private getAgentHover(agentAttribute: IHeaderAttribute, position: Position): Hover | undefined { + private getAgentHover(agentAttribute: IHeaderAttribute, position: Position, baseMessage: string): Hover | undefined { const lines: string[] = []; const value = agentAttribute.value; if (value.type === 'string' && value.range.containsPosition(position)) { @@ -232,7 +179,7 @@ export class PromptHoverProvider implements HoverProvider { } } else { const agents = this.chatModeService.getModes(); - lines.push(localize('promptHeader.prompt.agent.description', 'The agent to use when running this prompt.')); + lines.push(baseMessage); lines.push(''); // Built-in agents @@ -255,7 +202,7 @@ export class PromptHoverProvider implements HoverProvider { } private getHandsOffHover(attribute: IHeaderAttribute, position: Position, isGitHubTarget: boolean): Hover | undefined { - const handoffsBaseMessage = localize('promptHeader.agent.handoffs', 'Possible handoff actions when the agent has completed its task.'); + const handoffsBaseMessage = getAttributeDescription(PromptHeaderAttributes.handOffs, PromptsType.agent)!; if (isGitHubTarget) { return this.createHover(handoffsBaseMessage + '\n\n' + localize('promptHeader.agent.handoffs.githubCopilot', 'Note: This attribute is not used when target is github-copilot.'), attribute.range); } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index 2314291840c..15c27113c54 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -620,6 +620,69 @@ export function isNonRecommendedAttribute(attributeName: string): boolean { return attributeName === PromptHeaderAttributes.advancedOptions || attributeName === PromptHeaderAttributes.excludeAgent || attributeName === PromptHeaderAttributes.mode; } +export function getAttributeDescription(attributeName: string, promptType: PromptsType): string | undefined { + switch (promptType) { + case PromptsType.instructions: + switch (attributeName) { + case PromptHeaderAttributes.name: + return localize('promptHeader.instructions.name', 'The name of the instruction file as shown in the UI. If not set, the name is derived from the file name.'); + case PromptHeaderAttributes.description: + return localize('promptHeader.instructions.description', 'The description of the instruction file. It can be used to provide additional context or information about the instructions and is passed to the language model as part of the prompt.'); + case PromptHeaderAttributes.applyTo: + return localize('promptHeader.instructions.applyToRange', 'One or more glob pattern (separated by comma) that describe for which files the instructions apply to. Based on these patterns, the file is automatically included in the prompt, when the context contains a file that matches one or more of these patterns. Use `**` when you want this file to always be added.\nExample: `**/*.ts`, `**/*.js`, `client/**`'); + } + break; + case PromptsType.skill: + switch (attributeName) { + case PromptHeaderAttributes.name: + return localize('promptHeader.skill.name', 'The name of the skill.'); + case PromptHeaderAttributes.description: + return localize('promptHeader.skill.description', 'The description of the skill. The description is added to every request and will be used by the agent to decide when to load the skill.'); + } + break; + case PromptsType.agent: + switch (attributeName) { + case PromptHeaderAttributes.name: + return localize('promptHeader.agent.name', 'The name of the agent as shown in the UI.'); + case PromptHeaderAttributes.description: + return localize('promptHeader.agent.description', 'The description of the custom agent, what it does and when to use it.'); + case PromptHeaderAttributes.argumentHint: + return localize('promptHeader.agent.argumentHint', 'The argument-hint describes what inputs the custom agent expects or supports.'); + case PromptHeaderAttributes.model: + return localize('promptHeader.agent.model', 'Specify the model that runs this custom agent. Can also be a list of models. The first available model will be used.'); + case PromptHeaderAttributes.tools: + return localize('promptHeader.agent.tools', 'The set of tools that the custom agent has access to.'); + case PromptHeaderAttributes.handOffs: + return localize('promptHeader.agent.handoffs', 'Possible handoff actions when the agent has completed its task.'); + case PromptHeaderAttributes.target: + return localize('promptHeader.agent.target', 'The target to which the header attributes like tools apply to. Possible values are `github-copilot` and `vscode`.'); + case PromptHeaderAttributes.infer: + return localize('promptHeader.agent.infer', 'Controls visibility of the agent.'); + case PromptHeaderAttributes.agents: + return localize('promptHeader.agent.agents', 'One or more agents that this agent can use as subagents. Use \'*\' to specify all available agents.'); + } + break; + case PromptsType.prompt: + switch (attributeName) { + case PromptHeaderAttributes.name: + return localize('promptHeader.prompt.name', 'The name of the prompt. This is also the name of the slash command that will run this prompt.'); + case PromptHeaderAttributes.description: + return localize('promptHeader.prompt.description', 'The description of the reusable prompt, what it does and when to use it.'); + case PromptHeaderAttributes.argumentHint: + return localize('promptHeader.prompt.argumentHint', 'The argument-hint describes what inputs the prompt expects or supports.'); + case PromptHeaderAttributes.model: + return localize('promptHeader.prompt.model', 'The model to use in this prompt. Can also be a list of models. The first available model will be used.'); + case PromptHeaderAttributes.tools: + return localize('promptHeader.prompt.tools', 'The tools to use in this prompt.'); + case PromptHeaderAttributes.agent: + case PromptHeaderAttributes.mode: + return localize('promptHeader.prompt.agent.description', 'The agent to use when running this prompt.'); + } + break; + } + return undefined; +} + // The list of tools known to be used by GitHub Copilot custom agents export const knownGithubCopilotTools = [ SpecedToolAliases.execute, SpecedToolAliases.read, SpecedToolAliases.edit, SpecedToolAliases.search, SpecedToolAliases.agent, From 8e7a5cd45f2763db1d0bbe935fbe7751cfd60f43 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 28 Jan 2026 18:33:47 +0100 Subject: [PATCH 018/152] tool picker back in chat input --- .../contrib/chat/browser/chat.contribution.ts | 2 +- .../widget/input/modePickerActionItem.ts | 74 +++++++++++++------ 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index c364d06164e..fe519db63af 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -614,7 +614,7 @@ configurationRegistry.registerConfiguration({ [ChatConfiguration.AlternativeToolAction]: { type: 'boolean', description: nls.localize('chat.alternativeToolAction', "When enabled, shows the Configure Tools action in the mode picker dropdown on hover instead of in the chat input."), - default: true, + default: false, tags: ['experimental'], experiment: { mode: 'auto' diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index 381f97bfe4d..fa7d95407ce 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -32,6 +32,7 @@ import { PromptsStorage } from '../../../common/promptSyntax/service/promptsServ import { getOpenChatActionIdForMode } from '../../actions/chatActions.js'; import { IToggleChatModeArgs, ToggleAgentModeActionId } from '../../actions/chatExecuteActions.js'; import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; +import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; export interface IModePickerDelegate { readonly currentMode: IObservable; @@ -60,7 +61,8 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { @IMenuService private readonly menuService: IMenuService, @ICommandService commandService: ICommandService, @IProductService private readonly _productService: IProductService, - @ITelemetryService telemetryService: ITelemetryService + @ITelemetryService telemetryService: ITelemetryService, + @IOpenerService openerService: IOpenerService ) { // Get custom agent target (if filtering is enabled) const customAgentTarget = delegate.customAgentTarget?.(); @@ -71,7 +73,6 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { const policyDisabledCategory = { label: localize('managedByOrganization', "Managed by your organization"), order: 999, showHeader: true }; const agentModeDisabledViaPolicy = configurationService.inspect(ChatConfiguration.AgentEnabled).policyValue === false; - const alternativeToolActionEnabled = configurationService.getValue(ChatConfiguration.AlternativeToolAction); const makeAction = (mode: IChatMode, currentMode: IChatMode): IActionWidgetDropdownAction => { const isDisabledViaPolicy = @@ -80,30 +81,55 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { const tooltip = chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, mode.kind)?.description ?? action.tooltip; + // Add toolbar actions for Agent modes const toolbarActions: IAction[] = []; - if (alternativeToolActionEnabled && mode.kind === ChatModeKind.Agent && !isDisabledViaPolicy) { - // Add toolbar actions for Agent modes when alternative tool action is enabled - const label = localize('configureToolsFor', "Configure tools for {0} {1}", mode.label.get(), isModeConsideredBuiltIn(mode, this._productService) ? 'mode' : 'agent'); - toolbarActions.push({ - id: 'configureToolsForMode', - label: label, - tooltip: label, - class: ThemeIcon.asClassName(Codicon.tools), - enabled: true, - run: async () => { - // Hide the picker before opening the tools configuration - actionWidgetService.hide(); - // First switch to the mode if not already selected - if (currentMode.id !== mode.id) { - await commandService.executeCommand( - ToggleAgentModeActionId, - { modeId: mode.id, sessionResource: this.delegate.sessionResource() } satisfies IToggleChatModeArgs - ); - } - // Then open the tools picker - await commandService.executeCommand('workbench.action.chat.configureTools', pickerOptions.actionContext, { source: 'modePicker' }); + if (mode.kind === ChatModeKind.Agent && !isDisabledViaPolicy) { + if (mode.uri) { + let label, icon, id; + if (mode.source?.storage === PromptsStorage.extension) { + icon = Codicon.eye; + id = `viewAgent:${mode.id}`; + label = localize('viewModeConfiguration', "View {0} agent", mode.label.get()); + } else { + icon = Codicon.edit; + id = `editAgent:${mode.id}`; + label = localize('editModeConfiguration', "Edit {0} agent", mode.label.get()); } - }); + + const modeResource = mode.uri; + toolbarActions.push({ + id, + label, + tooltip: label, + class: ThemeIcon.asClassName(icon), + enabled: true, + run: async () => { + openerService.open(modeResource.get()); + } + }); + } else { + const label = localize('configureToolsFor', "Configure tools for {0} agent", mode.label.get()); + toolbarActions.push({ + id: `configureTools:${mode.id}`, + label, + tooltip: label, + class: ThemeIcon.asClassName(Codicon.tools), + enabled: true, + run: async () => { + // Hide the picker before opening the tools configuration + actionWidgetService.hide(); + // First switch to the mode if not already selected + if (currentMode.id !== mode.id) { + await commandService.executeCommand( + ToggleAgentModeActionId, + { modeId: mode.id, sessionResource: this.delegate.sessionResource() } satisfies IToggleChatModeArgs + ); + } + // Then open the tools picker + await commandService.executeCommand('workbench.action.chat.configureTools', pickerOptions.actionContext, { source: 'modePicker' }); + } + }); + } } return { From 3d17932be829b0c55f55ceab4bafd243161b33bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:56:34 +0000 Subject: [PATCH 019/152] Initial plan From caf8104c0fe3ff7c2dd8f7dd8f06296f6ccce5e4 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 28 Jan 2026 19:02:11 +0100 Subject: [PATCH 020/152] Remove inline chat affordance suppression logic (#291350) Users disliked that the inline chat affordance (sparkle icon) would only show once per selection and require changing the selection to see it again. This change removes the suppressAffordance mechanism so the affordance remains visible as long as there is a valid text selection. Fixes #291036 --- .../inlineChat/browser/inlineChatAffordance.ts | 17 +---------------- .../browser/inlineChatEditorAffordance.ts | 4 +--- .../browser/inlineChatGutterAffordance.ts | 3 +-- 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts index 6bfd83b73f4..da6e88ea8bd 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -5,7 +5,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { autorun, debouncedObservable, derived, observableValue, runOnChange, waitForState } from '../../../../base/common/observable.js'; -import { ICodeEditor, isIOverlayWidgetPositionCoordinates } from '../../../../editor/browser/editorBrowser.js'; +import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { InlineChatConfigKeys } from '../common/inlineChat.js'; @@ -34,7 +34,6 @@ export class InlineChatAffordance extends Disposable { const editorObs = observableCodeEditor(this._editor); const affordance = observableConfigValue<'off' | 'gutter' | 'editor'>(InlineChatConfigKeys.Affordance, 'off', configurationService); - const suppressAffordance = observableValue(this, false); const debouncedSelection = debouncedObservable(editorObs.cursorSelection, 500); const selectionData = observableValue(this, undefined); @@ -61,16 +60,12 @@ export class InlineChatAffordance extends Disposable { if (chatEntiteldService.sentimentObs.read(r).hidden) { selectionData.set(undefined, undefined); } - if (suppressAffordance.read(r)) { - selectionData.set(undefined, undefined); - } })); this._store.add(this._instantiationService.createInstance( InlineChatGutterAffordance, editorObs, derived(r => affordance.read(r) === 'gutter' ? selectionData.read(r) : undefined), - suppressAffordance, this._menuData )); @@ -78,15 +73,9 @@ export class InlineChatAffordance extends Disposable { InlineChatEditorAffordance, this._editor, derived(r => affordance.read(r) === 'editor' ? selectionData.read(r) : undefined), - suppressAffordance, this._menuData )); - this._store.add(autorun(reader => { - editorObs.cursorSelection.read(reader); - suppressAffordance.set(false, undefined); - })); - this._store.add(autorun(r => { const data = this._menuData.read(r); if (!data) { @@ -104,12 +93,8 @@ export class InlineChatAffordance extends Disposable { this._store.add(autorun(r => { const pos = this._inputWidget.position.read(r); if (pos === null) { - suppressAffordance.set(true, undefined); this._menuData.set(undefined, undefined); this._editor.focus(); - - } else if (isIOverlayWidgetPositionCoordinates(pos.preference) && pos.preference.left >= _editor.getLayoutInfo().contentLeft) { - suppressAffordance.set(true, undefined); } })); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts index 8f2b2622e19..a46d6af54c6 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts @@ -99,7 +99,6 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi constructor( private readonly _editor: ICodeEditor, selection: IObservable, - suppressAffordance: ISettableObservable, _hover: ISettableObservable<{ rect: DOMRect; above: boolean; lineNumber: number } | undefined>, @IInstantiationService instantiationService: IInstantiationService, ) { @@ -124,8 +123,7 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi this._store.add(autorun(r => { const sel = selection.read(r); - const suppressed = suppressAffordance.read(r); - if (sel && !suppressed) { + if (sel) { this._show(sel); } else { this._hide(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts index 4a9ab672790..3b3be83e50e 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts @@ -24,7 +24,6 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { constructor( private readonly _myEditorObs: ObservableCodeEditor, selection: IObservable, - suppressAffordance: ISettableObservable, private readonly _hover: ISettableObservable<{ rect: DOMRect; above: boolean; lineNumber: number } | undefined>, @IKeybindingService private readonly _keybindingService: IKeybindingService, @IHoverService hoverService: HoverService, @@ -34,7 +33,7 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { ) { const data = derived(r => { const value = selection.read(r); - if (!value || suppressAffordance.read(r)) { + if (!value) { return undefined; } From db70877d18af4d0c0393374f9cc0b347a53e0f93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 18:05:04 +0000 Subject: [PATCH 021/152] Fix "Done" affordance not going away after inline chat exit without changes Co-authored-by: jrieken <1794099+jrieken@users.noreply.github.com> --- .../contrib/inlineChat/browser/inlineChatController.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 53c72418a5b..b4ffe698c33 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -330,7 +330,9 @@ export class InlineChatController implements IEditorContribution { const lastRequest = session.chatModel.lastRequestObs.read(r); const isInProgress = lastRequest?.response?.isInProgress.read(r); const entry = session.editingSession.readEntry(session.uri, r); - const isNotSettled = !entry || entry.state.read(r) === ModifiedFileEntryState.Modified; + // When there's no entry (no changes made) and the response is complete, the widget should be hidden. + // When there's an entry in Modified state, it needs to be settled (accepted/rejected). + const isNotSettled = entry ? entry.state.read(r) === ModifiedFileEntryState.Modified : false; if (isInProgress || isNotSettled) { sessionOverlayWidget.show(session); } else { From 7b2081533c63181f0ae9cd26d528e2492431acde Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:09:48 -0800 Subject: [PATCH 022/152] updates --- .../chatContentParts/chatSuggestNextWidget.ts | 28 ++++++++++++++++--- .../contrib/chat/browser/widget/chatWidget.ts | 2 +- .../languageProviders/promptValidator.ts | 7 ++++- .../common/promptSyntax/promptFileParser.ts | 10 +++++-- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSuggestNextWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSuggestNextWidget.ts index c24470548f7..c9e3d0c4a96 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSuggestNextWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSuggestNextWidget.ts @@ -104,6 +104,14 @@ export class ChatSuggestNextWidget extends Disposable { private createPromptButton(handoff: IHandOff): HTMLElement { const disposables = new DisposableStore(); + // Capture the label to look up the current handoff at click time + // This ensures we get the latest handoff data (e.g., updated model from settings) + const handoffLabel = handoff.label; + const getCurrentHandoff = (): IHandOff | undefined => { + const currentHandoffs = this._currentMode?.handOffs?.get(); + return currentHandoffs?.find(h => h.label === handoffLabel) ?? handoff; + }; + const button = dom.$('.chat-welcome-view-suggested-prompt'); button.setAttribute('tabindex', '0'); button.setAttribute('role', 'button'); @@ -155,7 +163,10 @@ export class ChatSuggestNextWidget extends Disposable { ThemeIcon.isThemeIcon(icon) ? ThemeIcon.asClassName(icon) : undefined, true, () => { - this._onDidSelectPrompt.fire({ handoff, agentId: contrib.name }); + const currentHandoff = getCurrentHandoff(); + if (currentHandoff) { + this._onDidSelectPrompt.fire({ handoff: currentHandoff, agentId: contrib.name }); + } } ); }); @@ -180,18 +191,27 @@ export class ChatSuggestNextWidget extends Disposable { if (dom.isHTMLElement(e.target) && e.target.closest('.chat-suggest-next-dropdown')) { return; } - this._onDidSelectPrompt.fire({ handoff }); + const currentHandoff = getCurrentHandoff(); + if (currentHandoff) { + this._onDidSelectPrompt.fire({ handoff: currentHandoff }); + } })); } else { disposables.add(dom.addDisposableListener(button, 'click', () => { - this._onDidSelectPrompt.fire({ handoff }); + const currentHandoff = getCurrentHandoff(); + if (currentHandoff) { + this._onDidSelectPrompt.fire({ handoff: currentHandoff }); + } })); } disposables.add(dom.addDisposableListener(button, 'keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); - this._onDidSelectPrompt.fire({ handoff }); + const currentHandoff = getCurrentHandoff(); + if (currentHandoff) { + this._onDidSelectPrompt.fire({ handoff: currentHandoff }); + } } })); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 430d6cf6d6c..11e38cc7612 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -1210,7 +1210,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this._switchToAgentByName(handoff.agent); // Switch to the specified model if provided if (handoff.model) { - this.input.switchModelByQualifiedName(handoff.model); + this.input.switchModelByQualifiedName([handoff.model]); } // Insert the handoff prompt into the input this.input.setValue(promptToUse, false); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index 2314291840c..2140a1a488a 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -514,8 +514,13 @@ export class PromptValidator { report(toMarker(localize('promptValidator.handoffShowContinueOnMustBeBoolean', "The 'showContinueOn' property in a handoff must be a boolean."), prop.value.range, MarkerSeverity.Error)); } break; + case 'model': + if (prop.value.type !== 'string') { + report(toMarker(localize('promptValidator.handoffModelMustBeString', "The 'model' property in a handoff must be a string."), prop.value.range, MarkerSeverity.Error)); + } + break; default: - report(toMarker(localize('promptValidator.unknownHandoffProperty', "Unknown property '{0}' in handoff object. Supported properties are 'label', 'agent', 'prompt' and optional 'send', 'showContinueOn'.", prop.key.value), prop.value.range, MarkerSeverity.Warning)); + report(toMarker(localize('promptValidator.unknownHandoffProperty', "Unknown property '{0}' in handoff object. Supported properties are 'label', 'agent', 'prompt' and optional 'send', 'showContinueOn', 'model'.", prop.key.value), prop.value.range, MarkerSeverity.Warning)); } required.delete(prop.key.value); } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts index c69a453afe2..cdd864e7e75 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts @@ -238,7 +238,7 @@ export class PromptHeader { return undefined; } if (handoffsAttribute.value.type === 'array') { - // Array format: list of objects: { agent, label, prompt, send?, showContinueOn? } + // Array format: list of objects: { agent, label, prompt, send?, showContinueOn?, model? } const handoffs: IHandOff[] = []; for (const item of handoffsAttribute.value.items) { if (item.type === 'object') { @@ -247,6 +247,7 @@ export class PromptHeader { let prompt: string | undefined; let send: boolean | undefined; let showContinueOn: boolean | undefined; + let model: string | undefined; for (const prop of item.properties) { if (prop.key.value === 'agent' && prop.value.type === 'string') { agent = prop.value.value; @@ -258,6 +259,8 @@ export class PromptHeader { send = prop.value.value; } else if (prop.key.value === 'showContinueOn' && prop.value.type === 'boolean') { showContinueOn = prop.value.value; + } else if (prop.key.value === 'model' && prop.value.type === 'string') { + model = prop.value.value; } } if (agent && label && prompt !== undefined) { @@ -266,7 +269,8 @@ export class PromptHeader { label, prompt, ...(send !== undefined ? { send } : {}), - ...(showContinueOn !== undefined ? { showContinueOn } : {}) + ...(showContinueOn !== undefined ? { showContinueOn } : {}), + ...(model !== undefined ? { model } : {}) }; handoffs.push(handoff); } @@ -325,7 +329,7 @@ export interface IHandOff { readonly prompt: string; readonly send?: boolean; readonly showContinueOn?: boolean; // treated exactly like send (optional boolean) - readonly model?: readonly string[]; // qualified model name(s) to switch to (e.g., "GPT-4o (copilot)") + readonly model?: string; // qualified model name to switch to (e.g., "GPT-4o (copilot)") } export interface IHeaderAttribute { From 9470784c27a6ebe33ba3352ff41dd9ea32a68b89 Mon Sep 17 00:00:00 2001 From: Dirk Baeumer Date: Wed, 28 Jan 2026 19:12:20 +0100 Subject: [PATCH 023/152] Handle deletions correctly in rename tracker --- .../browser/renameSymbolTrackerService.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/inlineCompletions/browser/renameSymbolTrackerService.ts b/src/vs/workbench/contrib/inlineCompletions/browser/renameSymbolTrackerService.ts index 22466135414..2ac90546d65 100644 --- a/src/vs/workbench/contrib/inlineCompletions/browser/renameSymbolTrackerService.ts +++ b/src/vs/workbench/contrib/inlineCompletions/browser/renameSymbolTrackerService.ts @@ -22,13 +22,13 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta * Checks if a model content change event was caused only by typing or pasting. * Returns false for AI edits, refactorings, undo/redo, etc. */ -function isTypingOrPasteEdit(event: IModelContentChangedEvent): boolean { +function isUserEdit(event: IModelContentChangedEvent): boolean { if (event.isUndoing || event.isRedoing || event.isFlush) { return false; } for (const source of event.detailedReasons) { - if (!isTypingOrPasteSource(source)) { + if (!isUserEditSource(source)) { return false; } } @@ -36,13 +36,14 @@ function isTypingOrPasteEdit(event: IModelContentChangedEvent): boolean { return event.detailedReasons.length > 0; } -function isTypingOrPasteSource(source: TextModelEditSource): boolean { +const userEditKinds = new Set(['type', 'paste', 'cut', 'executeCommands', 'executeCommand', 'compositionType', 'compositionEnd']); +function isUserEditSource(source: TextModelEditSource): boolean { const metadata = source.metadata; if (metadata.source !== 'cursor') { return false; } - const kind = (metadata as { kind?: string }).kind; - return kind === 'type' || kind === 'paste' || kind === 'compositionType' || kind === 'compositionEnd'; + const kind = metadata.kind; + return userEditKinds.has(kind); } type WordState = { @@ -74,9 +75,9 @@ class ModelSymbolRenameTracker extends Disposable { // Listen to content changes - only reset on non-typing/paste edits this._register(this._model.onDidChangeContent(e => { - if (!isTypingOrPasteEdit(e)) { - // Non-typing/paste edit occurred - reset tracking and start a new - // rename tracking at the current cursor position (if any) + if (!isUserEdit(e)) { + // Non-user edit has occurred - reset rename tracking at + // the current cursor position (if any) const position = this._lastCursorPosition; this.reset(); if (position !== undefined) { From afc58805a3632aab3f517da8fa5a04cca7f2befd Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:15:31 -0800 Subject: [PATCH 024/152] Fix "add element to chat" overlay not always hiding (#291348) --- .../browser/attachments/simpleBrowserEditorOverlay.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/simpleBrowserEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/attachments/simpleBrowserEditorOverlay.ts index f62b55b32c2..8ef8c035627 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/simpleBrowserEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/simpleBrowserEditorOverlay.ts @@ -397,6 +397,7 @@ class SimpleBrowserOverlayController { container.appendChild(connectingWebviewElement); } + cts.cancel(); cts = new CancellationTokenSource(); try { await this._browserElementsService.startDebugSession(cts.token, locator); @@ -405,6 +406,10 @@ class SimpleBrowserOverlayController { return; } + if (cts.token.isCancellationRequested) { + return; + } + if (!container.contains(this._domNode)) { container.appendChild(this._domNode); } @@ -413,8 +418,8 @@ class SimpleBrowserOverlayController { const hide = () => { widget.setActiveLocator(undefined, undefined); + cts.cancel(); if (container.contains(this._domNode)) { - cts.cancel(); this._domNode.remove(); } connectingWebviewElement.remove(); @@ -473,7 +478,7 @@ export class SimpleBrowserOverlay implements IWorkbenchContribution { () => editorGroupsService.groups ); - const overlayWidgets = new DisposableMap(); + const overlayWidgets = this._store.add(new DisposableMap()); this._store.add(autorun(r => { From 3a334a88aa7f99e1343f1883e836a2f72f3e8f74 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 28 Jan 2026 13:32:47 -0500 Subject: [PATCH 025/152] allow prompt for input detection in bg terminals (#291325) fix #290983 --- .../chatAgentTools/browser/tools/runInTerminalTool.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index c49e493924d..3f07cbaadeb 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -782,6 +782,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { RunInTerminalTool._activeExecutions.set(termId, execution); // Set up OutputMonitor when start marker is created + const startMarkerPromise = Event.toPromise(execution.strategy.onDidCreateStartMarker); store.add(execution.strategy.onDidCreateStartMarker(startMarker => { if (!outputMonitor) { outputMonitor = store.add(this._instantiationService.createInstance( @@ -805,6 +806,8 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { if (args.isBackground) { // Background mode: wait for OutputMonitor to detect idle, then return this._logService.debug(`RunInTerminalTool: Starting background execution \`${command}\``); + // Wait for the start marker to be created (which creates the outputMonitor) + await startMarkerPromise; if (outputMonitor) { await Event.toPromise(outputMonitor.onDidFinishCommand); pollingResult = outputMonitor.pollingResult; From 738a982b7f176a1fbbc902e549e64d0e9326a27c Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Wed, 28 Jan 2026 13:39:59 -0500 Subject: [PATCH 026/152] Improve context widget UX based on feedback (#291344) * Fix hover color * Refactor hover behaviro to match usage widget * Make the context widget more compact * Relocate context widget * Update src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix memory leak --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../browser/widget/input/chatInputPart.ts | 38 +++++ .../chat/browser/widget/media/chat.css | 9 ++ .../viewPane/chatContextUsageDetails.ts | 30 +++- .../viewPane/chatContextUsageWidget.ts | 130 +++++++++--------- .../viewPane/chatViewTitleControl.ts | 26 ---- .../media/chatContextUsageDetails.css | 47 +++++-- .../viewPane/media/chatContextUsageWidget.css | 35 +---- .../viewPane/media/chatViewTitleControl.css | 6 +- 8 files changed, 176 insertions(+), 145 deletions(-) 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 3bcee9ee00b..cc848df7dbc 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -122,6 +122,7 @@ import { IModelPickerDelegate, ModelPickerActionItem } from './modelPickerAction import { IModePickerDelegate, ModePickerActionItem } from './modePickerActionItem.js'; import { SessionTypePickerActionItem } from './sessionTargetPickerActionItem.js'; import { WorkspacePickerActionItem } from './workspacePickerActionItem.js'; +import { ChatContextUsageWidget } from '../../widgetHosts/viewPane/chatContextUsageWidget.js'; const $ = dom.$; @@ -285,6 +286,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private chatInputWidgetsContainer!: HTMLElement; private readonly _widgetController = this._register(new MutableDisposable()); + private contextUsageWidget?: ChatContextUsageWidget; + private contextUsageWidgetContainer!: HTMLElement; + private readonly _contextUsageDisposables = this._register(new MutableDisposable()); + readonly height = observableValue(this, 0); private _inputEditor!: CodeEditorWidget; @@ -1672,6 +1677,31 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } + /** + * Updates the context usage widget based on the current model. + */ + private updateContextUsageWidget(): void { + this._contextUsageDisposables.clear(); + + const model = this._widget?.viewModel?.model; + if (!model || !this.contextUsageWidget) { + return; + } + + const store = new DisposableStore(); + this._contextUsageDisposables.value = store; + + // Subscribe to model changes to update when requests complete + store.add(model.onDidChange(e => { + if (e.kind === 'completedRequest') { + this.contextUsageWidget?.update(model.lastRequest); + } + })); + + // Initial update + this.contextUsageWidget.update(model.lastRequest); + } + render(container: HTMLElement, initialValue: string, widget: IChatWidget) { this._widget = widget; this.computeVisibleOptionGroups(); @@ -1691,6 +1721,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.updateAgentSessionTypeContextKey(); this.refreshChatSessionPickers(); this.tryUpdateWidgetController(); + this.updateContextUsageWidget(); })); let elements; @@ -1702,6 +1733,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.h('.chat-editing-session@chatEditingSessionWidgetContainer'), dom.h('.interactive-input-and-side-toolbar@inputAndSideToolbar', [ dom.h('.chat-input-container@inputContainer', [ + dom.h('.chat-context-usage-container@contextUsageWidgetContainer'), dom.h('.chat-editor-container@editorContainer'), dom.h('.chat-input-toolbars@inputToolbars'), ]), @@ -1721,6 +1753,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.h('.chat-editing-session@chatEditingSessionWidgetContainer'), dom.h('.interactive-input-and-side-toolbar@inputAndSideToolbar', [ dom.h('.chat-input-container@inputContainer', [ + dom.h('.chat-context-usage-container@contextUsageWidgetContainer'), dom.h('.chat-attachments-container@attachmentsContainer', [ dom.h('.chat-attachment-toolbar@attachmentToolbar'), dom.h('.chat-attached-context@attachedContextContainer'), @@ -1752,6 +1785,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer; this.chatInputTodoListWidgetContainer = elements.chatInputTodoListWidgetContainer; this.chatInputWidgetsContainer = elements.chatInputWidgetsContainer; + this.contextUsageWidgetContainer = elements.contextUsageWidgetContainer; + + // Context usage widget + this.contextUsageWidget = this._register(this.instantiationService.createInstance(ChatContextUsageWidget)); + this.contextUsageWidgetContainer.appendChild(this.contextUsageWidget.domNode); if (this.options.enableImplicitContext && !this._implicitContext) { this._implicitContext = this._register( diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index c76aa1eeacc..6663d5f2a08 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -776,6 +776,15 @@ have to be updated for changes to the rules above, or to support more deeply nes padding: 0 6px 6px 6px; /* top padding is inside the editor widget */ width: 100%; + position: relative; +} + +/* Context usage widget container - positioned at top right of chat input */ +.interactive-session .chat-input-container .chat-context-usage-container { + position: absolute; + top: 4px; + right: 6px; + z-index: 1; } .interactive-input-part:has(.chat-editing-session > .chat-editing-session-container) .chat-input-container, diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts index 77741595adb..647486aa963 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts @@ -37,6 +37,7 @@ export class ChatContextUsageDetails extends Disposable { private readonly quotaItem: HTMLElement; private readonly percentageLabel: HTMLElement; + private readonly tokenCountLabel: HTMLElement; private readonly progressFill: HTMLElement; private readonly tokenDetailsContainer: HTMLElement; private readonly warningMessage: HTMLElement; @@ -55,11 +56,15 @@ export class ChatContextUsageDetails extends Disposable { // Using same structure as ChatUsageWidget quota items this.quotaItem = this.domNode.appendChild($('.quota-item')); - // Header row with label and percentage + // Header row with label const quotaItemHeader = this.quotaItem.appendChild($('.quota-item-header')); const quotaItemLabel = quotaItemHeader.appendChild($('.quota-item-label')); quotaItemLabel.textContent = localize('contextWindow', "Context Window"); - this.percentageLabel = quotaItemHeader.appendChild($('.quota-item-value')); + + // Token count and percentage row (on same line) + const tokenRow = this.quotaItem.appendChild($('.token-row')); + this.tokenCountLabel = tokenRow.appendChild($('.token-count-label')); + this.percentageLabel = tokenRow.appendChild($('.quota-item-value')); // Progress bar - using same structure as chat usage widget const progressBar = this.quotaItem.appendChild($('.quota-bar')); @@ -98,10 +103,16 @@ export class ChatContextUsageDetails extends Disposable { } update(data: IChatContextUsageData): void { - const { percentage, promptTokenDetails } = data; + const { percentage, promptTokens, maxInputTokens, promptTokenDetails } = data; - // Update percentage label - this.percentageLabel.textContent = `${percentage.toFixed(0)}%`; + // Update token count and percentage on same line + this.tokenCountLabel.textContent = localize( + 'tokenCount', + "{0} / {1} tokens", + this.formatTokenCount(promptTokens, 1), + this.formatTokenCount(maxInputTokens, 0) + ); + this.percentageLabel.textContent = `• ${percentage.toFixed(0)}%`; // Update progress bar this.progressFill.style.width = `${Math.min(100, percentage)}%`; @@ -121,6 +132,15 @@ export class ChatContextUsageDetails extends Disposable { this.warningMessage.style.display = percentage >= 75 ? '' : 'none'; } + private formatTokenCount(count: number, decimals: number): string { + if (count >= 1000000) { + return `${(count / 1000000).toFixed(decimals)}M`; + } else if (count >= 1000) { + return `${(count / 1000).toFixed(decimals)}K`; + } + return count.toString(); + } + private renderTokenDetails(details: readonly IChatContextUsagePromptTokenDetail[] | undefined, contextWindowPercentage: number): void { // Clear previous content dom.clearNode(this.tokenDetailsContainer); diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts index 6d166534e17..d50a6efd846 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts @@ -6,8 +6,9 @@ import './media/chatContextUsageWidget.css'; import * as dom from '../../../../../../base/browser/dom.js'; import { EventType, addDisposableListener } from '../../../../../../base/browser/dom.js'; +import { IDelayedHoverOptions } from '../../../../../../base/browser/ui/hover/hover.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; -import { Disposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; import { autorun, IObservable, observableValue } from '../../../../../../base/common/observable.js'; import { localize } from '../../../../../../nls.js'; import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; @@ -15,6 +16,8 @@ import { IInstantiationService } from '../../../../../../platform/instantiation/ import { IChatRequestModel, IChatResponseModel } from '../../../common/model/chatModel.js'; import { ILanguageModelsService } from '../../../common/languageModels.js'; import { ChatContextUsageDetails, IChatContextUsageData } from './chatContextUsageDetails.js'; +import { StandardKeyboardEvent } from '../../../../../../base/browser/keyboardEvent.js'; +import { KeyCode } from '../../../../../../base/common/keyCodes.js'; const $ = dom.$; @@ -99,13 +102,14 @@ export class ChatContextUsageWidget extends Disposable { readonly domNode: HTMLElement; - private readonly tokenLabel: HTMLElement; private readonly progressIndicator: CircularProgressIndicator; private readonly _isVisible = observableValue(this, false); get isVisible(): IObservable { return this._isVisible; } private readonly _lastRequestDisposable = this._register(new MutableDisposable()); + private readonly _hoverDisposable = this._register(new MutableDisposable()); + private readonly _contextUsageDetails = this._register(new MutableDisposable()); private currentData: IChatContextUsageData | undefined; @@ -116,7 +120,7 @@ export class ChatContextUsageWidget extends Disposable { ) { super(); - this.domNode = $('.chat-context-usage-widget.action-label'); + this.domNode = $('.chat-context-usage-widget'); this.domNode.style.display = 'none'; this.domNode.setAttribute('tabindex', '0'); this.domNode.setAttribute('role', 'button'); @@ -127,55 +131,74 @@ export class ChatContextUsageWidget extends Disposable { this.progressIndicator = new CircularProgressIndicator(); iconContainer.appendChild(this.progressIndicator.domNode); - // Token label (shown on hover/focus) - this.tokenLabel = this.domNode.appendChild($('.token-label')); - - // Show details popup on click - this._register(addDisposableListener(this.domNode, EventType.CLICK, () => { - this.showDetails(); - })); - - // Show details on Enter/Space for keyboard accessibility - this._register(addDisposableListener(this.domNode, EventType.KEY_DOWN, (e: KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - this.showDetails(); - } - })); + // Set up hover - will be configured when data is available + this.setupHover(); } - private showDetails(): void { - if (!this.currentData) { - return; - } + private setupHover(): void { + this._hoverDisposable.clear(); + const store = new DisposableStore(); + this._hoverDisposable.value = store; - // Add expanded class to keep token label visible while details are shown - this.domNode.classList.add('expanded'); + const getOrCreateDetails = (): ChatContextUsageDetails => { + if (!this._contextUsageDetails.value) { + this._contextUsageDetails.value = this.instantiationService.createInstance(ChatContextUsageDetails); + } + if (this.currentData) { + this._contextUsageDetails.value.update(this.currentData); + } + return this._contextUsageDetails.value; + }; - const details = this.instantiationService.createInstance(ChatContextUsageDetails); - details.update(this.currentData); + const resolveHoverOptions = (): IDelayedHoverOptions => { + const details = getOrCreateDetails(); + return { + content: details.domNode, + appearance: { showPointer: true, compact: true }, + persistence: { hideOnHover: false }, + trapFocus: true + }; + }; - const hover = this.hoverService.showInstantHover({ - content: details.domNode, - target: { - targetElements: [this.domNode], - dispose: () => { - this.domNode.classList.remove('expanded'); - details.dispose(); - } - }, - persistence: { sticky: true, hideOnHover: false, hideOnKeyDown: false }, - appearance: { showPointer: true } - }, true); + store.add(this.hoverService.setupDelayedHover( + this.domNode, + resolveHoverOptions + )); - // Focus the details widget - details.focus(); + // Helper to show sticky hover with focus + const showStickyHover = () => { + if (this.currentData) { + // Force hide any existing hover to ensure we can show our sticky one + this.hoverService.hideHover(true); - // Handle case where hover couldn't be shown - if (!hover) { - this.domNode.classList.remove('expanded'); - details.dispose(); - } + const details = getOrCreateDetails(); + this.hoverService.showInstantHover( + { + content: details.domNode, + target: this.domNode, + appearance: { showPointer: true, compact: true }, + persistence: { hideOnHover: false, sticky: true }, + trapFocus: true, + }, + true + ); + } + }; + + // Show sticky + focused hover on click + store.add(addDisposableListener(this.domNode, EventType.CLICK, e => { + e.stopPropagation(); + showStickyHover(); + })); + + // Show sticky + focused hover on keyboard activation (Space/Enter) + store.add(addDisposableListener(this.domNode, EventType.KEY_DOWN, e => { + const evt = new StandardKeyboardEvent(e); + if (evt.equals(KeyCode.Space) || evt.equals(KeyCode.Enter)) { + e.preventDefault(); + showStickyHover(); + } + })); } /** @@ -235,23 +258,6 @@ export class ChatContextUsageWidget extends Disposable { } else if (percentage >= 75) { this.domNode.classList.add('warning'); } - - // Update token label (shown on hover/focus) - this.tokenLabel.textContent = localize( - 'tokenCount', - "{0} / {1} T", - this.formatTokenCount(promptTokens, 1), - this.formatTokenCount(maxTokens, 0) - ); - } - - private formatTokenCount(count: number, decimals: number): string { - if (count >= 1000000) { - return `${(count / 1000000).toFixed(decimals)}M`; - } else if (count >= 1000) { - return `${(count / 1000).toFixed(decimals)}K`; - } - return count.toString(); } private show(): void { diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts index 8d61871b819..dac6fd6bf45 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts @@ -22,7 +22,6 @@ import { ChatConfiguration } from '../../../common/constants.js'; import { ActionViewItem, IActionViewItemOptions } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IAction } from '../../../../../../base/common/actions.js'; import { AgentSessionsPicker } from '../../agentSessions/agentSessionsPicker.js'; -import { ChatContextUsageWidget } from './chatContextUsageWidget.js'; export interface IChatViewTitleDelegate { focusChat(): void; @@ -46,7 +45,6 @@ export class ChatViewTitleControl extends Disposable { private navigationToolbar?: MenuWorkbenchToolBar; private actionsToolbar?: MenuWorkbenchToolBar; - private contextUsageWidget?: ChatContextUsageWidget; private lastKnownHeight = 0; @@ -101,7 +99,6 @@ export class ChatViewTitleControl extends Disposable { private render(parent: HTMLElement): void { const elements = h('div.chat-view-title-container', [ h('div.chat-view-title-navigation-toolbar@navigationToolbar'), - h('div.chat-view-title-context-usage@contextUsage'), h('div.chat-view-title-actions-toolbar@actionsToolbar'), ]); @@ -121,13 +118,6 @@ export class ChatViewTitleControl extends Disposable { menuOptions: { shouldForwardArgs: true } })); - // Context usage widget - this.contextUsageWidget = this._register(this.instantiationService.createInstance(ChatContextUsageWidget)); - elements.contextUsage.appendChild(this.contextUsageWidget.domNode); - this._register(this.contextUsageWidget.onDidChangeVisibility(() => { - this.checkHeight(); - })); - // Actions toolbar on the right this.actionsToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, elements.actionsToolbar, MenuId.ChatViewSessionTitleToolbar, { menuOptions: { shouldForwardArgs: true }, @@ -153,17 +143,9 @@ export class ChatViewTitleControl extends Disposable { if (e.kind === 'setCustomTitle' || e.kind === 'addRequest') { this.doUpdate(); } - if (e.kind === 'completedRequest') { - this.updateContextUsage(); - } }); this.doUpdate(); - this.updateContextUsage(); - } - - private updateContextUsage(): void { - this.contextUsageWidget?.update(this.model?.lastRequest); } private doUpdate(): void { @@ -186,14 +168,6 @@ export class ChatViewTitleControl extends Disposable { } } - private checkHeight(): void { - const currentHeight = this.getHeight(); - if (currentHeight !== this.lastKnownHeight) { - this.lastKnownHeight = currentHeight; - this._onDidChangeHeight.fire(); - } - } - private updateTitle(title: string): void { if (!this.titleContainer) { return; diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css index d3d8616f643..41b7a7ad9d1 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css @@ -3,11 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/* Remove the outline and change border color on focus instead */ +.workbench-hover-container.locked:has(.chat-context-usage-details) .monaco-hover.workbench-hover { + outline: none; +} +.workbench-hover-container:focus-within.locked:has(.chat-context-usage-details) .monaco-hover.workbench-hover { + outline: none; + border-color: var(--vscode-focusBorder); +} + .chat-context-usage-details { display: flex; flex-direction: column; - padding: 12px; - min-width: 220px; + padding: 4px 0; + min-width: 200px; } .chat-context-usage-details:focus { @@ -16,14 +25,14 @@ /* Using same structure as ChatUsageWidget quota items */ .chat-context-usage-details .quota-item { - margin-bottom: 8px; + margin-bottom: 4px; } .chat-context-usage-details .quota-item-header { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 4px; + margin-bottom: 2px; } .chat-context-usage-details .quota-item-label { @@ -34,6 +43,18 @@ color: var(--vscode-descriptionForeground); } +.chat-context-usage-details .token-row { + display: flex; + align-items: center; + gap: 4px; + margin-bottom: 2px; +} + +.chat-context-usage-details .token-count-label { + font-size: 12px; + color: var(--vscode-descriptionForeground); +} + /* Progress bar - matching chat usage implementation */ .chat-context-usage-details .quota-item .quota-bar { width: 100%; @@ -41,7 +62,7 @@ background-color: var(--vscode-gauge-background); border-radius: 4px; border: 1px solid var(--vscode-gauge-border); - margin: 4px 0; + margin: 2px 0; } .chat-context-usage-details .quota-item .quota-bar .quota-bit { @@ -70,22 +91,22 @@ .chat-context-usage-details .warning-message { font-size: 12px; color: var(--vscode-descriptionForeground); - margin-bottom: 8px; + margin-bottom: 4px; } /* Token details breakdown */ .chat-context-usage-details .token-details-container { - margin-top: 8px; + margin-top: 4px; } .chat-context-usage-details .token-category { - margin-bottom: 8px; + margin-bottom: 4px; } .chat-context-usage-details .token-category-header { font-weight: 600; color: var(--vscode-foreground); - margin-bottom: 4px; + margin-bottom: 2px; } .chat-context-usage-details .token-detail-item { @@ -105,24 +126,24 @@ .chat-context-usage-details .actions-section .separator { border-top: 1px solid var(--vscode-editorHoverWidget-border); - margin: 8px 0; + margin: 4px 0; } .chat-context-usage-details .actions-section .actions-header { font-weight: 600; color: var(--vscode-foreground); - margin-bottom: 8px; + margin-bottom: 4px; } .chat-context-usage-details .actions-section .button-bar-container { display: flex; flex-direction: column; - gap: 6px; + gap: 4px; } .chat-context-usage-details .actions-section .button-bar-container .monaco-button-bar { flex-direction: column; - gap: 6px; + gap: 4px; } .chat-context-usage-details .actions-section .button-bar-container .monaco-button { diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css index ffad8682a1a..9906a0b8171 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css @@ -6,15 +6,13 @@ .chat-context-usage-widget { display: flex; align-items: center; - justify-content: flex-end; + justify-content: center; height: 22px; flex-shrink: 0; cursor: pointer; padding: 3px; border-radius: 5px; box-sizing: border-box; - overflow: hidden; - transition: background-color 0.15s ease; } .chat-context-usage-widget .icon-container { @@ -26,28 +24,6 @@ flex-shrink: 0; } -.chat-context-usage-widget .token-label { - max-width: 0; - opacity: 0; - overflow: hidden; - white-space: nowrap; - font-size: 11px; - color: var(--vscode-foreground); - transition: max-width 0.2s ease, opacity 0.2s ease, margin-right 0.2s ease; - margin-right: 0; - order: -1; -} - -/* Expand on hover, focus, or when details are shown (expanded class) */ -.chat-context-usage-widget:hover .token-label, -.chat-context-usage-widget:focus .token-label, -.chat-context-usage-widget:focus-within .token-label, -.chat-context-usage-widget.expanded .token-label { - max-width: 100px; - opacity: 1; - margin-right: 6px; -} - .chat-context-usage-widget:hover { background-color: var(--vscode-toolbar-hoverBackground); outline: 1px dashed var(--vscode-toolbar-hoverOutline); @@ -79,7 +55,6 @@ .chat-context-usage-widget .progress-pie { fill: var(--vscode-icon-foreground); opacity: 0.8; - transition: d 0.3s ease; pointer-events: none; } @@ -90,11 +65,3 @@ .chat-context-usage-widget.error .progress-pie { fill: var(--vscode-editorError-foreground); } - -.chat-context-usage-widget.warning .token-label { - color: var(--vscode-editorWarning-foreground); -} - -.chat-context-usage-widget.error .token-label { - color: var(--vscode-editorError-foreground); -} diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewTitleControl.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewTitleControl.css index f267e867323..9434f08cef3 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewTitleControl.css +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewTitleControl.css @@ -36,12 +36,8 @@ min-width: 0; } - .chat-view-title-context-usage { - margin-left: auto; - flex-shrink: 0; - } - .chat-view-title-actions-toolbar { + margin-left: auto; padding-left: 4px; flex-shrink: 0; } From ddf7703270c5703283b915cfc64e8cbe116ec0f2 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:40:48 -0800 Subject: [PATCH 027/152] fix: update example model name in inline chat configuration description --- .../contrib/inlineChat/browser/inlineChatDefaultModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts index ca5f2ac9194..05514e5c594 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts @@ -112,7 +112,7 @@ Registry.as(ConfigurationExtensions.Configuration).regis ...{ id: 'inlineChat', title: localize('inlineChatConfigurationTitle', 'Inline Chat'), order: 30, type: 'object' }, properties: { [InlineChatDefaultModel.configName]: { - description: localize('inlineChatDefaultModelDescription', "Select the default language model to use for inline chat from the available providers. Model names may include the provider in parentheses, for example 'GPT-4o (copilot)'."), + description: localize('inlineChatDefaultModelDescription', "Select the default language model to use for inline chat from the available providers. Model names may include the provider in parentheses, for example 'Claude Haiku 4.5 (copilot)'."), type: 'string', default: '', enum: InlineChatDefaultModel.modelIds, From 7be7cc7f5691d48fd0b63de0c684651be924f09c Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 28 Jan 2026 19:41:02 +0100 Subject: [PATCH 028/152] agents field not validated in custom agent file (#291352) --- .../languageProviders/promptValidator.ts | 21 ++- .../languageProviders/promptValidator.test.ts | 131 +++++++++++++++++- 2 files changed, 141 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index 15c27113c54..8479a840d37 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -11,7 +11,7 @@ import { IModelService } from '../../../../../../editor/common/services/model.js import { localize } from '../../../../../../nls.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../../platform/markers/common/markers.js'; -import { IChatMode, IChatModeService } from '../../chatModes.js'; +import { ChatMode, IChatMode, IChatModeService } from '../../chatModes.js'; import { ChatModeKind } from '../../constants.js'; import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js'; import { ILanguageModelToolsService, SpecedToolAliases } from '../../tools/languageModelToolsService.js'; @@ -25,6 +25,7 @@ import { IPromptsService } from '../service/promptsService.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; import { AGENTS_SOURCE_FOLDER, LEGACY_MODE_FILE_EXTENSION } from '../config/promptFileLocations.js'; import { Lazy } from '../../../../../../base/common/lazy.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; export const MARKERS_OWNER_ID = 'prompts-diagnostics-provider'; @@ -40,7 +41,7 @@ export class PromptValidator { public async validate(promptAST: ParsedPromptFile, promptType: PromptsType, report: (markers: IMarkerData) => void): Promise { promptAST.header?.errors.forEach(error => report(toMarker(error.message, error.range, MarkerSeverity.Error))); - this.validateHeader(promptAST, promptType, report); + await this.validateHeader(promptAST, promptType, report); await this.validateBody(promptAST, promptType, report); await this.validateFileName(promptAST, promptType, report); await this.validateSkillFolderName(promptAST, promptType, report); @@ -155,7 +156,7 @@ export class PromptValidator { await Promise.all(fileReferenceChecks); } - private validateHeader(promptAST: ParsedPromptFile, promptType: PromptsType, report: (markers: IMarkerData) => void): void { + private async validateHeader(promptAST: ParsedPromptFile, promptType: PromptsType, report: (markers: IMarkerData) => void): Promise { const header = promptAST.header; if (!header) { return; @@ -186,7 +187,7 @@ export class PromptValidator { if (!isGitHubTarget) { this.validateModel(attributes, ChatModeKind.Agent, report); this.validateHandoffs(attributes, report); - this.validateAgentsAttribute(attributes, header, report); + await this.validateAgentsAttribute(attributes, header, report); } break; } @@ -565,7 +566,7 @@ export class PromptValidator { } } - private validateAgentsAttribute(attributes: IHeaderAttribute[], header: PromptHeader, report: (markers: IMarkerData) => void): undefined { + private async validateAgentsAttribute(attributes: IHeaderAttribute[], header: PromptHeader, report: (markers: IMarkerData) => void): Promise { const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.agents); if (!attribute) { return; @@ -575,13 +576,21 @@ export class PromptValidator { return; } - // Check each item is a string + // Collect available agent names + const agents = await this.promptsService.getCustomAgents(CancellationToken.None); + const availableAgentNames = new Set(agents.map(agent => agent.name)); + availableAgentNames.add(ChatMode.Agent.name.get()); // include default agent + + // Check each item is a string and agent exists const agentNames: string[] = []; for (const item of attribute.value.items) { if (item.type !== 'string') { report(toMarker(localize('promptValidator.eachAgentMustBeString', "Each agent name in the 'agents' attribute must be a string."), item.range, MarkerSeverity.Error)); } else if (item.value) { agentNames.push(item.value); + if (item.value !== '*' && !availableAgentNames.has(item.value)) { + report(toMarker(localize('promptValidator.agentInAgentsNotFound', "Unknown agent '{0}'. Available agents: {1}.", item.value, Array.from(availableAgentNames).join(', ')), item.range, MarkerSeverity.Warning)); + } } } diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts index 2dd04d73c43..21226981e1b 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts @@ -25,8 +25,9 @@ import { getPromptFileExtension } from '../../../../common/promptSyntax/config/p import { PromptValidator } from '../../../../common/promptSyntax/languageProviders/promptValidator.js'; import { PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; import { PromptFileParser } from '../../../../common/promptSyntax/promptFileParser.js'; -import { PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; +import { ICustomAgent, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; import { MockChatModeService } from '../../../common/mockChatModeService.js'; +import { MockPromptsService } from '../../../common/promptSyntax/service/mockPromptsService.js'; suite('PromptValidator', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -144,6 +145,17 @@ suite('PromptValidator', () => { return Promise.resolve(existingFiles.has(uri)); } }); + const promptsService = new MockPromptsService(); + const customMode: ICustomAgent = { + uri: URI.parse('file:///test/custom-mode.md'), + name: 'Plan', + description: 'A test custom mode', + tools: ['tool1', 'tool2'], + agentInstructions: { content: 'Custom mode body', toolReferences: [] }, + source: { storage: PromptsStorage.local } + }; + promptsService.setCustomModes([customMode]); + instaService.stub(IPromptsService, promptsService); }); async function validate(code: string, promptType: PromptsType, uri?: URI): Promise { @@ -976,7 +988,7 @@ suite('PromptValidator', () => { const content = [ '---', 'description: "Test"', - `agents: ['valid', 123]`, + `agents: ['agent', 123]`, `tools: ['agent']`, '---', ].join('\n'); @@ -984,11 +996,25 @@ suite('PromptValidator', () => { assert.deepStrictEqual(markers.map(m => m.message), [`Each agent name in the 'agents' attribute must be a string.`]); }); + test('unknown agent in agents attribute shows warning', async () => { + const content = [ + '---', + 'description: "Test"', + `agents: ['UnknownAgent']`, + `tools: ['agent']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Warning); + assert.strictEqual(markers[0].message, `Unknown agent 'UnknownAgent'. Available agents: Plan, agent.`); + }); + test('agents attribute with non-empty value requires agent tool 1', async () => { const content = [ '---', 'description: "Test"', - `agents: ['Planning', 'Research']`, + `agents: ['agent', 'Plan']`, '---', ].join('\n'); const markers = await validate(content, PromptsType.agent); @@ -999,7 +1025,7 @@ suite('PromptValidator', () => { const content = [ '---', 'description: "Test"', - `agents: ['Planning', 'Research']`, + `agents: ['agent', 'Plan']`, `tools: ['shell']`, '---', ].join('\n'); @@ -1011,7 +1037,7 @@ suite('PromptValidator', () => { const content = [ '---', 'description: "Test"', - `agents: ['Planning', 'Research']`, + `agents: ['agent', 'Plan']`, `tools: ['agent']`, '---', ].join('\n'); @@ -1394,6 +1420,101 @@ suite('PromptValidator', () => { assert.deepStrictEqual(markers, [], 'Expected no validation issues when name is missing'); }); + test('skill with empty name does not validate folder match', async () => { + const content = [ + '---', + 'name: ""', + 'description: Test Skill', + '---', + 'This is a skill.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///.github/skills/my-skill/SKILL.md')); + // Should get error for empty name, but no folder mismatch warning since name is empty + assert.ok(markers.some(m => m.message.includes('must not be empty')), 'Expected error for empty name'); + assert.ok(!markers.some(m => m.message.includes('should match the folder name')), 'Should not warn about folder mismatch for empty name'); + }); + + test('skill name with whitespace trimmed matches folder name', async () => { + const content = [ + '---', + 'name: " my-skill "', + 'description: Test Skill', + '---', + 'This is a skill.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///.github/skills/my-skill/SKILL.md')); + assert.deepStrictEqual(markers, [], 'Expected no validation issues when trimmed name matches folder'); + }); + + test('skill name validation with different folder depths', async () => { + // Test with deeper path structure + { + const content = [ + '---', + 'name: advanced-skill', + 'description: Test Skill', + '---', + 'This is a skill.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///home/user/.github/skills/advanced-skill/SKILL.md')); + assert.deepStrictEqual(markers, [], 'Expected no issues for deeper path when name matches'); + } + + // Test with mismatch in deeper path + { + const content = [ + '---', + 'name: wrong-name', + 'description: Test Skill', + '---', + 'This is a skill.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///home/user/.github/skills/correct-folder/SKILL.md')); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].message, `The skill name 'wrong-name' should match the folder name 'correct-folder'.`); + } + }); + + test('skill name validation with special characters in folder', async () => { + const content = [ + '---', + 'name: my_special-skill.v2', + 'description: Test Skill', + '---', + 'This is a skill.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///.github/skills/my_special-skill.v2/SKILL.md')); + assert.deepStrictEqual(markers, [], 'Expected no issues when name with special chars matches folder'); + }); + + test('skill with non-string name type does not validate folder match', async () => { + const content = [ + '---', + 'name: 123', + 'description: Test Skill', + '---', + 'This is a skill.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///.github/skills/my-skill/SKILL.md')); + // Should get error for non-string name type, but no folder mismatch warning + assert.ok(markers.some(m => m.message.includes('must be a string')), 'Expected error for non-string name'); + assert.ok(!markers.some(m => m.message.includes('should match the folder name')), 'Should not warn about folder mismatch for non-string name'); + }); + + test('skill folder name validation only for skill type', async () => { + // Verify that folder name validation doesn't run for non-skill prompt types + const content = [ + '---', + 'name: different-name', + 'description: Test Agent', + '---', + 'This is an agent.' + ].join('\n'); + const markers = await validate(content, PromptsType.agent, URI.parse('file:///.github/agents/my-agent/AGENT.md')); + // Should not get folder name mismatch warning for agents + assert.ok(!markers.some(m => m.message.includes('should match the folder name')), 'Should not validate folder names for agents'); + }); + test('skill with unknown attributes shows warning', async () => { const content = [ '---', From 3b35824fd84b642d37760bae47727383c848661d Mon Sep 17 00:00:00 2001 From: RedCMD Date: Wed, 28 Jan 2026 18:20:12 +1300 Subject: [PATCH 029/152] fix: VB `increaseIndentPattern` --- extensions/vb/language-configuration.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/vb/language-configuration.json b/extensions/vb/language-configuration.json index 734448101e0..2fefe78dbe7 100644 --- a/extensions/vb/language-configuration.json +++ b/extensions/vb/language-configuration.json @@ -32,7 +32,7 @@ "flags": "i" }, "increaseIndentPattern": { - "pattern": "^\\s*((If|ElseIf).*Then(?!.*End\\s+If)\\s*(('|REM).*)?|(Else|While|For|Do|Select\\s+Case|Case|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Try|Catch|Finally|SyncLock|Using|Property|Get|Set|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\\b(?!.*\\bEnd\\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\\b).*(('|REM).*)?)$", + "pattern": "^\\s*((If|ElseIf)\\b.*\\bThen\\s*(('|REM).*)?|(Else|While|For|Do|Select\\s+Case|Case|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Try|Catch|Finally|SyncLock|Using|Property|Get|Set|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\\b(?!.*\\bEnd\\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\\b).*(('|REM).*)?)$", "flags": "i" } }, From 9295def4c2a8dee035abaa5c9d1a691aa7ea532b Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 28 Jan 2026 11:29:29 -0800 Subject: [PATCH 030/152] chat: use ConnectionObserverElement to track template mount state (#291370) * chat: use ConnectionObserverElement to track template mount state Replaces the hacky 'renderedPartsMounted' boolean with a custom element that leverages the native connectedCallback and disconnectedCallback lifecycle hooks to track whether parts are mounted in the DOM. - Adds ConnectionObserverElement custom element (connection-observer) that fires onDidConnect and onDidDisconnect callbacks - Updates IChatListItemTemplate to use connectionObserver element instead of renderedPartsMounted boolean - Removes manual mount state management in disposeElement since the element's disconnectedCallback handles it automatically - Uses native element.isConnected property to check mount state Fixes https://github.com/microsoft/vscode/issues/290903 (Commit message generated by Copilot) * pr comments --- src/vs/base/browser/dom.ts | 30 +++++++++++++++++++ .../toolInvocationParts/chatMcpAppModel.ts | 8 +++-- .../toolInvocationParts/chatMcpAppSubPart.ts | 4 +++ .../chat/browser/widget/chatListRenderer.ts | 12 +++++--- 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index a9f9fcfee7e..df985c36c16 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -2804,3 +2804,33 @@ function setOrRemoveAttribute(element: HTMLOrSVGElement, key: string, value: unk type ElementAttributeKeys = Partial<{ [K in keyof T]: T[K] extends Function ? never : T[K] extends object ? ElementAttributeKeys : Value; }>; + +/** + * A custom element that fires callbacks when connected to or disconnected from the DOM. + * Useful for tracking whether a template or component is currently mounted, especially + * with iframes/webviews that are sensitive to movement. + * + * @example + * ```ts + * const observer = document.createElement('connection-observer') as ConnectionObserverElement; + * observer.onDidConnect = () => console.log('mounted'); + * observer.onDidDisconnect = () => console.log('unmounted'); + * container.appendChild(observer); + * ``` + */ +export class ConnectionObserverElement extends HTMLElement { + public onDidConnect?: () => void; + public onDidDisconnect?: () => void; + + disconnectedCallback() { + this.onDidDisconnect?.(); + } + + connectedCallback() { + this.onDidConnect?.(); + } +} + +if (!customElements.get('connection-observer')) { + customElements.define('connection-observer', ConnectionObserverElement); +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts index fa8b0f00aed..34fbe127cd9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts @@ -50,6 +50,8 @@ export type McpAppLoadState = * The webview is created lazily on first claim and survives across re-renders. */ export class ChatMcpAppModel extends Disposable { + private static readonly heightCache = new WeakMap(); + /** Origin store for persistent webview origins per server */ private readonly _originStore: WebviewOriginStore; @@ -69,7 +71,7 @@ export class ChatMcpAppModel extends Disposable { private _latestCsp: McpApps.McpUiResourceCsp | undefined = undefined; /** Current height of the webview */ - private _height: number = 300; + private _height: number; /** The persistent webview origin */ private readonly _webviewOrigin: string; @@ -104,6 +106,7 @@ export class ChatMcpAppModel extends Disposable { this._originStore = new WebviewOriginStore(ORIGIN_STORE_KEY, storageService); this._webviewOrigin = this._originStore.getOrigin('mcpApp', renderData.serverDefinitionId); this._mcpToolCallUI = this._register(this._instantiationService.createInstance(McpToolCallUI, renderData)); + this._height = ChatMcpAppModel.heightCache.get(this.toolInvocation) ?? 300; // Create the webview element this._webview = this._register(this._webviewService.createWebviewElement({ @@ -648,8 +651,9 @@ export class ChatMcpAppModel extends Disposable { } private _handleSizeChanged(params: McpApps.McpUiSizeChangedNotification['params']): void { - if (params.height !== undefined) { + if (params.height !== undefined && params.height !== this._height) { this._height = params.height; + ChatMcpAppModel.heightCache.set(this.toolInvocation, params.height); this._onDidChangeHeight.fire(); } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts index 98a2cef2f07..54776b9e5a3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts @@ -110,6 +110,10 @@ export class ChatMcpAppSubPart extends BaseChatToolInvocationSubPart { this._updateContainerHeight(); })); + this._register(onDidRemount(() => { + this._model.remount(); + })); + this._register(context.onDidChangeVisibility(visible => { if (visible) { this._model.remount(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 51c7137a2b6..05a012b08f0 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -115,11 +115,10 @@ export interface IChatListItemTemplate { */ renderedParts?: IChatContentPart[]; /** - * Whether the parts are mounted in the DOM. This is undefined after - * the element is disposed so the `renderedParts.onDidMount` can be - * called on the next render as appropriate. + * Element used to track whether the template is mounted in the DOM. */ renderedPartsMounted?: boolean; + readonly rowContainer: HTMLElement; readonly titleToolbar?: MenuWorkbenchToolBar; readonly header?: HTMLElement; @@ -535,8 +534,14 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + template.renderedPartsMounted = false; + }; + templateDisposables.add(this._onDidUpdateViewModel.event(() => { if (!template.currentElement || !this.viewModel?.sessionResource || !isEqual(template.currentElement.sessionResource, this.viewModel.sessionResource)) { this.clearRenderedParts(template); @@ -2115,7 +2120,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer, index: number, templateData: IChatListItemTemplate, details?: IListElementRenderDetails): void { this.traceLayout('disposeElement', `Disposing element, index=${index}`); templateData.elementDisposables.clear(); - templateData.renderedPartsMounted = false; if (templateData.currentElement && !this.viewModel?.editing) { this.templateDataByRequestId.delete(templateData.currentElement.id); From 7ee6a1d941a4b0cfbfeed9b061aaf2d1fe2313d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 18:47:08 +0000 Subject: [PATCH 031/152] Add chat.disableAIFeatures support to editor.aiStats.enabled Co-authored-by: hediet <2931520+hediet@users.noreply.github.com> --- .../editTelemetry/browser/editTelemetryContribution.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetryContribution.ts b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetryContribution.ts index 0616e6fb0f0..273fe2e4a9e 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetryContribution.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetryContribution.ts @@ -14,6 +14,7 @@ import { EditTrackingFeature } from './telemetry/editSourceTrackingFeature.js'; import { VSCodeWorkspace } from './helpers/vscodeObservableWorkspace.js'; import { AiStatsFeature } from './editStats/aiStatsFeature.js'; import { EDIT_TELEMETRY_SETTING_ID, AI_STATS_SETTING_ID } from './settingIds.js'; +import { ChatConfiguration } from '../../../contrib/chat/common/constants.js'; export class EditTelemetryContribution extends Disposable { constructor( @@ -36,9 +37,11 @@ export class EditTelemetryContribution extends Disposable { })); const aiStatsEnabled = observableConfigValue(AI_STATS_SETTING_ID, true, this._configurationService); + const chatDisabled = observableConfigValue(ChatConfiguration.AIDisabled, false, this._configurationService); this._register(autorun(r => { const enabled = aiStatsEnabled.read(r); - if (!enabled) { + const aiDisabled = chatDisabled.read(r); + if (!enabled || aiDisabled) { return; } From cafabc758fed479206a162c0785caf21f500d62f Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:34:48 -0800 Subject: [PATCH 032/152] bug: prioritize user-selected model during same vscode session for inline chat --- .../browser/inlineChatController.ts | 52 ++++++++++++------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 53c72418a5b..941d0c94fc6 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -108,10 +108,10 @@ export class InlineChatController implements IEditorContribution { } /** - * Guard flag indicating whether model defaults (including vendor/default model selection) - * should be applied for this session. + * Stores the user's explicitly chosen model (qualified name) from a previous inline chat request in the same session. + * When set, this takes priority over the inlineChat.defaultModel setting. */ - private static _applyModelDefaultsThisSession: boolean = true; + private static _userSelectedModel: string | undefined; private readonly _store = new DisposableStore(); private readonly _isActiveController = observableValue(this, false); @@ -482,12 +482,6 @@ export class InlineChatController implements IEditorContribution { // Store for tracking model changes during this session const sessionStore = new DisposableStore(); - // Check for default model setting - const defaultModelSetting = this._configurationService.getValue(InlineChatConfigKeys.DefaultModel); - if (defaultModelSetting && !this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([defaultModelSetting])) { - this._logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`); - } - try { await this._applyModelDefaults(session, sessionStore); @@ -599,19 +593,35 @@ export class InlineChatController implements IEditorContribution { * Prioritization: user session choice > inlineChat.defaultModel setting > vendor default */ private async _applyModelDefaults(session: IInlineChatSession2, sessionStore: DisposableStore): Promise { - if (InlineChatController._applyModelDefaultsThisSession) { - const defaultModelSetting = this._configurationService.getValue(InlineChatConfigKeys.DefaultModel); - if (defaultModelSetting) { - if (!this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([defaultModelSetting])) { - this._logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`); - await this._selectVendorDefaultModel(session); - } - } else { - await this._selectVendorDefaultModel(session); + // Prioritization: user session choice > inlineChat.defaultModel setting > vendor default + const userSelectedModel = InlineChatController._userSelectedModel; + const defaultModelSetting = this._configurationService.getValue(InlineChatConfigKeys.DefaultModel); + + let modelApplied = false; + + // 1. Try user's explicitly chosen model from a previous inline chat in the same session + if (userSelectedModel) { + modelApplied = this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([userSelectedModel]); + if (!modelApplied) { + // User's previously selected model is no longer available, clear it + InlineChatController._userSelectedModel = undefined; } } - // Track model changes - disable automatic defaults once user explicitly changes the model. + // 2. Try inlineChat.defaultModel setting + if (!modelApplied && defaultModelSetting) { + modelApplied = this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([defaultModelSetting]); + if (!modelApplied) { + this._logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`); + } + } + + // 3. Fall back to vendor default + if (!modelApplied) { + await this._selectVendorDefaultModel(session); + } + + // Track model changes - store user's explicit choice in the given sessions. // NOTE: This currently detects any model change, not just user-initiated ones. let initialModelId: string | undefined; sessionStore.add(autorun(r => { @@ -624,7 +634,9 @@ export class InlineChatController implements IEditorContribution { return; } if (initialModelId !== newModel.identifier) { - InlineChatController._applyModelDefaultsThisSession = false; + // User explicitly changed model, store their choice as qualified name + InlineChatController._userSelectedModel = `${newModel.metadata.name} (${newModel.metadata.vendor})`; + initialModelId = newModel.identifier; } })); } From c2b2a7ab98fe26683802b97cbe0d6620d9b70c9d Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:54:22 -0800 Subject: [PATCH 033/152] address comments --- .../contrib/inlineChat/browser/inlineChatController.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 941d0c94fc6..e35f4dab372 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -46,7 +46,7 @@ import { IChatService } from '../../chat/common/chatService/chatService.js'; import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from '../../chat/common/attachments/chatVariableEntries.js'; import { isResponseVM } from '../../chat/common/model/chatViewModel.js'; import { ChatAgentLocation } from '../../chat/common/constants.js'; -import { ILanguageModelChatSelector, ILanguageModelsService, isILanguageModelChatSelector } from '../../chat/common/languageModels.js'; +import { ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsService, isILanguageModelChatSelector } from '../../chat/common/languageModels.js'; import { isNotebookContainingCellEditor as isNotebookWithCellEditor } from '../../notebook/browser/notebookEditor.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommon.js'; @@ -593,7 +593,6 @@ export class InlineChatController implements IEditorContribution { * Prioritization: user session choice > inlineChat.defaultModel setting > vendor default */ private async _applyModelDefaults(session: IInlineChatSession2, sessionStore: DisposableStore): Promise { - // Prioritization: user session choice > inlineChat.defaultModel setting > vendor default const userSelectedModel = InlineChatController._userSelectedModel; const defaultModelSetting = this._configurationService.getValue(InlineChatConfigKeys.DefaultModel); @@ -635,7 +634,7 @@ export class InlineChatController implements IEditorContribution { } if (initialModelId !== newModel.identifier) { // User explicitly changed model, store their choice as qualified name - InlineChatController._userSelectedModel = `${newModel.metadata.name} (${newModel.metadata.vendor})`; + InlineChatController._userSelectedModel = ILanguageModelChatMetadata.asQualifiedName(newModel.metadata); initialModelId = newModel.identifier; } })); From 89518b68de36b22c7511ec3c72ada3233063c9d2 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 28 Jan 2026 20:03:58 +0000 Subject: [PATCH 034/152] Fix issue with runSubagent result text (#291383) * Fix issue with runSubagent result text It was collecting text between tool calls when it should only collect the text after the final tool calls. And edits in subagents left behind an empty codeblock in the result text. * Delete comment --- .../tools/languageModelToolsService.ts | 12 +- .../tools/builtinTools/runSubagentTool.ts | 24 ++- .../common/tools/languageModelToolsService.ts | 8 + .../builtinTools/runSubagentTool.test.ts | 196 ++++++++++++++++++ .../tools/mockLanguageModelToolsService.ts | 19 +- 5 files changed, 245 insertions(+), 14 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index 606aac556c7..6717a86baff 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -42,7 +42,7 @@ import { ChatConfiguration } from '../../common/constants.js'; import { ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { ChatToolInvocation } from '../../common/model/chatProgressTypes/chatToolInvocation.js'; import { ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.js'; -import { CountTokensCallback, createToolSchemaUri, IBeginToolCallOptions, ILanguageModelToolsService, IPreparedToolInvocation, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, SpecedToolAliases, stringifyPromptTsxPart, isToolSet, ToolDataSource, toolMatchesModel, ToolSet, VSCodeToolReference, IToolSet, ToolSetForModel } from '../../common/tools/languageModelToolsService.js'; +import { CountTokensCallback, createToolSchemaUri, IBeginToolCallOptions, ILanguageModelToolsService, IPreparedToolInvocation, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, SpecedToolAliases, stringifyPromptTsxPart, isToolSet, ToolDataSource, toolMatchesModel, ToolSet, VSCodeToolReference, IToolSet, ToolSetForModel, IToolInvokedEvent } from '../../common/tools/languageModelToolsService.js'; import { getToolConfirmationAlert } from '../accessibility/chatAccessibilityProvider.js'; import { URI } from '../../../../../base/common/uri.js'; import { chatSessionResourceToId } from '../../common/model/chatUri.js'; @@ -88,6 +88,8 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo readonly onDidChangeTools = this._onDidChangeTools.event; private readonly _onDidPrepareToolCallBecomeUnresponsive = this._register(new Emitter<{ sessionResource: URI; toolData: IToolData }>()); readonly onDidPrepareToolCallBecomeUnresponsive = this._onDidPrepareToolCallBecomeUnresponsive.event; + private readonly _onDidInvokeTool = this._register(new Emitter()); + readonly onDidInvokeTool = this._onDidInvokeTool.event; /** Throttle tools updates because it sends all tools and runs on context key updates */ private readonly _onDidChangeToolsScheduler = new RunOnceScheduler(() => this._onDidChangeTools.fire(), 750); @@ -357,6 +359,14 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo async invokeTool(dto: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { this._logService.trace(`[LanguageModelToolsService#invokeTool] Invoking tool ${dto.toolId} with parameters ${JSON.stringify(dto.parameters)}`); + // Fire the event to notify listeners that a tool is being invoked + this._onDidInvokeTool.fire({ + toolId: dto.toolId, + sessionResource: dto.context?.sessionResource, + requestId: dto.chatRequestId, + subagentInvocationId: dto.subAgentInvocationId, + }); + // When invoking a tool, don't validate the "when" clause. An extension may have invoked a tool just as it was becoming disabled, and just let it go through rather than throw and break the chat. let tool = this._tools.get(dto.toolId); if (!tool) { 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 7fdab8e2ae3..56100a090d6 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -9,7 +9,7 @@ import { Event } from '../../../../../../base/common/event.js'; import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { generateUuid } from '../../../../../../base/common/uuid.js'; import { IJSONSchema, IJSONSchemaMap } from '../../../../../../base/common/jsonSchema.js'; -import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { localize } from '../../../../../../nls.js'; import { IConfigurationChangeEvent, IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; @@ -129,6 +129,8 @@ export class RunSubagentTool extends Disposable implements IToolImpl { const request = model.getRequests().at(-1)!; + const store = new DisposableStore(); + try { // Get the default agent const defaultAgent = this.chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, ChatModeKind.Agent); @@ -194,7 +196,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { const progressCallback = (parts: IChatProgress[]) => { for (const part of parts) { // Write certain parts immediately to the model - if (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized' || part.kind === 'textEdit' || part.kind === 'notebookEdit' || part.kind === 'codeblockUri') { + if (part.kind === 'textEdit' || part.kind === 'notebookEdit' || part.kind === 'codeblockUri') { if (part.kind === 'codeblockUri' && !inEdit) { inEdit = true; model.acceptResponseProgress(request, { kind: 'markdownContent', content: new MarkdownString('```\n') }); @@ -205,11 +207,6 @@ export class RunSubagentTool extends Disposable implements IToolImpl { } else { model.acceptResponseProgress(request, part); } - - // When we see a tool invocation starting, reset markdown collection - if (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') { - markdownParts.length = 0; // Clear previously collected markdown - } } else if (part.kind === 'markdownContent') { if (inEdit) { model.acceptResponseProgress(request, { kind: 'markdownContent', content: new MarkdownString('\n```\n\n') }); @@ -246,6 +243,13 @@ export class RunSubagentTool extends Disposable implements IToolImpl { modeInstructions, }; + // Subscribe to tool invocations to clear markdown parts when a tool is invoked + store.add(this.languageModelToolsService.onDidInvokeTool(e => { + if (e.subagentInvocationId === subAgentInvocationId) { + markdownParts.length = 0; + } + })); + // Invoke the agent const result = await this.chatAgentService.invokeAgent( defaultAgent.id, @@ -260,7 +264,9 @@ export class RunSubagentTool extends Disposable implements IToolImpl { return createToolSimpleTextResult(`Agent error: ${result.errorDetails.message}`); } - const resultText = markdownParts.join('') || 'Agent completed with no output'; + // This is a hack due to the fact that edits are represented as empty codeblocks with URIs. That needs to be cleaned up, + // in the meantime, just strip an empty codeblock left behind. + const resultText = markdownParts.join('').replace(/^\n*```\n+```\n*/g, '').trim() || 'Agent completed with no output'; // Store result in toolSpecificData for serialization if (invocation.toolSpecificData?.kind === 'subagent') { @@ -284,6 +290,8 @@ export class RunSubagentTool extends Disposable implements IToolImpl { const errorMessage = `Error invoking subagent: ${error instanceof Error ? error.message : 'Unknown error'}`; this.logService.error(errorMessage, error); return createToolSimpleTextResult(errorMessage); + } finally { + store.dispose(); } } diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts index 7e573bd40b1..6fb55c9754e 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts @@ -470,6 +470,13 @@ export interface IBeginToolCallOptions { subagentInvocationId?: string; } +export interface IToolInvokedEvent { + readonly toolId: string; + readonly sessionResource: URI | undefined; + readonly requestId: string | undefined; + readonly subagentInvocationId: string | undefined; +} + export const ILanguageModelToolsService = createDecorator('ILanguageModelToolsService'); export type CountTokensCallback = (input: string, token: CancellationToken) => Promise; @@ -482,6 +489,7 @@ export interface ILanguageModelToolsService { readonly agentToolSet: ToolSet; readonly onDidChangeTools: Event; readonly onDidPrepareToolCallBecomeUnresponsive: Event<{ readonly sessionResource: URI; readonly toolData: IToolData }>; + readonly onDidInvokeTool: Event; registerToolData(toolData: IToolData): IDisposable; registerToolImplementation(id: string, tool: IToolImpl): IDisposable; registerTool(toolData: IToolData, tool: IToolImpl): IDisposable; 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 new file mode 100644 index 00000000000..9d7940db1de --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts @@ -0,0 +1,196 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CancellationToken } from '../../../../../../../base/common/cancellation.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { NullLogService } from '../../../../../../../platform/log/common/log.js'; +import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { RunSubagentTool } from '../../../../common/tools/builtinTools/runSubagentTool.js'; +import { MockLanguageModelToolsService } from '../mockLanguageModelToolsService.js'; +import { IChatAgentService } from '../../../../common/participants/chatAgents.js'; +import { IChatModeService } from '../../../../common/chatModes.js'; +import { IChatService } from '../../../../common/chatService/chatService.js'; +import { ILanguageModelsService } from '../../../../common/languageModels.js'; +import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; + +suite('RunSubagentTool', () => { + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + + suite('resultText trimming', () => { + test('trims leading empty codeblocks (```\\n```) from result', () => { + // This tests the regex: /^\n*```\n+```\n*/g + const testCases = [ + { input: '```\n```\nActual content', expected: 'Actual content' }, + { input: '\n```\n```\nActual content', expected: 'Actual content' }, + { input: '\n\n```\n\n```\n\nActual content', expected: 'Actual content' }, + { input: '```\n```\n```\n```\nActual content', expected: '```\n```\nActual content' }, // Only trims leading + { input: 'No codeblock here', expected: 'No codeblock here' }, + { input: '```\n```\n', expected: '' }, + { input: '', expected: '' }, + ]; + + for (const { input, expected } of testCases) { + const result = input.replace(/^\n*```\n+```\n*/g, '').trim(); + assert.strictEqual(result, expected, `Failed for input: ${JSON.stringify(input)}`); + } + }); + }); + + suite('prepareToolInvocation', () => { + test('returns correct toolSpecificData', async () => { + const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); + const configService = new TestConfigurationService(); + + const tool = testDisposables.add(new RunSubagentTool( + {} as IChatAgentService, + {} as IChatService, + {} as IChatModeService, + mockToolsService, + {} as ILanguageModelsService, + new NullLogService(), + mockToolsService, + configService, + {} as IInstantiationService, + )); + + const result = await tool.prepareToolInvocation( + { + parameters: { + prompt: 'Test prompt', + description: 'Test task', + agentName: 'CustomAgent', + }, + chatSessionResource: URI.parse('test://session'), + }, + CancellationToken.None + ); + + assert.ok(result); + assert.strictEqual(result.invocationMessage, 'Test task'); + assert.deepStrictEqual(result.toolSpecificData, { + kind: 'subagent', + description: 'Test task', + agentName: 'CustomAgent', + prompt: 'Test prompt', + }); + }); + }); + + suite('getToolData', () => { + test('returns basic tool data', () => { + const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); + const configService = new TestConfigurationService(); + + const tool = testDisposables.add(new RunSubagentTool( + {} as IChatAgentService, + {} as IChatService, + {} as IChatModeService, + mockToolsService, + {} as ILanguageModelsService, + new NullLogService(), + mockToolsService, + configService, + {} as IInstantiationService, + )); + + const toolData = tool.getToolData(); + + assert.strictEqual(toolData.id, 'runSubagent'); + assert.ok(toolData.inputSchema); + assert.ok(toolData.inputSchema.properties?.prompt); + assert.ok(toolData.inputSchema.properties?.description); + assert.deepStrictEqual(toolData.inputSchema.required, ['prompt', 'description']); + }); + + test('includes agentName property when SubagentToolCustomAgents is enabled', () => { + const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); + const configService = new TestConfigurationService({ + 'chat.customAgentInSubagent.enabled': true, + }); + + const tool = testDisposables.add(new RunSubagentTool( + {} as IChatAgentService, + {} as IChatService, + {} as IChatModeService, + mockToolsService, + {} as ILanguageModelsService, + new NullLogService(), + mockToolsService, + configService, + {} as IInstantiationService, + )); + + const toolData = tool.getToolData(); + + assert.ok(toolData.inputSchema?.properties?.agentName, 'agentName should be in schema when custom agents enabled'); + }); + }); + + suite('onDidInvokeTool event', () => { + test('mock service fires onDidInvokeTool events with correct data', () => { + const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); + const sessionResource = URI.parse('test://session'); + const receivedEvents: { toolId: string; sessionResource: URI | undefined; requestId: string | undefined; subagentInvocationId: string | undefined }[] = []; + + testDisposables.add(mockToolsService.onDidInvokeTool(e => { + receivedEvents.push(e); + })); + + mockToolsService.fireOnDidInvokeTool({ + toolId: 'test-tool', + sessionResource, + requestId: 'request-123', + subagentInvocationId: 'subagent-456', + }); + + assert.strictEqual(receivedEvents.length, 1); + assert.deepStrictEqual(receivedEvents[0], { + toolId: 'test-tool', + sessionResource, + requestId: 'request-123', + subagentInvocationId: 'subagent-456', + }); + }); + + test('events with different subagentInvocationId are distinguishable', () => { + // This tests the filtering logic used in RunSubagentTool.invoke() + // The tool subscribes to onDidInvokeTool and checks if e.subagentInvocationId matches its own callId + const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); + const targetSubagentId = 'target-subagent'; + + const matchingEvents: string[] = []; + testDisposables.add(mockToolsService.onDidInvokeTool(e => { + if (e.subagentInvocationId === targetSubagentId) { + matchingEvents.push(e.toolId); + } + })); + + // Fire events with different subagentInvocationIds + mockToolsService.fireOnDidInvokeTool({ + toolId: 'unrelated-tool', + sessionResource: undefined, + requestId: undefined, + subagentInvocationId: 'different-subagent', + }); + mockToolsService.fireOnDidInvokeTool({ + toolId: 'matching-tool', + sessionResource: undefined, + requestId: undefined, + subagentInvocationId: targetSubagentId, + }); + mockToolsService.fireOnDidInvokeTool({ + toolId: 'another-unrelated-tool', + sessionResource: undefined, + requestId: undefined, + subagentInvocationId: undefined, + }); + + // Only the matching event should be captured + assert.deepStrictEqual(matchingEvents, ['matching-tool']); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts index bd8095a8d53..a0bd9c40fe2 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts @@ -5,29 +5,38 @@ 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 { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; import { constObservable, IObservable, IReader } from '../../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { URI } from '../../../../../../base/common/uri.js'; import { IProgressStep } from '../../../../../../platform/progress/common/progress.js'; import { ChatRequestToolReferenceEntry } from '../../../common/attachments/chatVariableEntries.js'; import { IVariableReference } from '../../../common/chatModes.js'; import { IChatToolInvocation } from '../../../common/chatService/chatService.js'; -import { CountTokensCallback, IBeginToolCallOptions, ILanguageModelToolsService, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolSet, ToolDataSource, ToolSet } from '../../../common/tools/languageModelToolsService.js'; -import { URI } from '../../../../../../base/common/uri.js'; import { ILanguageModelChatMetadata } from '../../../common/languageModels.js'; +import { CountTokensCallback, IBeginToolCallOptions, ILanguageModelToolsService, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolInvokedEvent, IToolResult, IToolSet, ToolDataSource, ToolSet } from '../../../common/tools/languageModelToolsService.js'; -export class MockLanguageModelToolsService implements ILanguageModelToolsService { +export class MockLanguageModelToolsService extends Disposable implements ILanguageModelToolsService { _serviceBrand: undefined; vscodeToolSet: ToolSet = new ToolSet('vscode', 'vscode', ThemeIcon.fromId(Codicon.code.id), ToolDataSource.Internal); executeToolSet: ToolSet = new ToolSet('execute', 'execute', ThemeIcon.fromId(Codicon.terminal.id), ToolDataSource.Internal); readToolSet: ToolSet = new ToolSet('read', 'read', ThemeIcon.fromId(Codicon.book.id), ToolDataSource.Internal); agentToolSet: ToolSet = new ToolSet('agent', 'agent', ThemeIcon.fromId(Codicon.agent.id), ToolDataSource.Internal); - constructor() { } + private readonly _onDidInvokeTool = this._register(new Emitter()); + + constructor() { + super(); + } readonly onDidChangeTools: Event = Event.None; readonly onDidPrepareToolCallBecomeUnresponsive: Event<{ sessionResource: URI; toolData: IToolData }> = Event.None; + readonly onDidInvokeTool = this._onDidInvokeTool.event; + + fireOnDidInvokeTool(event: IToolInvokedEvent): void { + this._onDidInvokeTool.fire(event); + } registerToolData(toolData: IToolData): IDisposable { return Disposable.None; From 354a53ee3a6906edcd7dc2fcd3eb9aa08ec4c697 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 28 Jan 2026 21:07:54 +0100 Subject: [PATCH 035/152] agent sessions - harden read/unread tracking (#291389) --- .../agentSessions/agentSessionsModel.ts | 58 ++++++++----------- .../agentSessionViewModel.test.ts | 3 +- 2 files changed, 25 insertions(+), 36 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index 4fcf0bbdef0..28f07ee13b4 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -417,7 +417,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode )); this.logger.logAllStatsIfTrace('Loaded cached sessions'); - this.runMarkAllReadMigrationOnce(); // TODO@bpasero remove this in the future + this.readDateBaseline = this.resolveReadDateBaseline(); // we use this to account for bugfixes in the read/unread tracking this.registerListeners(); } @@ -568,6 +568,8 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode //#region States + private static readonly UNREAD_MARKER = -1; + private readonly sessionStates: ResourceMap; private isArchived(session: IInternalAgentSessionData): boolean { @@ -599,7 +601,12 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode return true; // archived sessions are always read } - const readDate = this.sessionStates.get(session.resource)?.read ?? 0; + const storedReadDate = this.sessionStates.get(session.resource)?.read; + if (storedReadDate === AgentSessionsModel.UNREAD_MARKER) { + return false; + } + + const readDate = Math.max(storedReadDate ?? 0, this.readDateBaseline /* Use read date baseline when no read date is stored */); // Install a heuristic to reduce false positives: a user might observe // the output of a session and quickly click on another session before @@ -628,8 +635,8 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode return; // already read with a sufficient timestamp } } else { - newRead = 0; - if (state.read === 0) { + newRead = AgentSessionsModel.UNREAD_MARKER; + if (state.read === AgentSessionsModel.UNREAD_MARKER) { return; // already unread } } @@ -641,42 +648,25 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } } - private static readonly MARK_ALL_READ_MIGRATION_KEY = 'agentSessions.markAllReadMigration'; - private static readonly MARK_ALL_READ_MIGRATION_VERSION = 1; + private static readonly READ_DATE_BASELINE_KEY = 'agentSessions.readDateBaseline'; - private migrationCompleted = false; + private readonly readDateBaseline: number; - private runMarkAllReadMigrationOnce(): void { - if (this.migrationCompleted) { - return; + private resolveReadDateBaseline(): number { + let readDateBaseline = this.storageService.getNumber(AgentSessionsModel.READ_DATE_BASELINE_KEY, StorageScope.WORKSPACE, 0); + if (readDateBaseline > 0) { + return readDateBaseline; // already resolved } - const storedVersion = this.storageService.getNumber(AgentSessionsModel.MARK_ALL_READ_MIGRATION_KEY, StorageScope.WORKSPACE, 0); - if (storedVersion >= AgentSessionsModel.MARK_ALL_READ_MIGRATION_VERSION) { - this.migrationCompleted = true; - return; // migration already completed for this version - } + // For stable, preserve unread state for sessions from the last 7 days + // For other qualities, mark all sessions as read + readDateBaseline = this.productService.quality === 'stable' + ? Date.now() - (7 * 24 * 60 * 60 * 1000) + : Date.now(); - this.logger.logIfTrace(`Running mark-all-read migration from version ${storedVersion} to ${AgentSessionsModel.MARK_ALL_READ_MIGRATION_VERSION}`); + this.storageService.store(AgentSessionsModel.READ_DATE_BASELINE_KEY, readDateBaseline, StorageScope.WORKSPACE, StorageTarget.MACHINE); - const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000); - for (const session of this._sessions.values()) { - if (!this.isRead(session)) { - let markRead = true; - if (this.productService.quality === 'stable' && this.sessionTimeForReadStateTracking(session) >= sevenDaysAgo) { - markRead = false; // for stable, preserve state for up to 1 week ago - } - - if (markRead) { - this.setRead(session, true, true /* skipEvent */); - } - } - } - - // Store the migration version - this.storageService.store(AgentSessionsModel.MARK_ALL_READ_MIGRATION_KEY, AgentSessionsModel.MARK_ALL_READ_MIGRATION_VERSION, StorageScope.WORKSPACE, StorageTarget.MACHINE); - - this.migrationCompleted = true; + return readDateBaseline; } //#endregion diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts index 7c6cd0d2395..fb1a219a05c 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts @@ -1400,9 +1400,8 @@ suite('AgentSessions', () => { instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); - // Set migration key to indicate migration has already happened const storageService = instantiationService.get(IStorageService); - storageService.store('agentSessions.markAllReadMigration', 1, StorageScope.WORKSPACE, StorageTarget.MACHINE); + storageService.store('agentSessions.readDateBaseline', 1, StorageScope.WORKSPACE, StorageTarget.MACHINE); }); teardown(() => { From bddac8596ed18cebd1f4fd365ea2e9ab4e87c9e1 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 28 Jan 2026 21:23:10 +0100 Subject: [PATCH 036/152] inline chat input: clamp widget to viewport when scrolling with content (#291390) When the input has content, the widget now stays visible and clamps to viewport edges (respecting sticky scroll height) instead of hiding when the anchor line scrolls out of view. Fixes https://github.com/microsoft/vscode/issues/291075 --- .../browser/inlineChatOverlayWidget.ts | 32 ++++++++++++++----- .../browser/media/inlineChatOverlayWidget.css | 4 +++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index e256be7a65c..9585c9972d0 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -51,15 +51,15 @@ export class InlineChatInputWidget extends Disposable { private readonly _input: IActiveCodeEditor; private readonly _position = observableValue(this, null); readonly position: IObservable = this._position; - readonly minContentWidthInPx = constObservable(0); + private readonly _showStore = this._store.add(new DisposableStore()); + private readonly _stickyScrollHeight: IObservable; private _inlineStartAction: IAction | undefined; private _anchorLineNumber: number = 0; private _anchorLeft: number = 0; private _anchorAbove: boolean = false; - readonly allowEditorOverflow = true; constructor( private readonly _editorObs: ObservableCodeEditor, @@ -108,6 +108,10 @@ export class InlineChatInputWidget extends Disposable { this._input.setModel(model); this._input.layout({ width: 200, height: 18 }); + // Initialize sticky scroll height observable + const stickyScrollController = StickyScrollController.get(this._editorObs.editor); + this._stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0); + // Update placeholder based on selection state this._store.add(autorun(r => { const selection = this._editorObs.cursorSelection.read(r); @@ -216,8 +220,8 @@ export class InlineChatInputWidget extends Disposable { this._showStore.add(this._editorObs.createOverlayWidget({ domNode: this._domNode, position: this._position, - minContentWidthInPx: this.minContentWidthInPx, - allowEditorOverflow: this.allowEditorOverflow, + minContentWidthInPx: constObservable(0), + allowEditorOverflow: true, })); // If anchoring above, adjust position after render to account for widget height @@ -225,13 +229,14 @@ export class InlineChatInputWidget extends Disposable { this._updatePosition(); } - // Update position on scroll, hide if anchor line is out of view + // Update position on scroll, hide if anchor line is out of view (only when input is empty) this._showStore.add(this._editorObs.editor.onDidScrollChange(() => { const visibleRanges = this._editorObs.editor.getVisibleRanges(); const isLineVisible = visibleRanges.some(range => this._anchorLineNumber >= range.startLineNumber && this._anchorLineNumber <= range.endLineNumber ); - if (!isLineVisible) { + const hasContent = !!this._input.getModel().getValue(); + if (!isLineVisible && !hasContent) { this._hide(); } else { this._updatePosition(); @@ -244,6 +249,7 @@ export class InlineChatInputWidget extends Disposable { private _updatePosition(): void { const editor = this._editorObs.editor; + const lineHeight = editor.getOption(EditorOption.lineHeight); const top = editor.getTopForLineNumber(this._anchorLineNumber) - editor.getScrollTop(); let adjustedTop = top; @@ -251,12 +257,22 @@ export class InlineChatInputWidget extends Disposable { const widgetHeight = this._domNode.offsetHeight; adjustedTop = top - widgetHeight; } else { - const lineHeight = editor.getOption(EditorOption.lineHeight); adjustedTop = top + lineHeight; } + // Clamp to viewport bounds when anchor line is out of view + const stickyScrollHeight = this._stickyScrollHeight.get(); + const layoutInfo = editor.getLayoutInfo(); + const widgetHeight = this._domNode.offsetHeight; + const minTop = stickyScrollHeight; + const maxTop = layoutInfo.height - widgetHeight; + + const clampedTop = Math.max(minTop, Math.min(adjustedTop, maxTop)); + const isClamped = clampedTop !== adjustedTop; + this._domNode.classList.toggle('clamped', isClamped); + this._position.set({ - preference: { top: adjustedTop, left: this._anchorLeft }, + preference: { top: clampedTop, left: this._anchorLeft }, stackOrdinal: 10000, }, undefined); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css index 864a19e8ede..1af3ff339a1 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css @@ -3,6 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +.inline-chat-gutter-menu.clamped { + transition: top 100ms; +} + .inline-chat-gutter-menu .input .monaco-editor-background { background-color: var(--vscode-menu-background); } From b21a7bf5209f91c48a8fe99fae32fe4dbab97907 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 28 Jan 2026 21:29:05 +0100 Subject: [PATCH 037/152] fix: update argument for using custom source in nuget package installation (#291304) * fix: update argument for using custom source in nuget package installation * fix tests --- src/vs/platform/mcp/common/mcpManagementService.ts | 2 +- src/vs/platform/mcp/test/common/mcpManagementService.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/mcp/common/mcpManagementService.ts b/src/vs/platform/mcp/common/mcpManagementService.ts index 6a50e743653..2885f2baa50 100644 --- a/src/vs/platform/mcp/common/mcpManagementService.ts +++ b/src/vs/platform/mcp/common/mcpManagementService.ts @@ -149,7 +149,7 @@ export abstract class AbstractCommonMcpManagementService extends Disposable impl args.push(serverPackage.version ? `${serverPackage.identifier}@${serverPackage.version}` : serverPackage.identifier); args.push('--yes'); // installation is confirmed by the UI, so --yes is appropriate here if (serverPackage.registryBaseUrl) { - args.push('--add-source', serverPackage.registryBaseUrl); + args.push('--source', serverPackage.registryBaseUrl); } if (serverPackage.packageArguments?.length) { args.push('--'); diff --git a/src/vs/platform/mcp/test/common/mcpManagementService.test.ts b/src/vs/platform/mcp/test/common/mcpManagementService.test.ts index e3d63c7d275..78ee3299652 100644 --- a/src/vs/platform/mcp/test/common/mcpManagementService.test.ts +++ b/src/vs/platform/mcp/test/common/mcpManagementService.test.ts @@ -548,7 +548,7 @@ suite('McpManagementService - getMcpServerConfigurationFromManifest', () => { assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [ 'Company.Internal.McpServer@4.5.6', '--yes', - '--add-source', 'https://nuget.company.com/v3/index.json' + '--source', 'https://nuget.company.com/v3/index.json' ]); } }); From b895a24030e75700438e64f92eda3c1b6cd01ba2 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:15:08 -0800 Subject: [PATCH 038/152] A bit more polish on mermaid chat items - Remove context menu for open in editor since there's a button now - Faster pinch zoom - Only pan when holding alt/option - Fix transparent button on hover --- .../chat-webview-src/mermaidWebview.ts | 24 ++++++++++++------- extensions/mermaid-chat-features/package.json | 4 ---- .../src/chatOutputRenderer.ts | 1 + 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/extensions/mermaid-chat-features/chat-webview-src/mermaidWebview.ts b/extensions/mermaid-chat-features/chat-webview-src/mermaidWebview.ts index a2fb1081995..fb11ad44e5c 100644 --- a/extensions/mermaid-chat-features/chat-webview-src/mermaidWebview.ts +++ b/extensions/mermaid-chat-features/chat-webview-src/mermaidWebview.ts @@ -35,7 +35,7 @@ export class PanZoomHandler { this.content = content; this.content.style.transformOrigin = '0 0'; this.container.style.overflow = 'hidden'; - this.container.style.cursor = 'grab'; + this.container.style.cursor = 'default'; this.setupEventListeners(); } @@ -77,11 +77,11 @@ export class PanZoomHandler { if ((e.key === 'Alt' || e.key === 'Shift') && !this.isPanning) { e.preventDefault(); if (e.altKey && !e.shiftKey) { - this.container.style.cursor = 'zoom-in'; + this.container.style.cursor = 'grab'; } else if (e.altKey && e.shiftKey) { this.container.style.cursor = 'zoom-out'; } else { - this.container.style.cursor = 'grab'; + this.container.style.cursor = 'default'; } } } @@ -91,11 +91,11 @@ export class PanZoomHandler { return; } if (e.altKey && !e.shiftKey) { - this.container.style.cursor = 'zoom-in'; + this.container.style.cursor = 'grab'; } else if (e.altKey && e.shiftKey) { this.container.style.cursor = 'zoom-out'; } else { - this.container.style.cursor = 'grab'; + this.container.style.cursor = 'default'; } } @@ -118,9 +118,15 @@ export class PanZoomHandler { } private handleWheel(e: WheelEvent): void { + // Only zoom when Alt is held (or ctrlKey for pinch-to-zoom gestures) // ctrlKey is set by browsers for pinch-to-zoom gestures const isPinchZoom = e.ctrlKey; + if (!e.altKey && !isPinchZoom) { + // Allow normal scrolling when Alt is not held + return; + } + if (isPinchZoom || e.altKey) { // Pinch gesture or Alt + two-finger drag = zoom e.preventDefault(); @@ -131,7 +137,9 @@ export class PanZoomHandler { const mouseY = e.clientY - rect.top; // Calculate zoom (scroll up = zoom in, scroll down = zoom out) - const delta = -e.deltaY * this.zoomFactor; + // Pinch gestures have smaller deltaY values, so use a higher factor + const effectiveZoomFactor = isPinchZoom ? this.zoomFactor * 5 : this.zoomFactor; + const delta = -e.deltaY * effectiveZoomFactor; const newScale = Math.min(this.maxScale, Math.max(this.minScale, this.scale * (1 + delta))); // Zoom toward mouse position @@ -146,7 +154,7 @@ export class PanZoomHandler { } private handleMouseDown(e: MouseEvent): void { - if (e.button !== 0) { + if (e.button !== 0 || !e.altKey) { return; } e.preventDefault(); @@ -182,7 +190,7 @@ export class PanZoomHandler { private handleMouseUp(): void { if (this.isPanning) { this.isPanning = false; - this.container.style.cursor = 'grab'; + this.container.style.cursor = 'default'; this.saveState(); } } diff --git a/extensions/mermaid-chat-features/package.json b/extensions/mermaid-chat-features/package.json index 6a447e16d57..f09aa5e82cd 100644 --- a/extensions/mermaid-chat-features/package.json +++ b/extensions/mermaid-chat-features/package.json @@ -62,10 +62,6 @@ "command": "_mermaid-chat.resetPanZoom", "when": "webviewId == 'vscode.chat-mermaid-features.chatOutputItem'" }, - { - "command": "_mermaid-chat.openInEditor", - "when": "webviewId == 'vscode.chat-mermaid-features.chatOutputItem'" - }, { "command": "_mermaid-chat.copySource", "when": "webviewId == 'vscode.chat-mermaid-features.chatOutputItem' || webviewId == 'vscode.chat-mermaid-features.preview'" diff --git a/extensions/mermaid-chat-features/src/chatOutputRenderer.ts b/extensions/mermaid-chat-features/src/chatOutputRenderer.ts index 810fa2011b1..4bf4d9de7e7 100644 --- a/extensions/mermaid-chat-features/src/chatOutputRenderer.ts +++ b/extensions/mermaid-chat-features/src/chatOutputRenderer.ts @@ -105,6 +105,7 @@ class MermaidChatOutputRenderer implements vscode.ChatOutputRenderer { opacity: 1; } .open-in-editor-btn:hover { + opacity: 1; background: var(--vscode-toolbar-hoverBackground); } From 96716220290fa0ec76bbe19e31792cd87cb39dc2 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:43:09 -0800 Subject: [PATCH 039/152] Enable chat mermaid tool by default Feeling more confident in its current state. Let's preview this feature for stable users too --- extensions/mermaid-chat-features/package.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/extensions/mermaid-chat-features/package.json b/extensions/mermaid-chat-features/package.json index f09aa5e82cd..690bb9ce086 100644 --- a/extensions/mermaid-chat-features/package.json +++ b/extensions/mermaid-chat-features/package.json @@ -73,12 +73,9 @@ "properties": { "mermaid-chat.enabled": { "type": "boolean", - "default": false, + "default": true, "description": "%config.enabled.description%", - "scope": "application", - "tags": [ - "experimental" - ] + "scope": "application" } } }, From a8773d175f5311aa30f7cbff393f806e4be0d0a5 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 28 Jan 2026 22:54:26 +0100 Subject: [PATCH 040/152] #290627 support extension management CLI to support forced versioning by quality (#291404) * #290627 support extension management CLI to support forced versioning by quality * feedback --- package.json | 4 ++-- src/vs/base/common/product.ts | 1 + src/vs/code/node/cliProcessMain.ts | 10 +++++----- .../common/extensionManagementCLI.ts | 10 +++++++++- src/vs/server/node/remoteExtensionHostAgentCli.ts | 3 ++- src/vs/server/node/serverServices.ts | 2 +- src/vs/workbench/api/browser/mainThreadCLICommands.ts | 4 +++- 7 files changed, 23 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 3bf229549b4..3ddd35087b5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.109.0", - "distro": "1468752e41ea530021336a556d31c13ff82e02b9", + "distro": "84c8b9580d546487fee8ff25a29c5f3f49d33799", "author": { "name": "Microsoft Corporation" }, @@ -240,4 +240,4 @@ "optionalDependencies": { "windows-foreground-love": "0.6.1" } -} +} \ No newline at end of file diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 6db227f4d8e..11aba6dd528 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -205,6 +205,7 @@ export interface IProductConfiguration { readonly hasPrereleaseVersion?: boolean; readonly excludeVersionRange?: string; }>; + readonly extensionsForceVersionByQuality?: readonly string[]; readonly msftInternalDomains?: string[]; readonly linkProtectionTrustedDomains?: readonly string[]; diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index e0a2115ade8..41d94cc492f 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -314,27 +314,27 @@ class CliMain extends Disposable { // List Extensions if (this.argv['list-extensions']) { - return instantiationService.createInstance(ExtensionManagementCLI, new ConsoleLogger(LogLevel.Info, false)).listExtensions(!!this.argv['show-versions'], this.argv['category'], profileLocation); + return instantiationService.createInstance(ExtensionManagementCLI, [], new ConsoleLogger(LogLevel.Info, false)).listExtensions(!!this.argv['show-versions'], this.argv['category'], profileLocation); } // Install Extension else if (this.argv['install-extension'] || this.argv['install-builtin-extension']) { const installOptions: InstallOptions = { isMachineScoped: !!this.argv['do-not-sync'], installPreReleaseVersion: !!this.argv['pre-release'], donotIncludePackAndDependencies: !!this.argv['do-not-include-pack-dependencies'], profileLocation }; - return instantiationService.createInstance(ExtensionManagementCLI, new ConsoleLogger(LogLevel.Info, false)).installExtensions(this.asExtensionIdOrVSIX(this.argv['install-extension'] || []), this.asExtensionIdOrVSIX(this.argv['install-builtin-extension'] || []), installOptions, !!this.argv['force']); + return instantiationService.createInstance(ExtensionManagementCLI, [], new ConsoleLogger(LogLevel.Info, false)).installExtensions(this.asExtensionIdOrVSIX(this.argv['install-extension'] || []), this.asExtensionIdOrVSIX(this.argv['install-builtin-extension'] || []), installOptions, !!this.argv['force']); } // Uninstall Extension else if (this.argv['uninstall-extension']) { - return instantiationService.createInstance(ExtensionManagementCLI, new ConsoleLogger(LogLevel.Info, false)).uninstallExtensions(this.asExtensionIdOrVSIX(this.argv['uninstall-extension']), !!this.argv['force'], profileLocation); + return instantiationService.createInstance(ExtensionManagementCLI, [], new ConsoleLogger(LogLevel.Info, false)).uninstallExtensions(this.asExtensionIdOrVSIX(this.argv['uninstall-extension']), !!this.argv['force'], profileLocation); } else if (this.argv['update-extensions']) { - return instantiationService.createInstance(ExtensionManagementCLI, new ConsoleLogger(LogLevel.Info, false)).updateExtensions(profileLocation); + return instantiationService.createInstance(ExtensionManagementCLI, [], new ConsoleLogger(LogLevel.Info, false)).updateExtensions(profileLocation); } // Locate Extension else if (this.argv['locate-extension']) { - return instantiationService.createInstance(ExtensionManagementCLI, new ConsoleLogger(LogLevel.Info, false)).locateExtension(this.argv['locate-extension']); + return instantiationService.createInstance(ExtensionManagementCLI, [], new ConsoleLogger(LogLevel.Info, false)).locateExtension(this.argv['locate-extension']); } // Install MCP server diff --git a/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts b/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts index a40e4319815..3bc65f49b03 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts @@ -14,6 +14,7 @@ import { EXTENSION_IDENTIFIER_REGEX, IExtensionGalleryService, IExtensionInfo, I import { areSameExtensions, getExtensionId, getGalleryExtensionId, getIdAndVersion } from './extensionManagementUtil.js'; import { ExtensionType, EXTENSION_CATEGORIES, IExtensionManifest } from '../../extensions/common/extensions.js'; import { ILogger } from '../../log/common/log.js'; +import { IProductService } from '../../product/common/productService.js'; const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id); @@ -25,10 +26,14 @@ type InstallGalleryExtensionInfo = { id: string; version?: string; installOption export class ExtensionManagementCLI { constructor( + private readonly extensionsForceVersionByQuality: readonly string[], protected readonly logger: ILogger, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, - ) { } + @IProductService private readonly productService: IProductService, + ) { + this.extensionsForceVersionByQuality = this.extensionsForceVersionByQuality.map(e => e.toLowerCase()); + } protected get location(): string | undefined { return undefined; @@ -81,6 +86,9 @@ export class ExtensionManagementCLI { const installVSIXInfos: InstallVSIXInfo[] = []; const installExtensionInfos: InstallGalleryExtensionInfo[] = []; const addInstallExtensionInfo = (id: string, version: string | undefined, isBuiltin: boolean) => { + if (this.extensionsForceVersionByQuality?.some(e => e === id.toLowerCase())) { + version = this.productService.quality !== 'stable' ? 'prerelease' : undefined; + } installExtensionInfos.push({ id, version: version !== 'prerelease' ? version : undefined, installOptions: { ...installOptions, isBuiltin, installPreReleaseVersion: version === 'prerelease' || installOptions.installPreReleaseVersion } }); }; for (const extension of extensions) { diff --git a/src/vs/server/node/remoteExtensionHostAgentCli.ts b/src/vs/server/node/remoteExtensionHostAgentCli.ts index abc8a460e92..46238862457 100644 --- a/src/vs/server/node/remoteExtensionHostAgentCli.ts +++ b/src/vs/server/node/remoteExtensionHostAgentCli.ts @@ -71,6 +71,7 @@ class CliMain extends Disposable { await instantiationService.invokeFunction(async accessor => { const configurationService = accessor.get(IConfigurationService); const logService = accessor.get(ILogService); + const productService = accessor.get(IProductService); // On Windows, configure the UNC allow list based on settings if (isWindows) { @@ -82,7 +83,7 @@ class CliMain extends Disposable { } try { - await this.doRun(instantiationService.createInstance(ExtensionManagementCLI, new ConsoleLogger(logService.getLevel(), false))); + await this.doRun(instantiationService.createInstance(ExtensionManagementCLI, productService.extensionsForceVersionByQuality ?? [], new ConsoleLogger(logService.getLevel(), false))); } catch (error) { logService.error(error); console.error(getErrorMessage(error)); diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index c0100c3df82..34b41d8d00b 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -243,7 +243,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken socketServer.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new RemoteTerminalChannel(environmentService, logService, ptyHostService, productService, extensionManagementService, configurationService)); - const remoteExtensionsScanner = new RemoteExtensionsScannerService(instantiationService.createInstance(ExtensionManagementCLI, logService), environmentService, userDataProfilesService, extensionsScannerService, logService, extensionGalleryService, languagePackService, extensionManagementService); + const remoteExtensionsScanner = new RemoteExtensionsScannerService(instantiationService.createInstance(ExtensionManagementCLI, productService.extensionsForceVersionByQuality ?? [], logService), environmentService, userDataProfilesService, extensionsScannerService, logService, extensionGalleryService, languagePackService, extensionManagementService); socketServer.registerChannel(RemoteExtensionsScannerChannelName, new RemoteExtensionsScannerChannel(remoteExtensionsScanner, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority))); socketServer.registerChannel(NativeMcpDiscoveryHelperChannelName, instantiationService.createInstance(NativeMcpDiscoveryHelperChannel, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority))); diff --git a/src/vs/workbench/api/browser/mainThreadCLICommands.ts b/src/vs/workbench/api/browser/mainThreadCLICommands.ts index be73afd9fd4..ccc7a4f6131 100644 --- a/src/vs/workbench/api/browser/mainThreadCLICommands.ts +++ b/src/vs/workbench/api/browser/mainThreadCLICommands.ts @@ -18,6 +18,7 @@ import { ServiceCollection } from '../../../platform/instantiation/common/servic import { ILabelService } from '../../../platform/label/common/label.js'; import { AbstractMessageLogger, ILogger, LogLevel } from '../../../platform/log/common/log.js'; import { IOpenerService } from '../../../platform/opener/common/opener.js'; +import { IProductService } from '../../../platform/product/common/productService.js'; import { IOpenWindowOptions, IWindowOpenable } from '../../../platform/window/common/window.js'; import { IWorkbenchEnvironmentService } from '../../services/environment/common/environmentService.js'; import { IExtensionManagementServerService } from '../../services/extensionManagement/common/extensionManagement.js'; @@ -106,8 +107,9 @@ class RemoteExtensionManagementCLI extends ExtensionManagementCLI { @ILabelService labelService: ILabelService, @IWorkbenchEnvironmentService envService: IWorkbenchEnvironmentService, @IExtensionManifestPropertiesService private readonly _extensionManifestPropertiesService: IExtensionManifestPropertiesService, + @IProductService productService: IProductService, ) { - super(logger, extensionManagementService, extensionGalleryService); + super([], logger, extensionManagementService, extensionGalleryService, productService); const remoteAuthority = envService.remoteAuthority; this._location = remoteAuthority ? labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority) : undefined; From 9f2bc7c81c5f7e77b3af20ebbb4e186f177704d4 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Wed, 28 Jan 2026 13:58:50 -0800 Subject: [PATCH 041/152] Adding border to chat input in welcome view --- src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index d7c9b5c4c7a..38b7fc6c5ac 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -756,7 +756,7 @@ have to be updated for changes to the rules above, or to support more deeply nes box-sizing: border-box; cursor: text; background-color: var(--vscode-input-background); - border: 1px solid var(--vscode-input-border, transparent); + border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-border, transparent)); border-radius: 4px; padding: 0 6px 6px 6px; /* top padding is inside the editor widget */ From 4d8241eeaa2ae0dd7c7062b846bff655ee1c6f15 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Wed, 28 Jan 2026 13:58:56 -0800 Subject: [PATCH 042/152] Sanity tests updates (#291399) * Sanity tests updates * Revert qemu accidental change. --- build/azure-pipelines/common/sanity-tests.yml | 18 ++++---- .../azure-pipelines/product-sanity-tests.yml | 35 ++++++++------- test/sanity/containers/alpine.dockerfile | 9 ++-- test/sanity/containers/centos.dockerfile | 14 +++--- test/sanity/containers/debian-10.dockerfile | 7 +-- test/sanity/containers/debian-12.dockerfile | 16 ++++--- test/sanity/containers/entrypoint.sh | 2 + test/sanity/containers/fedora.dockerfile | 7 +-- test/sanity/containers/opensuse.dockerfile | 4 -- test/sanity/containers/redhat.dockerfile | 4 -- test/sanity/containers/ubuntu.dockerfile | 27 ++++++++--- test/sanity/scripts/qemu-init.sh | 21 ++++++--- test/sanity/scripts/run-docker.cmd | 12 ++++- test/sanity/scripts/run-docker.sh | 7 ++- test/sanity/scripts/run-macOS.sh | 4 ++ test/sanity/scripts/run-qemu-64k.sh | 20 ++++----- test/sanity/scripts/run-ubuntu.sh | 7 +++ test/sanity/scripts/run-win32.cmd | 4 ++ test/sanity/src/context.ts | 45 ++++++++++++++++++- test/sanity/src/desktop.test.ts | 33 ++++++++++++++ test/sanity/src/wsl.test.ts | 6 +-- 21 files changed, 206 insertions(+), 96 deletions(-) diff --git a/build/azure-pipelines/common/sanity-tests.yml b/build/azure-pipelines/common/sanity-tests.yml index ebf415ba719..305ce4fea29 100644 --- a/build/azure-pipelines/common/sanity-tests.yml +++ b/build/azure-pipelines/common/sanity-tests.yml @@ -89,9 +89,9 @@ jobs: - ${{ if ne(parameters.container, '') }}: - task: Cache@2 inputs: - key: 'docker-v3 | "${{ parameters.container }}" | "${{ parameters.arch }}" | "$(Agent.OS)" | $(TEST_DIR)/containers/${{ parameters.container }}.dockerfile' + key: 'docker-v3 | "${{ parameters.container }}" | "${{ parameters.arch }}" | "${{ parameters.pageSize }}" | "$(Agent.OS)" | $(TEST_DIR)/containers/${{ parameters.container }}.dockerfile' path: $(DOCKER_CACHE_DIR) - restoreKeys: docker-v3 | "${{ parameters.container }}" | "${{ parameters.arch }}" | "$(Agent.OS)" + restoreKeys: docker-v3 | "${{ parameters.container }}" | "${{ parameters.arch }}" | "${{ parameters.pageSize }}" | "$(Agent.OS)" cacheHitVar: DOCKER_CACHE_HIT displayName: Download Docker Image @@ -110,16 +110,16 @@ jobs: --quality "$(BUILD_QUALITY)" \ --commit "$(BUILD_COMMIT)" \ --test-results "/root/results.xml" \ - --verbose + --verbose \ + ${{ parameters.args }} workingDirectory: $(TEST_DIR) displayName: Run Sanity Tests - - ${{ if eq(parameters.pageSize, '') }}: - - bash: | - mkdir -p "$(DOCKER_CACHE_DIR)" - docker save -o "$(DOCKER_CACHE_FILE)" "${{ parameters.container }}" - condition: and(succeeded(), ne(variables.DOCKER_CACHE_HIT, 'true')) - displayName: Save Docker Image + - bash: | + mkdir -p "$(DOCKER_CACHE_DIR)" + docker save -o "$(DOCKER_CACHE_FILE)" "${{ parameters.container }}" + condition: and(succeeded(), ne(variables.DOCKER_CACHE_HIT, 'true')) + displayName: Save Docker Image - task: PublishTestResults@2 inputs: diff --git a/build/azure-pipelines/product-sanity-tests.yml b/build/azure-pipelines/product-sanity-tests.yml index ec53d46f366..3875aaa5333 100644 --- a/build/azure-pipelines/product-sanity-tests.yml +++ b/build/azure-pipelines/product-sanity-tests.yml @@ -94,11 +94,11 @@ extends: os: windows args: --no-detection --grep "win32-arm64" - # Alpine 3.23 + # Alpine 3.22 - template: build/azure-pipelines/common/sanity-tests.yml@self parameters: name: alpine_amd64 - displayName: Alpine 3.23 amd64 + displayName: Alpine 3.22 amd64 poolName: 1es-ubuntu-22.04-x64 container: alpine arch: amd64 @@ -106,8 +106,8 @@ extends: - template: build/azure-pipelines/common/sanity-tests.yml@self parameters: name: alpine_arm64 - displayName: Alpine 3.23 arm64 - poolName: 1es-mariner-2.0-arm64 + displayName: Alpine 3.22 arm64 + poolName: 1es-azure-linux-3-arm64 container: alpine arch: arm64 @@ -124,7 +124,7 @@ extends: parameters: name: centos_stream9_arm64 displayName: CentOS Stream 9 arm64 - poolName: 1es-mariner-2.0-arm64 + poolName: 1es-azure-linux-3-arm64 container: centos arch: arm64 @@ -141,7 +141,7 @@ extends: parameters: name: debian_10_arm32 displayName: Debian 10 arm32 - poolName: 1es-mariner-2.0-arm64 + poolName: 1es-azure-linux-3-arm64 container: debian-10 arch: arm @@ -149,7 +149,7 @@ extends: parameters: name: debian_10_arm64 displayName: Debian 10 arm64 - poolName: 1es-mariner-2.0-arm64 + poolName: 1es-azure-linux-3-arm64 container: debian-10 arch: arm64 @@ -166,7 +166,7 @@ extends: parameters: name: debian_12_arm32 displayName: Debian 12 arm32 - poolName: 1es-mariner-2.0-arm64 + poolName: 1es-azure-linux-3-arm64 container: debian-12 arch: arm @@ -174,7 +174,7 @@ extends: parameters: name: debian_12_arm64 displayName: Debian 12 arm64 - poolName: 1es-mariner-2.0-arm64 + poolName: 1es-azure-linux-3-arm64 container: debian-12 arch: arm64 @@ -192,7 +192,7 @@ extends: parameters: name: fedora_36_arm64 displayName: Fedora 36 arm64 - poolName: 1es-mariner-2.0-arm64 + poolName: 1es-azure-linux-3-arm64 container: fedora baseImage: fedora:36 arch: arm64 @@ -211,7 +211,7 @@ extends: parameters: name: fedora_40_arm64 displayName: Fedora 40 arm64 - poolName: 1es-mariner-2.0-arm64 + poolName: 1es-azure-linux-3-arm64 container: fedora baseImage: fedora:40 arch: arm64 @@ -229,7 +229,7 @@ extends: parameters: name: opensuse_leap_arm64 displayName: openSUSE Leap 16.0 arm64 - poolName: 1es-mariner-2.0-arm64 + poolName: 1es-azure-linux-3-arm64 container: opensuse arch: arm64 @@ -246,7 +246,7 @@ extends: parameters: name: redhat_ubi9_arm64 displayName: Red Hat UBI 9 arm64 - poolName: 1es-mariner-2.0-arm64 + poolName: 1es-azure-linux-3-arm64 container: redhat arch: arm64 @@ -272,7 +272,7 @@ extends: parameters: name: ubuntu_22_04_arm32 displayName: Ubuntu 22.04 arm32 - poolName: 1es-mariner-2.0-arm64 + poolName: 1es-azure-linux-3-arm64 container: ubuntu baseImage: ubuntu:22.04 arch: arm @@ -281,7 +281,7 @@ extends: parameters: name: ubuntu_22_04_arm64 displayName: Ubuntu 22.04 arm64 - poolName: 1es-mariner-2.0-arm64 + poolName: 1es-azure-linux-3-arm64 container: ubuntu baseImage: ubuntu:22.04 arch: arm64 @@ -300,7 +300,7 @@ extends: parameters: name: ubuntu_24_04_arm32 displayName: Ubuntu 24.04 arm32 - poolName: 1es-mariner-2.0-arm64 + poolName: 1es-azure-linux-3-arm64 container: ubuntu baseImage: ubuntu:24.04 arch: arm @@ -309,7 +309,7 @@ extends: parameters: name: ubuntu_24_04_arm64 displayName: Ubuntu 24.04 arm64 - poolName: 1es-mariner-2.0-arm64 + poolName: 1es-azure-linux-3-arm64 container: ubuntu baseImage: ubuntu:24.04 arch: arm64 @@ -323,3 +323,4 @@ extends: baseImage: ubuntu:24.04 arch: arm64 pageSize: 64k + args: --grep "desktop-linux-arm64" diff --git a/test/sanity/containers/alpine.dockerfile b/test/sanity/containers/alpine.dockerfile index a464b5b2fc7..61ac9439a18 100644 --- a/test/sanity/containers/alpine.dockerfile +++ b/test/sanity/containers/alpine.dockerfile @@ -1,10 +1,9 @@ -ARG BASE_IMAGE=node:22.21.1-alpine3.23 +ARG BASE_IMAGE=mcr.microsoft.com/devcontainers/base:alpine-3.22 FROM ${BASE_IMAGE} +# Node.js 22 +RUN apk add --no-cache nodejs + # Chromium RUN apk add --no-cache chromium ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser - -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh -ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/test/sanity/containers/centos.dockerfile b/test/sanity/containers/centos.dockerfile index 4f19112f299..ddff56dd8e1 100644 --- a/test/sanity/containers/centos.dockerfile +++ b/test/sanity/containers/centos.dockerfile @@ -3,26 +3,22 @@ FROM ${BASE_IMAGE} # Node.js 22 RUN dnf module enable -y nodejs:22 && \ - dnf install -y nodejs + dnf install -y nodejs # Chromium -RUN dnf install -y epel-release && \ - dnf install -y chromium +RUN dnf install -y epel-release && \ + dnf install -y chromium ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser # Desktop Bus -RUN dnf install -y dbus-x11 && \ +RUN dnf install -y dbus-x11 && \ mkdir -p /run/dbus # X11 Server -RUN dnf install -y xorg-x11-server-Xvfb +RUN dnf install -y xorg-x11-server-Xvfb # VS Code dependencies RUN dnf install -y \ ca-certificates \ xdg-utils - -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh -ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/test/sanity/containers/debian-10.dockerfile b/test/sanity/containers/debian-10.dockerfile index fb91c01da36..61d8e713eb0 100644 --- a/test/sanity/containers/debian-10.dockerfile +++ b/test/sanity/containers/debian-10.dockerfile @@ -1,6 +1,7 @@ +ARG MIRROR ARG BASE_IMAGE=debian:10 ARG TARGETARCH -FROM ${BASE_IMAGE} +FROM ${MIRROR}${BASE_IMAGE} # Update to archive repos since Debian 10 is EOL RUN sed -i 's|http://deb.debian.org|http://archive.debian.org|g' /etc/apt/sources.list && \ @@ -38,7 +39,3 @@ RUN apt-get install -y xvfb # Install newer libxkbfile1 from Debian 11 since Debian 10 version is too old RUN apt-get install -y -t bullseye libxkbfile1 - -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh -ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/test/sanity/containers/debian-12.dockerfile b/test/sanity/containers/debian-12.dockerfile index 21ba756ff6e..3163d9d8d92 100644 --- a/test/sanity/containers/debian-12.dockerfile +++ b/test/sanity/containers/debian-12.dockerfile @@ -1,8 +1,14 @@ -ARG BASE_IMAGE=node:22-bookworm -FROM ${BASE_IMAGE} +ARG MIRROR +ARG BASE_IMAGE=debian:bookworm +FROM ${MIRROR}${BASE_IMAGE} # Utilities -RUN apt-get update +RUN apt-get update && \ + apt-get install -y curl + +# Node.js 22 +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get install -y nodejs # Chromium RUN apt-get install -y chromium @@ -14,7 +20,3 @@ RUN apt-get install -y dbus-x11 && \ # X11 Server RUN apt-get install -y xvfb - -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh -ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/test/sanity/containers/entrypoint.sh b/test/sanity/containers/entrypoint.sh index 19aea71c25b..bdf72651107 100644 --- a/test/sanity/containers/entrypoint.sh +++ b/test/sanity/containers/entrypoint.sh @@ -2,6 +2,8 @@ set -e echo "System: $(uname -s) $(uname -r) $(uname -m), page size: $(getconf PAGESIZE) bytes" +echo "Memory: $(awk '/MemTotal/ {t=$2} /MemAvailable/ {a=$2} END {printf "%.0f MB total, %.0f MB available", t/1024, a/1024}' /proc/meminfo)" +echo "Disk: $(df -h / | awk 'NR==2 {print $2 " total, " $3 " used, " $4 " available"}')" if command -v Xvfb > /dev/null 2>&1; then echo "Starting X11 Server" diff --git a/test/sanity/containers/fedora.dockerfile b/test/sanity/containers/fedora.dockerfile index 89b8feb02d3..9b97ec60578 100644 --- a/test/sanity/containers/fedora.dockerfile +++ b/test/sanity/containers/fedora.dockerfile @@ -1,5 +1,6 @@ +ARG MIRROR ARG BASE_IMAGE=fedora:36 -FROM ${BASE_IMAGE} +FROM ${MIRROR}${BASE_IMAGE} # Node.js 22 RUN curl -fsSL https://rpm.nodesource.com/setup_22.x | bash - && \ @@ -18,7 +19,3 @@ RUN dnf install -y xorg-x11-server-Xvfb # VS Code dependencies RUN dnf install -y xdg-utils - -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh -ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/test/sanity/containers/opensuse.dockerfile b/test/sanity/containers/opensuse.dockerfile index 3e778747c2d..4f53a6e9cfa 100644 --- a/test/sanity/containers/opensuse.dockerfile +++ b/test/sanity/containers/opensuse.dockerfile @@ -19,7 +19,3 @@ RUN zypper install -y dbus-1-x11 && \ RUN zypper install -y \ liberation-fonts \ libgtk-3-0 - -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh -ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/test/sanity/containers/redhat.dockerfile b/test/sanity/containers/redhat.dockerfile index 6beb294d400..03fca173549 100644 --- a/test/sanity/containers/redhat.dockerfile +++ b/test/sanity/containers/redhat.dockerfile @@ -4,7 +4,3 @@ FROM ${BASE_IMAGE} # Node.js 22 RUN curl -fsSL https://rpm.nodesource.com/setup_22.x | bash - && \ dnf install -y nodejs-22.21.1 - -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh -ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/test/sanity/containers/ubuntu.dockerfile b/test/sanity/containers/ubuntu.dockerfile index d403546d5ad..028f916ff22 100644 --- a/test/sanity/containers/ubuntu.dockerfile +++ b/test/sanity/containers/ubuntu.dockerfile @@ -1,13 +1,30 @@ +ARG MIRROR ARG BASE_IMAGE=ubuntu:22.04 -FROM ${BASE_IMAGE} +FROM ${MIRROR}${BASE_IMAGE} + +# Use Azure package mirrors +ARG TARGETARCH +RUN if [ "$TARGETARCH" = "amd64" ]; then \ + if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then \ + sed -i 's|http://archive.ubuntu.com|http://azure.archive.ubuntu.com|g' /etc/apt/sources.list.d/ubuntu.sources; \ + else \ + sed -i 's|http://archive.ubuntu.com|http://azure.archive.ubuntu.com|g' /etc/apt/sources.list; \ + fi; \ + else \ + if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then \ + sed -i 's|http://ports.ubuntu.com|http://azure.ports.ubuntu.com|g' /etc/apt/sources.list.d/ubuntu.sources; \ + else \ + sed -i 's|http://ports.ubuntu.com|http://azure.ports.ubuntu.com|g' /etc/apt/sources.list; \ + fi; \ + fi # Utilities RUN apt-get update && \ -apt-get install -y curl iproute2 + apt-get install -y curl iproute2 # Node.js 22 RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ -apt-get install -y nodejs + apt-get install -y nodejs # No UI on arm32 on Ubuntu 24.04 ARG BASE_IMAGE @@ -28,7 +45,3 @@ RUN apt-get install -y libasound2 || apt-get install -y libasound2t64 && \ libgbm1 \ libnss3 \ xdg-utils - -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh -ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/test/sanity/scripts/qemu-init.sh b/test/sanity/scripts/qemu-init.sh index 288d88d081a..c4c95755d42 100755 --- a/test/sanity/scripts/qemu-init.sh +++ b/test/sanity/scripts/qemu-init.sh @@ -1,19 +1,30 @@ #!/bin/sh +set -e -echo "Mounting essential filesystems" +# Mount kernel filesystems (proc for process info, sysfs for device info) +echo "Mounting kernel filesystems" mount -t proc proc /proc mount -t sysfs sys /sys + +# Mount pseudo-terminal and shared memory filesystems +echo "Mounting PTY and shared memory" mkdir -p /dev/pts mount -t devpts devpts /dev/pts mkdir -p /dev/shm mount -t tmpfs tmpfs /dev/shm + +# Mount temporary directories with proper permissions +echo "Mounting temporary directories" mount -t tmpfs tmpfs /tmp chmod 1777 /tmp +mount -t tmpfs tmpfs /var/tmp + +# Mount runtime directory for services (D-Bus, XDG) +echo "Mounting runtime directories" mount -t tmpfs tmpfs /run mkdir -p /run/dbus mkdir -p /run/user/0 chmod 700 /run/user/0 -mount -t tmpfs tmpfs /var/tmp echo "Setting up machine-id for D-Bus" cat /proc/sys/kernel/random/uuid | tr -d '-' > /etc/machine-id @@ -32,10 +43,8 @@ export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin export XDG_RUNTIME_DIR=/run/user/0 echo "Starting entrypoint" -ARGS=$(cat /test-args) -/entrypoint.sh $ARGS -EXIT_CODE=$? -echo $EXIT_CODE > /exit-code +sh /root/containers/entrypoint.sh $(cat /test-args) +echo $? > /exit-code sync echo "Powering off" diff --git a/test/sanity/scripts/run-docker.cmd b/test/sanity/scripts/run-docker.cmd index c45919e6c5e..fd1ab024eb8 100644 --- a/test/sanity/scripts/run-docker.cmd +++ b/test/sanity/scripts/run-docker.cmd @@ -4,6 +4,7 @@ setlocal enabledelayedexpansion set ROOT=%~dp0.. set CONTAINER= set ARCH=amd64 +set MIRROR=mcr.microsoft.com/mirror/docker/library/ set BASE_IMAGE= set ARGS= @@ -34,12 +35,20 @@ if "%CONTAINER%"=="" ( exit /b 1 ) +set HOST_ARCH=amd64 +if "%PROCESSOR_ARCHITECTURE%"=="ARM64" set HOST_ARCH=arm64 +if not "%ARCH%"=="%HOST_ARCH%" ( + echo Setting up QEMU emulation for %ARCH% on %HOST_ARCH% host + docker run --privileged --rm tonistiigi/binfmt --install all >nul 2>&1 +) + set BASE_IMAGE_ARG= if not "%BASE_IMAGE%"=="" set BASE_IMAGE_ARG=--build-arg "BASE_IMAGE=%BASE_IMAGE%" echo Building container image: %CONTAINER% docker buildx build ^ --platform "linux/%ARCH%" ^ + --build-arg "MIRROR=%MIRROR%" ^ %BASE_IMAGE_ARG% ^ --tag "%CONTAINER%" ^ --file "%ROOT%\containers\%CONTAINER%.dockerfile" ^ @@ -50,5 +59,6 @@ docker run ^ --rm ^ --platform "linux/%ARCH%" ^ --volume "%ROOT%:/root" ^ + --entrypoint sh ^ "%CONTAINER%" ^ - %ARGS% + /root/containers/entrypoint.sh %ARGS% diff --git a/test/sanity/scripts/run-docker.sh b/test/sanity/scripts/run-docker.sh index 5c2a7830e11..0007f9b98f0 100755 --- a/test/sanity/scripts/run-docker.sh +++ b/test/sanity/scripts/run-docker.sh @@ -3,6 +3,7 @@ set -e CONTAINER="" ARCH="amd64" +MIRROR="mcr.microsoft.com/mirror/docker/library/" BASE_IMAGE="" PAGE_SIZE="" ARGS="" @@ -27,7 +28,7 @@ ROOT_DIR=$(cd "$SCRIPT_DIR/.." && pwd) # Only build if image doesn't exist (i.e., not loaded from cache) if ! docker image inspect "$CONTAINER" > /dev/null 2>&1; then - if [ "$ARCH" != "amd64" ]; then + if [ "$PAGE_SIZE" != "" ]; then echo "Setting up QEMU user-mode emulation for $ARCH" docker run --privileged --rm tonistiigi/binfmt --install "$ARCH" fi @@ -35,6 +36,7 @@ if ! docker image inspect "$CONTAINER" > /dev/null 2>&1; then echo "Building container image: $CONTAINER" docker buildx build \ --platform "linux/$ARCH" \ + --build-arg "MIRROR=$MIRROR" \ ${BASE_IMAGE:+--build-arg "BASE_IMAGE=$BASE_IMAGE"} \ --tag "$CONTAINER" \ --file "$ROOT_DIR/containers/$CONTAINER.dockerfile" \ @@ -54,6 +56,7 @@ else --rm \ --platform "linux/$ARCH" \ --volume "$ROOT_DIR:/root" \ + --entrypoint sh \ "$CONTAINER" \ - $ARGS + /root/containers/entrypoint.sh $ARGS fi diff --git a/test/sanity/scripts/run-macOS.sh b/test/sanity/scripts/run-macOS.sh index 7445fe2e2c2..7ac2197ad25 100755 --- a/test/sanity/scripts/run-macOS.sh +++ b/test/sanity/scripts/run-macOS.sh @@ -1,6 +1,10 @@ #!/bin/sh set -e +echo "System: $(uname -s) $(uname -r) $(uname -m)" +echo "Memory: $(( $(sysctl -n hw.memsize) / 1024 / 1024 / 1024 )) GB" +echo "Disk: $(df -h / | awk 'NR==2 {print $2 " total, " $3 " used, " $4 " available"}')" + echo "Installing Playwright WebKit browser" npx playwright install --with-deps webkit diff --git a/test/sanity/scripts/run-qemu-64k.sh b/test/sanity/scripts/run-qemu-64k.sh index 2b602adc2a9..55198489922 100755 --- a/test/sanity/scripts/run-qemu-64k.sh +++ b/test/sanity/scripts/run-qemu-64k.sh @@ -26,8 +26,8 @@ ROOTFS_DIR=$(mktemp -d) docker export "$CONTAINER_ID" | sudo tar -xf - -C "$ROOTFS_DIR" docker rm -f "$CONTAINER_ID" -echo "Removing container image to free disk space" -docker rmi "$CONTAINER" || true +# echo "Removing container image to free disk space" +# docker rmi "$CONTAINER" || true docker system prune -f || true echo "Copying test files into root filesystem" @@ -35,12 +35,13 @@ TEST_DIR=$(cd "$(dirname "$0")/.." && pwd) sudo cp -r "$TEST_DIR"/* "$ROOTFS_DIR/root/" echo "Downloading Ubuntu 24.04 generic-64k kernel for ARM64" -KERNEL_URL="http://ports.ubuntu.com/ubuntu-ports/pool/main/l/linux/linux-image-unsigned-6.8.0-90-generic-64k_6.8.0-90.91_arm64.deb" +KERNEL_URL="https://ports.ubuntu.com/ubuntu-ports/pool/main/l/linux/linux-image-unsigned-6.8.0-90-generic-64k_6.8.0-90.91_arm64.deb" KERNEL_DIR=$(mktemp -d) curl -fL "$KERNEL_URL" -o "$KERNEL_DIR/kernel.deb" echo "Extracting kernel" -(cd "$KERNEL_DIR" && ar x kernel.deb && tar xf data.tar*) +cd "$KERNEL_DIR" && ar x kernel.deb && rm kernel.deb +tar xf data.tar* && rm -f debian-binary control.tar* data.tar* VMLINUZ="$KERNEL_DIR/boot/vmlinuz-6.8.0-90-generic-64k" if [ ! -f "$VMLINUZ" ]; then echo "Error: Could not find kernel at $VMLINUZ" @@ -72,15 +73,14 @@ timeout 1800 qemu-system-aarch64 \ -netdev user,id=net0 \ -device virtio-net-pci,netdev=net0 \ -nographic \ - -no-reboot \ - || true + -no-reboot echo "Extracting test results from disk image" MOUNT_DIR=$(mktemp -d) sudo mount -o loop "$DISK_IMG" "$MOUNT_DIR" -if [ -f "$MOUNT_DIR/root/results.xml" ]; then - cp "$MOUNT_DIR/root/results.xml" "$TEST_DIR/results.xml" -fi -EXIT_CODE=$(cat "$MOUNT_DIR/exit-code" 2>/dev/null || echo 1) +sudo cp "$MOUNT_DIR/root/results.xml" "$TEST_DIR/results.xml" +sudo chown "$(id -u):$(id -g)" "$TEST_DIR/results.xml" + +EXIT_CODE=$(sudo cat "$MOUNT_DIR/exit-code" 2>/dev/null || echo 1) sudo umount "$MOUNT_DIR" exit $EXIT_CODE diff --git a/test/sanity/scripts/run-ubuntu.sh b/test/sanity/scripts/run-ubuntu.sh index 4fcbc48990c..884f292c38c 100755 --- a/test/sanity/scripts/run-ubuntu.sh +++ b/test/sanity/scripts/run-ubuntu.sh @@ -1,6 +1,13 @@ #!/bin/sh set -e +echo "System: $(uname -s) $(uname -r) $(uname -m)" +echo "Memory: $(free -h | awk '/^Mem:/ {print $2 " total, " $3 " used, " $7 " available"}')" +echo "Disk: $(df -h / | awk 'NR==2 {print $2 " total, " $3 " used, " $4 " available"}')" + +echo "Configuring Azure mirror" +sudo sed -i 's|http://archive.ubuntu.com|http://azure.archive.ubuntu.com|g' /etc/apt/sources.list + echo "Installing dependencies" sudo apt-get update sudo apt-get install -y dbus-x11 x11-utils xvfb diff --git a/test/sanity/scripts/run-win32.cmd b/test/sanity/scripts/run-win32.cmd index 52b0a27134a..e1dfacd1370 100644 --- a/test/sanity/scripts/run-win32.cmd +++ b/test/sanity/scripts/run-win32.cmd @@ -1,6 +1,10 @@ @echo off setlocal +echo System: %OS% %PROCESSOR_ARCHITECTURE% +powershell -NoProfile -Command "$mem = (Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory; Write-Host ('Memory: {0:N0} GB' -f ($mem/1GB))" +powershell -NoProfile -Command "$disk = Get-PSDrive C; Write-Host ('Disk C: {0:N0} GB free of {1:N0} GB' -f ($disk.Free/1GB), (($disk.Used+$disk.Free)/1GB))" + set "UBUNTU_ROOTFS=%TEMP%\ubuntu-rootfs.tar.gz" set "UBUNTU_INSTALL=%LOCALAPPDATA%\WSL\Ubuntu" diff --git a/test/sanity/src/context.ts b/test/sanity/src/context.ts index f5412c73e14..7e32e1bcac9 100644 --- a/test/sanity/src/context.ts +++ b/test/sanity/src/context.ts @@ -169,7 +169,7 @@ export class TestContext { */ public deleteWslDir(dir: string): void { this.log(`Deleting WSL directory: ${dir}`); - this.run('wsl', 'rm', '-rf', dir); + this.runNoErrors('wsl', 'rm', '-rf', dir); } /** @@ -470,6 +470,38 @@ export class TestContext { return dir; } + /** + * Mounts a macOS DMG file and returns the mount point. + * @param dmgPath The path to the DMG file. + * @returns The path to the mounted volume. + */ + public mountDmg(dmgPath: string): string { + this.log(`Mounting DMG ${dmgPath}`); + const result = this.runNoErrors('hdiutil', 'attach', dmgPath, '-nobrowse', '-readonly'); + + // Parse the output to find the mount point (last column of the last line) + const lines = result.stdout.trim().split('\n'); + const lastLine = lines[lines.length - 1]; + const mountPoint = lastLine.split('\t').pop()?.trim(); + + if (!mountPoint || !fs.existsSync(mountPoint)) { + this.error(`Failed to find mount point for DMG ${dmgPath}`); + } + + this.log(`Mounted DMG at ${mountPoint}`); + return mountPoint; + } + + /** + * Unmounts a macOS DMG volume. + * @param mountPoint The path to the mounted volume. + */ + public unmountDmg(mountPoint: string): void { + this.log(`Unmounting DMG ${mountPoint}`); + this.runNoErrors('hdiutil', 'detach', mountPoint); + this.log(`Unmounted DMG ${mountPoint}`); + } + /** * Runs a command synchronously. * @param command The command to run. @@ -914,7 +946,16 @@ export class TestContext { default: { const executablePath = process.env['PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH'] ?? '/usr/bin/chromium-browser'; this.log(`Using Chromium executable at: ${executablePath}`); - return await chromium.launch({ headless, executablePath }); + return await chromium.launch({ + headless, + executablePath, + args: [ + '--disable-gpu', + '--disable-gpu-compositing', + '--disable-software-rasterizer', + '--no-zygote', + ] + }); } } } diff --git a/test/sanity/src/desktop.test.ts b/test/sanity/src/desktop.test.ts index 77305c7c173..905edc89dfd 100644 --- a/test/sanity/src/desktop.test.ts +++ b/test/sanity/src/desktop.test.ts @@ -36,6 +36,39 @@ export function setup(context: TestContext) { } }); + context.test('desktop-darwin-x64-dmg', ['darwin', 'x64', 'desktop'], async () => { + const dir = await context.downloadTarget('darwin-x64-dmg'); + if (!context.options.downloadOnly) { + const mountPoint = context.mountDmg(dir); + context.validateAllCodesignSignatures(mountPoint); + const entryPoint = context.getDesktopEntryPoint(mountPoint); + await testDesktopApp(entryPoint); + context.unmountDmg(mountPoint); + } + }); + + context.test('desktop-darwin-arm64-dmg', ['darwin', 'arm64', 'desktop'], async () => { + const dir = await context.downloadTarget('darwin-arm64-dmg'); + if (!context.options.downloadOnly) { + const mountPoint = context.mountDmg(dir); + context.validateAllCodesignSignatures(mountPoint); + const entryPoint = context.getDesktopEntryPoint(mountPoint); + await testDesktopApp(entryPoint); + context.unmountDmg(mountPoint); + } + }); + + context.test('desktop-darwin-universal-dmg', ['darwin', 'desktop'], async () => { + const dir = await context.downloadTarget('darwin-universal-dmg'); + if (!context.options.downloadOnly) { + const mountPoint = context.mountDmg(dir); + context.validateAllCodesignSignatures(mountPoint); + const entryPoint = context.getDesktopEntryPoint(mountPoint); + await testDesktopApp(entryPoint); + context.unmountDmg(mountPoint); + } + }); + context.test('desktop-linux-arm64', ['linux', 'arm64', 'desktop'], async () => { let dir = await context.downloadAndUnpack('linux-arm64'); if (!context.options.downloadOnly) { diff --git a/test/sanity/src/wsl.test.ts b/test/sanity/src/wsl.test.ts index cc2647a04eb..d5c7212607f 100644 --- a/test/sanity/src/wsl.test.ts +++ b/test/sanity/src/wsl.test.ts @@ -204,7 +204,7 @@ export function setup(context: TestContext) { const app = await _electron.launch({ executablePath: entryPoint, args }); const window = await context.getPage(app.firstWindow()); - context.log('Installing WPS extension'); + context.log('Installing WSL extension'); await window.getByRole('button', { name: 'Install and Reload' }).click(); context.log('Waiting for WSL connection'); @@ -234,13 +234,13 @@ class WslUITest extends UITest { protected override verifyTextFileCreated() { this.context.log('Verifying file contents in WSL'); - const result = this.context.run('wsl', 'cat', `${this.wslWorkspaceDir}/helloWorld.txt`); + const result = this.context.runNoErrors('wsl', 'cat', `${this.wslWorkspaceDir}/helloWorld.txt`); assert.strictEqual(result.stdout.trim(), 'Hello, World!', 'File contents in WSL do not match expected value'); } protected override verifyExtensionInstalled() { this.context.log(`Verifying extension is installed in WSL at ${this.wslExtensionsDir}`); - const result = this.context.run('wsl', 'ls', this.wslExtensionsDir); + const result = this.context.runNoErrors('wsl', 'ls', this.wslExtensionsDir); const hasExtension = result.stdout.split('\n').some(ext => ext.startsWith('github.vscode-pull-request-github')); assert.strictEqual(hasExtension, true, 'GitHub Pull Requests extension is not installed in WSL'); } From f13da104b22a4b9615209cce1ac29be4f1666909 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:30:27 +0000 Subject: [PATCH 043/152] Add chat.agent.maxRequests to commonly used settings list (#291398) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rzhao271 <7199958+rzhao271@users.noreply.github.com> --- src/vs/workbench/contrib/preferences/browser/settingsLayout.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index 3c72f219979..a2e37c2a0c0 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -33,6 +33,7 @@ const defaultCommonlyUsedSettings: string[] = [ 'files.autoSave', 'editor.defaultFormatter', 'editor.fontFamily', + 'chat.agent.maxRequests', 'editor.wordWrap', 'files.exclude', 'workbench.colorTheme', From afd4046bad661d20f22e4875758c3bfa6d07820f Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:35:02 -0800 Subject: [PATCH 044/152] smarter check for confirmation (#291400) * smarter check for confirmation * address some comments * fix carousel question icon not showing * undo commit * undo commit 2 --- .../chat/browser/widget/chatListRenderer.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 05a012b08f0..16bbb662184 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -1831,18 +1831,23 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer + type === IChatToolInvocation.StateKind.Streaming || type === IChatToolInvocation.StateKind.Executing; + + if (!isWorkingState(currentState.type)) { return; } - let wasStreaming = true; + let didRemoveConfirmationWidget = false; const disposable = autorun(reader => { const state = toolInvocation.state.read(reader); - if (wasStreaming && state.type !== IChatToolInvocation.StateKind.Streaming) { - wasStreaming = false; - if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation || state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { - removeConfirmationWidget(); + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation || state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { + if (didRemoveConfirmationWidget) { + return; } + didRemoveConfirmationWidget = true; + disposable.dispose(); + removeConfirmationWidget(); } }); From 37f9dffb156ecfc2cf0ceb0d0e5b3fa2267ffb7b Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:36:06 -0800 Subject: [PATCH 045/152] fix questions icons not showing (#291421) --- .../widget/chatContentParts/media/chatQuestionCarousel.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css index 4094e8ca2eb..772b09d77c7 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css @@ -79,6 +79,7 @@ padding: 0; border: none; background: transparent !important; + color: var(--vscode-foreground) !important; } .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow:hover:not(.disabled) { From 032f6b93dbaf8360f18fe0ca45fb58a018907e8e Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:41:38 -0800 Subject: [PATCH 046/152] cleanup --- .../contrib/chat/browser/widget/input/modePickerActionItem.ts | 4 +--- .../contrib/chat/common/promptSyntax/promptFileParser.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index 381f97bfe4d..9f4f6151999 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -151,8 +151,6 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { return mode.source.storage === PromptsStorage.local || mode.source.storage === PromptsStorage.user; }; - const isImplementMode = (mode: IChatMode) => isBuiltinImplementMode(mode, this._productService); - const actionProviderWithCustomAgentTarget: IActionWidgetDropdownActionProvider = { getActions: () => { const modes = chatModeService.getModes(); @@ -182,7 +180,7 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { const otherBuiltinModes = modes.builtin.filter(mode => mode.id !== ChatMode.Agent.id && !(shouldHideEditMode && mode.id === ChatMode.Edit.id)); // Filter out 'implement' mode from the dropdown - it's available for handoffs but not user-selectable const customModes = groupBy( - modes.custom.filter(mode => !isImplementMode(mode)), + modes.custom, mode => isModeConsideredBuiltIn(mode, this._productService) ? 'builtin' : 'custom'); const customBuiltinModeActions = customModes.builtin?.map(mode => { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts index cdd864e7e75..bbaacb90a73 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts @@ -329,7 +329,7 @@ export interface IHandOff { readonly prompt: string; readonly send?: boolean; readonly showContinueOn?: boolean; // treated exactly like send (optional boolean) - readonly model?: string; // qualified model name to switch to (e.g., "GPT-4o (copilot)") + readonly model?: string; // qualified model name to switch to (e.g., "GPT-5 (copilot)") } export interface IHeaderAttribute { From 51c5f16d28cc6b1b8b5b7a5f5833fe69cda0d589 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Wed, 28 Jan 2026 15:11:14 -0800 Subject: [PATCH 047/152] PR feedback from previous commit for sanity tests (#291431) --- test/sanity/containers/centos.dockerfile | 2 +- test/sanity/src/desktop.test.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/sanity/containers/centos.dockerfile b/test/sanity/containers/centos.dockerfile index ddff56dd8e1..6d46c33a5af 100644 --- a/test/sanity/containers/centos.dockerfile +++ b/test/sanity/containers/centos.dockerfile @@ -19,6 +19,6 @@ RUN dnf install -y dbus-x11 && \ RUN dnf install -y xorg-x11-server-Xvfb # VS Code dependencies -RUN dnf install -y \ +RUN dnf install -y \ ca-certificates \ xdg-utils diff --git a/test/sanity/src/desktop.test.ts b/test/sanity/src/desktop.test.ts index 905edc89dfd..2ef0a11925a 100644 --- a/test/sanity/src/desktop.test.ts +++ b/test/sanity/src/desktop.test.ts @@ -37,9 +37,9 @@ export function setup(context: TestContext) { }); context.test('desktop-darwin-x64-dmg', ['darwin', 'x64', 'desktop'], async () => { - const dir = await context.downloadTarget('darwin-x64-dmg'); + const packagePath = await context.downloadTarget('darwin-x64-dmg'); if (!context.options.downloadOnly) { - const mountPoint = context.mountDmg(dir); + const mountPoint = context.mountDmg(packagePath); context.validateAllCodesignSignatures(mountPoint); const entryPoint = context.getDesktopEntryPoint(mountPoint); await testDesktopApp(entryPoint); @@ -48,9 +48,9 @@ export function setup(context: TestContext) { }); context.test('desktop-darwin-arm64-dmg', ['darwin', 'arm64', 'desktop'], async () => { - const dir = await context.downloadTarget('darwin-arm64-dmg'); + const packagePath = await context.downloadTarget('darwin-arm64-dmg'); if (!context.options.downloadOnly) { - const mountPoint = context.mountDmg(dir); + const mountPoint = context.mountDmg(packagePath); context.validateAllCodesignSignatures(mountPoint); const entryPoint = context.getDesktopEntryPoint(mountPoint); await testDesktopApp(entryPoint); @@ -59,9 +59,9 @@ export function setup(context: TestContext) { }); context.test('desktop-darwin-universal-dmg', ['darwin', 'desktop'], async () => { - const dir = await context.downloadTarget('darwin-universal-dmg'); + const packagePath = await context.downloadTarget('darwin-universal-dmg'); if (!context.options.downloadOnly) { - const mountPoint = context.mountDmg(dir); + const mountPoint = context.mountDmg(packagePath); context.validateAllCodesignSignatures(mountPoint); const entryPoint = context.getDesktopEntryPoint(mountPoint); await testDesktopApp(entryPoint); From 7de8a0de7cc7bf7e6bad22d77ae0ab6b6718a2b3 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 28 Jan 2026 15:37:19 -0800 Subject: [PATCH 048/152] chat: make tool invocation source property optional (#291439) Fixes compatibility with pre-1.104 versions where the source property may be undefined for deserialized tool invocations. Uses optional chaining when accessing the source property to prevent errors. - Make source property optional in IChatToolInvocationSerialized interface - Use optional chaining operator when checking source.type in chat renderer - Add comment explaining the pre-1.104 compatibility requirement Ref https://github.com/microsoft/vscode/issues/288489 (Commit message generated by Copilot) --- .../workbench/contrib/chat/browser/widget/chatListRenderer.ts | 2 +- src/vs/workbench/contrib/chat/common/chatService/chatService.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 16bbb662184..47c0bca94ab 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -1337,7 +1337,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer Date: Wed, 28 Jan 2026 15:51:21 -0800 Subject: [PATCH 049/152] Increase timeout for body download in fetchUrl to accommodate large files (#291443) --- build/lib/fetch.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build/lib/fetch.ts b/build/lib/fetch.ts index 970887b3e55..0d2c47a7fd8 100644 --- a/build/lib/fetch.ts +++ b/build/lib/fetch.ts @@ -50,7 +50,7 @@ export async function fetchUrl(url: string, options: IFetchOptions, retries = 10 startTime = new Date().getTime(); } const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 30 * 1000); + let timeout = setTimeout(() => controller.abort(), 30 * 1000); try { const response = await fetch(url, { ...options.nodeFetchOptions, @@ -60,6 +60,9 @@ export async function fetchUrl(url: string, options: IFetchOptions, retries = 10 log(`Fetch completed: Status ${response.status}. Took ${ansiColors.magenta(`${new Date().getTime() - startTime} ms`)}`); } if (response.ok && (response.status >= 200 && response.status < 300)) { + // Reset timeout for body download - large files need more time + clearTimeout(timeout); + timeout = setTimeout(() => controller.abort(), 5 * 60 * 1000); const contents = Buffer.from(await response.arrayBuffer()); if (options.checksumSha256) { const actualSHA256Checksum = crypto.createHash('sha256').update(contents).digest('hex'); From 8841b29bd580aa1ee704cc13ecf4d793eada567b Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Wed, 28 Jan 2026 16:18:48 -0800 Subject: [PATCH 050/152] Enhance snippet schema documentation with glob pattern details (#291451) --- .../contrib/snippets/browser/snippets.contribution.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts index 46ccc93bb59..c8859acb39a 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts @@ -77,14 +77,14 @@ const snippetSchemaProperties: IJSONSchemaMap = { type: ['string', 'array'] }, include: { - markdownDescription: nls.localize('snippetSchema.json.include', 'A list of glob patterns to include the snippet for specific files, e.g. `["**/*.test.ts", "*.spec.ts"]` or `"**/*.spec.ts"`.'), + markdownDescription: nls.localize('snippetSchema.json.include', 'A list of [glob patterns](https://aka.ms/vscode-glob-patterns) to include the snippet for specific files, e.g. `["**/*.test.ts", "*.spec.ts"]` or `"**/*.spec.ts"`. Patterns will match on the absolute path of a file if they contain a path separator and will match on the name of the file otherwise. You can exclude matching files via the `exclude` property.'), type: ['string', 'array'], items: { type: 'string' } }, exclude: { - markdownDescription: nls.localize('snippetSchema.json.exclude', 'A list of glob patterns to exclude the snippet from specific files, e.g. `["**/*.min.js"]` or `"*.min.js"`.'), + markdownDescription: nls.localize('snippetSchema.json.exclude', 'A list of [glob patterns](https://aka.ms/vscode-glob-patterns) to exclude the snippet from specific files, e.g. `["**/*.min.js"]` or `"*.min.js"`. Patterns will match on the absolute path of a file if they contain a path separator and will match on the name of the file otherwise. Exclude patterns take precedence over `include` patterns.'), type: ['string', 'array'], items: { type: 'string' From d3d2601e4a2b6d4d5c547357ec95faf7ee650823 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:30:45 -0800 Subject: [PATCH 051/152] Add missing plumbing for extension Quick Pick Item toggles (#291452) Also cancel event propegation in toggles which would cause he item to be accepted when you clicked on the the toggle. Fixes https://github.com/microsoft/vscode/issues/290775 --- src/vs/base/browser/ui/toggle/toggle.ts | 1 + .../workbench/api/browser/mainThreadQuickOpen.ts | 8 +++++++- src/vs/workbench/api/common/extHost.protocol.ts | 2 +- src/vs/workbench/api/common/extHostQuickOpen.ts | 15 +++++++++++---- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/vs/base/browser/ui/toggle/toggle.ts b/src/vs/base/browser/ui/toggle/toggle.ts index f310bff8965..0b2fcbbb274 100644 --- a/src/vs/base/browser/ui/toggle/toggle.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -173,6 +173,7 @@ export class Toggle extends Widget { this.checked = !this._checked; this._onChange.fire(false); ev.preventDefault(); + ev.stopPropagation(); } }); diff --git a/src/vs/workbench/api/browser/mainThreadQuickOpen.ts b/src/vs/workbench/api/browser/mainThreadQuickOpen.ts index 1b1f906d1a6..e58f5d07981 100644 --- a/src/vs/workbench/api/browser/mainThreadQuickOpen.ts +++ b/src/vs/workbench/api/browser/mainThreadQuickOpen.ts @@ -159,7 +159,13 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { this._proxy.$onDidChangeSelection(sessionId, items.map(item => (item as TransferQuickPickItem).handle)); })); store.add(quickPick.onDidTriggerItemButton((e) => { - this._proxy.$onDidTriggerItemButton(sessionId, (e.item as TransferQuickPickItem).handle, (e.button as TransferQuickInputButton).handle); + const transferButton = e.button as TransferQuickInputButton; + this._proxy.$onDidTriggerItemButton( + sessionId, + (e.item as TransferQuickPickItem).handle, + transferButton.handle, + transferButton.toggle?.checked + ); })); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 82292bd6198..cb2b3cd4b1f 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2576,7 +2576,7 @@ export interface ExtHostQuickOpenShape { $onDidAccept(sessionId: number): void; $onDidChangeValue(sessionId: number, value: string): void; $onDidTriggerButton(sessionId: number, handle: number, checked?: boolean): void; - $onDidTriggerItemButton(sessionId: number, itemHandle: number, buttonHandle: number): void; + $onDidTriggerItemButton(sessionId: number, itemHandle: number, buttonHandle: number, checked?: boolean): void; $onDidHide(sessionId: number): void; } diff --git a/src/vs/workbench/api/common/extHostQuickOpen.ts b/src/vs/workbench/api/common/extHostQuickOpen.ts index 55e4d8bbe96..3b94c47a855 100644 --- a/src/vs/workbench/api/common/extHostQuickOpen.ts +++ b/src/vs/workbench/api/common/extHostQuickOpen.ts @@ -252,10 +252,10 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx session?._fireDidTriggerButton(handle, checked); } - $onDidTriggerItemButton(sessionId: number, itemHandle: number, buttonHandle: number): void { + $onDidTriggerItemButton(sessionId: number, itemHandle: number, buttonHandle: number, checked?: boolean): void { const session = this._sessions.get(sessionId); if (session instanceof ExtHostQuickPick) { - session._fireDidTriggerItemButton(itemHandle, buttonHandle); + session._fireDidTriggerItemButton(itemHandle, buttonHandle, checked); } } @@ -568,7 +568,11 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx return { iconPathDto: IconPath.from(button.iconPath), tooltip: button.tooltip, - handle: i + handle: i, + toggle: + typeof button.toggle === 'object' && typeof button.toggle.checked === 'boolean' + ? { checked: button.toggle.checked } + : undefined, }; }), }); @@ -670,13 +674,16 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx onDidTriggerItemButton = this._onDidTriggerItemButtonEmitter.event; - _fireDidTriggerItemButton(itemHandle: number, buttonHandle: number) { + _fireDidTriggerItemButton(itemHandle: number, buttonHandle: number, checked?: boolean) { const item = this._handlesToItems.get(itemHandle)!; if (!item || !item.buttons || !item.buttons.length) { return; } const button = item.buttons[buttonHandle]; if (button) { + if (checked !== undefined && button.toggle) { + button.toggle.checked = checked; + } this._onDidTriggerItemButtonEmitter.fire({ button, item From c84bdba0a431663013dc9c2d1b10e50c7205dd7c Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 28 Jan 2026 16:40:56 -0800 Subject: [PATCH 052/152] Prompt file diagnostics feedback (#291450) --- .../chatCustomizationDiagnosticsAction.ts | 40 +++++++----- ...chatCustomizationDiagnosticsAction.test.ts | 62 +++++++++++++++---- 2 files changed, 76 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCustomizationDiagnosticsAction.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCustomizationDiagnosticsAction.ts index e6c881e8a0e..a5b99a144dd 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCustomizationDiagnosticsAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCustomizationDiagnosticsAction.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { Schemas } from '../../../../../base/common/network.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { localize2 } from '../../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; @@ -38,15 +39,20 @@ function encodePathForMarkdown(path: string): string { * The returned path is URL encoded for use in markdown link targets. */ function getRelativePath(uri: URI, workspaceFolders: readonly IWorkspaceFolder[]): string { + // On desktop, vscode-userdata scheme maps 1:1 to file scheme paths via FileUserDataProvider. + // Convert to file scheme so relativePath() can compute paths correctly. + // On web, vscode-userdata uses IndexedDB so this conversion has no effect (different schemes won't match workspace folders). + const normalizedUri = uri.scheme === Schemas.vscodeUserData ? uri.with({ scheme: Schemas.file }) : uri; + for (const folder of workspaceFolders) { - const relative = relativePath(folder.uri, uri); + const relative = relativePath(folder.uri, normalizedUri); if (relative) { return encodePathForMarkdown(relative); } } // Fall back to fsPath if not under any workspace folder // Use forward slashes for consistency in markdown links - return encodePathForMarkdown(uri.fsPath.replace(/\\/g, '/')); + return encodePathForMarkdown(normalizedUri.fsPath.replace(/\\/g, '/')); } // Tree prefixes @@ -117,8 +123,13 @@ export function registerChatCustomizationDiagnosticsAction() { }, { id: CHAT_CONFIG_MENU_ID, when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)), - order: 20, + order: 14, group: '3_configure' + }, { + id: MenuId.ChatWelcomeContext, + group: '2_settings', + order: 0, + when: ChatContextKeys.inChatEditor.negate() }] }); } @@ -423,12 +434,14 @@ export function formatStatusOutput( // Count loaded and skipped files (overwritten counts as skipped) let loadedCount = info.files.filter(f => f.status === 'loaded').length; const skippedCount = info.files.filter(f => f.status === 'skipped' || f.status === 'overwritten').length; - // Include special files in the loaded count - if (info.type === PromptsType.agent && specialFiles.agentsMd.enabled) { - loadedCount += specialFiles.agentsMd.files.length; - } - if (info.type === PromptsType.instructions && specialFiles.copilotInstructions.enabled) { - loadedCount += specialFiles.copilotInstructions.files.length; + // Include special files in the loaded count for instructions + if (info.type === PromptsType.instructions) { + if (specialFiles.agentsMd.enabled) { + loadedCount += specialFiles.agentsMd.files.length; + } + if (specialFiles.copilotInstructions.enabled) { + loadedCount += specialFiles.copilotInstructions.files.length; + } } lines.push(`**${typeName}**${enabledStatus}
`); @@ -558,8 +571,9 @@ export function formatStatusOutput( hasContent = true; } - // Add special files for agents (AGENTS.md) - if (info.type === PromptsType.agent) { + // Add special files for instructions (AGENTS.md and copilot-instructions.md) + if (info.type === PromptsType.instructions) { + // AGENTS.md if (specialFiles.agentsMd.enabled && specialFiles.agentsMd.files.length > 0) { lines.push(`AGENTS.md
`); for (let i = 0; i < specialFiles.agentsMd.files.length; i++) { @@ -575,10 +589,8 @@ export function formatStatusOutput( lines.push(`AGENTS.md -
`); hasContent = true; } - } - // Add special files for instructions (copilot-instructions.md) - if (info.type === PromptsType.instructions) { + // copilot-instructions.md if (specialFiles.copilotInstructions.enabled && specialFiles.copilotInstructions.files.length > 0) { lines.push(`${COPILOT_CUSTOM_INSTRUCTIONS_FILENAME}
`); for (let i = 0; i < specialFiles.copilotInstructions.files.length; i++) { diff --git a/src/vs/workbench/contrib/chat/test/browser/actions/chatCustomizationDiagnosticsAction.test.ts b/src/vs/workbench/contrib/chat/test/browser/actions/chatCustomizationDiagnosticsAction.test.ts index 1aa7974773e..105de058437 100644 --- a/src/vs/workbench/contrib/chat/test/browser/actions/chatCustomizationDiagnosticsAction.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/actions/chatCustomizationDiagnosticsAction.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { Schemas } from '../../../../../../base/common/network.js'; import { URI } from '../../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { formatStatusOutput, IFileStatusInfo, IPathInfo, ITypeStatusInfo } from '../../../browser/actions/chatCustomizationDiagnosticsAction.js'; @@ -91,7 +92,6 @@ suite('formatStatusOutput', () => { '.github/agents
', `${TREE_BRANCH} [\`code-reviewer.agent.md\`](${filePath('.github/agents/code-reviewer.agent.md')})
`, `${TREE_END} [\`test-helper.agent.md\`](${filePath('.github/agents/test-helper.agent.md')})
`, - 'AGENTS.md -
', '' )); }); @@ -119,7 +119,6 @@ suite('formatStatusOutput', () => { '.github/agents
', `${TREE_BRANCH} [\`good-agent.agent.md\`](${filePath('.github/agents/good-agent.agent.md')})
`, `${TREE_END} ${ICON_ERROR} [\`broken-agent.agent.md\`](${filePath('.github/agents/broken-agent.agent.md')}) - *Missing name attribute*
`, - 'AGENTS.md -
', '' )); }); @@ -151,7 +150,6 @@ suite('formatStatusOutput', () => { `${TREE_END} [\`my-agent.agent.md\`](${filePath('.github/agents/my-agent.agent.md')})
`, '~/.copilot/agents
', `${TREE_END} ${ICON_WARN} [\`my-agent.agent.md\`](${filePath('home/.copilot/agents/my-agent.agent.md')}) - *Overwritten by higher priority file*
`, - 'AGENTS.md -
', '' )); }); @@ -229,16 +227,17 @@ suite('formatStatusOutput', () => { '', '.github/instructions
', `${TREE_END} [\`testing.instructions.md\`](${filePath('.github/instructions/testing.instructions.md')})
`, + 'AGENTS.md -
', 'copilot-instructions.md
', `${TREE_END} [\`copilot-instructions.md\`](${filePath('.github/copilot-instructions.md')})
`, '' )); }); - test('agents with AGENTS.md enabled', () => { + test('instructions with AGENTS.md enabled', () => { const statusInfos: ITypeStatusInfo[] = [{ - type: PromptsType.agent, - paths: [createPath('.github/agents', true)], + type: PromptsType.instructions, + paths: [createPath('.github/instructions', true)], files: [], enabled: true }]; @@ -254,13 +253,14 @@ suite('formatStatusOutput', () => { '## Chat Customization Diagnostics', '*WARNING: This file may contain sensitive information.*', '', - '**Custom Agents**
', + '**Instructions**
', '*2 files loaded*', '', - '.github/agents
', + '.github/instructions
', 'AGENTS.md
', `${TREE_BRANCH} [\`AGENTS.md\`](${filePath('AGENTS.md')})
`, `${TREE_END} [\`AGENTS.md\`](${filePath('docs/AGENTS.md')})
`, + 'copilot-instructions.md -
', '' )); }); @@ -286,7 +286,6 @@ suite('formatStatusOutput', () => { '', '.github/agents
', `${ICON_ERROR} custom/agents - *Folder does not exist*
`, - 'AGENTS.md -
', '' )); }); @@ -310,7 +309,6 @@ suite('formatStatusOutput', () => { '**Custom Agents**
', '', '.github/agents
', - 'AGENTS.md -
', '' )); }); @@ -341,7 +339,6 @@ suite('formatStatusOutput', () => { `${TREE_END} [\`local-agent.agent.md\`](${filePath('.github/agents/local-agent.agent.md')})
`, 'Extension: my-publisher.my-extension
', `${TREE_END} [\`ext-agent.agent.md\`](${filePath('extensions/my-publisher.my-extension/agents/ext-agent.agent.md')})
`, - 'AGENTS.md -
', '' )); }); @@ -406,13 +403,13 @@ suite('formatStatusOutput', () => { '', '.github/agents
', `${TREE_END} [\`helper.agent.md\`](${filePath('.github/agents/helper.agent.md')})
`, - 'AGENTS.md -
', '', '**Instructions**
', '*1 file loaded*', '', '.github/instructions
', `${TREE_END} [\`code-style.instructions.md\`](${filePath('.github/instructions/code-style.instructions.md')})
`, + 'AGENTS.md -
', 'copilot-instructions.md -
', '', '**Prompt Files**
', @@ -483,4 +480,45 @@ suite('formatStatusOutput', () => { assert.ok(output.includes('docs%20%26%20notes'), 'Ampersand should be URL-encoded'); assert.ok(output.includes('test%5B1%5D.prompt.md'), 'Brackets should be URL-encoded'); }); + + test('vscode-userdata scheme URIs are converted to file scheme for relative paths', () => { + // Create a workspace folder + const workspaceFolderUri = URI.file('/Users/test/workspace'); + const workspaceFolder = { + uri: workspaceFolderUri, + name: 'workspace', + index: 0, + toResource: (relativePath: string) => URI.joinPath(workspaceFolderUri, relativePath) + }; + + // Create a vscode-userdata URI that maps to a path under the workspace + const userDataUri = URI.file('/Users/test/workspace/.github/agents/my-agent.agent.md').with({ scheme: Schemas.vscodeUserData }); + + const statusInfos: ITypeStatusInfo[] = [{ + type: PromptsType.agent, + paths: [{ + uri: URI.file('/Users/test/workspace/.github/agents'), + exists: true, + storage: PromptsStorage.local, + scanOrder: 1, + displayPath: '.github/agents', + isDefault: true + }], + files: [{ + uri: userDataUri, + status: 'loaded', + name: 'my-agent.agent.md', + storage: PromptsStorage.local + }], + enabled: true + }]; + + const output = formatStatusOutput(statusInfos, emptySpecialFiles, [workspaceFolder]); + + // The vscode-userdata URI should be converted to file scheme internally, + // allowing relative path computation against workspace folders + assert.ok(output.includes('.github/agents/my-agent.agent.md'), 'Should use relative path from workspace folder'); + // Should not contain the full absolute path + assert.ok(!output.includes('/Users/test/workspace/.github'), 'Should not contain absolute path when relative path is available'); + }); }); From ddfbf274ffbd9bf780a12c8d160c9e4ea2bf2362 Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Wed, 28 Jan 2026 16:52:34 -0800 Subject: [PATCH 053/152] Strengthen skill adherence system prompt with blocking requirement (#291460) --- .../computeAutomaticInstructions.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index 10f12a1c991..d6775015ef1 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -318,13 +318,17 @@ export class ComputeAutomaticInstructions { entries.push(''); if (useSkillAdherencePrompt) { // Stronger skill adherence prompt for experimental feature - entries.push('Skills are curated collections of best practices and detailed guidance for producing high-quality outputs across various domains. Each skill folder contains refined instructions developed through extensive testing to help achieve professional results. Examples include presentation skills for creating polished slides, document skills for well-formatted reports, and many others. Multiple skills can be combined when a task spans different domains.'); - entries.push(`IMPORTANT: Reading skill documentation BEFORE taking action significantly improves output quality. When you receive a task, first review the available skills listed below to identify which ones apply. Use the ${readTool.variable} tool to load the relevant SKILL.md file(s) and follow their guidance.`); - entries.push('Examples of proper skill usage:'); - entries.push(`- Task: "Create a presentation about our product roadmap" -> First load the presentation skill via ${readTool.variable}`); - entries.push(`- Task: "Clean up the formatting in this document" -> First load the document editing skill via ${readTool.variable}`); - entries.push(`- Task: "Add a flowchart to the README" -> Load both the documentation skill and any diagram-related skill`); - entries.push('Taking time to read the skill instructions first leads to substantially better results.'); + entries.push('Skills provide specialized capabilities, domain knowledge, and refined workflows for producing high-quality outputs. Each skill folder contains tested instructions for specific domains like testing strategies, API design, or performance optimization. Multiple skills can be combined when a task spans different domains.'); + entries.push(`BLOCKING REQUIREMENT: When a skill applies to the user's request, you MUST load and read the SKILL.md file IMMEDIATELY as your first action, BEFORE generating any other response or taking action on the task. Use ${readTool.variable} to load the relevant skill(s).`); + entries.push('NEVER just mention or reference a skill in your response without actually reading it first. If a skill is relevant, load it before proceeding.'); + entries.push('How to determine if a skill applies:'); + entries.push('1. Review the available skills below and match their descriptions against the user\'s request'); + entries.push('2. If any skill\'s domain overlaps with the task, load that skill immediately'); + entries.push('3. When multiple skills apply (e.g., a flowchart in documentation), load all relevant skills'); + entries.push('Examples:'); + entries.push(`- "Help me write unit tests for this module" -> Load the testing skill via ${readTool.variable} FIRST, then proceed`); + entries.push(`- "Optimize this slow function" -> Load the performance-profiling skill via ${readTool.variable} FIRST, then proceed`); + entries.push(`- "Add a discount code field to checkout" -> Load both the checkout-flow and form-validation skills FIRST`); entries.push('Available skills:'); } else { entries.push('Here is a list of skills that contain domain specific knowledge on a variety of topics.'); From c6c601febd59b5c94ce3b00caba713893a7650a2 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:06:04 -0800 Subject: [PATCH 054/152] ask questions not in thinking + ux improvements (#291456) --- .../chatQuestionCarouselPart.ts | 42 ++++++++++++++++++- .../chat/browser/widget/chatListRenderer.ts | 6 +++ 2 files changed, 46 insertions(+), 2 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 7ccd4dc7bcc..6ed3d82f9de 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts @@ -512,6 +512,24 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent freeformTextarea.value = previousFreeform; } + this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { + const event = new StandardKeyboardEvent(e); + if (event.keyCode === KeyCode.Enter && !event.shiftKey && freeformTextarea.value.trim()) { + e.preventDefault(); + e.stopPropagation(); + this.handleNext(); + } + })); + + // uncheck radio when there is text + this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.INPUT, () => { + if (freeformTextarea.value.trim()) { + for (const radio of radioInputs) { + radio.checked = false; + } + } + })); + freeformContainer.appendChild(freeformTextarea); container.appendChild(freeformContainer); this._freeformTextareas.set(question.id, freeformTextarea); @@ -586,6 +604,24 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent freeformTextarea.value = previousFreeform; } + this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { + const event = new StandardKeyboardEvent(e); + if (event.keyCode === KeyCode.Enter && !event.shiftKey && freeformTextarea.value.trim()) { + e.preventDefault(); + e.stopPropagation(); + this.handleNext(); + } + })); + + // uncheck checkboxes when there is text + this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.INPUT, () => { + if (freeformTextarea.value.trim()) { + for (const checkbox of checkboxInputs) { + checkbox.checked = false; + } + } + })); + freeformContainer.appendChild(freeformTextarea); container.appendChild(freeformContainer); this._freeformTextareas.set(question.id, freeformTextarea); @@ -627,7 +663,8 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const freeformTextarea = this._freeformTextareas.get(question.id); const freeformValue = freeformTextarea?.value !== '' ? freeformTextarea?.value : undefined; if (freeformValue || selectedValue !== undefined) { - return { selectedValue, freeformValue }; + // if there is text in freeform, don't include selected + return { selectedValue: freeformValue ? undefined : selectedValue, freeformValue }; } return undefined; } @@ -666,7 +703,8 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const freeformTextarea = this._freeformTextareas.get(question.id); const freeformValue = freeformTextarea?.value !== '' ? freeformTextarea?.value : undefined; if (freeformValue || finalSelectedValues.length > 0) { - return { selectedValues: finalSelectedValues, freeformValue }; + // if there is text in freeform, don't include selected + return { selectedValues: freeformValue ? [] : finalSelectedValues, freeformValue }; } return undefined; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 47c0bca94ab..653b9601202 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -1348,6 +1348,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer Date: Wed, 28 Jan 2026 19:48:04 -0800 Subject: [PATCH 055/152] consistent transparency, white background --- extensions/theme-2026/themes/2026-light.json | 105 ++++++++++--------- extensions/theme-2026/themes/styles.css | 2 + 2 files changed, 57 insertions(+), 50 deletions(-) diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index 0b2736f937b..0e99b1e1a58 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -31,10 +31,10 @@ "dropdown.foreground": "#202020", "dropdown.listBackground": "#FFFFFF", "input.background": "#FFFFFF", - "input.border": "#D8D8D880", + "input.border": "#D8D8D866", "input.foreground": "#202020", "input.placeholderForeground": "#999999", - "inputOption.activeBackground": "#0069CC33", + "inputOption.activeBackground": "#0069CC26", "inputOption.activeForeground": "#202020", "inputOption.activeBorder": "#ECEDEEFF", "inputValidation.errorBackground": "#FFFFFF", @@ -46,28 +46,33 @@ "inputValidation.warningBackground": "#FFFFFF", "inputValidation.warningBorder": "#ECEDEEFF", "inputValidation.warningForeground": "#202020", - "scrollbar.shadow": "#FFFFFF4D", - "scrollbarSlider.background": "#99999933", - "scrollbarSlider.hoverBackground": "#9999994D", - "scrollbarSlider.activeBackground": "#99999966", + "scrollbar.shadow": "#00000000", + "widget.shadow": "#00000000", + "editorStickyScroll.shadow": "#00000000", + "sideBarStickyScroll.shadow": "#00000000", + "panelStickyScroll.shadow": "#00000000", + "listFilterWidget.shadow": "#00000000", + "scrollbarSlider.background": "#99999926", + "scrollbarSlider.hoverBackground": "#99999940", + "scrollbarSlider.activeBackground": "#99999955", "badge.background": "#0069CC", "badge.foreground": "#FFFFFF", "progressBar.background": "#0069CC", - "list.activeSelectionBackground": "#0069CC26", + "list.activeSelectionBackground": "#0069CC1A", "list.activeSelectionForeground": "#202020", "list.inactiveSelectionBackground": "#EDEDED", "list.inactiveSelectionForeground": "#202020", "list.hoverBackground": "#F7F7F7", "list.hoverForeground": "#202020", - "list.dropBackground": "#0069CC1A", - "list.focusBackground": "#0069CC26", + "list.dropBackground": "#0069CC15", + "list.focusBackground": "#0069CC1A", "list.focusForeground": "#202020", "list.focusOutline": "#0069CCFF", "list.highlightForeground": "#0069CC", "list.invalidItemForeground": "#BBBBBB", "list.errorForeground": "#ad0707", "list.warningForeground": "#667309", - "activityBar.background": "#FFFFFF", + "activityBar.background": "#F5F7FA", "activityBar.foreground": "#202020", "activityBar.inactiveForeground": "#666666", "activityBar.border": "#ECEDEEFF", @@ -82,62 +87,62 @@ "sideBarSectionHeader.background": "#FFFFFF", "sideBarSectionHeader.foreground": "#202020", "sideBarSectionHeader.border": "#ECEDEEFF", - "titleBar.activeBackground": "#FFFFFF", + "titleBar.activeBackground": "#F5F7FA", "titleBar.activeForeground": "#424242", - "titleBar.inactiveBackground": "#FFFFFF", + "titleBar.inactiveBackground": "#F5F7FA", "titleBar.inactiveForeground": "#666666", "titleBar.border": "#ECEDEEFF", "menubar.selectionBackground": "#EDEDED", "menubar.selectionForeground": "#202020", "menu.background": "#FFFFFF", "menu.foreground": "#202020", - "menu.selectionBackground": "#0069CC26", + "menu.selectionBackground": "#0069CC1A", "menu.selectionForeground": "#202020", "menu.separatorBackground": "#F7F7F7", "menu.border": "#ECEDEEFF", "commandCenter.foreground": "#202020", "commandCenter.activeForeground": "#202020", - "commandCenter.background": "#FFFFFF", - "commandCenter.activeBackground": "#FFFFFFCC", - "commandCenter.border": "#D8D8D880", - "editor.background": "#FAFAFA", + "commandCenter.background": "#F5F7FA", + "commandCenter.activeBackground": "#F5F7FAB3", + "commandCenter.border": "#D8D8D866", + "editor.background": "#FFFFFF", "editor.foreground": "#202020", "editorLineNumber.foreground": "#666666", "editorLineNumber.activeForeground": "#202020", "editorCursor.foreground": "#202020", - "editor.selectionBackground": "#0069CC26", - "editor.inactiveSelectionBackground": "#0069CC26", - "editor.selectionHighlightBackground": "#0069CC1A", - "editor.wordHighlightBackground": "#0069CC33", - "editor.wordHighlightStrongBackground": "#0069CC33", - "editor.findMatchBackground": "#0069CC4D", - "editor.findMatchHighlightBackground": "#0069CC26", + "editor.selectionBackground": "#0069CC1A", + "editor.inactiveSelectionBackground": "#0069CC1A", + "editor.selectionHighlightBackground": "#0069CC15", + "editor.wordHighlightBackground": "#0069CC26", + "editor.wordHighlightStrongBackground": "#0069CC26", + "editor.findMatchBackground": "#0069CC40", + "editor.findMatchHighlightBackground": "#0069CC1A", "editor.findRangeHighlightBackground": "#EDEDED", "editor.hoverHighlightBackground": "#EDEDED", - "editor.lineHighlightBackground": "#EDEDED55", + "editor.lineHighlightBackground": "#EDEDED40", "editor.rangeHighlightBackground": "#EDEDED", "editorLink.activeForeground": "#0069CC", - "editorWhitespace.foreground": "#6666664D", - "editorIndentGuide.background": "#F7F7F74D", + "editorWhitespace.foreground": "#66666640", + "editorIndentGuide.background": "#F7F7F740", "editorIndentGuide.activeBackground": "#F7F7F7", "editorRuler.foreground": "#F7F7F7", "editorCodeLens.foreground": "#666666", - "editorBracketMatch.background": "#0069CC55", + "editorBracketMatch.background": "#0069CC40", "editorBracketMatch.border": "#ECEDEEFF", - "editorWidget.background": "#FFFFFF99", + "editorWidget.background": "#F5F7FAE6", "editorWidget.border": "#ECEDEEFF", "editorWidget.foreground": "#202020", - "editorSuggestWidget.background": "#FFFFFF", + "editorSuggestWidget.background": "#F5F7FAE6", "editorSuggestWidget.border": "#ECEDEEFF", "editorSuggestWidget.foreground": "#202020", "editorSuggestWidget.highlightForeground": "#0069CC", "editorSuggestWidget.selectedBackground": "#0069CC26", - "editorHoverWidget.background": "#FFFFFF", + "editorHoverWidget.background": "#F5F7FAE6", "editorHoverWidget.border": "#ECEDEEFF", "peekView.border": "#0069CC", - "peekViewEditor.background": "#FFFFFF", + "peekViewEditor.background": "#F5F7FAE6", "peekViewEditor.matchHighlightBackground": "#0069CC33", - "peekViewResult.background": "#FFFFFF", + "peekViewResult.background": "#F5F7FAE6", "peekViewResult.fileForeground": "#202020", "peekViewResult.lineForeground": "#666666", "peekViewResult.matchHighlightBackground": "#0069CC33", @@ -146,7 +151,6 @@ "peekViewTitle.background": "#FFFFFF", "peekViewTitleDescription.foreground": "#666666", "peekViewTitleLabel.foreground": "#202020", - "editorGutter.background": "#FAFAFA", "editorGutter.addedBackground": "#587c0c", "editorGutter.deletedBackground": "#ad0707", "diffEditor.insertedTextBackground": "#587c0c26", @@ -158,18 +162,19 @@ "editorOverviewRuler.deletedForeground": "#ad0707", "editorOverviewRuler.errorForeground": "#ad0707", "editorOverviewRuler.warningForeground": "#667309", + "editorGutter.background": "#FFFFFF", "panel.background": "#FFFFFF", "panel.border": "#ECEDEEFF", "panelTitle.activeBorder": "#0069CC", "panelTitle.activeForeground": "#202020", "panelTitle.inactiveForeground": "#666666", - "statusBar.background": "#FFFFFF", + "statusBar.background": "#F5F7FA", "statusBar.foreground": "#666666", "statusBar.border": "#ECEDEEFF", "statusBar.focusBorder": "#0069CCFF", "statusBar.debuggingBackground": "#0069CC", "statusBar.debuggingForeground": "#FFFFFF", - "statusBar.noFolderBackground": "#FFFFFF", + "statusBar.noFolderBackground": "#F5F7FA", "statusBar.noFolderForeground": "#666666", "statusBarItem.activeBackground": "#E6E6E6", "statusBarItem.hoverBackground": "#F7F7F7", @@ -177,23 +182,23 @@ "statusBarItem.prominentBackground": "#0069CCDD", "statusBarItem.prominentForeground": "#FFFFFF", "statusBarItem.prominentHoverBackground": "#0069CC", - "tab.activeBackground": "#FAFAFA", + "tab.activeBackground": "#FFFFFF", "tab.activeForeground": "#202020", "tab.inactiveBackground": "#FFFFFF", "tab.inactiveForeground": "#666666", "tab.border": "#ECEDEEFF", "tab.lastPinnedBorder": "#ECEDEEFF", - "tab.activeBorder": "#FAFAFA", + "tab.activeBorder": "#FFFFFF", "tab.hoverBackground": "#F7F7F7", "tab.hoverForeground": "#202020", - "tab.unfocusedActiveBackground": "#FAFAFA", + "tab.unfocusedActiveBackground": "#FFFFFF", "tab.unfocusedActiveForeground": "#666666", "tab.unfocusedInactiveBackground": "#FFFFFF", "tab.unfocusedInactiveForeground": "#BBBBBB", "editorGroupHeader.tabsBackground": "#FFFFFF", "editorGroupHeader.tabsBorder": "#ECEDEEFF", "breadcrumb.foreground": "#666666", - "breadcrumb.background": "#FAFAFA", + "breadcrumb.background": "#FFFFFF", "breadcrumb.focusForeground": "#202020", "breadcrumb.activeSelectionForeground": "#202020", "breadcrumbPicker.background": "#FFFFFF", @@ -217,13 +222,13 @@ "extensionButton.prominentHoverBackground": "#0064CC", "pickerGroup.border": "#ECEDEEFF", "pickerGroup.foreground": "#202020", - "quickInput.background": "#FFFFFF", + "quickInput.background": "#F5F7FAE6", "quickInput.foreground": "#202020", - "quickInputList.focusBackground": "#0069CC26", + "quickInputList.focusBackground": "#0069CC1A", "quickInputList.focusForeground": "#202020", "quickInputList.focusIconForeground": "#202020", - "quickInputList.hoverBackground": "#E7E7E7", - "terminal.selectionBackground": "#0069CC33", + "quickInputList.hoverBackground": "#EDF0F5E6", + "terminal.selectionBackground": "#0069CC26", "terminalCursor.foreground": "#202020", "terminalCursor.background": "#FFFFFF", "gitDecoration.addedResourceForeground": "#587c0c", @@ -234,21 +239,21 @@ "gitDecoration.conflictingResourceForeground": "#ad0707", "gitDecoration.stageModifiedResourceForeground": "#667309", "gitDecoration.stageDeletedResourceForeground": "#ad0707", - "commandCenter.activeBorder": "#D8D8D8CC", + "commandCenter.activeBorder": "#D8D8D8A6", "quickInput.border": "#D8D8D8", "gauge.foreground": "#0069CC", - "gauge.background": "#0069CC4D", + "gauge.background": "#0069CC40", "gauge.border": "#ECEDEEFF", "gauge.warningForeground": "#B69500", - "gauge.warningBackground": "#B695004D", + "gauge.warningBackground": "#B6950040", "gauge.errorForeground": "#ad0707", - "gauge.errorBackground": "#ad07074D", + "gauge.errorBackground": "#ad070740", "statusBarItem.prominentHoverForeground": "#FFFFFF", - "quickInputTitle.background": "#FFFFFF", + "quickInputTitle.background": "#F5F7FAE6", "chat.requestBubbleBackground": "#EEF4FB", "chat.requestBubbleHoverBackground": "#E6EDFA", "charts.foreground": "#202020", - "charts.lines": "#20202080", + "charts.lines": "#20202066", "charts.blue": "#1A5CFF", "charts.red": "#ad0707", "charts.yellow": "#667309", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 33ba0ada647..a7a5e22fd0a 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -38,10 +38,12 @@ .monaco-workbench .part.auxiliarybar { box-shadow: var(--shadow-md); z-index: 40; position: relative; } /* Ensure iframe containers in pane-body render above sidebar z-index */ +/* Commented out - may cause content to be hidden by z-index issues .monaco-workbench > div[data-keybinding-context], .monaco-workbench > div[data-keybinding-context] { z-index: 50 !important; } +*/ /* Ensure webview containers render above sidebar z-index */ .monaco-workbench .part.sidebar .webview, From 9197ab8dc10357cd72a25c5ab55e618860eff89c Mon Sep 17 00:00:00 2001 From: eli-w-king Date: Wed, 28 Jan 2026 20:05:43 -0800 Subject: [PATCH 056/152] fixed chat input buttons foreground --- extensions/theme-2026/themes/2026-light.json | 8 ++++---- extensions/theme-2026/themes/styles.css | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index 0e99b1e1a58..6090fdb4c9a 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -148,7 +148,7 @@ "peekViewResult.matchHighlightBackground": "#0069CC33", "peekViewResult.selectionBackground": "#0069CC26", "peekViewResult.selectionForeground": "#202020", - "peekViewTitle.background": "#FFFFFF", + "peekViewTitle.background": "#F5F7FAE6", "peekViewTitleDescription.foreground": "#666666", "peekViewTitleLabel.foreground": "#202020", "editorGutter.addedBackground": "#587c0c", @@ -201,13 +201,13 @@ "breadcrumb.background": "#FFFFFF", "breadcrumb.focusForeground": "#202020", "breadcrumb.activeSelectionForeground": "#202020", - "breadcrumbPicker.background": "#FFFFFF", + "breadcrumbPicker.background": "#F5F7FAE6", "notificationCenter.border": "#ECEDEEFF", "notificationCenterHeader.foreground": "#202020", - "notificationCenterHeader.background": "#FFFFFF", + "notificationCenterHeader.background": "#F5F7FAE6", "notificationToast.border": "#ECEDEEFF", "notifications.foreground": "#202020", - "notifications.background": "#FFFFFF", + "notifications.background": "#F5F7FAE6", "notifications.border": "#ECEDEEFF", "notificationLink.foreground": "#0069CC", "notificationsWarningIcon.foreground": "#B69500", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index a7a5e22fd0a..c9b0d7695f3 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -283,6 +283,12 @@ color: var(--vscode-icon-foreground) !important; } +/* Chat input toolbar icons should use proper foreground color, not the muted icon.foreground */ +.monaco-workbench .interactive-session .chat-input-toolbars .monaco-action-bar .action-item .codicon, +.monaco-workbench .interactive-session .chat-input-toolbars .action-label .codicon { + color: var(--vscode-foreground) !important; +} + /* Buttons */ .monaco-workbench .monaco-button { box-shadow: var(--shadow-xs); } .monaco-workbench .monaco-button:hover { box-shadow: var(--shadow-sm); } From 748751e9f522cbffc7216fa23b9a42ec5631323a Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 29 Jan 2026 05:06:58 +0100 Subject: [PATCH 057/152] Workspace trust - update proposed APIs (#291408) Workspace trust - add proposed API to check whether a URI is trusted and an event for when the list of trusted folders/workspaces change --- src/vs/workbench/api/browser/mainThreadWorkspace.ts | 11 +++++++++++ src/vs/workbench/api/common/extHost.api.impl.ts | 8 ++++++++ src/vs/workbench/api/common/extHost.protocol.ts | 2 ++ src/vs/workbench/api/common/extHostWorkspace.ts | 11 +++++++++++ src/vscode-dts/vscode.proposed.workspaceTrust.d.ts | 11 +++++++++++ 5 files changed, 43 insertions(+) diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 062c43408a0..30ebb309289 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -70,6 +70,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { this._contextService.onDidChangeWorkspaceFolders(this._onDidChangeWorkspace, this, this._toDispose); this._contextService.onDidChangeWorkbenchState(this._onDidChangeWorkspace, this, this._toDispose); this._workspaceTrustManagementService.onDidChangeTrust(this._onDidGrantWorkspaceTrust, this, this._toDispose); + this._workspaceTrustManagementService.onDidChangeTrustedFolders(this._onDidChangeWorkspaceTrustedFolders, this, this._toDispose); } dispose(): void { @@ -251,6 +252,12 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { return this._workspaceTrustRequestService.requestWorkspaceTrust(options); } + async $isResourceTrusted(resource: UriComponents): Promise { + const uri = URI.revive(resource); + const trustInfo = await this._workspaceTrustManagementService.getUriTrustInfo(uri); + return trustInfo.trusted; + } + private isWorkspaceTrusted(): boolean { return this._workspaceTrustManagementService.isWorkspaceTrusted(); } @@ -259,6 +266,10 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { this._proxy.$onDidGrantWorkspaceTrust(); } + private _onDidChangeWorkspaceTrustedFolders(): void { + this._proxy.$onDidChangeWorkspaceTrustedFolders(); + } + // --- edit sessions --- private registeredEditSessionProviders = new Map(); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 21cb7bb1839..0f9b9ec6e87 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1271,6 +1271,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'workspaceTrust'); return extHostWorkspace.requestWorkspaceTrust(options); }, + isResourceTrusted: (resource: vscode.Uri) => { + checkProposedApiEnabled(extension, 'workspaceTrust'); + return extHostWorkspace.isResourceTrusted(resource); + }, + onDidChangeWorkspaceTrustedFolders: (listener, thisArgs?, disposables?) => { + checkProposedApiEnabled(extension, 'workspaceTrust'); + return _asExtensionEvent(extHostWorkspace.onDidChangeWorkspaceTrustedFolders)(listener, thisArgs, disposables); + }, onDidGrantWorkspaceTrust: (listener, thisArgs?, disposables?) => { return _asExtensionEvent(extHostWorkspace.onDidGrantWorkspaceTrust)(listener, thisArgs, disposables); }, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index cb2b3cd4b1f..305568f3b3d 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1613,6 +1613,7 @@ export interface MainThreadWorkspaceShape extends IDisposable { $loadCertificates(): Promise; $requestResourceTrust(options: ResourceTrustRequestOptionsDto): Promise; $requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise; + $isResourceTrusted(resource: UriComponents): Promise; $registerEditSessionIdentityProvider(handle: number, scheme: string): void; $unregisterEditSessionIdentityProvider(handle: number): void; $registerCanonicalUriProvider(handle: number, scheme: string): void; @@ -2096,6 +2097,7 @@ export interface ExtHostWorkspaceShape { $acceptWorkspaceData(workspace: IWorkspaceData | null): void; $handleTextSearchResult(result: search.IRawFileMatch2, requestId: number): void; $onDidGrantWorkspaceTrust(): void; + $onDidChangeWorkspaceTrustedFolders(): void; $getEditSessionIdentifier(folder: UriComponents, token: CancellationToken): Promise; $provideEditSessionIdentityMatch(folder: UriComponents, identity1: string, identity2: string, token: CancellationToken): Promise; $onWillCreateEditSessionIdentity(folder: UriComponents, token: CancellationToken, timeout: number): Promise; diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index 89b4ecf179a..55ca51e9a64 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -189,6 +189,9 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac private readonly _onDidGrantWorkspaceTrust = new Emitter(); readonly onDidGrantWorkspaceTrust: Event = this._onDidGrantWorkspaceTrust.event; + private readonly _onDidChangeWorkspaceTrustedFolders = new Emitter(); + readonly onDidChangeWorkspaceTrustedFolders: Event = this._onDidChangeWorkspaceTrustedFolders.event; + private readonly _logService: ILogService; private readonly _requestIdProvider: Counter; private readonly _barrier: Barrier; @@ -821,6 +824,14 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac } } + $onDidChangeWorkspaceTrustedFolders(): void { + this._onDidChangeWorkspaceTrustedFolders.fire(); + } + + isResourceTrusted(resource: vscode.Uri): Promise { + return this._proxy.$isResourceTrusted(resource); + } + // --- edit sessions --- private _providerHandlePool = 0; diff --git a/src/vscode-dts/vscode.proposed.workspaceTrust.d.ts b/src/vscode-dts/vscode.proposed.workspaceTrust.d.ts index 2c8edbd9d18..aad6987c2b5 100644 --- a/src/vscode-dts/vscode.proposed.workspaceTrust.d.ts +++ b/src/vscode-dts/vscode.proposed.workspaceTrust.d.ts @@ -34,6 +34,17 @@ declare module 'vscode' { } export namespace workspace { + /** + * Event fired when the list of workspace trusted folders changes. + */ + export const onDidChangeWorkspaceTrustedFolders: Event; + + /** + * Check whether the given resource is trusted + * @param resource + */ + export function isResourceTrusted(resource: Uri): Thenable; + /** * Prompt the user to chose whether to trust the specified resource (ex: folder) * @param options Object describing the properties of the resource trust request. From b6aa349a9b44351ed806e3a87f365470a9aa921f Mon Sep 17 00:00:00 2001 From: eli-w-king Date: Wed, 28 Jan 2026 20:30:54 -0800 Subject: [PATCH 058/152] added color to boolean values --- extensions/theme-2026/themes/2026-dark.json | 11 +++++++++++ extensions/theme-2026/themes/2026-light.json | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 4010ebd1e13..983d5aed259 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -246,6 +246,8 @@ "gauge.errorBackground": "#F287724D", "chat.requestBubbleBackground": "#488FAE26", "chat.requestBubbleHoverBackground": "#488FAE46", + "editorCommentsWidget.rangeBackground": "#488FAE26", + "editorCommentsWidget.rangeActiveBackground": "#488FAE46", "charts.foreground": "#CCCCCC", "charts.lines": "#C8CACC80", "charts.blue": "#57A3F8", @@ -287,6 +289,15 @@ "foreground": "#C48081" } }, + { + "name": "Language constants", + "scope": [ + "constant.language" + ], + "settings": { + "foreground": "#4F8FDD" + } + }, { "name": "HTML/XML tags", "scope": [ diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index 6090fdb4c9a..f010fca5da5 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -252,6 +252,8 @@ "quickInputTitle.background": "#F5F7FAE6", "chat.requestBubbleBackground": "#EEF4FB", "chat.requestBubbleHoverBackground": "#E6EDFA", + "editorCommentsWidget.rangeBackground": "#EEF4FB", + "editorCommentsWidget.rangeActiveBackground": "#E6EDFA", "charts.foreground": "#202020", "charts.lines": "#20202066", "charts.blue": "#1A5CFF", @@ -293,6 +295,15 @@ "foreground": "#B86855" } }, + { + "name": "Language constants", + "scope": [ + "constant.language" + ], + "settings": { + "foreground": "#5460C1" + } + }, { "name": "HTML/XML tags", "scope": [ From 544e425b4d0f37ad683a2fba47e96503a4aafe5c Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 29 Jan 2026 15:34:13 +1100 Subject: [PATCH 059/152] Hide tool picker in Agents for contributed sessions (#291473) --- .../contrib/chat/browser/widget/input/modePickerActionItem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index 5f329770d4f..4def3e504b0 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -107,7 +107,7 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { openerService.open(modeResource.get()); } }); - } else { + } else if (!customAgentTarget) { const label = localize('configureToolsFor', "Configure tools for {0} agent", mode.label.get()); toolbarActions.push({ id: `configureTools:${mode.id}`, From b498a63c1c78297bcce901aa6f20491988111bde Mon Sep 17 00:00:00 2001 From: eli-w-king Date: Wed, 28 Jan 2026 20:52:07 -0800 Subject: [PATCH 060/152] revised inline titles --- extensions/theme-2026/themes/2026-dark.json | 4 ++-- extensions/theme-2026/themes/2026-light.json | 2 +- extensions/theme-2026/themes/styles.css | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 983d5aed259..c74d06e9776 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -6,7 +6,7 @@ "foreground": "#bfbfbf", "disabledForeground": "#444444", "errorForeground": "#f48771", - "descriptionForeground": "#888888", + "descriptionForeground": "#999999", "icon.foreground": "#888888", "focusBorder": "#3994BCB3", "textBlockQuote.background": "#242526", @@ -547,4 +547,4 @@ "customLiteral": "#DCDCAA", "numberLiteral": "#b5cea8" } -} \ No newline at end of file +} diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index f010fca5da5..c44c76ff275 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -6,7 +6,7 @@ "foreground": "#202020", "disabledForeground": "#BBBBBB", "errorForeground": "#ad0707", - "descriptionForeground": "#666666", + "descriptionForeground": "#555555", "icon.foreground": "#666666", "focusBorder": "#0069CCFF", "textBlockQuote.background": "#EDEDED", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index c9b0d7695f3..f695504e7c2 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -438,6 +438,12 @@ border-top-width: 0; } +/* Quick Input List - use descriptionForeground color for descriptions */ +.monaco-workbench .quick-input-list .monaco-icon-label .label-description { + opacity: 1; + color: var(--vscode-descriptionForeground); +} + /* Remove Borders */ .monaco-workbench.vs .part.sidebar { border-right: none !important; border-left: none !important; } .monaco-workbench.vs .part.auxiliarybar { border-right: none !important; border-left: none !important; } From 7d83ce8c7ef3a3b21357ddf3dac127a058004319 Mon Sep 17 00:00:00 2001 From: Robo Date: Thu, 29 Jan 2026 13:52:07 +0900 Subject: [PATCH 061/152] chore: update node-pty@1.2.0-beta.10 (#291471) --- package-lock.json | 8 ++++---- package.json | 4 ++-- remote/package-lock.json | 8 ++++---- remote/package.json | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7c1db07073a..e486587a3af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "minimist": "^1.2.8", "native-is-elevated": "0.9.0", "native-keymap": "^3.3.5", - "node-pty": "^1.2.0-beta.8", + "node-pty": "^1.2.0-beta.10", "open": "^10.1.2", "tas-client": "0.3.1", "undici": "^7.18.2", @@ -12876,9 +12876,9 @@ } }, "node_modules/node-pty": { - "version": "1.2.0-beta.8", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.8.tgz", - "integrity": "sha512-2gDjTGB/VaMV8cmFMg0d7IfLcWkxtyekn9VSqpq+tUOiu5+nnLfVXYHZbjZCq1kXhnxbdlBjaKLvvVWIbFkicw==", + "version": "1.2.0-beta.10", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.10.tgz", + "integrity": "sha512-vONwSCtAiOVNxeaP/lzDdRw733Q6uB/ELOCFM8DUfKMw6rTFovwFCuvqr9usya7JXV2pfaers3EwuzZfv0QtwA==", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 3ddd35087b5..5023a7daebb 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "minimist": "^1.2.8", "native-is-elevated": "0.9.0", "native-keymap": "^3.3.5", - "node-pty": "^1.2.0-beta.8", + "node-pty": "^1.2.0-beta.10", "open": "^10.1.2", "tas-client": "0.3.1", "undici": "^7.18.2", @@ -240,4 +240,4 @@ "optionalDependencies": { "windows-foreground-love": "0.6.1" } -} \ No newline at end of file +} diff --git a/remote/package-lock.json b/remote/package-lock.json index d7e6a9a38ce..5af6b961bff 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -38,7 +38,7 @@ "katex": "^0.16.22", "kerberos": "2.1.1", "minimist": "^1.2.8", - "node-pty": "^1.2.0-beta.8", + "node-pty": "^1.2.0-beta.10", "tas-client": "0.3.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", @@ -1053,9 +1053,9 @@ } }, "node_modules/node-pty": { - "version": "1.2.0-beta.8", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.8.tgz", - "integrity": "sha512-2gDjTGB/VaMV8cmFMg0d7IfLcWkxtyekn9VSqpq+tUOiu5+nnLfVXYHZbjZCq1kXhnxbdlBjaKLvvVWIbFkicw==", + "version": "1.2.0-beta.10", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.10.tgz", + "integrity": "sha512-vONwSCtAiOVNxeaP/lzDdRw733Q6uB/ELOCFM8DUfKMw6rTFovwFCuvqr9usya7JXV2pfaers3EwuzZfv0QtwA==", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/remote/package.json b/remote/package.json index 9d5968c3f89..7f9b6f8b80d 100644 --- a/remote/package.json +++ b/remote/package.json @@ -33,7 +33,7 @@ "katex": "^0.16.22", "kerberos": "2.1.1", "minimist": "^1.2.8", - "node-pty": "^1.2.0-beta.8", + "node-pty": "^1.2.0-beta.10", "tas-client": "0.3.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", From 8ca63698440460dac90dc3084e23e25bd925f68c Mon Sep 17 00:00:00 2001 From: eli-w-king Date: Wed, 28 Jan 2026 20:55:16 -0800 Subject: [PATCH 062/152] more contrast between selected and highlighted (right click) files --- extensions/theme-2026/themes/2026-dark.json | 4 ++-- extensions/theme-2026/themes/2026-light.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index c74d06e9776..49376f5c90a 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -53,9 +53,9 @@ "badge.background": "#3994BC", "badge.foreground": "#FFFFFF", "progressBar.background": "#878889", - "list.activeSelectionBackground": "#3994BC26", + "list.activeSelectionBackground": "#3994BC55", "list.activeSelectionForeground": "#bfbfbf", - "list.inactiveSelectionBackground": "#242526", + "list.inactiveSelectionBackground": "#2C2D2E", "list.inactiveSelectionForeground": "#bfbfbf", "list.hoverBackground": "#262728", "list.hoverForeground": "#bfbfbf", diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index c44c76ff275..2e1078a4cb3 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -58,9 +58,9 @@ "badge.background": "#0069CC", "badge.foreground": "#FFFFFF", "progressBar.background": "#0069CC", - "list.activeSelectionBackground": "#0069CC1A", + "list.activeSelectionBackground": "#0069CC44", "list.activeSelectionForeground": "#202020", - "list.inactiveSelectionBackground": "#EDEDED", + "list.inactiveSelectionBackground": "#E0E0E0", "list.inactiveSelectionForeground": "#202020", "list.hoverBackground": "#F7F7F7", "list.hoverForeground": "#202020", From 7357248471fd273d13d29e61bc56c0a2e4ac9a9a Mon Sep 17 00:00:00 2001 From: eli-w-king Date: Wed, 28 Jan 2026 21:16:06 -0800 Subject: [PATCH 063/152] added active tab indicator --- extensions/theme-2026/themes/2026-dark.json | 4 +++- extensions/theme-2026/themes/2026-light.json | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 49376f5c90a..b2687faaa13 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -71,10 +71,11 @@ "activityBar.foreground": "#bfbfbf", "activityBar.inactiveForeground": "#888888", "activityBar.border": "#2A2B2CFF", - "activityBar.activeBorder": "#2A2B2CFF", + "activityBar.activeBorder": "#3994BC", "activityBar.activeFocusBorder": "#3994BCB3", "activityBarBadge.background": "#3994BC", "activityBarBadge.foreground": "#FFFFFF", + "activityBarTop.activeBorder": "#3994BC", "sideBar.background": "#191A1B", "sideBar.foreground": "#bfbfbf", "sideBar.border": "#2A2B2CFF", @@ -184,6 +185,7 @@ "tab.border": "#2A2B2CFF", "tab.lastPinnedBorder": "#2A2B2CFF", "tab.activeBorder": "#121314", + "tab.activeBorderTop": "#3994BC", "tab.hoverBackground": "#262728", "tab.hoverForeground": "#bfbfbf", "tab.unfocusedActiveBackground": "#121314", diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index 2e1078a4cb3..c099200cfe5 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -76,10 +76,11 @@ "activityBar.foreground": "#202020", "activityBar.inactiveForeground": "#666666", "activityBar.border": "#ECEDEEFF", - "activityBar.activeBorder": "#ECEDEEFF", + "activityBar.activeBorder": "#0069CC", "activityBar.activeFocusBorder": "#0069CCFF", "activityBarBadge.background": "#0069CC", "activityBarBadge.foreground": "#FFFFFF", + "activityBarTop.activeBorder": "#0069CC", "sideBar.background": "#FFFFFF", "sideBar.foreground": "#202020", "sideBar.border": "#ECEDEEFF", @@ -189,6 +190,7 @@ "tab.border": "#ECEDEEFF", "tab.lastPinnedBorder": "#ECEDEEFF", "tab.activeBorder": "#FFFFFF", + "tab.activeBorderTop": "#0069CC", "tab.hoverBackground": "#F7F7F7", "tab.hoverForeground": "#202020", "tab.unfocusedActiveBackground": "#FFFFFF", From f9775502365e6225878e00eb3c58c2ba6a0fce4b Mon Sep 17 00:00:00 2001 From: eli-w-king Date: Wed, 28 Jan 2026 21:29:43 -0800 Subject: [PATCH 064/152] added active tab indicators --- extensions/theme-2026/themes/2026-dark.json | 12 ++--- extensions/theme-2026/themes/2026-light.json | 48 ++++++++++---------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index b2687faaa13..26f52ae7f54 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -71,11 +71,11 @@ "activityBar.foreground": "#bfbfbf", "activityBar.inactiveForeground": "#888888", "activityBar.border": "#2A2B2CFF", - "activityBar.activeBorder": "#3994BC", + "activityBar.activeBorder": "#bfbfbf", "activityBar.activeFocusBorder": "#3994BCB3", "activityBarBadge.background": "#3994BC", "activityBarBadge.foreground": "#FFFFFF", - "activityBarTop.activeBorder": "#3994BC", + "activityBarTop.activeBorder": "#bfbfbf", "sideBar.background": "#191A1B", "sideBar.foreground": "#bfbfbf", "sideBar.border": "#2A2B2CFF", @@ -160,13 +160,13 @@ "editorOverviewRuler.errorForeground": "#f48771", "editorOverviewRuler.warningForeground": "#e5ba7d", "panel.background": "#191A1B", - "panel.border": "#2A2B2CFF", - "panelTitle.activeBorder": "#3994BC", + "panel.border": "#00000000", + "panelTitle.activeBorder": "#bfbfbf", "panelTitle.activeForeground": "#bfbfbf", "panelTitle.inactiveForeground": "#888888", "statusBar.background": "#191A1B", "statusBar.foreground": "#888888", - "statusBar.border": "#2A2B2CFF", + "statusBar.border": "#00000000", "statusBar.focusBorder": "#3994BCB3", "statusBar.debuggingBackground": "#3994BC", "statusBar.debuggingForeground": "#FFFFFF", @@ -185,7 +185,7 @@ "tab.border": "#2A2B2CFF", "tab.lastPinnedBorder": "#2A2B2CFF", "tab.activeBorder": "#121314", - "tab.activeBorderTop": "#3994BC", + "tab.activeBorderTop": "#bfbfbf", "tab.hoverBackground": "#262728", "tab.hoverForeground": "#bfbfbf", "tab.unfocusedActiveBackground": "#121314", diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index c099200cfe5..ea840807b51 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -72,15 +72,15 @@ "list.invalidItemForeground": "#BBBBBB", "list.errorForeground": "#ad0707", "list.warningForeground": "#667309", - "activityBar.background": "#F5F7FA", + "activityBar.background": "#E8ECF2", "activityBar.foreground": "#202020", "activityBar.inactiveForeground": "#666666", "activityBar.border": "#ECEDEEFF", - "activityBar.activeBorder": "#0069CC", + "activityBar.activeBorder": "#000000", "activityBar.activeFocusBorder": "#0069CCFF", "activityBarBadge.background": "#0069CC", "activityBarBadge.foreground": "#FFFFFF", - "activityBarTop.activeBorder": "#0069CC", + "activityBarTop.activeBorder": "#000000", "sideBar.background": "#FFFFFF", "sideBar.foreground": "#202020", "sideBar.border": "#ECEDEEFF", @@ -88,9 +88,9 @@ "sideBarSectionHeader.background": "#FFFFFF", "sideBarSectionHeader.foreground": "#202020", "sideBarSectionHeader.border": "#ECEDEEFF", - "titleBar.activeBackground": "#F5F7FA", + "titleBar.activeBackground": "#E8ECF2", "titleBar.activeForeground": "#424242", - "titleBar.inactiveBackground": "#F5F7FA", + "titleBar.inactiveBackground": "#E8ECF2", "titleBar.inactiveForeground": "#666666", "titleBar.border": "#ECEDEEFF", "menubar.selectionBackground": "#EDEDED", @@ -103,8 +103,8 @@ "menu.border": "#ECEDEEFF", "commandCenter.foreground": "#202020", "commandCenter.activeForeground": "#202020", - "commandCenter.background": "#F5F7FA", - "commandCenter.activeBackground": "#F5F7FAB3", + "commandCenter.background": "#E8ECF2", + "commandCenter.activeBackground": "#E8ECF2B3", "commandCenter.border": "#D8D8D866", "editor.background": "#FFFFFF", "editor.foreground": "#202020", @@ -130,26 +130,26 @@ "editorCodeLens.foreground": "#666666", "editorBracketMatch.background": "#0069CC40", "editorBracketMatch.border": "#ECEDEEFF", - "editorWidget.background": "#F5F7FAE6", + "editorWidget.background": "#E8ECF2E6", "editorWidget.border": "#ECEDEEFF", "editorWidget.foreground": "#202020", - "editorSuggestWidget.background": "#F5F7FAE6", + "editorSuggestWidget.background": "#E8ECF2E6", "editorSuggestWidget.border": "#ECEDEEFF", "editorSuggestWidget.foreground": "#202020", "editorSuggestWidget.highlightForeground": "#0069CC", "editorSuggestWidget.selectedBackground": "#0069CC26", - "editorHoverWidget.background": "#F5F7FAE6", + "editorHoverWidget.background": "#E8ECF2E6", "editorHoverWidget.border": "#ECEDEEFF", "peekView.border": "#0069CC", - "peekViewEditor.background": "#F5F7FAE6", + "peekViewEditor.background": "#E8ECF2E6", "peekViewEditor.matchHighlightBackground": "#0069CC33", - "peekViewResult.background": "#F5F7FAE6", + "peekViewResult.background": "#E8ECF2E6", "peekViewResult.fileForeground": "#202020", "peekViewResult.lineForeground": "#666666", "peekViewResult.matchHighlightBackground": "#0069CC33", "peekViewResult.selectionBackground": "#0069CC26", "peekViewResult.selectionForeground": "#202020", - "peekViewTitle.background": "#F5F7FAE6", + "peekViewTitle.background": "#E8ECF2E6", "peekViewTitleDescription.foreground": "#666666", "peekViewTitleLabel.foreground": "#202020", "editorGutter.addedBackground": "#587c0c", @@ -165,17 +165,17 @@ "editorOverviewRuler.warningForeground": "#667309", "editorGutter.background": "#FFFFFF", "panel.background": "#FFFFFF", - "panel.border": "#ECEDEEFF", - "panelTitle.activeBorder": "#0069CC", + "panel.border": "#00000000", + "panelTitle.activeBorder": "#000000", "panelTitle.activeForeground": "#202020", "panelTitle.inactiveForeground": "#666666", - "statusBar.background": "#F5F7FA", + "statusBar.background": "#E8ECF2", "statusBar.foreground": "#666666", - "statusBar.border": "#ECEDEEFF", + "statusBar.border": "#00000000", "statusBar.focusBorder": "#0069CCFF", "statusBar.debuggingBackground": "#0069CC", "statusBar.debuggingForeground": "#FFFFFF", - "statusBar.noFolderBackground": "#F5F7FA", + "statusBar.noFolderBackground": "#E8ECF2", "statusBar.noFolderForeground": "#666666", "statusBarItem.activeBackground": "#E6E6E6", "statusBarItem.hoverBackground": "#F7F7F7", @@ -190,7 +190,7 @@ "tab.border": "#ECEDEEFF", "tab.lastPinnedBorder": "#ECEDEEFF", "tab.activeBorder": "#FFFFFF", - "tab.activeBorderTop": "#0069CC", + "tab.activeBorderTop": "#000000", "tab.hoverBackground": "#F7F7F7", "tab.hoverForeground": "#202020", "tab.unfocusedActiveBackground": "#FFFFFF", @@ -203,13 +203,13 @@ "breadcrumb.background": "#FFFFFF", "breadcrumb.focusForeground": "#202020", "breadcrumb.activeSelectionForeground": "#202020", - "breadcrumbPicker.background": "#F5F7FAE6", + "breadcrumbPicker.background": "#E8ECF2E6", "notificationCenter.border": "#ECEDEEFF", "notificationCenterHeader.foreground": "#202020", - "notificationCenterHeader.background": "#F5F7FAE6", + "notificationCenterHeader.background": "#E8ECF2E6", "notificationToast.border": "#ECEDEEFF", "notifications.foreground": "#202020", - "notifications.background": "#F5F7FAE6", + "notifications.background": "#E8ECF2E6", "notifications.border": "#ECEDEEFF", "notificationLink.foreground": "#0069CC", "notificationsWarningIcon.foreground": "#B69500", @@ -224,7 +224,7 @@ "extensionButton.prominentHoverBackground": "#0064CC", "pickerGroup.border": "#ECEDEEFF", "pickerGroup.foreground": "#202020", - "quickInput.background": "#F5F7FAE6", + "quickInput.background": "#E8ECF2E6", "quickInput.foreground": "#202020", "quickInputList.focusBackground": "#0069CC1A", "quickInputList.focusForeground": "#202020", @@ -251,7 +251,7 @@ "gauge.errorForeground": "#ad0707", "gauge.errorBackground": "#ad070740", "statusBarItem.prominentHoverForeground": "#FFFFFF", - "quickInputTitle.background": "#F5F7FAE6", + "quickInputTitle.background": "#E8ECF2E6", "chat.requestBubbleBackground": "#EEF4FB", "chat.requestBubbleHoverBackground": "#E6EDFA", "editorCommentsWidget.rangeBackground": "#EEF4FB", From cbd6d81bd5ce1e7e2f8d7cc2d48da7f841e2ae63 Mon Sep 17 00:00:00 2001 From: eli-w-king Date: Wed, 28 Jan 2026 21:33:21 -0800 Subject: [PATCH 065/152] inactive window indicator --- extensions/theme-2026/themes/2026-light.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index ea840807b51..97c155e4d96 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -90,7 +90,7 @@ "sideBarSectionHeader.border": "#ECEDEEFF", "titleBar.activeBackground": "#E8ECF2", "titleBar.activeForeground": "#424242", - "titleBar.inactiveBackground": "#E8ECF2", + "titleBar.inactiveBackground": "#FFFFFF", "titleBar.inactiveForeground": "#666666", "titleBar.border": "#ECEDEEFF", "menubar.selectionBackground": "#EDEDED", From 9f5995dd3ff6606eeb13e67f34d889ce81b846d7 Mon Sep 17 00:00:00 2001 From: Robo Date: Thu, 29 Jan 2026 15:13:32 +0900 Subject: [PATCH 066/152] ci: restore reliability of terrapin check (#291484) * ci: remove continueOnError setting * fix: misconfigured peerdepedency setting for subfolders during postinstall * fix: restore package lock urls for reliable git check --- .../product-npm-package-validate.yml | 7 +++- build/npm/postinstall.ts | 36 ++++++++++++++++++- test/mcp/package-lock.json | 10 ++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/build/azure-pipelines/product-npm-package-validate.yml b/build/azure-pipelines/product-npm-package-validate.yml index d596f9f7b37..b256107437d 100644 --- a/build/azure-pipelines/product-npm-package-validate.yml +++ b/build/azure-pipelines/product-npm-package-validate.yml @@ -17,7 +17,6 @@ jobs: name: 1es-ubuntu-22.04-x64 os: linux timeoutInMinutes: 40000 - continueOnError: true variables: VSCODE_ARCH: x64 steps: @@ -106,6 +105,12 @@ jobs: timeoutInMinutes: 400 condition: and(succeeded(), eq(variables['SHOULD_VALIDATE'], 'true')) + - script: | + set -e + find . -name 'package-lock.json' -exec sed -i "s|$NPM_REGISTRY|https://registry.npmjs.org/|g" {} \; + displayName: Restore registry URLs in package-lock.json + condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none'), eq(variables['SHOULD_VALIDATE'], 'true')) + - script: .github/workflows/check-clean-git-state.sh displayName: Check clean git state condition: and(succeeded(), eq(variables['SHOULD_VALIDATE'], 'true')) diff --git a/build/npm/postinstall.ts b/build/npm/postinstall.ts index c4bbbf52960..b6a934f74b3 100644 --- a/build/npm/postinstall.ts +++ b/build/npm/postinstall.ts @@ -11,6 +11,7 @@ import { dirs } from './dirs.ts'; const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; const root = path.dirname(path.dirname(import.meta.dirname)); +const rootNpmrcConfigKeys = getNpmrcConfigKeys(path.join(root, '.npmrc')); function log(dir: string, message: string) { if (process.stdout.isTTY) { @@ -125,6 +126,36 @@ function removeParcelWatcherPrebuild(dir: string) { } } +function getNpmrcConfigKeys(npmrcPath: string): string[] { + if (!fs.existsSync(npmrcPath)) { + return []; + } + const lines = fs.readFileSync(npmrcPath, 'utf8').split('\n'); + const keys: string[] = []; + for (const line of lines) { + const trimmedLine = line.trim(); + if (trimmedLine && !trimmedLine.startsWith('#')) { + const eqIndex = trimmedLine.indexOf('='); + if (eqIndex > 0) { + keys.push(trimmedLine.substring(0, eqIndex).trim()); + } + } + } + return keys; +} + +function clearInheritedNpmrcConfig(dir: string, env: NodeJS.ProcessEnv): void { + const dirNpmrcPath = path.join(root, dir, '.npmrc'); + if (fs.existsSync(dirNpmrcPath)) { + return; + } + + for (const key of rootNpmrcConfigKeys) { + const envKey = `npm_config_${key.replace(/-/g, '_')}`; + delete env[envKey]; + } +} + for (const dir of dirs) { if (dir === '') { @@ -179,7 +210,10 @@ for (const dir of dirs) { continue; } - npmInstall(dir, opts); + // For directories that don't define their own .npmrc, clear inherited config + const env = { ...process.env }; + clearInheritedNpmrcConfig(dir, env); + npmInstall(dir, { env }); } child_process.execSync('git config pull.rebase merges'); diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index 7438ab0d27a..75ce8f4867d 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -839,6 +839,16 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.11.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", + "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", From 52c040f24c19b99773549919923307ee86a92827 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 28 Jan 2026 22:46:43 -0800 Subject: [PATCH 067/152] fix: update freeform input handling to always show for single/multi select questions (#291488) * fix: update freeform input handling to always show for single/multi select questions * fix: update skip functionality to return structured answers for questions --- .../chatQuestionCarouselPart.ts | 72 ++++++++----------- .../chatQuestionCarouselPart.test.ts | 19 +++-- 2 files changed, 42 insertions(+), 49 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 6ed3d82f9de..2986268d84f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts @@ -321,10 +321,9 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent : undefined; const selectedValue = defaultOption?.value; - if (question.allowFreeformInput) { - return selectedValue !== undefined ? { selectedValue, freeformValue: undefined } : undefined; - } - return selectedValue; + // Note: Freeform input is always shown regardless of the `allowFreeformInput` API property. + // The property is kept for backwards compatibility but is no longer used. + return selectedValue !== undefined ? { selectedValue, freeformValue: undefined } : undefined; } case 'multiSelect': { @@ -336,10 +335,9 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent .map(opt => opt.value) .filter(v => v !== undefined) ?? []; - if (question.allowFreeformInput) { - return selectedValues.length > 0 ? { selectedValues, freeformValue: undefined } : undefined; - } - return selectedValues; + // Note: Freeform input is always shown regardless of the `allowFreeformInput` API property. + // The property is kept for backwards compatibility but is no longer used. + return selectedValues.length > 0 ? { selectedValues, freeformValue: undefined } : undefined; } default: @@ -494,8 +492,9 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._radioInputs.set(question.id, radioInputs); - // Add freeform input if allowed - if (question.allowFreeformInput) { + // Note: Freeform input is always shown regardless of the `allowFreeformInput` API property. + // The property is kept for backwards compatibility but is no longer used. + { const freeformContainer = dom.$('.chat-question-freeform'); const freeformLabelId = `freeform-label-${question.id}`; const freeformLabel = dom.$('.chat-question-freeform-label'); @@ -586,8 +585,9 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._checkboxInputs.set(question.id, checkboxInputs); - // Add freeform input if allowed - if (question.allowFreeformInput) { + // Note: Freeform input is always shown regardless of the `allowFreeformInput` API property. + // The property is kept for backwards compatibility but is no longer used. + { const freeformContainer = dom.$('.chat-question-freeform'); const freeformLabelId = `freeform-label-${question.id}`; const freeformLabel = dom.$('.chat-question-freeform-label'); @@ -613,14 +613,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent } })); - // uncheck checkboxes when there is text - this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.INPUT, () => { - if (freeformTextarea.value.trim()) { - for (const checkbox of checkboxInputs) { - checkbox.checked = false; - } - } - })); + // For multiSelect, both checkboxes and freeform input are combined, so don't uncheck on input freeformContainer.appendChild(freeformTextarea); container.appendChild(freeformContainer); @@ -658,18 +651,16 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent selectedValue = defaultOption?.value; } - // Include freeform value if allowed - if (question.allowFreeformInput) { - const freeformTextarea = this._freeformTextareas.get(question.id); - const freeformValue = freeformTextarea?.value !== '' ? freeformTextarea?.value : undefined; - if (freeformValue || selectedValue !== undefined) { - // if there is text in freeform, don't include selected - return { selectedValue: freeformValue ? undefined : selectedValue, freeformValue }; - } - return undefined; + // Note: Freeform input is always shown regardless of the `allowFreeformInput` API property. + // The property is kept for backwards compatibility but is no longer used. + // For singleSelect, if freeform value is provided, use only that (ignore selected value). + const freeformTextarea = this._freeformTextareas.get(question.id); + const freeformValue = freeformTextarea?.value !== '' ? freeformTextarea?.value : undefined; + if (freeformValue || selectedValue !== undefined) { + // if there is text in freeform, don't include selected + return { selectedValue: freeformValue ? undefined : selectedValue, freeformValue }; } - - return selectedValue; + return undefined; } case 'multiSelect': { @@ -698,18 +689,15 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent finalSelectedValues = defaultValues?.filter(v => v !== undefined) || []; } - // Include freeform value if allowed - if (question.allowFreeformInput) { - const freeformTextarea = this._freeformTextareas.get(question.id); - const freeformValue = freeformTextarea?.value !== '' ? freeformTextarea?.value : undefined; - if (freeformValue || finalSelectedValues.length > 0) { - // if there is text in freeform, don't include selected - return { selectedValues: freeformValue ? [] : finalSelectedValues, freeformValue }; - } - return undefined; + // Note: Freeform input is always shown regardless of the `allowFreeformInput` API property. + // The property is kept for backwards compatibility but is no longer used. + // For multiSelect, include both selected values and freeform input together. + const freeformTextarea = this._freeformTextareas.get(question.id); + const freeformValue = freeformTextarea?.value !== '' ? freeformTextarea?.value : undefined; + if (freeformValue || finalSelectedValues.length > 0) { + return { selectedValues: finalSelectedValues, freeformValue }; } - - return finalSelectedValues; + return undefined; } default: diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts index 59b21243c07..552a3b3daf2 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts @@ -396,7 +396,9 @@ suite('ChatQuestionCarouselPart', () => { widget.skip(); assert.ok(submittedAnswers instanceof Map); - assert.strictEqual(submittedAnswers?.get('q1'), 'value_b'); + const answer = submittedAnswers?.get('q1') as { selectedValue: unknown; freeformValue: unknown }; + assert.strictEqual(answer.selectedValue, 'value_b'); + assert.strictEqual(answer.freeformValue, undefined); }); test('skip returns default values for multiSelect questions', () => { @@ -417,11 +419,12 @@ suite('ChatQuestionCarouselPart', () => { widget.skip(); assert.ok(submittedAnswers instanceof Map); - const values = submittedAnswers?.get('q1') as unknown[]; - assert.ok(Array.isArray(values)); - assert.strictEqual(values.length, 2); - assert.ok(values.includes('value_a')); - assert.ok(values.includes('value_c')); + const answer = submittedAnswers?.get('q1') as { selectedValues: unknown[]; freeformValue: unknown }; + assert.ok(Array.isArray(answer.selectedValues)); + assert.strictEqual(answer.selectedValues.length, 2); + assert.ok(answer.selectedValues.includes('value_a')); + assert.ok(answer.selectedValues.includes('value_c')); + assert.strictEqual(answer.freeformValue, undefined); }); test('skip returns defaults for multiple questions', () => { @@ -442,7 +445,9 @@ suite('ChatQuestionCarouselPart', () => { widget.skip(); assert.ok(submittedAnswers instanceof Map); assert.strictEqual(submittedAnswers?.get('q1'), 'text default'); - assert.strictEqual(submittedAnswers?.get('q2'), 'first_value'); + const answer = submittedAnswers?.get('q2') as { selectedValue: unknown; freeformValue: unknown }; + assert.strictEqual(answer.selectedValue, 'first_value'); + assert.strictEqual(answer.freeformValue, undefined); }); test('skip returns empty map when no defaults are provided', () => { From e4e61e5ffaf535a8477969fff6ad930f9e563be1 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:12:59 +0100 Subject: [PATCH 068/152] Git - dispose untrusted repositories when trust state changes (#291498) --- extensions/git/src/model.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index b2bcadb3a71..aabfa256039 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -290,6 +290,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi this._unsafeRepositoriesManager = new UnsafeRepositoriesManager(); workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables); + workspace.onDidChangeWorkspaceTrustedFolders(this.onDidChangeWorkspaceTrustedFolders, this, this.disposables); window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables); window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, this.disposables); workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); @@ -488,6 +489,27 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi } } + private async onDidChangeWorkspaceTrustedFolders(): Promise { + try { + const openRepositoriesToDispose: OpenRepository[] = []; + + for (const openRepository of this.openRepositories) { + const dotGitPath = openRepository.repository.dotGit.commonPath ?? openRepository.repository.dotGit.path; + const isTrusted = await workspace.isResourceTrusted(Uri.file(path.dirname(dotGitPath))); + + if (!isTrusted) { + openRepositoriesToDispose.push(openRepository); + this.logger.trace(`[Model][onDidChangeWorkspaceTrustedFolders] Repository is no longer trusted: ${openRepository.repository.root}`); + } + } + + openRepositoriesToDispose.forEach(r => r.dispose()); + } + catch (err) { + this.logger.warn(`[Model][onDidChangeWorkspaceTrustedFolders] Error: ${err}`); + } + } + private onDidChangeConfiguration(): void { const possibleRepositoryFolders = (workspace.workspaceFolders || []) .filter(folder => workspace.getConfiguration('git', folder.uri).get('enabled') === true) From 246b74bd911e5d6379d9b64f79f90a40b25ef1a7 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 08:29:35 +0000 Subject: [PATCH 069/152] Fix VoiceOver announcement for Agent Sessions view (#291320) * Initial plan * Add proper ARIA roles to AgentSessionsAccessibilityProvider for VoiceOver Add getWidgetRole() returning 'list' and getRole() returning 'listitem' to improve VoiceOver announcement of session titles. This matches the pattern used by ChatAccessibilityProvider. Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> * Add unit tests for AgentSessionsAccessibilityProvider Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> --- .../agentSessions/agentSessionsViewer.ts | 9 ++ ...agentSessionsAccessibilityProvider.test.ts | 98 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsAccessibilityProvider.test.ts diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 97106a0be49..0e6c259d0da 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -7,6 +7,7 @@ import './media/agentsessionsviewer.css'; import { h } from '../../../../../base/browser/dom.js'; import { localize } from '../../../../../nls.js'; import { IIdentityProvider, IListVirtualDelegate, NotSelectableGroupId, NotSelectableGroupIdType } from '../../../../../base/browser/ui/list/list.js'; +import { AriaRole } from '../../../../../base/browser/ui/aria/aria.js'; import { IListAccessibilityProvider } from '../../../../../base/browser/ui/list/listWidget.js'; import { ITreeCompressionDelegate } from '../../../../../base/browser/ui/tree/asyncDataTree.js'; import { ICompressedTreeNode } from '../../../../../base/browser/ui/tree/compressedObjectTreeModel.js'; @@ -537,6 +538,14 @@ export class AgentSessionsListDelegate implements IListVirtualDelegate { + getWidgetRole(): AriaRole { + return 'list'; + } + + getRole(element: AgentSessionListItem): AriaRole | undefined { + return 'listitem'; + } + getWidgetAriaLabel(): string { return localize('agentSessions', "Agent Sessions"); } diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsAccessibilityProvider.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsAccessibilityProvider.test.ts new file mode 100644 index 00000000000..336955f5bb0 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsAccessibilityProvider.test.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { AgentSessionsAccessibilityProvider } from '../../../browser/agentSessions/agentSessionsViewer.js'; +import { AgentSessionSection, IAgentSession, IAgentSessionSection } from '../../../browser/agentSessions/agentSessionsModel.js'; +import { ChatSessionStatus } from '../../../common/chatSessionsService.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; + +suite('AgentSessionsAccessibilityProvider', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + let accessibilityProvider: AgentSessionsAccessibilityProvider; + + function createMockSession(overrides: Partial<{ + id: string; + label: string; + providerLabel: string; + status: ChatSessionStatus; + }> = {}): IAgentSession { + const now = Date.now(); + return { + providerType: 'test', + providerLabel: overrides.providerLabel ?? 'Test', + resource: URI.parse(`test://session/${overrides.id ?? 'default'}`), + status: overrides.status ?? ChatSessionStatus.Completed, + label: overrides.label ?? `Session ${overrides.id ?? 'default'}`, + icon: Codicon.terminal, + timing: { + created: now, + lastRequestEnded: undefined, + lastRequestStarted: undefined, + }, + changes: undefined, + isArchived: () => false, + setArchived: () => { }, + isRead: () => true, + setRead: () => { }, + }; + } + + function createMockSection(section: AgentSessionSection = AgentSessionSection.Today): IAgentSessionSection { + return { + section, + label: 'Today', + sessions: [] + }; + } + + setup(() => { + accessibilityProvider = new AgentSessionsAccessibilityProvider(); + }); + + test('getWidgetRole returns list', () => { + assert.strictEqual(accessibilityProvider.getWidgetRole(), 'list'); + }); + + test('getRole returns listitem for session', () => { + const session = createMockSession(); + assert.strictEqual(accessibilityProvider.getRole(session), 'listitem'); + }); + + test('getRole returns listitem for section', () => { + const section = createMockSection(); + assert.strictEqual(accessibilityProvider.getRole(section), 'listitem'); + }); + + test('getWidgetAriaLabel returns correct label', () => { + assert.strictEqual(accessibilityProvider.getWidgetAriaLabel(), 'Agent Sessions'); + }); + + test('getAriaLabel returns correct label for session', () => { + const session = createMockSession({ + id: 'test-session', + label: 'Test Session Title', + providerLabel: 'Agent' + }); + + const ariaLabel = accessibilityProvider.getAriaLabel(session); + + assert.ok(ariaLabel); + assert.ok(ariaLabel.includes('Test Session Title'), 'Aria label should include the session title'); + assert.ok(ariaLabel.includes('Agent'), 'Aria label should include the provider label'); + }); + + test('getAriaLabel returns correct label for section', () => { + const section = createMockSection(); + const ariaLabel = accessibilityProvider.getAriaLabel(section); + + assert.ok(ariaLabel); + assert.ok(ariaLabel.includes('sessions section'), 'Aria label should indicate it is a section'); + }); +}); From e7ac236d600b0a7d42573748275fd85ed998921a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 29 Jan 2026 09:34:23 +0100 Subject: [PATCH 070/152] chat - drop option to hide chat title (#291505) * chat - drop option to hide chat title * . --- .../chat/browser/actions/chatActions.ts | 23 ------------------- .../agentSessions/agentSessionsActions.ts | 4 ++-- .../contrib/chat/browser/chat.contribution.ts | 5 ---- .../viewPane/chatViewTitleControl.ts | 22 ------------------ .../contrib/chat/common/constants.ts | 1 - .../agentSessionViewModel.test.ts | 6 ----- .../test/browser/workbenchTestServices.ts | 1 - 7 files changed, 2 insertions(+), 60 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 37b1c58b434..7b64c28cbbd 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -1283,26 +1283,3 @@ registerAction2(class EditToolApproval extends Action2 { confirmationService.manageConfirmationPreferences([...toolsService.getAllToolsIncludingDisabled()], scope ? { defaultScope: scope } : undefined); } }); - -registerAction2(class ToggleChatViewTitleAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.chat.toggleChatViewTitle', - title: localize2('chat.toggleChatViewTitle.label', "Show Chat Title"), - toggled: ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewTitleEnabled}`, true), - menu: { - id: MenuId.ChatWelcomeContext, - group: '1_modify', - order: 2, - when: ChatContextKeys.inChatEditor.negate() - } - }); - } - - async run(accessor: ServicesAccessor): Promise { - const configurationService = accessor.get(IConfigurationService); - - const chatViewTitleEnabled = configurationService.getValue(ChatConfiguration.ChatViewTitleEnabled); - await configurationService.updateValue(ChatConfiguration.ChatViewTitleEnabled, !chatViewTitleEnabled); - } -}); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts index b62deb9db0a..9d5bcea5555 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts @@ -48,7 +48,7 @@ export class ToggleShowAgentSessionsAction extends Action2 { menu: { id: MenuId.ChatWelcomeContext, group: '0_sessions', - order: 1, + order: 2, when: ChatContextKeys.inChatEditor.negate() } }); @@ -66,7 +66,7 @@ MenuRegistry.appendMenuItem(MenuId.ChatWelcomeContext, { submenu: agentSessionsOrientationSubmenu, title: localize2('chat.sessionsOrientation', "Sessions Orientation"), group: '0_sessions', - order: 2, + order: 1, when: ChatContextKeys.inChatEditor.negate() }); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index fe519db63af..f1ca19dc37d 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -435,11 +435,6 @@ configurationRegistry.registerConfiguration({ default: 'sideBySide', description: nls.localize('chat.viewSessions.orientation', "Controls the orientation of the chat agent sessions view when it is shown alongside the chat."), }, - [ChatConfiguration.ChatViewTitleEnabled]: { - type: 'boolean', - default: true, - description: nls.localize('chat.viewTitle.enabled', "Show the title of the chat above the chat in the chat view."), - }, [ChatConfiguration.ChatViewProgressBadgeEnabled]: { type: 'boolean', default: false, diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts index dac6fd6bf45..8cd698568b8 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts @@ -14,11 +14,9 @@ import { MarshalledId } from '../../../../../../base/common/marshallingIds.js'; import { localize } from '../../../../../../nls.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; import { Action2, MenuId, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; -import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IChatViewTitleActionContext } from '../../../common/actions/chatActions.js'; import { IChatModel } from '../../../common/model/chatModel.js'; -import { ChatConfiguration } from '../../../common/constants.js'; import { ActionViewItem, IActionViewItemOptions } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IAction } from '../../../../../../base/common/actions.js'; import { AgentSessionsPicker } from '../../agentSessions/agentSessionsPicker.js'; @@ -51,27 +49,15 @@ export class ChatViewTitleControl extends Disposable { constructor( private readonly container: HTMLElement, private readonly delegate: IChatViewTitleDelegate, - @IConfigurationService private readonly configurationService: IConfigurationService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this.render(this.container); - this.registerListeners(); this.registerActions(); } - private registerListeners(): void { - - // Update on configuration changes - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(ChatConfiguration.ChatViewTitleEnabled)) { - this.doUpdate(); - } - })); - } - private registerActions(): void { this._register(registerAction2(class extends Action2 { constructor() { @@ -185,17 +171,9 @@ export class ChatViewTitleControl extends Disposable { } private shouldRender(): boolean { - if (!this.isEnabled()) { - return false; // title hidden via setting - } - return !!this.model?.title; // we need a chat showing and not being empty } - private isEnabled(): boolean { - return this.configurationService.getValue(ChatConfiguration.ChatViewTitleEnabled) === true; - } - getHeight(): number { if (!this.titleContainer || this.titleContainer.style.display === 'none') { return 0; diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 0315ae9ca5e..13056d6991a 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -37,7 +37,6 @@ export enum ChatConfiguration { ChatViewSessionsEnabled = 'chat.viewSessions.enabled', ChatViewSessionsGrouping = 'chat.viewSessions.grouping', ChatViewSessionsOrientation = 'chat.viewSessions.orientation', - ChatViewTitleEnabled = 'chat.viewTitle.enabled', ChatViewProgressBadgeEnabled = 'chat.viewProgressBadge.enabled', SubagentToolCustomAgents = 'chat.customAgentInSubagent.enabled', ShowCodeBlockProgressAnimation = 'chat.agent.codeBlockProgress', diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts index fb1a219a05c..30fea75e913 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts @@ -1412,7 +1412,6 @@ suite('AgentSessions', () => { test('should mark session as read and unread', async () => { return runWithFakedTimers({}, async () => { - // Create session with timing after READ_STATE_INITIAL_DATE so it can be marked unread const futureSessionTiming: IChatSessionItem['timing'] = { created: Date.UTC(2026, 1 /* February */, 1), lastRequestStarted: Date.UTC(2026, 1 /* February */, 1), @@ -1565,7 +1564,6 @@ suite('AgentSessions', () => { test('should consider sessions after initial date as unread by default', async () => { return runWithFakedTimers({}, async () => { - // Session with timing after the READ_STATE_INITIAL_DATE (January 28, 2026) const newSessionTiming: IChatSessionItem['timing'] = { created: Date.UTC(2026, 1 /* February */, 1), lastRequestStarted: Date.UTC(2026, 1 /* February */, 1), @@ -1661,8 +1659,6 @@ suite('AgentSessions', () => { test('should treat archived sessions as read', async () => { return runWithFakedTimers({}, async () => { - // Session with timing after the READ_STATE_INITIAL_DATE (January 28, 2026) - // which would normally be unread const newSessionTiming: IChatSessionItem['timing'] = { created: Date.UTC(2026, 1 /* February */, 1), lastRequestStarted: Date.UTC(2026, 1 /* February */, 1), @@ -1702,7 +1698,6 @@ suite('AgentSessions', () => { test('should mark session as read when archiving', async () => { return runWithFakedTimers({}, async () => { - // Session with timing after the READ_STATE_INITIAL_DATE (January 28, 2026) const newSessionTiming: IChatSessionItem['timing'] = { created: Date.UTC(2026, 1 /* February */, 1), lastRequestStarted: Date.UTC(2026, 1 /* February */, 1), @@ -1748,7 +1743,6 @@ suite('AgentSessions', () => { test('should fire onDidChangeSessions when archiving an unread session', async () => { return runWithFakedTimers({}, async () => { - // Session with timing after the READ_STATE_INITIAL_DATE const newSessionTiming: IChatSessionItem['timing'] = { created: Date.UTC(2026, 1 /* February */, 1), lastRequestStarted: Date.UTC(2026, 1 /* February */, 1), diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 9e8feee8336..81cd3af34f8 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -2127,7 +2127,6 @@ export class TestChatWidgetService implements IChatWidgetService { getWidgetByInputUri(uri: URI): IChatWidget | undefined { return undefined; } openSession(sessionResource: URI): Promise; openSession(sessionResource: URI, target?: PreferredGroup, options?: IChatEditorOptions): Promise; - openSession(sessionResource: URI): Promise; async openSession(sessionResource: unknown, target?: unknown, options?: unknown): Promise { return undefined; } getWidgetBySessionResource(sessionResource: URI): IChatWidget | undefined { return undefined; } getWidgetsByLocations(location: ChatAgentLocation): ReadonlyArray { return []; } From 3b0916710e5ede034f6d385423cfa24ed725f5b8 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 29 Jan 2026 09:35:59 +0100 Subject: [PATCH 071/152] better understanding of picker utilization --- .../chat/browser/actions/chatToolPicker.ts | 186 ++++++++++++++++-- 1 file changed, 174 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts b/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts index e55f256ac76..91e60f599de 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts @@ -558,44 +558,206 @@ export async function showToolsPicker( } // Capture initial state for telemetry comparison - const initialStateString = serializeToolsState(collectResults()); + const initialState = collectResults(); treePicker.show(); await Promise.race([Event.toPromise(Event.any(treePicker.onDidHide, didAcceptFinalItem.event), store)]); - // Send telemetry whether the tool selection changed - sendDidChangeEvent(source, telemetryService, initialStateString !== serializeToolsState(collectResults())); + // Send telemetry about tool selection changes + sendDidChangeEvent(source, telemetryService, initialState, collectResults(), mcpRegistry); store.dispose(); return didAccept ? collectResults() : undefined; } -function serializeToolsState(state: ReadonlyMap): string { - const entries: [string, boolean][] = []; - state.forEach((value, key) => { - entries.push([key.id, value]); - }); - entries.sort((a, b) => a[0].localeCompare(b[0])); - return JSON.stringify(entries); +/** + * Categorizes a tool or toolset source for privacy-safe telemetry. + * Returns identifying info only for built-in/extension tools where names are public. + * For user-defined and user MCP tools, only the category is returned. + * + * @param item - The tool or toolset to categorize + * @param mcpRegistry - The MCP registry to look up collection sources for MCP tools + */ +function categorizeTool(item: IToolData | IToolSet, mcpRegistry: IMcpRegistry): { category: 'builtin' | 'extension' | 'extension-mcp' | 'user-mcp' | 'user-toolset'; name?: string; extensionId?: string } { + const source = item.source; + switch (source.type) { + case 'internal': + // Built-in tools are safe to identify by name + return { category: 'builtin', name: item.id }; + case 'extension': + // Extension tools are public, safe to include name and extension ID + return { category: 'extension', name: item.id, extensionId: source.extensionId.value }; + case 'mcp': { + // MCP tools: check if the collection comes from an extension + // Never include tool names for privacy, but include extension ID if from an extension + const collection = mcpRegistry.collections.get().find(c => c.id === source.collectionId); + if (collection?.source instanceof ExtensionIdentifier) { + return { category: 'extension-mcp', extensionId: collection.source.value }; + } + // User-configured MCP server - don't include any identifying info + return { category: 'user-mcp' }; + } + case 'user': + // User-defined tool sets: don't include names for privacy + return { category: 'user-toolset' }; + case 'external': + // External tools shouldn't appear in the picker, treat as user-defined for safety + return { category: 'user-toolset' }; + default: + assertNever(source); + } } -function sendDidChangeEvent(source: string, telemetryService: ITelemetryService, changed: boolean): void { +interface IToolToggleSummary { + /** Number of built-in tools enabled */ + builtinEnabled: number; + /** Number of built-in tools disabled */ + builtinDisabled: number; + /** Number of extension tools enabled */ + extensionEnabled: number; + /** Number of extension tools disabled */ + extensionDisabled: number; + /** Number of extension MCP tools enabled */ + extensionMcpEnabled: number; + /** Number of extension MCP tools disabled */ + extensionMcpDisabled: number; + /** Number of user MCP tools enabled */ + userMcpEnabled: number; + /** Number of user MCP tools disabled */ + userMcpDisabled: number; + /** Number of user tool sets enabled */ + userToolsetEnabled: number; + /** Number of user tool sets disabled */ + userToolsetDisabled: number; + /** Detailed list of toggled items (only safe-to-log items include names) */ + details: string; +} + +function computeToolToggleSummary( + initialState: ReadonlyMap, + finalState: ReadonlyMap, + mcpRegistry: IMcpRegistry +): IToolToggleSummary { + const summary: IToolToggleSummary = { + builtinEnabled: 0, + builtinDisabled: 0, + extensionEnabled: 0, + extensionDisabled: 0, + extensionMcpEnabled: 0, + extensionMcpDisabled: 0, + userMcpEnabled: 0, + userMcpDisabled: 0, + userToolsetEnabled: 0, + userToolsetDisabled: 0, + details: '' + }; + + const detailItems: { category: string; name?: string; extensionId?: string; enabled: boolean }[] = []; + + // Compare states and record changes + for (const [item, finalEnabled] of finalState) { + const initialEnabled = initialState.get(item) ?? false; + if (initialEnabled === finalEnabled) { + continue; // No change + } + + const categorized = categorizeTool(item, mcpRegistry); + const enabled = finalEnabled; + + switch (categorized.category) { + case 'builtin': + if (enabled) { summary.builtinEnabled++; } else { summary.builtinDisabled++; } + detailItems.push({ category: 'builtin', name: categorized.name, enabled }); + break; + case 'extension': + if (enabled) { summary.extensionEnabled++; } else { summary.extensionDisabled++; } + detailItems.push({ category: 'extension', name: categorized.name, extensionId: categorized.extensionId, enabled }); + break; + case 'extension-mcp': + if (enabled) { summary.extensionMcpEnabled++; } else { summary.extensionMcpDisabled++; } + detailItems.push({ category: 'extension-mcp', extensionId: categorized.extensionId, enabled }); + break; + case 'user-mcp': + if (enabled) { summary.userMcpEnabled++; } else { summary.userMcpDisabled++; } + // Don't include name for privacy + detailItems.push({ category: 'user-mcp', enabled }); + break; + case 'user-toolset': + if (enabled) { summary.userToolsetEnabled++; } else { summary.userToolsetDisabled++; } + // Don't include name for privacy + detailItems.push({ category: 'user-toolset', enabled }); + break; + } + } + + // Serialize details as JSON + summary.details = JSON.stringify(detailItems); + return summary; +} + +function sendDidChangeEvent( + source: string, + telemetryService: ITelemetryService, + initialState: ReadonlyMap, + finalState: ReadonlyMap, + mcpRegistry: IMcpRegistry +): void { + const summary = computeToolToggleSummary(initialState, finalState, mcpRegistry); + const changed = summary.builtinEnabled > 0 || summary.builtinDisabled > 0 || + summary.extensionEnabled > 0 || summary.extensionDisabled > 0 || + summary.extensionMcpEnabled > 0 || summary.extensionMcpDisabled > 0 || + summary.userMcpEnabled > 0 || summary.userMcpDisabled > 0 || + summary.userToolsetEnabled > 0 || summary.userToolsetDisabled > 0; + type ToolPickerClosedEvent = { changed: boolean; source: string; + builtinEnabled: number; + builtinDisabled: number; + extensionEnabled: number; + extensionDisabled: number; + extensionMcpEnabled: number; + extensionMcpDisabled: number; + userMcpEnabled: number; + userMcpDisabled: number; + userToolsetEnabled: number; + userToolsetDisabled: number; + details: string; }; type ToolPickerClosedClassification = { changed: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user changed the tool selection from the initial state.' }; source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the tool picker event.' }; + builtinEnabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of built-in tools that were enabled.' }; + builtinDisabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of built-in tools that were disabled.' }; + extensionEnabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of extension tools that were enabled.' }; + extensionDisabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of extension tools that were disabled.' }; + extensionMcpEnabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of extension MCP tools that were enabled.' }; + extensionMcpDisabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of extension MCP tools that were disabled.' }; + userMcpEnabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of user MCP tools that were enabled.' }; + userMcpDisabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of user MCP tools that were disabled.' }; + userToolsetEnabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of user tool sets that were enabled.' }; + userToolsetDisabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of user tool sets that were disabled.' }; + details: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'JSON array of toggled items. Built-in and extension tools include names; user-defined items only include category.' }; owner: 'benibenj'; - comment: 'Tracks whether users modify tool selection in the tool picker.'; + comment: 'Tracks which tools users toggle in the tool picker, with privacy-safe categorization.'; }; telemetryService.publicLog2('chatToolPickerClosed', { source, changed, + builtinEnabled: summary.builtinEnabled, + builtinDisabled: summary.builtinDisabled, + extensionEnabled: summary.extensionEnabled, + extensionDisabled: summary.extensionDisabled, + extensionMcpEnabled: summary.extensionMcpEnabled, + extensionMcpDisabled: summary.extensionMcpDisabled, + userMcpEnabled: summary.userMcpEnabled, + userMcpDisabled: summary.userMcpDisabled, + userToolsetEnabled: summary.userToolsetEnabled, + userToolsetDisabled: summary.userToolsetDisabled, + details: summary.details, }); } From 7dc9ccc9a09f02c6097a2f8b8de34fd9f0527b98 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 29 Jan 2026 09:40:36 +0100 Subject: [PATCH 072/152] chat - use the sentiment property for AI enablement (#291506) --- .../browser/editTelemetryContribution.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetryContribution.ts b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetryContribution.ts index 273fe2e4a9e..da642b8a11d 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetryContribution.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetryContribution.ts @@ -13,39 +13,39 @@ import { AnnotatedDocuments } from './helpers/annotatedDocuments.js'; import { EditTrackingFeature } from './telemetry/editSourceTrackingFeature.js'; import { VSCodeWorkspace } from './helpers/vscodeObservableWorkspace.js'; import { AiStatsFeature } from './editStats/aiStatsFeature.js'; -import { EDIT_TELEMETRY_SETTING_ID, AI_STATS_SETTING_ID } from './settingIds.js'; -import { ChatConfiguration } from '../../../contrib/chat/common/constants.js'; +import { AI_STATS_SETTING_ID, EDIT_TELEMETRY_SETTING_ID } from './settingIds.js'; +import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; export class EditTelemetryContribution extends Disposable { constructor( - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService, + @ITelemetryService telemetryService: ITelemetryService, + @IChatEntitlementService chatEntitlementService: IChatEntitlementService ) { super(); - const workspace = derived(reader => reader.store.add(this._instantiationService.createInstance(VSCodeWorkspace))); - const annotatedDocuments = derived(reader => reader.store.add(this._instantiationService.createInstance(AnnotatedDocuments, workspace.read(reader)))); + const workspace = derived(reader => reader.store.add(instantiationService.createInstance(VSCodeWorkspace))); + const annotatedDocuments = derived(reader => reader.store.add(instantiationService.createInstance(AnnotatedDocuments, workspace.read(reader)))); - const editSourceTrackingEnabled = observableConfigValue(EDIT_TELEMETRY_SETTING_ID, true, this._configurationService); + const editSourceTrackingEnabled = observableConfigValue(EDIT_TELEMETRY_SETTING_ID, true, configurationService); this._register(autorun(r => { const enabled = editSourceTrackingEnabled.read(r); - if (!enabled || !telemetryLevelEnabled(this._telemetryService, TelemetryLevel.USAGE)) { + if (!enabled || !telemetryLevelEnabled(telemetryService, TelemetryLevel.USAGE)) { return; } - r.store.add(this._instantiationService.createInstance(EditTrackingFeature, workspace.read(r), annotatedDocuments.read(r))); + r.store.add(instantiationService.createInstance(EditTrackingFeature, workspace.read(r), annotatedDocuments.read(r))); })); - const aiStatsEnabled = observableConfigValue(AI_STATS_SETTING_ID, true, this._configurationService); - const chatDisabled = observableConfigValue(ChatConfiguration.AIDisabled, false, this._configurationService); + const aiStatsEnabled = observableConfigValue(AI_STATS_SETTING_ID, true, configurationService); this._register(autorun(r => { const enabled = aiStatsEnabled.read(r); - const aiDisabled = chatDisabled.read(r); + const aiDisabled = chatEntitlementService.sentimentObs.read(r).hidden; if (!enabled || aiDisabled) { return; } - r.store.add(this._instantiationService.createInstance(AiStatsFeature, annotatedDocuments.read(r))); + r.store.add(instantiationService.createInstance(AiStatsFeature, annotatedDocuments.read(r))); })); } } From a56e5a19cdd96af91fd6af6cc2711affef54a7df Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Thu, 29 Jan 2026 00:42:58 -0800 Subject: [PATCH 073/152] Retry network operations on recoverable server errors (#290046) --- .../platform/request/node/requestService.ts | 53 +++- .../request/test/node/requestService.test.ts | 264 +++++++++++++++++- 2 files changed, 313 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/request/node/requestService.ts b/src/vs/platform/request/node/requestService.ts index 73f6f826d39..45209759090 100644 --- a/src/vs/platform/request/node/requestService.ts +++ b/src/vs/platform/request/node/requestService.ts @@ -6,7 +6,7 @@ import type * as http from 'http'; import type * as https from 'https'; import { parse as parseUrl } from 'url'; -import { Promises } from '../../../base/common/async.js'; +import { Promises, timeout } from '../../../base/common/async.js'; import { streamToBufferReadableStream } from '../../../base/common/buffer.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { CancellationError, getErrorMessage } from '../../../base/common/errors.js'; @@ -21,6 +21,26 @@ import { AbstractRequestService, AuthInfo, Credentials, IRequestService, systemC import { Agent, getProxyAgent } from './proxy.js'; import { createGunzip } from 'zlib'; +const TRANSIENT_ERROR_CODES = new Set([ + 'EAI_AGAIN', // DNS lookup timed out + 'ECONNREFUSED', // Connection refused by server + 'EHOSTDOWN', // Host is down + 'EHOSTUNREACH', // No route to host + 'ENETDOWN', // Network is down + 'ENETUNREACH', // Network is unreachable + 'EPROTO' // Protocol error (TLS/SSL handshake failure) +]); + +const IDEMPOTENT_HTTP_METHODS_REGEX = /^(GET|HEAD|OPTIONS)$/i; + +function isTransientError(error: unknown): boolean { + if (error instanceof Error) { + const code = (error as NodeJS.ErrnoException).code; + return !!code && TRANSIENT_ERROR_CODES.has(code); + } + return false; +} + export interface IRawRequestFunction { (options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void): http.ClientRequest; } @@ -153,6 +173,31 @@ async function getNodeRequest(options: IRequestOptions): Promise { + const maxRetries = 3; + let lastError: Error | undefined; + const isIdempotent = IDEMPOTENT_HTTP_METHODS_REGEX.test(options.type || 'GET'); + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + return await nodeRequestAttempt(options, token); + } catch (error) { + lastError = error as Error; + if (error instanceof CancellationError) { + throw error; + } + + if (!isIdempotent || !isTransientError(error) || attempt === maxRetries) { + throw error; + } + + await timeout(100 * attempt, token); + } + } + + throw lastError; +} + +async function nodeRequestAttempt(options: NodeRequestOptions, token: CancellationToken): Promise { return Promises.withAsyncBody(async (resolve, reject) => { const endpoint = parseUrl(options.url!); const rawRequest = options.getRawRequest @@ -238,10 +283,14 @@ export async function nodeRequest(options: NodeRequestOptions, token: Cancellati req.end(); - token.onCancellationRequested(() => { + const cancellationListener = token.onCancellationRequested(() => { + cancellationListener.dispose(); req.abort(); reject(new CancellationError()); }); + + req.on('response', () => cancellationListener.dispose()); + req.on('error', () => cancellationListener.dispose()); }); } diff --git a/src/vs/platform/request/test/node/requestService.test.ts b/src/vs/platform/request/test/node/requestService.test.ts index 18de3cafb8b..8e8c8850149 100644 --- a/src/vs/platform/request/test/node/requestService.test.ts +++ b/src/vs/platform/request/test/node/requestService.test.ts @@ -6,9 +6,10 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { NullLogService } from '../../../log/common/log.js'; -import { lookupKerberosAuthorization } from '../../node/requestService.js'; +import { IRawRequestFunction, lookupKerberosAuthorization, nodeRequest } from '../../node/requestService.js'; import { isWindows } from '../../../../base/common/platform.js'; - +import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { CancellationError } from '../../../../base/common/errors.js'; suite('Request Service', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); @@ -28,4 +29,263 @@ suite('Request Service', () => { , `Unexpected error: ${err}`); } }); + + test('Request cancellation during retry backoff', async () => { + const cts = store.add(new CancellationTokenSource()); + const startTime = Date.now(); + setTimeout(() => cts.cancel(), 50); + + try { + await nodeRequest({ url: 'http://localhost:9999/nonexistent' }, cts.token); + assert.fail('Request should have been cancelled'); + } catch (err) { + const elapsed = Date.now() - startTime; + assert.ok(err instanceof CancellationError, 'Error should be CancellationError'); + assert.ok(elapsed < 200, `Request should be cancelled quickly, but took ${elapsed}ms`); + } + }); + + test('should retry GET requests on transient errors', async () => { + let attemptCount = 0; + const mockRawRequest = (_opts: any, callback: Function) => { + attemptCount++; + const currentAttempt = attemptCount; + const mockReq: any = { + on: (event: string, handler: Function) => { + if (event === 'error' && currentAttempt < 3) { + const err = new Error('Connection refused') as NodeJS.ErrnoException; + err.code = 'ECONNREFUSED'; + setTimeout(() => handler(err), 0); + } + }, + end: () => { + if (currentAttempt >= 3) { + // Succeed on third attempt by calling the response callback + setTimeout(() => callback({ statusCode: 200, headers: {}, on: () => { }, pipe: () => ({ on: () => { } }) }), 0); + } + }, + abort: () => { }, + setTimeout: () => { } + }; + return mockReq; + }; + + try { + await nodeRequest({ + url: 'http://example.com', + type: 'GET', + getRawRequest: () => mockRawRequest as IRawRequestFunction + }, CancellationToken.None); + } catch (err) { + // Expected to eventually succeed or fail after retries + } + + assert.ok(attemptCount > 1, 'GET request should have been retried'); + }); + + test('should NOT retry POST requests', async () => { + let attemptCount = 0; + const mockRawRequest = () => { + attemptCount++; + const mockReq: any = { + on: (event: string, handler: Function) => { + if (event === 'error') { + const err = new Error('Connection refused') as NodeJS.ErrnoException; + err.code = 'ECONNREFUSED'; + setTimeout(() => handler(err), 0); + } + }, + end: () => { }, + abort: () => { }, + setTimeout: () => { } + }; + return mockReq; + }; + + try { + await nodeRequest({ + url: 'http://example.com', + type: 'POST', + getRawRequest: () => mockRawRequest + }, CancellationToken.None); + assert.fail('Should have thrown an error'); + } catch (err) { + assert.ok(err instanceof Error); + } + + assert.strictEqual(attemptCount, 1, 'POST request should not have been retried'); + }); + + test('should retry HEAD requests on transient errors', async () => { + let attemptCount = 0; + const mockRawRequest = (_opts: any, callback: Function) => { + attemptCount++; + const currentAttempt = attemptCount; + const mockReq: any = { + on: (event: string, handler: Function) => { + if (event === 'error' && currentAttempt < 3) { + const err = new Error('Host unreachable') as NodeJS.ErrnoException; + err.code = 'EHOSTUNREACH'; + setTimeout(() => handler(err), 0); + } + }, + end: () => { + if (currentAttempt >= 3) { + setTimeout(() => callback({ statusCode: 200, headers: {}, on: () => { }, pipe: () => ({ on: () => { } }) }), 0); + } + }, + abort: () => { }, + setTimeout: () => { } + }; + return mockReq; + }; + + try { + await nodeRequest({ + url: 'http://example.com', + type: 'HEAD', + getRawRequest: () => mockRawRequest as IRawRequestFunction + }, CancellationToken.None); + } catch (err) { + // Expected to eventually succeed or fail after retries + } + + assert.ok(attemptCount > 1, 'HEAD request should have been retried'); + }); + + test('should retry OPTIONS requests on transient errors', async () => { + let attemptCount = 0; + const mockRawRequest = (_opts: any, callback: Function) => { + attemptCount++; + const currentAttempt = attemptCount; + const mockReq: any = { + on: (event: string, handler: Function) => { + if (event === 'error' && currentAttempt < 3) { + const err = new Error('Network unreachable') as NodeJS.ErrnoException; + err.code = 'ENETUNREACH'; + setTimeout(() => handler(err), 0); + } + }, + end: () => { + if (currentAttempt >= 3) { + setTimeout(() => callback({ statusCode: 200, headers: {}, on: () => { }, pipe: () => ({ on: () => { } }) }), 0); + } + }, + abort: () => { }, + setTimeout: () => { } + }; + return mockReq; + }; + + try { + await nodeRequest({ + url: 'http://example.com', + type: 'OPTIONS', + getRawRequest: () => mockRawRequest as IRawRequestFunction + }, CancellationToken.None); + } catch (err) { + // Expected to eventually succeed or fail after retries + } + + assert.ok(attemptCount > 1, 'OPTIONS request should have been retried'); + }); + + test('should NOT retry DELETE requests', async () => { + let attemptCount = 0; + const mockRawRequest = () => { + attemptCount++; + const mockReq: any = { + on: (event: string, handler: Function) => { + if (event === 'error') { + const err = new Error('Connection refused') as NodeJS.ErrnoException; + err.code = 'ECONNREFUSED'; + setTimeout(() => handler(err), 0); + } + }, + end: () => { }, + abort: () => { }, + setTimeout: () => { } + }; + return mockReq; + }; + + try { + await nodeRequest({ + url: 'http://example.com', + type: 'DELETE', + getRawRequest: () => mockRawRequest + }, CancellationToken.None); + assert.fail('Should have thrown an error'); + } catch (err) { + assert.ok(err instanceof Error); + } + + assert.strictEqual(attemptCount, 1, 'DELETE request should not have been retried'); + }); + + test('should NOT retry PUT requests', async () => { + let attemptCount = 0; + const mockRawRequest = () => { + attemptCount++; + const mockReq: any = { + on: (event: string, handler: Function) => { + if (event === 'error') { + const err = new Error('Connection refused') as NodeJS.ErrnoException; + err.code = 'ECONNREFUSED'; + setTimeout(() => handler(err), 0); + } + }, + end: () => { }, + abort: () => { }, + setTimeout: () => { } + }; + return mockReq; + }; + + try { + await nodeRequest({ + url: 'http://example.com', + type: 'PUT', + getRawRequest: () => mockRawRequest + }, CancellationToken.None); + assert.fail('Should have thrown an error'); + } catch (err) { + assert.ok(err instanceof Error); + } + + assert.strictEqual(attemptCount, 1, 'PUT request should not have been retried'); + }); + + test('should NOT retry PATCH requests', async () => { + let attemptCount = 0; + const mockRawRequest = () => { + attemptCount++; + const mockReq: any = { + on: (event: string, handler: Function) => { + if (event === 'error') { + const err = new Error('Connection refused') as NodeJS.ErrnoException; + err.code = 'ECONNREFUSED'; + setTimeout(() => handler(err), 0); + } + }, + end: () => { }, + abort: () => { }, + setTimeout: () => { } + }; + return mockReq; + }; + + try { + await nodeRequest({ + url: 'http://example.com', + type: 'PATCH', + getRawRequest: () => mockRawRequest + }, CancellationToken.None); + assert.fail('Should have thrown an error'); + } catch (err) { + assert.ok(err instanceof Error); + } + + assert.strictEqual(attemptCount, 1, 'PATCH request should not have been retried'); + }); }); From 3b42759b8b501e68106c72b5683dcc114ed789e1 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 29 Jan 2026 09:43:38 +0100 Subject: [PATCH 074/152] fix #290724 (#291298) --- .../electron-browser/workbench/workbench.ts | 66 +------------------ .../browser/userDataProfile.ts | 6 +- 2 files changed, 2 insertions(+), 70 deletions(-) diff --git a/src/vs/code/electron-browser/workbench/workbench.ts b/src/vs/code/electron-browser/workbench/workbench.ts index 7102f4eb9f8..767fbe08db3 100644 --- a/src/vs/code/electron-browser/workbench/workbench.ts +++ b/src/vs/code/electron-browser/workbench/workbench.ts @@ -24,14 +24,7 @@ function showSplash(configuration: INativeWindowConfiguration) { performance.mark('code/willShowPartsSplash'); - - const isAgentSessionsWindow = configuration.profiles?.profile?.id === 'agent-sessions'; - if (isAgentSessionsWindow) { - showAgentSessionsSplash(configuration); - } else { - showDefaultSplash(configuration); - } - + showDefaultSplash(configuration); performance.mark('code/didShowPartsSplash'); } @@ -278,63 +271,6 @@ } } - function showAgentSessionsSplash(configuration: INativeWindowConfiguration) { - - // Agent sessions windows render a very opinionated splash: - // - Dark theme background (agent sessions use 2026-dark-experimental) - // - Title bar only for window controls - // - Secondary sidebar takes all remaining space (maximized) - // - No status bar, no activity bar, no sidebar - - const baseTheme = 'vs-dark'; - const shellBackground = '#191A1B'; // 2026-dark-experimental sidebar background - const shellForeground = '#CCCCCC'; - - // Apply base colors - const style = document.createElement('style'); - style.className = 'initialShellColors'; - window.document.head.appendChild(style); - style.textContent = `body { background-color: ${shellBackground}; color: ${shellForeground}; margin: 0; padding: 0; }`; - - // Set zoom level from splash data if available - if (typeof configuration.partsSplash?.zoomLevel === 'number' && typeof preloadGlobals?.webFrame?.setZoomLevel === 'function') { - preloadGlobals.webFrame.setZoomLevel(configuration.partsSplash.zoomLevel); - } - - const splash = document.createElement('div'); - splash.id = 'monaco-parts-splash'; - splash.className = baseTheme; - - // Title bar height - use stored value or default - const titleBarHeight = configuration.partsSplash?.layoutInfo?.titleBarHeight ?? 35; - - // Title bar for window dragging - if (titleBarHeight > 0) { - const titleDiv = document.createElement('div'); - titleDiv.style.position = 'absolute'; - titleDiv.style.width = '100%'; - titleDiv.style.height = `${titleBarHeight}px`; - titleDiv.style.left = '0'; - titleDiv.style.top = '0'; - titleDiv.style.backgroundColor = shellBackground; - (titleDiv.style as CSSStyleDeclaration & { '-webkit-app-region': string })['-webkit-app-region'] = 'drag'; - splash.appendChild(titleDiv); - } - - // Secondary sidebar (maximized, takes all remaining space) - // This is the main content area for agent sessions - const auxSideDiv = document.createElement('div'); - auxSideDiv.style.position = 'absolute'; - auxSideDiv.style.width = '100%'; - auxSideDiv.style.height = `calc(100% - ${titleBarHeight}px)`; - auxSideDiv.style.top = `${titleBarHeight}px`; - auxSideDiv.style.left = '0'; - auxSideDiv.style.backgroundColor = shellBackground; - splash.appendChild(auxSideDiv); - - window.document.body.appendChild(splash); - } - //#endregion //#region Window Helpers diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts index f3a3836bf20..6b18596e49d 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts @@ -35,7 +35,6 @@ import { IBrowserWorkbenchEnvironmentService } from '../../../services/environme import { Extensions as DndExtensions, IDragAndDropContributionRegistry, IResourceDropHandler } from '../../../../platform/dnd/browser/dnd.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { ITextEditorService } from '../../../services/textfile/common/textEditorService.js'; -import { ChatEntitlementContextKeys } from '../../../services/chat/common/chatEntitlementService.js'; export const OpenProfileMenu = new MenuId('OpenProfile'); const ProfilesMenu = new MenuId('Profiles'); @@ -285,10 +284,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements const disposables = new DisposableStore(); const id = `workbench.action.openProfile.${profile.name.replace('/\s+/', '_')}`; - let precondition: ContextKeyExpression | undefined = HAS_PROFILES_CONTEXT; - if (profile.id === 'agent-sessions') { - precondition = ContextKeyExpr.and(precondition, ChatEntitlementContextKeys.Setup.hidden.negate()); - } + const precondition: ContextKeyExpression | undefined = HAS_PROFILES_CONTEXT; disposables.add(registerAction2(class NewWindowAction extends Action2 { From bb1eec4bece645f747a7549b807fd8383061c20a Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 29 Jan 2026 10:30:47 +0100 Subject: [PATCH 075/152] hide agent session mode actions in stable (#291511) --- .../electron-browser/agentSessions/agentSessionsActions.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 19cb4fbee53..274ca60b475 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts @@ -14,6 +14,7 @@ import { ChatEntitlementContextKeys } from '../../../../services/chat/common/cha import { IWorkbenchModeService } from '../../../../services/layout/common/workbenchModeService.js'; import { IsAgentSessionsWorkspaceContext, WorkbenchModeContext } from '../../../../common/contextkeys.js'; import { CHAT_CATEGORY } from '../../browser/actions/chatActions.js'; +import { ProductQualityContext } from '../../../../../platform/contextkey/common/contextkeys.js'; export class OpenAgentSessionsWindowAction extends Action2 { constructor() { @@ -21,7 +22,7 @@ export class OpenAgentSessionsWindowAction extends Action2 { id: 'workbench.action.openAgentSessionsWindow', title: localize2('openAgentSessionsWindow', "Open Agent Sessions Window"), category: CHAT_CATEGORY, - precondition: ChatEntitlementContextKeys.Setup.hidden.negate(), + precondition: ContextKeyExpr.and(ProductQualityContext.notEqualsTo('stable'), ChatEntitlementContextKeys.Setup.hidden.negate()), f1: true, }); } @@ -54,6 +55,7 @@ export class SwitchToAgentSessionsModeAction extends Action2 { title: localize2('switchToAgentSessionsMode', "Switch to Agent Sessions Mode"), category: CHAT_CATEGORY, precondition: ContextKeyExpr.and( + ProductQualityContext.notEqualsTo('stable'), ChatEntitlementContextKeys.Setup.hidden.negate(), IsAgentSessionsWorkspaceContext.toNegated(), WorkbenchModeContext.notEqualsTo('agent-sessions') @@ -75,6 +77,7 @@ export class SwitchToNormalModeAction extends Action2 { title: localize2('switchToNormalMode', "Switch to Default Mode"), category: CHAT_CATEGORY, precondition: ContextKeyExpr.and( + ProductQualityContext.notEqualsTo('stable'), ChatEntitlementContextKeys.Setup.hidden.negate(), IsAgentSessionsWorkspaceContext.toNegated(), WorkbenchModeContext.notEqualsTo('') From 7e43c30f54c04836a069e222b13254582be21755 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 29 Jan 2026 10:33:00 +0100 Subject: [PATCH 076/152] agent sessions - always take the timings from the provider (#291514) * agent sessions - always take the timings from the provider * . --- .../agentSessions/agentSessionsModel.ts | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index 28f07ee13b4..d68707587b8 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -486,8 +486,6 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } for (const session of providerSessions) { - - // Icon + Label let icon: ThemeIcon; let providerLabel: string; const agentSessionProvider = getAgentSessionProvider(chatSessionType); @@ -504,27 +502,6 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode ? { files: changes.files, insertions: changes.insertions, deletions: changes.deletions } : changes; - // Times: it is important to always provide timing information to track - // unread/read state for example. - // If somehow the provider does not provide any, fallback to last known - let { created, lastRequestStarted, lastRequestEnded } = session.timing; - if (!created || !lastRequestEnded) { - const existing = this._sessions.get(session.resource); - if (!created && existing?.timing.created) { - created = existing.timing.created; - } - - if (!lastRequestEnded && existing?.timing.lastRequestEnded) { - lastRequestEnded = existing.timing.lastRequestEnded; - } - - if (!lastRequestStarted && existing?.timing.lastRequestStarted) { - lastRequestStarted = existing.timing.lastRequestStarted; - } - } - - this.logger.logIfTrace(`Resolved session ${session.resource.toString()} with timings: created=${created}, lastRequestStarted=${lastRequestStarted}, lastRequestEnded=${lastRequestEnded}`); - sessions.set(session.resource, this.toAgentSession({ providerType: chatSessionType, providerLabel, @@ -536,7 +513,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode tooltip: session.tooltip, status: session.status ?? AgentSessionStatus.Completed, archived: session.archived, - timing: { created, lastRequestStarted, lastRequestEnded, }, + timing: session.timing, changes: normalizedChanges, })); } From 93a1e4ffb0e15852fb789820b5705be3a59c8c63 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 29 Jan 2026 10:47:13 +0100 Subject: [PATCH 077/152] agent sessions - drop session providers that are not built-in and not registered (#291516) --- .../browser/agentSessions/agentSessions.ts | 7 ++++++ .../agentSessions/agentSessionsModel.ts | 6 ++--- .../agentSessionViewModel.test.ts | 24 +++++++------------ 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts index e9e4553e093..d0dec2b7280 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts @@ -19,6 +19,13 @@ export enum AgentSessionProviders { Codex = 'openai-codex', } +export function isBuiltInAgentSessionProvider(provider: string): boolean { + return provider === AgentSessionProviders.Local || + provider === AgentSessionProviders.Background || + provider === AgentSessionProviders.Cloud || + provider === AgentSessionProviders.Claude; +} + export function getAgentSessionProvider(sessionResource: URI | string): AgentSessionProviders | undefined { const type = URI.isUri(sessionResource) ? getChatSessionType(sessionResource) : sessionResource; switch (type) { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index d68707587b8..a8814377244 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -25,7 +25,7 @@ import { ILifecycleService } from '../../../../services/lifecycle/common/lifecyc import { Extensions, IOutputChannelRegistry, IOutputService } from '../../../../services/output/common/output.js'; import { ChatSessionStatus as AgentSessionStatus, IChatSessionFileChange, IChatSessionFileChange2, IChatSessionItem, IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js'; import { IChatWidgetService } from '../chat.js'; -import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName } from './agentSessions.js'; +import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName, isBuiltInAgentSessionProvider } from './agentSessions.js'; //#region Interfaces, Types @@ -520,8 +520,8 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } for (const [, session] of this._sessions) { - if (!resolvedProviders.has(session.providerType)) { - sessions.set(session.resource, session); // fill in existing sessions for providers that did not resolve + if (!resolvedProviders.has(session.providerType) && (isBuiltInAgentSessionProvider(session.providerType) || mapSessionContributionToType.has(session.providerType))) { + sessions.set(session.resource, session); // fill in existing sessions for providers that did not resolve if they are known or built-in } } diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts index 30fea75e913..ab75922685a 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts @@ -230,17 +230,12 @@ suite('AgentSessions', () => { chatSessionType: 'type-2', onDidChangeChatSessionItems: Event.None, provideChatSessionItems: async () => [ - { - id: 'session-2', - resource: URI.parse('test://session-2'), - label: 'Session 2', - timing: makeNewSessionTiming() - } + makeSimpleSessionItem('session-2'), ] }; - mockChatSessionsService.registerChatSessionItemProvider(provider1); - mockChatSessionsService.registerChatSessionItemProvider(provider2); + disposables.add(mockChatSessionsService.registerChatSessionItemProvider(provider1)); + disposables.add(mockChatSessionsService.registerChatSessionItemProvider(provider2)); viewModel = createViewModel(); @@ -250,8 +245,8 @@ suite('AgentSessions', () => { // Now resolve only type-1 await viewModel.resolve('type-1'); - // Should still have both sessions, but only type-1 was re-resolved - assert.strictEqual(viewModel.sessions.length, 2); + // Only type-1 sessions remain since non-resolved providers are cleared + assert.strictEqual(viewModel.sessions.length, 1); }); }); @@ -550,7 +545,7 @@ suite('AgentSessions', () => { }); }); - test('should preserve sessions from non-resolved providers', async () => { + test('should not preserve sessions from non-resolved providers', async () => { return runWithFakedTimers({}, async () => { let provider1CallCount = 0; let provider2CallCount = 0; @@ -595,19 +590,16 @@ suite('AgentSessions', () => { assert.strictEqual(viewModel.sessions.length, 2); assert.strictEqual(provider1CallCount, 1); assert.strictEqual(provider2CallCount, 1); - const originalSession1Label = viewModel.sessions[0].label; // Now resolve only type-2 await viewModel.resolve('type-2'); - // Should still have both sessions - assert.strictEqual(viewModel.sessions.length, 2); + // Should still have only one session + assert.strictEqual(viewModel.sessions.length, 1); // Provider 1 should not be called again assert.strictEqual(provider1CallCount, 1); // Provider 2 should be called again assert.strictEqual(provider2CallCount, 2); - // Session 1 should be preserved with original label - assert.strictEqual(viewModel.sessions.find(s => s.resource.toString() === 'test://session-1')?.label, originalSession1Label); }); }); From cff94e73ad951e5a84deadfdfa7ce67e6d289aef Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 29 Jan 2026 10:47:40 +0100 Subject: [PATCH 078/152] Chat Sessions: Add Mark All Read action (fix #291213) (#291523) * Chat Sessions: Add Mark All Read action (fix #291213) * . --- .../agentSessions/agentSessionsActions.ts | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts index 9d5bcea5555..3af559ab6c8 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts @@ -23,7 +23,7 @@ import { ChatEditorInput, showClearEditingSessionConfirmation } from '../widgetH import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ChatConfiguration } from '../../common/constants.js'; -import { ACTION_ID_NEW_CHAT, CHAT_CATEGORY } from '../actions/chatActions.js'; +import { ACTION_ID_NEW_CHAT } from '../actions/chatActions.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; @@ -36,6 +36,8 @@ import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { coalesce } from '../../../../../base/common/arrays.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +const AGENT_SESSIONS_CATEGORY = localize2('chatSessions', "Chat Agent Sessions"); + //#region Chat View export class ToggleShowAgentSessionsAction extends Action2 { @@ -143,7 +145,7 @@ export class PickAgentSessionAction extends Action2 { when: ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), } ], - category: CHAT_CATEGORY, + category: AGENT_SESSIONS_CATEGORY, icon: Codicon.history, f1: true, precondition: ChatContextKeys.enabled @@ -165,7 +167,7 @@ export class ArchiveAllAgentSessionsAction extends Action2 { id: 'workbench.action.chat.archiveAllAgentSessions', title: localize2('archiveAll.label', "Archive All Workspace Agent Sessions"), precondition: ChatContextKeys.enabled, - category: CHAT_CATEGORY, + category: AGENT_SESSIONS_CATEGORY, f1: true, }); } @@ -201,10 +203,16 @@ export class MarkAllAgentSessionsReadAction extends Action2 { constructor() { super({ id: 'workbench.action.chat.markAllAgentSessionsRead', - title: localize2('markAllRead.label', "Mark All Workspace Agent Sessions as Read"), + title: localize2('markAllRead.label', "Mark All as Read"), precondition: ChatContextKeys.enabled, - category: CHAT_CATEGORY, + category: AGENT_SESSIONS_CATEGORY, f1: true, + menu: { + id: MenuId.AgentSessionsContext, + group: '0_read', + order: 2, + when: ChatContextKeys.isArchivedAgentSession.negate() // no read state for archived sessions + } }); } async run(accessor: ServicesAccessor) { @@ -403,7 +411,7 @@ export class MarkAgentSessionUnreadAction extends BaseAgentSessionAction { title: localize2('markUnread', "Mark as Unread"), menu: { id: MenuId.AgentSessionsContext, - group: '1_edit', + group: '0_read', order: 1, when: ContextKeyExpr.and( ChatContextKeys.isReadAgentSession, @@ -428,7 +436,7 @@ export class MarkAgentSessionReadAction extends BaseAgentSessionAction { title: localize2('markRead', "Mark as Read"), menu: { id: MenuId.AgentSessionsContext, - group: '1_edit', + group: '0_read', order: 1, when: ContextKeyExpr.and( ChatContextKeys.isReadAgentSession.negate(), @@ -631,7 +639,7 @@ export class DeleteAllLocalSessionsAction extends Action2 { id: 'workbench.action.chat.clearHistory', title: localize2('agentSessions.deleteAll', "Delete All Local Workspace Chat Sessions"), precondition: ChatContextKeys.enabled, - category: CHAT_CATEGORY, + category: AGENT_SESSIONS_CATEGORY, f1: true, }); } @@ -939,7 +947,7 @@ export class ShowAgentSessionsSidebar extends UpdateChatViewWidthAction { AuxiliaryBarMaximizedContext.negate() ), f1: true, - category: CHAT_CATEGORY, + category: AGENT_SESSIONS_CATEGORY, }); } @@ -963,7 +971,7 @@ export class HideAgentSessionsSidebar extends UpdateChatViewWidthAction { AuxiliaryBarMaximizedContext.negate() ), f1: true, - category: CHAT_CATEGORY, + category: AGENT_SESSIONS_CATEGORY, }); } @@ -986,7 +994,7 @@ export class ToggleAgentSessionsSidebar extends Action2 { AuxiliaryBarMaximizedContext.negate() ), f1: true, - category: CHAT_CATEGORY, + category: AGENT_SESSIONS_CATEGORY, }); } @@ -1017,7 +1025,7 @@ export class FocusAgentSessionsAction extends Action2 { ChatContextKeys.enabled, ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsEnabled}`, true) ), - category: CHAT_CATEGORY, + category: AGENT_SESSIONS_CATEGORY, f1: true, }); } From f7e21013d978618544ed49d44f8464869f7d133e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:48:56 +0000 Subject: [PATCH 079/152] Fix inline chat gutter affordance with sticky scroll (#291357) * Initial plan * Reveal selection line when clicking inline chat gutter affordance from sticky scroll Co-authored-by: jrieken <1794099+jrieken@users.noreply.github.com> * Move reveal call to central location in InlineChatAffordance Co-authored-by: jrieken <1794099+jrieken@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jrieken <1794099+jrieken@users.noreply.github.com> Co-authored-by: Johannes Rieken --- .../contrib/inlineChat/browser/inlineChatAffordance.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts index da6e88ea8bd..2991b874a0c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -7,6 +7,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { autorun, debouncedObservable, derived, observableValue, runOnChange, waitForState } from '../../../../base/common/observable.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; +import { ScrollType } from '../../../../editor/common/editorCommon.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { InlineChatConfigKeys } from '../common/inlineChat.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -82,6 +83,9 @@ export class InlineChatAffordance extends Disposable { return; } + // Reveal the line in case it's outside the viewport (e.g., when triggered from sticky scroll) + this._editor.revealLineInCenterIfOutsideViewport(data.lineNumber, ScrollType.Immediate); + const editorDomNode = this._editor.getDomNode()!; const editorRect = editorDomNode.getBoundingClientRect(); const left = data.rect.left - editorRect.left; From cceba815b0d6a91d0665261e101a12bed91cb037 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 29 Jan 2026 10:49:15 +0100 Subject: [PATCH 080/152] fix #291360 (#291528) --- .../chatManagement/chatModelsWidget.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts index 7fafe8ad4ed..528f4110c72 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts @@ -226,18 +226,18 @@ class ModelsSearchFilterDropdownMenuActionViewItem extends DropdownMenuActionVie private getActions(): IAction[] { const actions: IAction[] = []; - // Visibility filters - actions.push(this.createVisibleAction(true, localize('filter.visible', 'Visible'))); - actions.push(this.createVisibleAction(false, localize('filter.hidden', 'Hidden'))); - // Capability filters - actions.push(new Separator()); actions.push( - this.createCapabilityAction('tools', localize('capability.tools', 'Tools')), - this.createCapabilityAction('vision', localize('capability.vision', 'Vision')), - this.createCapabilityAction('agent', localize('capability.agent', 'Agent Mode')) + this.createCapabilityAction('tools', localize('capability.tools', "Tools")), + this.createCapabilityAction('vision', localize('capability.vision', "Vision")), + this.createCapabilityAction('agent', localize('capability.agent', "Agent Mode")) ); + // Visibility filters + actions.push(new Separator()); + actions.push(this.createVisibleAction(true, localize('filter.visible', "Visible in Chat Model Picker"))); + actions.push(this.createVisibleAction(false, localize('filter.hidden', "Hidden in Chat Model Picker"))); + // Provider filters - only show providers with configured models const configuredVendors = this.viewModel.getConfiguredVendors(); if (configuredVendors.length > 1) { @@ -248,8 +248,8 @@ class ModelsSearchFilterDropdownMenuActionViewItem extends DropdownMenuActionVie // Group By actions.push(new Separator()); const groupByActions: IAction[] = []; - groupByActions.push(this.createGroupByAction(ChatModelGroup.Vendor, localize('groupBy.provider', 'Provider'))); - groupByActions.push(this.createGroupByAction(ChatModelGroup.Visibility, localize('groupBy.visibility', 'Visibility'))); + groupByActions.push(this.createGroupByAction(ChatModelGroup.Vendor, localize('groupBy.provider', "Provider"))); + groupByActions.push(this.createGroupByAction(ChatModelGroup.Visibility, localize('groupBy.visibility', "Visibility (Chat Model Picker)"))); actions.push(new SubmenuAction('groupBy', localize('groupBy', "Group By"), groupByActions)); return actions; From 7a488d0f074748d65b5e017eea3662dc0aa17fbd Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 29 Jan 2026 10:59:58 +0100 Subject: [PATCH 081/152] In agent session window, layout falls over when opening editors (fix #291089) (#291529) --- resources/workbenchModes/agent-sessions.code-workbench-mode | 1 + src/vs/workbench/browser/layout.ts | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/resources/workbenchModes/agent-sessions.code-workbench-mode b/resources/workbenchModes/agent-sessions.code-workbench-mode index 0fd107b02de..7aa37f33888 100644 --- a/resources/workbenchModes/agent-sessions.code-workbench-mode +++ b/resources/workbenchModes/agent-sessions.code-workbench-mode @@ -19,6 +19,7 @@ "workbench.sideBar.location": "right", "workbench.statusBar.visible": false, "workbench.secondarySideBar.forceMaximized": true, + "workbench.secondarySideBar.defaultVisibility": "maximized", "workbench.startupEditor": "none", "workbench.tips.enabled": false, "workbench.layoutControl.type": "toggles", diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index fc7da781979..abd34a528b5 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -2981,11 +2981,9 @@ class LayoutStateModel extends Disposable { private applyOverrides(configuration: ILayoutStateLoadConfiguration): void { // Auxiliary bar: Maximized settings - const auxiliaryBarForceMaximized = this.configurationService.getValue(WorkbenchLayoutSettings.AUXILIARYBAR_FORCE_MAXIMIZED); - if (this.isNew[StorageScope.WORKSPACE] || auxiliaryBarForceMaximized) { + if (this.isNew[StorageScope.WORKSPACE]) { const defaultAuxiliaryBarVisibility = this.configurationService.getValue(WorkbenchLayoutSettings.AUXILIARYBAR_DEFAULT_VISIBILITY); if ( - auxiliaryBarForceMaximized || defaultAuxiliaryBarVisibility === 'maximized' || (defaultAuxiliaryBarVisibility === 'maximizedInWorkspace' && this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY) ) { From 987f736f422fb1f88df13cf613aa216065b0ae8a Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 29 Jan 2026 11:10:35 +0100 Subject: [PATCH 082/152] fix #291340 (#291532) --- src/vs/platform/mcp/common/mcpManagementService.ts | 4 +++- src/vs/platform/mcp/node/mcpManagementService.ts | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/mcp/common/mcpManagementService.ts b/src/vs/platform/mcp/common/mcpManagementService.ts index 2885f2baa50..5f72d29a8fe 100644 --- a/src/vs/platform/mcp/common/mcpManagementService.ts +++ b/src/vs/platform/mcp/common/mcpManagementService.ts @@ -73,7 +73,9 @@ export abstract class AbstractCommonMcpManagementService extends Disposable impl // remote if (packageType === RegistryType.REMOTE && manifest.remotes?.length) { - const { inputs, variables } = this.processKeyValueInputs(manifest.remotes[0].headers ?? []); + const url = manifest.remotes[0].url; + const headers = manifest.remotes[0].headers ?? []; + const { inputs, variables } = this.processKeyValueInputs(url.startsWith('https://api.githubcopilot.com/mcp') ? headers.filter(h => h.name.toLowerCase() !== 'authorization') : headers); return { mcpServerConfiguration: { config: { diff --git a/src/vs/platform/mcp/node/mcpManagementService.ts b/src/vs/platform/mcp/node/mcpManagementService.ts index aed19e04a5e..687ddb55d58 100644 --- a/src/vs/platform/mcp/node/mcpManagementService.ts +++ b/src/vs/platform/mcp/node/mcpManagementService.ts @@ -32,7 +32,11 @@ export class McpUserResourceManagementService extends CommonMcpUserResourceManag try { const manifest = await this.updateMetadataFromGallery(server); - const packageType = options?.packageType ?? manifest.packages?.[0]?.registryType ?? RegistryType.REMOTE; + const packageType = options?.packageType ?? ( + manifest.remotes?.length + ? RegistryType.REMOTE + : (manifest.packages?.[0]?.registryType ?? RegistryType.REMOTE) + ); const { mcpServerConfiguration, notices } = this.getMcpServerConfigurationFromManifest(manifest, packageType); From c46f0c09fc9e2f2908dcc0f5200f531dcbea5ef5 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 29 Jan 2026 11:30:29 +0100 Subject: [PATCH 083/152] rename to open model picker when in overflow --- .../contrib/chat/browser/actions/chatExecuteActions.ts | 2 +- 1 file changed, 1 insertion(+), 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 829c553fbdf..548deb481d5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -586,7 +586,7 @@ export class ChatSessionPrimaryPickerAction extends Action2 { constructor() { super({ id: ChatSessionPrimaryPickerAction.ID, - title: localize2('interactive.openChatSessionPrimaryPicker.label', "Open Picker"), + title: localize2('interactive.openChatSessionPrimaryPicker.label', "Open Model Picker"), category: CHAT_CATEGORY, f1: false, precondition: ChatContextKeys.enabled, From d46d4d9de27a9cc28d359eb3263a60c0221bab03 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 29 Jan 2026 11:35:21 +0100 Subject: [PATCH 084/152] fix #291133 (#291546) --- .../contrib/chat/common/languageModels.ts | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index e6a883096fe..0df77c1014f 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -862,7 +862,7 @@ export class LanguageModelsService implements ILanguageModelsService { ? await this._languageModelsConfigurationService.updateLanguageModelsProviderGroup(existing, languageModelProviderGroup) : await this._languageModelsConfigurationService.addLanguageModelsProviderGroup(languageModelProviderGroup); - if (vendor.configuration && this.canConfigure(configuration ?? {}, vendor.configuration)) { + if (vendor.configuration && this.requireConfiguring(vendor.configuration)) { const snippet = this.getSnippetForFirstUnconfiguredProperty(configuration ?? {}, vendor.configuration); await this._languageModelsConfigurationService.configureLanguageModels({ group: saved, snippet }); } @@ -901,7 +901,7 @@ export class LanguageModelsService implements ILanguageModelsService { await this._languageModelsConfigurationService.removeLanguageModelsProviderGroup(existing); } - private canConfigure(configuration: IStringDictionary, schema: IJSONSchema): boolean { + private requireConfiguring(schema: IJSONSchema): boolean { if (schema.additionalProperties) { return true; } @@ -909,7 +909,7 @@ export class LanguageModelsService implements ILanguageModelsService { return false; } for (const property of Object.keys(schema.properties)) { - if (configuration[property] === undefined) { + if (!this.canPromptForProperty(schema.properties[property])) { return true; } } @@ -1003,7 +1003,11 @@ export class LanguageModelsService implements ILanguageModelsService { } private async promptForValue(groupName: string, property: string, propertySchema: IJSONSchema | undefined, required: boolean, existing: IStringDictionary | undefined): Promise { - if (!propertySchema || typeof propertySchema === 'boolean') { + if (!propertySchema) { + return undefined; + } + + if (!this.canPromptForProperty(propertySchema)) { return undefined; } @@ -1015,17 +1019,30 @@ export class LanguageModelsService implements ILanguageModelsService { return selectedItems; } - if (propertySchema.type !== 'string' && propertySchema.type !== 'number' && propertySchema.type !== 'integer' && propertySchema.type !== 'boolean') { - return undefined; - } - const value = await this.promptForInput(groupName, property, propertySchema, required, existing); if (value === undefined) { return undefined; } + return value; } + private canPromptForProperty(propertySchema: IJSONSchema | undefined): boolean { + if (!propertySchema || typeof propertySchema === 'boolean') { + return false; + } + + if (propertySchema.type === 'array' && propertySchema.items && !Array.isArray(propertySchema.items) && propertySchema.items.enum) { + return true; + } + + if (propertySchema.type === 'string' || propertySchema.type === 'number' || propertySchema.type === 'integer' || propertySchema.type === 'boolean') { + return true; + } + + return false; + } + private async promptForArray(groupName: string, property: string, propertySchema: IJSONSchema): Promise { if (!propertySchema.items || Array.isArray(propertySchema.items) || !propertySchema.items.enum) { return undefined; From 1872cb38547185ef27dbf6a295793657fc6d11c8 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 10:35:40 +0000 Subject: [PATCH 085/152] Add garbage collection for unused content-addressed askpass directories (#289723) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * Add garbage collection for old content-addressed askpass directories - Implement updateDirectoryMtime to update folder mtime when used - Add garbageCollectOldDirectories to remove folders older than 7 days - Update ensureAskpassScripts to call GC on every activation - Add comprehensive test coverage for GC functionality Co-authored-by: joaomoreno <22350+joaomoreno@users.noreply.github.com> * Remove GC from fast path to keep it fast Only run garbage collection when creating new directories, not when reusing existing ones. Old folders only accumulate when creating new content-addressed directories. Co-authored-by: joaomoreno <22350+joaomoreno@users.noreply.github.com> * Hoist askpassBaseDir variable to avoid duplication Declare askpassBaseDir once at the top of the function and reuse it when constructing askpassDir and when calling garbageCollectOldDirectories. Co-authored-by: joaomoreno <22350+joaomoreno@users.noreply.github.com> * Fix test failures and address bot review comments - Export ensureAskpassScripts for testing to avoid dependency on isWindowsUserOrSystemSetup check - Remove redundant success log after directory removal - Update tests to call ensureAskpassScripts directly instead of getAskpassPaths - Remove Windows-only restrictions from tests to make them cross-platform - Remove setTimeout workarounds - tests now properly await async operations Co-authored-by: joaomoreno <22350+joaomoreno@users.noreply.github.com> * formatting --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: joaomoreno <22350+joaomoreno@users.noreply.github.com> Co-authored-by: João Moreno --- extensions/git/src/askpassManager.ts | 83 ++++++- .../git/src/test/askpassManager.test.ts | 203 ++++++++++++++++++ 2 files changed, 284 insertions(+), 2 deletions(-) create mode 100644 extensions/git/src/test/askpassManager.test.ts diff --git a/extensions/git/src/askpassManager.ts b/extensions/git/src/askpassManager.ts index 15a6b2fa0e6..9b610346420 100644 --- a/extensions/git/src/askpassManager.ts +++ b/extensions/git/src/askpassManager.ts @@ -128,6 +128,74 @@ async function copyFileSecure( await setWindowsPermissions(dest, logger); } +/** + * Updates the modification time of a directory to mark it as recently used. + */ +async function updateDirectoryMtime(dirPath: string, logger: LogOutputChannel): Promise { + try { + const now = new Date(); + await fs.promises.utimes(dirPath, now, now); + logger.trace(`[askpassManager] Updated mtime for ${dirPath}`); + } catch (err) { + logger.warn(`[askpassManager] Failed to update mtime for ${dirPath}: ${err}`); + } +} + +/** + * Garbage collects old content-addressed askpass directories that haven't been used in 7 days. + * This prevents accumulation of old versions when VS Code updates. + */ +async function garbageCollectOldDirectories( + askpassBaseDir: string, + currentHash: string, + logger: LogOutputChannel +): Promise { + try { + // Check if the askpass base directory exists + try { + await fs.promises.access(askpassBaseDir); + } catch { + // Directory doesn't exist, nothing to clean + return; + } + + const entries = await fs.promises.readdir(askpassBaseDir); + const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000); + + for (const entry of entries) { + // Skip the current content-addressed directory + if (entry === currentHash) { + continue; + } + + const entryPath = path.join(askpassBaseDir, entry); + + try { + const stat = await fs.promises.stat(entryPath); + + // Only process directories + if (!stat.isDirectory()) { + continue; + } + + // Check if the directory hasn't been used in 7 days + if (stat.mtime.getTime() < sevenDaysAgo) { + logger.info(`[askpassManager] Removing old askpass directory: ${entryPath} (last used: ${stat.mtime.toISOString()})`); + + // Remove the directory and all its contents + await fs.promises.rm(entryPath, { recursive: true, force: true }); + } else { + logger.trace(`[askpassManager] Keeping askpass directory: ${entryPath} (last used: ${stat.mtime.toISOString()})`); + } + } catch (err) { + logger.warn(`[askpassManager] Failed to process/remove directory ${entryPath}: ${err}`); + } + } + } catch (err) { + logger.warn(`[askpassManager] Failed to garbage collect old directories: ${err}`); + } +} + export interface AskpassPaths { readonly askpass: string; readonly askpassMain: string; @@ -144,7 +212,7 @@ export interface AskpassPaths { * @param storageDir The user-controlled storage directory (context.storageUri.fsPath) * @param logger Logger for diagnostic output */ -async function ensureAskpassScripts( +export async function ensureAskpassScripts( sourceDir: string, storageDir: string, logger: LogOutputChannel @@ -162,7 +230,8 @@ async function ensureAskpassScripts( logger.trace(`[askpassManager] Content hash: ${contentHash}`); // Create content-addressed directory - const askpassDir = path.join(storageDir, 'askpass', contentHash); + const askpassBaseDir = path.join(storageDir, 'askpass'); + const askpassDir = path.join(askpassBaseDir, contentHash); const destPaths: AskpassPaths = { askpass: path.join(askpassDir, 'askpass.sh'), @@ -177,6 +246,10 @@ async function ensureAskpassScripts( const stat = await fs.promises.stat(destPaths.askpass); if (stat.isFile()) { logger.trace(`[askpassManager] Using existing content-addressed askpass at ${askpassDir}`); + + // Update mtime to mark this directory as recently used + await updateDirectoryMtime(askpassDir, logger); + return destPaths; } } catch { @@ -200,6 +273,12 @@ async function ensureAskpassScripts( logger.info(`[askpassManager] Successfully created content-addressed askpass scripts`); + // Update mtime to mark this directory as recently used + await updateDirectoryMtime(askpassDir, logger); + + // Garbage collect old directories + await garbageCollectOldDirectories(askpassBaseDir, contentHash, logger); + return destPaths; } diff --git a/extensions/git/src/test/askpassManager.test.ts b/extensions/git/src/test/askpassManager.test.ts new file mode 100644 index 00000000000..3a90c078873 --- /dev/null +++ b/extensions/git/src/test/askpassManager.test.ts @@ -0,0 +1,203 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { ensureAskpassScripts } from '../askpassManager'; +import { Event, EventEmitter, LogLevel, LogOutputChannel } from 'vscode'; + +class MockLogOutputChannel implements LogOutputChannel { + logLevel: LogLevel = LogLevel.Info; + onDidChangeLogLevel: Event = new EventEmitter().event; + private logs: { level: string; message: string }[] = []; + + trace(message: string, ..._args: any[]): void { + this.logs.push({ level: 'trace', message }); + } + debug(message: string, ..._args: any[]): void { + this.logs.push({ level: 'debug', message }); + } + info(message: string, ..._args: any[]): void { + this.logs.push({ level: 'info', message }); + } + warn(message: string, ..._args: any[]): void { + this.logs.push({ level: 'warn', message }); + } + error(error: string | Error, ..._args: any[]): void { + this.logs.push({ level: 'error', message: error.toString() }); + } + + name: string = 'MockLogOutputChannel'; + append(_value: string): void { } + appendLine(_value: string): void { } + replace(_value: string): void { } + clear(): void { } + show(_column?: unknown, _preserveFocus?: unknown): void { } + hide(): void { } + dispose(): void { } + + getLogs(): { level: string; message: string }[] { + return this.logs; + } + + hasLog(level: string, messageSubstring: string): boolean { + return this.logs.some(log => log.level === level && log.message.includes(messageSubstring)); + } +} + +// Helper to set mtime on a directory +async function setDirectoryMtime(dirPath: string, mtime: Date): Promise { + await fs.promises.utimes(dirPath, mtime, mtime); +} + +suite('askpassManager', () => { + let tempDir: string; + let sourceDir: string; + + setup(async () => { + // Create a temporary directory for testing + tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'askpass-test-')); + + // Create source directory with dummy askpass files + sourceDir = path.join(tempDir, 'source'); + await fs.promises.mkdir(sourceDir, { recursive: true }); + + const askpassFiles = ['askpass.sh', 'askpass-main.js', 'ssh-askpass.sh', 'askpass-empty.sh', 'ssh-askpass-empty.sh']; + for (const file of askpassFiles) { + await fs.promises.writeFile(path.join(sourceDir, file), `#!/bin/sh\n# ${file}\n`); + } + }); + + teardown(async () => { + // Clean up temporary directory + try { + await fs.promises.rm(tempDir, { recursive: true, force: true }); + } catch { + // Ignore errors during cleanup + } + }); + + test('garbage collection removes old directories', async function () { + const storageDir = path.join(tempDir, 'storage'); + const askpassBaseDir = path.join(storageDir, 'askpass'); + const logger = new MockLogOutputChannel(); + + // Create old directories with old mtimes (8 days ago) + const oldDate = new Date(Date.now() - (8 * 24 * 60 * 60 * 1000)); + const oldDirs = ['oldhash1', 'oldhash2']; + + for (const dirName of oldDirs) { + const dirPath = path.join(askpassBaseDir, dirName); + await fs.promises.mkdir(dirPath, { recursive: true }); + await fs.promises.writeFile(path.join(dirPath, 'test.txt'), 'old'); + await setDirectoryMtime(dirPath, oldDate); + } + + // Create a recent directory (1 day ago) + const recentDate = new Date(Date.now() - (1 * 24 * 60 * 60 * 1000)); + const recentDir = path.join(askpassBaseDir, 'recenthash'); + await fs.promises.mkdir(recentDir, { recursive: true }); + await fs.promises.writeFile(path.join(recentDir, 'test.txt'), 'recent'); + await setDirectoryMtime(recentDir, recentDate); + + // Call ensureAskpassScripts which should trigger garbage collection when creating a new directory + await ensureAskpassScripts(sourceDir, storageDir, logger); + + // Check that old directories were removed + for (const dirName of oldDirs) { + const dirPath = path.join(askpassBaseDir, dirName); + const exists = await fs.promises.access(dirPath).then(() => true).catch(() => false); + assert.strictEqual(exists, false, `Old directory ${dirName} should have been removed`); + } + + // Check that recent directory still exists + const recentExists = await fs.promises.access(recentDir).then(() => true).catch(() => false); + assert.strictEqual(recentExists, true, 'Recent directory should still exist'); + + // Check logs + assert.ok(logger.hasLog('info', 'Removing old askpass directory'), 'Should log removal of old directories'); + }); + + test('garbage collection skips non-directory entries', async function () { + const storageDir = path.join(tempDir, 'storage'); + const askpassBaseDir = path.join(storageDir, 'askpass'); + const logger = new MockLogOutputChannel(); + + // Create a file in the askpass directory (not a directory) + await fs.promises.mkdir(askpassBaseDir, { recursive: true }); + const filePath = path.join(askpassBaseDir, 'somefile.txt'); + await fs.promises.writeFile(filePath, 'test'); + + // Set old mtime + const oldDate = new Date(Date.now() - (8 * 24 * 60 * 60 * 1000)); + await fs.promises.utimes(filePath, oldDate, oldDate); + + // Call ensureAskpassScripts which should trigger garbage collection + await ensureAskpassScripts(sourceDir, storageDir, logger); + + // Check that file still exists (should not be removed) + const exists = await fs.promises.access(filePath).then(() => true).catch(() => false); + assert.strictEqual(exists, true, 'Non-directory file should not be removed'); + }); + + test('mtime is updated on existing directory', async function () { + const storageDir = path.join(tempDir, 'storage'); + const logger = new MockLogOutputChannel(); + + // Call ensureAskpassScripts to create the directory + const paths1 = await ensureAskpassScripts(sourceDir, storageDir, logger); + + // Get the directory path and its initial mtime + const askpassDir = path.dirname(paths1.askpass); + const stat1 = await fs.promises.stat(askpassDir); + const mtime1 = stat1.mtime.getTime(); + + // Wait a bit to ensure time difference + await new Promise(resolve => setTimeout(resolve, 100)); + + // Call again (should update mtime) + await ensureAskpassScripts(sourceDir, storageDir, logger); + + // Check that mtime was updated + const stat2 = await fs.promises.stat(askpassDir); + const mtime2 = stat2.mtime.getTime(); + + assert.ok(mtime2 > mtime1, 'Mtime should be updated on subsequent calls'); + }); + + test('garbage collection handles empty askpass directory', async function () { + const storageDir = path.join(tempDir, 'storage'); + const logger = new MockLogOutputChannel(); + + // Don't create any askpass directories, just call ensureAskpassScripts + await ensureAskpassScripts(sourceDir, storageDir, logger); + + // Should complete without errors + assert.ok(true, 'Should handle empty or non-existent askpass directory gracefully'); + }); + + test('current content-addressed directory is not removed', async function () { + const storageDir = path.join(tempDir, 'storage'); + const logger = new MockLogOutputChannel(); + + // Create the current content-addressed directory + const paths = await ensureAskpassScripts(sourceDir, storageDir, logger); + const currentDir = path.dirname(paths.askpass); + + // Set its mtime to 8 days ago (would normally be removed) + const oldDate = new Date(Date.now() - (8 * 24 * 60 * 60 * 1000)); + await setDirectoryMtime(currentDir, oldDate); + + // Call again which should trigger GC + await ensureAskpassScripts(sourceDir, storageDir, logger); + + // Current directory should still exist + const exists = await fs.promises.access(currentDir).then(() => true).catch(() => false); + assert.strictEqual(exists, true, 'Current content-addressed directory should not be removed'); + }); +}); From 32c2dd28912976a829df6ac9ccf8b10a1a32e16b Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 29 Jan 2026 10:49:11 +0000 Subject: [PATCH 086/152] Add secondary button border and update related styles and colors --- build/lib/stylelint/vscode-known-variables.json | 2 ++ src/vs/base/browser/ui/button/button.ts | 12 +++++++++++- src/vs/platform/theme/browser/defaultStyles.ts | 3 ++- src/vs/platform/theme/common/colors/inputColors.ts | 11 ++++++++--- .../contrib/extensions/browser/extensionsActions.ts | 9 ++++++++- .../extensions/browser/media/extensionActions.css | 4 ++-- 6 files changed, 33 insertions(+), 8 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index f5c0817dca7..51dc85f5574 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -39,6 +39,7 @@ "--vscode-button-foreground", "--vscode-button-hoverBackground", "--vscode-button-secondaryBackground", + "--vscode-button-secondaryBorder", "--vscode-button-secondaryForeground", "--vscode-button-secondaryHoverBackground", "--vscode-button-separator", @@ -360,6 +361,7 @@ "--vscode-extensionBadge-remoteBackground", "--vscode-extensionBadge-remoteForeground", "--vscode-extensionButton-background", + "--vscode-extensionButton-border", "--vscode-extensionButton-foreground", "--vscode-extensionButton-hoverBackground", "--vscode-extensionButton-prominentBackground", diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index fa1fa93d545..5b32ddc9d85 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -48,6 +48,7 @@ export interface IButtonStyles { readonly buttonSecondaryBackground: string | undefined; readonly buttonSecondaryHoverBackground: string | undefined; readonly buttonSecondaryForeground: string | undefined; + readonly buttonSecondaryBorder: string | undefined; readonly buttonBorder: string | undefined; } @@ -59,7 +60,8 @@ export const unthemedButtonStyles: IButtonStyles = { buttonBorder: undefined, buttonSecondaryBackground: undefined, buttonSecondaryForeground: undefined, - buttonSecondaryHoverBackground: undefined + buttonSecondaryHoverBackground: undefined, + buttonSecondaryBorder: undefined }; export interface IButton extends IDisposable { @@ -120,9 +122,13 @@ export class Button extends Disposable implements IButton { this._element.classList.toggle('small', !!options.small); const background = options.secondary ? options.buttonSecondaryBackground : options.buttonBackground; const foreground = options.secondary ? options.buttonSecondaryForeground : options.buttonForeground; + const border = options.secondary ? options.buttonSecondaryBorder : options.buttonBorder; this._element.style.color = foreground || ''; this._element.style.backgroundColor = background || ''; + if (border) { + this._element.style.border = `1px solid ${border}`; + } if (options.supportShortLabel) { this._labelShortElement = document.createElement('div'); @@ -223,16 +229,20 @@ export class Button extends Disposable implements IButton { private updateStyles(hover: boolean): void { let background; let foreground; + let border; if (this.options.secondary) { background = hover ? this.options.buttonSecondaryHoverBackground : this.options.buttonSecondaryBackground; foreground = this.options.buttonSecondaryForeground; + border = this.options.buttonSecondaryBorder; } else { background = hover ? this.options.buttonHoverBackground : this.options.buttonBackground; foreground = this.options.buttonForeground; + border = this.options.buttonBorder; } this._element.style.backgroundColor = background || ''; this._element.style.color = foreground || ''; + this._element.style.border = border ? `1px solid ${border}` : ''; } get element(): HTMLElement { diff --git a/src/vs/platform/theme/browser/defaultStyles.ts b/src/vs/platform/theme/browser/defaultStyles.ts index aad9500236b..58702a30a78 100644 --- a/src/vs/platform/theme/browser/defaultStyles.ts +++ b/src/vs/platform/theme/browser/defaultStyles.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IButtonStyles } from '../../../base/browser/ui/button/button.js'; import { IKeybindingLabelStyles } from '../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; -import { ColorIdentifier, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder, keybindingLabelForeground, asCssVariable, widgetShadow, buttonForeground, buttonSeparator, buttonBackground, buttonHoverBackground, buttonSecondaryForeground, buttonSecondaryBackground, buttonSecondaryHoverBackground, buttonBorder, progressBarBackground, inputActiveOptionBorder, inputActiveOptionForeground, inputActiveOptionBackground, editorWidgetBackground, editorWidgetForeground, contrastBorder, checkboxBorder, checkboxBackground, checkboxForeground, problemsErrorIconForeground, problemsWarningIconForeground, problemsInfoIconForeground, inputBackground, inputForeground, inputBorder, textLinkForeground, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBorder, inputValidationErrorBackground, inputValidationErrorForeground, listFilterWidgetBackground, listFilterWidgetNoMatchesOutline, listFilterWidgetOutline, listFilterWidgetShadow, badgeBackground, badgeForeground, breadcrumbsBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, activeContrastBorder, listActiveSelectionBackground, listActiveSelectionForeground, listActiveSelectionIconForeground, listDropOverBackground, listFocusAndSelectionOutline, listFocusBackground, listFocusForeground, listFocusOutline, listHoverBackground, listHoverForeground, listInactiveFocusBackground, listInactiveFocusOutline, listInactiveSelectionBackground, listInactiveSelectionForeground, listInactiveSelectionIconForeground, tableColumnsBorder, tableOddRowsBackgroundColor, treeIndentGuidesStroke, asCssVariableWithDefault, editorWidgetBorder, focusBorder, pickerGroupForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, selectBackground, selectBorder, selectForeground, selectListBackground, treeInactiveIndentGuidesStroke, menuBorder, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuSeparatorBackground, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, listDropBetweenBackground, radioActiveBackground, radioActiveForeground, radioInactiveBackground, radioInactiveForeground, radioInactiveBorder, radioInactiveHoverBackground, radioActiveBorder, checkboxDisabledBackground, checkboxDisabledForeground, widgetBorder } from '../common/colorRegistry.js'; +import { ColorIdentifier, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder, keybindingLabelForeground, asCssVariable, widgetShadow, buttonForeground, buttonSeparator, buttonBackground, buttonHoverBackground, buttonSecondaryForeground, buttonSecondaryBackground, buttonSecondaryHoverBackground, buttonSecondaryBorder, buttonBorder, progressBarBackground, inputActiveOptionBorder, inputActiveOptionForeground, inputActiveOptionBackground, editorWidgetBackground, editorWidgetForeground, contrastBorder, checkboxBorder, checkboxBackground, checkboxForeground, problemsErrorIconForeground, problemsWarningIconForeground, problemsInfoIconForeground, inputBackground, inputForeground, inputBorder, textLinkForeground, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBorder, inputValidationErrorBackground, inputValidationErrorForeground, listFilterWidgetBackground, listFilterWidgetNoMatchesOutline, listFilterWidgetOutline, listFilterWidgetShadow, badgeBackground, badgeForeground, breadcrumbsBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, activeContrastBorder, listActiveSelectionBackground, listActiveSelectionForeground, listActiveSelectionIconForeground, listDropOverBackground, listFocusAndSelectionOutline, listFocusBackground, listFocusForeground, listFocusOutline, listHoverBackground, listHoverForeground, listInactiveFocusBackground, listInactiveFocusOutline, listInactiveSelectionBackground, listInactiveSelectionForeground, listInactiveSelectionIconForeground, tableColumnsBorder, tableOddRowsBackgroundColor, treeIndentGuidesStroke, asCssVariableWithDefault, editorWidgetBorder, focusBorder, pickerGroupForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, selectBackground, selectBorder, selectForeground, selectListBackground, treeInactiveIndentGuidesStroke, menuBorder, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuSeparatorBackground, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, listDropBetweenBackground, radioActiveBackground, radioActiveForeground, radioInactiveBackground, radioInactiveForeground, radioInactiveBorder, radioInactiveHoverBackground, radioActiveBorder, checkboxDisabledBackground, checkboxDisabledForeground, widgetBorder } from '../common/colorRegistry.js'; import { IProgressBarStyles } from '../../../base/browser/ui/progressbar/progressbar.js'; import { ICheckboxStyles, IToggleStyles } from '../../../base/browser/ui/toggle/toggle.js'; import { IDialogStyles } from '../../../base/browser/ui/dialog/dialog.js'; @@ -51,6 +51,7 @@ export const defaultButtonStyles: IButtonStyles = { buttonSecondaryForeground: asCssVariable(buttonSecondaryForeground), buttonSecondaryBackground: asCssVariable(buttonSecondaryBackground), buttonSecondaryHoverBackground: asCssVariable(buttonSecondaryHoverBackground), + buttonSecondaryBorder: asCssVariable(buttonSecondaryBorder), buttonBorder: asCssVariable(buttonBorder), }; diff --git a/src/vs/platform/theme/common/colors/inputColors.ts b/src/vs/platform/theme/common/colors/inputColors.ts index 7608c078e0f..4f642bba506 100644 --- a/src/vs/platform/theme/common/colors/inputColors.ts +++ b/src/vs/platform/theme/common/colors/inputColors.ts @@ -12,6 +12,7 @@ import { registerColor, transparent, lighten, darken, ColorTransformType } from // Import the colors we need import { foreground, contrastBorder, focusBorder, iconForeground } from './baseColors.js'; import { editorWidgetBackground } from './editorColors.js'; +import { listHoverBackground } from './listColors.js'; // ----- input @@ -130,15 +131,19 @@ export const buttonBorder = registerColor('button.border', nls.localize('buttonBorder', "Button border color.")); export const buttonSecondaryForeground = registerColor('button.secondaryForeground', - { dark: Color.white, light: Color.white, hcDark: Color.white, hcLight: foreground }, + { dark: foreground, light: foreground, hcDark: Color.white, hcLight: foreground }, nls.localize('buttonSecondaryForeground', "Secondary button foreground color.")); export const buttonSecondaryBackground = registerColor('button.secondaryBackground', - { dark: '#3A3D41', light: '#5F6A79', hcDark: null, hcLight: Color.white }, + { dark: null, light: null, hcDark: null, hcLight: Color.white }, nls.localize('buttonSecondaryBackground', "Secondary button background color.")); +export const buttonSecondaryBorder = registerColor('button.secondaryBorder', + transparent(buttonSecondaryForeground, 0.2), + nls.localize('buttonSecondaryBorder', "Secondary button border color.")); + export const buttonSecondaryHoverBackground = registerColor('button.secondaryHoverBackground', - { dark: lighten(buttonSecondaryBackground, 0.2), light: darken(buttonSecondaryBackground, 0.2), hcDark: null, hcLight: null }, + { dark: listHoverBackground, light: listHoverBackground, hcDark: null, hcLight: null }, nls.localize('buttonSecondaryHoverBackground', "Secondary button background color when hovering.")); // ------ radio diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 2b8d2e64881..f2a1f70f54f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -29,7 +29,7 @@ import { CommandsRegistry, ICommandService } from '../../../../platform/commands import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from '../../../../platform/theme/common/themeService.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; -import { buttonBackground, buttonForeground, buttonHoverBackground, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground, registerColor, editorWarningForeground, editorInfoForeground, editorErrorForeground, buttonSeparator } from '../../../../platform/theme/common/colorRegistry.js'; +import { buttonBackground, buttonForeground, buttonHoverBackground, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground, buttonSecondaryBorder, registerColor, editorWarningForeground, editorInfoForeground, editorErrorForeground, buttonSeparator } from '../../../../platform/theme/common/colorRegistry.js'; import { IJSONEditingService } from '../../../services/configuration/common/jsonEditing.js'; import { ITextEditorSelection } from '../../../../platform/editor/common/editor.js'; import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; @@ -3196,6 +3196,13 @@ registerColor('extensionButton.hoverBackground', { hcLight: null }, localize('extensionButtonHoverBackground', "Button background hover color for extension actions.")); +registerColor('extensionButton.border', { + dark: buttonSecondaryBorder, + light: buttonSecondaryBorder, + hcDark: null, + hcLight: null +}, localize('extensionButtonBorder', "Button border color for extension actions.")); + registerColor('extensionButton.separator', buttonSeparator, localize('extensionButtonSeparator', "Button separator color for extension actions")); export const extensionButtonProminentBackground = registerColor('extensionButton.prominentBackground', { diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css index e999d53c5e5..6326d45f650 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css @@ -38,7 +38,7 @@ .monaco-action-bar .action-item .action-label.extension-action.label, .monaco-action-bar .action-item.action-dropdown-item > .action-dropdown-item-separator { background-color: var(--vscode-extensionButton-background); - border: 1px solid var(--vscode-button-border, transparent); + border: 1px solid var(--vscode-extensionButton-border, transparent); } .monaco-action-bar .action-item.action-dropdown-item > .action-label.extension-action.label { @@ -73,7 +73,7 @@ } .monaco-action-bar .action-item.action-dropdown-item > .action-dropdown-item-separator > div { - background-color: var(--vscode-extensionButton-separator); + background-color: var(--vscode-extensionButton-border, var(--vscode-extensionButton-separator)); } .vscode-high-contrast .monaco-action-bar .action-item.action-dropdown-item > .action-dropdown-item-separator > div { From 36b24b2b4b92c0d5232fb5d0278de562b8fe169c Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 29 Jan 2026 12:07:37 +0100 Subject: [PATCH 087/152] Tweak inline zone styles, make affordance and zone work together better (#291549) * fix: adjust padding and margin for chat input and attachments container * fix: update chat input container styles for better visibility fixes https://github.com/microsoft/vscode/issues/287245 * fix: integrate inline chat session service and update session handling logic --- .../inlineChat/browser/inlineChatAffordance.ts | 16 +++++++++++++++- .../inlineChat/browser/media/inlineChat.css | 14 ++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts index 2991b874a0c..736bf414c2d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../base/common/lifecycle.js'; -import { autorun, debouncedObservable, derived, observableValue, runOnChange, waitForState } from '../../../../base/common/observable.js'; +import { autorun, debouncedObservable, derived, observableSignalFromEvent, observableValue, runOnChange, waitForState } from '../../../../base/common/observable.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; import { ScrollType } from '../../../../editor/common/editorCommon.js'; @@ -19,6 +19,7 @@ import { InlineChatGutterAffordance } from './inlineChatGutterAffordance.js'; import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; import { assertType } from '../../../../base/common/types.js'; import { CursorChangeReason } from '../../../../editor/common/cursorEvents.js'; +import { IInlineChatSessionService } from './inlineChatSessionService.js'; export class InlineChatAffordance extends Disposable { @@ -30,6 +31,7 @@ export class InlineChatAffordance extends Disposable { @IInstantiationService private readonly _instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @IChatEntitlementService chatEntiteldService: IChatEntitlementService, + @IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService, ) { super(); @@ -63,6 +65,18 @@ export class InlineChatAffordance extends Disposable { } })); + const hasSessionObs = derived(r => { + observableSignalFromEvent(this, inlineChatSessionService.onDidChangeSessions).read(r); + const model = editorObs.model.read(r); + return model ? inlineChatSessionService.getSessionByTextModel(model.uri) !== undefined : false; + }); + + this._store.add(autorun(r => { + if (hasSessionObs.read(r)) { + selectionData.set(undefined, undefined); + } + })); + this._store.add(this._instantiationService.createInstance( InlineChatGutterAffordance, editorObs, diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index 842572dbad9..2215f0f84dd 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -96,14 +96,20 @@ .monaco-workbench .zone-widget.inline-chat-widget.inline-chat-2 { .inline-chat .chat-widget .interactive-session .interactive-input-part { - padding: 8px 0 0 0; + padding: 8px 0 4px 0; } + .interactive-session .chat-input-container.focused, .interactive-session .chat-input-container { - border-color: transparent; + border-color: var(--vscode-inlineChat-background); + background-color: var(--vscode-inlineChat-background); padding-left: 0; } + .chat-attachments-container { + margin-right: 0; + } + .chat-attachments-container > .chat-input-toolbar { margin-left: auto; margin-right: 16px; @@ -113,6 +119,10 @@ .request-in-progress .monaco-editor [class^="ced-chat-session-detail"]::after { animation: pulse-opacity 2.5s ease-in-out infinite; } + + .chat-editor-container .interactive-input-editor .monaco-editor .monaco-editor-background { + background-color: var(--vscode-inlineChat-background); + } } From fc46eded124eca55b795e372415aba600e65fddd Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 29 Jan 2026 12:08:03 +0100 Subject: [PATCH 088/152] fixes https://github.com/microsoft/vscode/issues/248372 (#291551) --- .../chat/browser/chatEditing/chatEditingEditorOverlay.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts index 9e4edd985d9..cbaa0bc4753 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts @@ -402,7 +402,7 @@ export class ChatEditingEditorOverlay implements IWorkbenchContribution { () => editorGroupsService.groups ); - const overlayWidgets = new DisposableMap(); + const overlayWidgets = this._store.add(new DisposableMap()); this._store.add(autorun(r => { From 481bea59059b23e7a0954cb5e591db22d6c084c1 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 29 Jan 2026 12:09:21 +0100 Subject: [PATCH 089/152] agent sessions - read/unread tracking tweaks (#291539) --- .../browser/agentSessions/agentSessionsModel.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index a8814377244..9efc88521e2 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -144,8 +144,8 @@ export function isAgentSessionsModel(obj: unknown): obj is IAgentSessionsModel { } interface IAgentSessionState { - readonly archived: boolean; - readonly read: number /* last date turned read */; + readonly archived?: boolean; + readonly read?: number /* last date turned read */; } export const enum AgentSessionSection { @@ -562,7 +562,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode return; // no change } - const state = this.sessionStates.get(session.resource) ?? { archived: false, read: 0 }; + const state = this.sessionStates.get(session.resource) ?? {}; this.sessionStates.set(session.resource, { ...state, archived }); const agentSession = this._sessions.get(session.resource); @@ -598,17 +598,17 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } private sessionTimeForReadStateTracking(session: IInternalAgentSessionData): number { - return session.timing.lastRequestEnded ?? session.timing.lastRequestStarted ?? session.timing.created; + return session.timing.lastRequestEnded ?? session.timing.created; } private setRead(session: IInternalAgentSessionData, read: boolean, skipEvent?: boolean): void { - const state = this.sessionStates.get(session.resource) ?? { archived: false, read: 0 }; + const state = this.sessionStates.get(session.resource) ?? {}; let newRead: number; if (read) { newRead = Math.max(Date.now(), this.sessionTimeForReadStateTracking(session)); - if (state.read >= newRead) { + if (typeof state.read === 'number' && state.read >= newRead) { return; // already read with a sufficient timestamp } } else { @@ -625,7 +625,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } } - private static readonly READ_DATE_BASELINE_KEY = 'agentSessions.readDateBaseline'; + private static readonly READ_DATE_BASELINE_KEY = 'agentSessions.readDateBaseline2'; private readonly readDateBaseline: number; From 5181d051eb08eafe786fc58159d8993f91fb841f Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:09:59 +0100 Subject: [PATCH 090/152] Add telemetry for tree view concurrent refresh (#291540) * Add telemetry for tree view concurrent refresh * Copilot PR feedback * Remove retry and instead wait for refresh --- .../api/browser/mainThreadTreeViews.ts | 24 +++++++++- .../workbench/api/common/extHost.protocol.ts | 1 + .../workbench/api/common/extHostTreeViews.ts | 47 +++++++++++++------ .../test/browser/mainThreadTreeViews.test.ts | 3 +- 4 files changed, 58 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 4a4a6526b90..5c562e5d6bb 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -20,6 +20,7 @@ import { DataTransferFileCache } from '../common/shared/dataTransferCache.js'; import * as typeConvert from '../common/extHostTypeConverters.js'; import { IMarkdownString } from '../../../base/common/htmlContent.js'; import { IViewsService } from '../../services/views/common/viewsService.js'; +import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js'; @extHostNamedCustomer(MainContext.MainThreadTreeViews) export class MainThreadTreeViews extends Disposable implements MainThreadTreeViewsShape { @@ -33,7 +34,8 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie @IViewsService private readonly viewsService: IViewsService, @INotificationService private readonly notificationService: INotificationService, @IExtensionService private readonly extensionService: IExtensionService, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @ITelemetryService private readonly telemetryService: ITelemetryService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTreeViews); @@ -138,6 +140,26 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie this._dataProviders.deleteAndDispose(treeViewId); } + $logResolveTreeNodeRetry(extensionId: string, retryCount: number, exhausted: boolean): void { + type TreeViewResolveRetryEvent = { + extensionId: string; + retryCount: number; + exhausted: boolean; + }; + type TreeViewResolveRetryClassification = { + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension identifier.' }; + retryCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Number of retry attempts made.' }; + exhausted: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether all retry attempts were exhausted.' }; + owner: 'alexr00'; + comment: 'Tracks tree view resolve retries due to concurrent refresh races.'; + }; + this.telemetryService.publicLog2('treeView.resolveRetry', { + extensionId, + retryCount, + exhausted + }); + } + private async reveal(treeView: ITreeView, dataProvider: TreeViewDataProvider, itemIn: ITreeItem, parentChain: ITreeItem[], options: IRevealOptions): Promise { options = options ? options : { select: false, focus: false }; const select = isUndefinedOrNull(options.select) ? false : options.select; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 305568f3b3d..3ea06f3076a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -354,6 +354,7 @@ export interface MainThreadTreeViewsShape extends IDisposable { $setBadge(treeViewId: string, badge: IViewBadge | undefined): void; $resolveDropFileData(destinationViewId: string, requestId: number, dataItemId: string): Promise; $disposeTree(treeViewId: string): Promise; + $logResolveTreeNodeRetry(extensionId: string, retryCount: number, exhausted: boolean): void; } export interface MainThreadDownloadServiceShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index f848dc043a1..ca9b9304256 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -699,24 +699,41 @@ class ExtHostTreeView extends Disposable { return asPromise(() => this._dataProvider.getParent!(element)); } - private _resolveTreeNode(element: T, parent?: TreeNode): Promise { + private async _resolveTreeNode(element: T, parent?: TreeNode): Promise { const node = this._nodes.get(element); if (node) { - return Promise.resolve(node); + return node; } - return asPromise(() => this._dataProvider.getTreeItem(element)) - .then(extTreeItem => this._createHandle(element, extTreeItem, parent, true)) - .then(handle => this.getChildren(parent ? parent.item.handle : undefined) - .then(() => { - const cachedElement = this.getExtensionElement(handle); - if (cachedElement) { - const node = this._nodes.get(cachedElement); - if (node) { - return Promise.resolve(node); - } - } - throw new Error(`Cannot resolve tree item for element ${handle} from extension ${this._extension.identifier.value}`); - })); + const extTreeItem = await asPromise(() => this._dataProvider.getTreeItem(element)); + const handle = this._createHandle(element, extTreeItem, parent, true); + const children = await this.getChildren(parent ? parent.item.handle : undefined); + // If getChildren returned undefined, it means a concurrent refresh invalidated + // the fetch. Wait for the refresh to complete and check if the element was resolved. + if (children === undefined) { + this._logService.warn(`[${this._viewId}] Concurrent refresh detected in _resolveTreeNode for element ${handle} from extension ${this._extension.identifier.value}, waiting for refresh to complete`); + this._proxy.$logResolveTreeNodeRetry(this._extension.identifier.value, 1, false); + // Wait for any pending refresh to complete + await this._refreshPromise; + // Check if the element is now in the cache after the refresh completed + const cachedElement = this.getExtensionElement(handle); + if (cachedElement) { + const node = this._nodes.get(cachedElement); + if (node) { + return node; + } + } + // Still not found after refresh completed - log and throw + this._proxy.$logResolveTreeNodeRetry(this._extension.identifier.value, 1, true); + throw new Error(`Cannot resolve tree item for element ${handle} from extension ${this._extension.identifier.value}`); + } + const cachedElement = this.getExtensionElement(handle); + if (cachedElement) { + const node = this._nodes.get(cachedElement); + if (node) { + return node; + } + } + throw new Error(`Cannot resolve tree item for element ${handle} from extension ${this._extension.identifier.value}`); } private _getChildrenNodes(parentNodeOrHandle: TreeNode | TreeItemHandle | Root): TreeNode[] | undefined { diff --git a/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts b/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts index ff73ee23a0a..dc20abb8c5e 100644 --- a/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts @@ -12,6 +12,7 @@ import { TestInstantiationService } from '../../../../platform/instantiation/tes import { NullLogService } from '../../../../platform/log/common/log.js'; import { TestNotificationService } from '../../../../platform/notification/test/common/testNotificationService.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; +import { NullTelemetryService } from '../../../../platform/telemetry/common/telemetryUtils.js'; import { MainThreadTreeViews } from '../../browser/mainThreadTreeViews.js'; import { ExtHostTreeViewsShape } from '../../common/extHost.protocol.js'; import { CustomTreeView } from '../../../browser/parts/views/treeView.js'; @@ -80,7 +81,7 @@ suite('MainThreadHostTreeView', function () { return extHostTreeViewsShape; } drain(): any { return null; } - }, new TestViewsService(), new TestNotificationService(), testExtensionService, new NullLogService())); + }, new TestViewsService(), new TestNotificationService(), testExtensionService, new NullLogService(), NullTelemetryService)); mainThreadTreeViews.$registerTreeViewDataProvider(testTreeViewId, { showCollapseAll: false, canSelectMany: false, dropMimeTypes: [], dragMimeTypes: [], hasHandleDrag: false, hasHandleDrop: false, manuallyManageCheckboxes: false }); await testExtensionService.whenInstalledExtensionsRegistered(); }); From 35bb52f5c114977be650457f7d2d3eb862ab3b35 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 29 Jan 2026 12:17:45 +0100 Subject: [PATCH 091/152] fix #290760 (#291558) --- .../chatManagement/chatModelsWidget.ts | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts index 528f4110c72..7f217c9be8e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts @@ -114,19 +114,40 @@ class ModelsFilterAction extends Action { } } -function toggleFilter(currentQuery: string, query: string, alternativeQueries: string[] = []): string { - const allQueries = [query, ...alternativeQueries]; - const isChecked = allQueries.some(q => currentQuery.includes(q)); +interface IFilterQuery { + /** The primary filter query string */ + query: string; + /** Alternative query strings that are treated as synonyms of the primary query */ + synonyms?: string[]; + /** Query strings that should be removed when adding this filter (mutually exclusive filters) */ + excludes?: string[]; +} - if (!isChecked) { - const trimmedQuery = currentQuery.trim(); - return trimmedQuery ? `${trimmedQuery} ${query}` : query; - } else { +function toggleFilter(currentQuery: string, filter: IFilterQuery): string { + const { query, synonyms = [], excludes = [] } = filter; + const allSynonyms = [query, ...synonyms]; + const isChecked = allSynonyms.some(q => currentQuery.includes(q)); + const hasExcludedQuery = excludes.some(q => currentQuery.includes(q)); + + if (isChecked) { + // Query or synonym is already set, remove all of them (toggle off) let queryWithRemovedFilter = currentQuery; - for (const q of allQueries) { + for (const q of allSynonyms) { queryWithRemovedFilter = queryWithRemovedFilter.replace(q, ''); } return queryWithRemovedFilter.replace(/\s+/g, ' ').trim(); + } else if (hasExcludedQuery) { + // An excluded query is set, replace it with the new query + let newQuery = currentQuery; + for (const q of excludes) { + newQuery = newQuery.replace(q, ''); + } + newQuery = newQuery.replace(/\s+/g, ' ').trim(); + return newQuery ? `${newQuery} ${query}` : query; + } else { + // No filter is set, add the new query + const trimmedQuery = currentQuery.trim(); + return trimmedQuery ? `${trimmedQuery} ${query}` : query; } } @@ -180,7 +201,7 @@ class ModelsSearchFilterDropdownMenuActionViewItem extends DropdownMenuActionVie class: undefined, enabled: true, checked: isChecked, - run: () => this.toggleFilterAndSearch(query, [`@provider:${vendor}`]) + run: () => this.toggleFilterAndSearch({ query, synonyms: [`@provider:${vendor}`] }) }; } @@ -196,13 +217,12 @@ class ModelsSearchFilterDropdownMenuActionViewItem extends DropdownMenuActionVie class: undefined, enabled: true, checked: isChecked, - run: () => this.toggleFilterAndSearch(query) + run: () => this.toggleFilterAndSearch({ query }) }; } private createVisibleAction(visible: boolean, label: string): IAction { const query = `@visible:${visible}`; - const oppositeQuery = `@visible:${!visible}`; const currentQuery = this.search.getValue(); const isChecked = currentQuery.includes(query); @@ -213,13 +233,13 @@ class ModelsSearchFilterDropdownMenuActionViewItem extends DropdownMenuActionVie class: undefined, enabled: true, checked: isChecked, - run: () => this.toggleFilterAndSearch(query, [oppositeQuery]) + run: () => this.toggleFilterAndSearch({ query, excludes: [`@visible:${!visible}`] }) }; } - private toggleFilterAndSearch(query: string, alternativeQueries: string[] = []): void { + private toggleFilterAndSearch(filter: IFilterQuery): void { const currentQuery = this.search.getValue(); - const newQuery = toggleFilter(currentQuery, query, alternativeQueries); + const newQuery = toggleFilter(currentQuery, filter); this.search.setValue(newQuery); } @@ -1013,7 +1033,7 @@ export class ChatModelsWidget extends Disposable { this.tableDisposables.add(capabilitiesColumnRenderer.onDidClickCapability(capability => { const currentQuery = this.searchWidget.getValue(); const query = `@capability:${capability}`; - const newQuery = toggleFilter(currentQuery, query); + const newQuery = toggleFilter(currentQuery, { query }); this.search(newQuery); })); From 2427fabdbf580521fb62a945982b45f3723bea13 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 29 Jan 2026 11:39:25 +0000 Subject: [PATCH 092/152] Refactor 2026 Dark theme colors and enhance tab styling --- extensions/theme-2026/themes/2026-dark.json | 14 ++++---- extensions/theme-2026/themes/styles.css | 36 ++++++++++++++++++--- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 26f52ae7f54..3e3c676bac1 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -20,9 +20,6 @@ "button.foreground": "#FFFFFF", "button.hoverBackground": "#3E9BC4", "button.border": "#2A2B2CFF", - "button.secondaryBackground": "#242526", - "button.secondaryForeground": "#bfbfbf", - "button.secondaryHoverBackground": "#313233", "checkbox.background": "#242526", "checkbox.border": "#333536", "checkbox.foreground": "#bfbfbf", @@ -160,13 +157,13 @@ "editorOverviewRuler.errorForeground": "#f48771", "editorOverviewRuler.warningForeground": "#e5ba7d", "panel.background": "#191A1B", - "panel.border": "#00000000", - "panelTitle.activeBorder": "#bfbfbf", + "panel.border": "#2A2B2CFF", + "panelTitle.activeBorder": "#3994BC", "panelTitle.activeForeground": "#bfbfbf", "panelTitle.inactiveForeground": "#888888", "statusBar.background": "#191A1B", "statusBar.foreground": "#888888", - "statusBar.border": "#00000000", + "statusBar.border": "#2A2B2CFF", "statusBar.focusBorder": "#3994BCB3", "statusBar.debuggingBackground": "#3994BC", "statusBar.debuggingForeground": "#FFFFFF", @@ -185,7 +182,7 @@ "tab.border": "#2A2B2CFF", "tab.lastPinnedBorder": "#2A2B2CFF", "tab.activeBorder": "#121314", - "tab.activeBorderTop": "#bfbfbf", + "tab.activeBorderTop": "#3994BC", "tab.hoverBackground": "#262728", "tab.hoverForeground": "#bfbfbf", "tab.unfocusedActiveBackground": "#121314", @@ -226,6 +223,9 @@ "quickInputList.focusIconForeground": "#bfbfbf", "quickInputList.hoverBackground": "#515253", "terminal.selectionBackground": "#3994BC33", + "terminal.background": "#121314", + "terminal.border": "#2A2B2CFF", + "terminal.tab.activeBorder": "#3994BC00", "terminalCursor.foreground": "#bfbfbf", "terminalCursor.background": "#191A1B", "gitDecoration.addedResourceForeground": "#73c991", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index f695504e7c2..2b4a8734afe 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -76,17 +76,37 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { box-shadow: inset var(--shadow-active-tab); position: relative; z-index: 5; - /* border-radius: var(--radius-sm) var(--radius-sm) 0 0; */ + border-radius: 0; border-top: none !important; + background: linear-gradient( + to bottom, + color-mix(in srgb, var(--vscode-focusBorder) 10%, transparent) 0%, + transparent 100% + ), var(--vscode-tab-activeBackground) !important; +} +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover:not(.active) { + box-shadow: var(--shadow-sm); } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover:not(.active) { box-shadow: var(--shadow-sm); } /* Tab border bottom - make transparent */ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container { --tabs-border-bottom-color: transparent !important; } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab { --tab-border-bottom-color: transparent !important; } /* Title Bar */ -.monaco-workbench .part.titlebar { box-shadow: var(--shadow-md); z-index: 60; position: relative; overflow: visible !important; } +.monaco-workbench.vs .part.titlebar { box-shadow: var(--shadow-md); } + +.monaco-workbench.vs-dark .part.titlebar { + position: relative; + overflow: visible !important; + background: linear-gradient( + to bottom, + color-mix(in srgb, var(--vscode-focusBorder) 10%, transparent) 0%, + transparent 100% + ), var(--vscode-titleBar-activeBackground) !important; +} +.monaco-workbench .part.titlebar.inactive { + background: var(--vscode-titleBar-inactiveBackground) !important; +} .monaco-workbench .part.titlebar .titlebar-container, .monaco-workbench .part.titlebar .titlebar-center, .monaco-workbench .part.titlebar .titlebar-center .window-title, @@ -321,13 +341,19 @@ .monaco-workbench .monaco-dropdown .dropdown-menu { box-shadow: var(--shadow-lg); border: none; border-radius: var(--radius-lg); } /* Terminal */ -.monaco-workbench .pane-body.integrated-terminal { box-shadow: var(--shadow-inset-white); } +.monaco-workbench.vs .pane-body.integrated-terminal { box-shadow: var(--shadow-inset-white); } /* SCM */ .monaco-workbench .scm-view .scm-provider { box-shadow: var(--shadow-sm); border-radius: var(--radius-md); } /* Debug Toolbar */ -.monaco-workbench .debug-toolbar { box-shadow: var(--shadow-lg); border: none; border-radius: var(--radius-lg); backdrop-filter: var(--backdrop-blur-lg) !important; -webkit-backdrop-filter: var(--backdrop-blur-lg) !important; } +.monaco-workbench .debug-toolbar { + box-shadow: var(--shadow-lg); + border: none; + border-radius: var(--radius-lg); + backdrop-filter: var(--backdrop-blur-lg) !important; + -webkit-backdrop-filter: var(--backdrop-blur-lg) !important; +} .monaco-workbench .debug-hover-widget { box-shadow: var(--shadow-hover); From f448b54db37d28665d004216f2531b3f8344f084 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 29 Jan 2026 12:39:42 +0100 Subject: [PATCH 093/152] Agents welcome view UI fixes --- .../chat/browser/widget/media/chat.css | 2 +- .../browser/agentSessionsWelcome.ts | 1 + .../browser/media/agentSessionsWelcome.css | 38 +++++++++---------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index f82ce561de4..6663d5f2a08 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -771,7 +771,7 @@ have to be updated for changes to the rules above, or to support more deeply nes box-sizing: border-box; cursor: text; background-color: var(--vscode-input-background); - border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-border, transparent)); + border: 1px solid var(--vscode-input-border, transparent); border-radius: 4px; padding: 0 6px 6px 6px; /* top padding is inside the editor widget */ diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index 8779c2596b1..e3d9eb4f3fc 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -208,6 +208,7 @@ export class AgentSessionsWelcomePage extends EditorPane { originalSessions = hasSessions; clearNode(sessionsSection); this.buildSessionsOrPrompts(sessionsSection); + this.layoutSessionsControl(); } })); diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css b/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css index a69ab12ebae..3021512101f 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css @@ -163,34 +163,34 @@ /* * Transform items into 2-column layout: - * - Items 0,1 form visual row 1 (top: 0) - * - Items 2,3 form visual row 2 (top: 52) - * - Items 4,5 form visual row 3 (top: 104) - * Left column (even): items stay in place or move up - * Right column (odd): items move right and up + * - Odd items (1, 3, 5, ...) stay in left column + * - Even items (2, 4, 6, ...) move to right column + * Each pair forms a visual row. + * Left column items need to move up by floor((index-1)/2) rows + * Right column items need to move right and up by (index/2) rows + * Row height is 52px. */ -/* Item 1 (index 1): move to right column of row 1 */ -.agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(2) { - transform: translateX(100%) translateY(-52px); -} - -/* Item 2 (index 2): move up to row 2 left column */ +/* Left column items (odd positions): move up to form 2-column layout */ +/* Item 3: move up 1 row */ .agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(3) { transform: translateY(-52px); } - -/* Item 3 (index 3): move to right column of row 2 */ -.agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(4) { - transform: translateX(100%) translateY(-104px); -} - -/* Item 4 (index 4): move up to row 3 left column */ +/* Item 5: move up 2 rows */ .agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(5) { transform: translateY(-104px); } -/* Item 5 (index 5): move to right column of row 3 */ +/* Right column items (even positions): move right and up */ +/* Item 2: move right, up 1 row */ +.agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(2) { + transform: translateX(100%) translateY(-52px); +} +/* Item 4: move right, up 2 rows */ +.agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(4) { + transform: translateX(100%) translateY(-104px); +} +/* Item 6: move right, up 3 rows */ .agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(6) { transform: translateX(100%) translateY(-156px); } From 0f9e7202506740f6da7c4e60f451a52c0f76807c Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 29 Jan 2026 12:44:25 +0100 Subject: [PATCH 094/152] only hide word suggestions when inline completions are enabled --- .../browser/services/editorWorkerService.ts | 5 +- .../common/services/completionsEnablement.ts | 77 +++++++++++++++++++ .../browser/model/inlineCompletionsSource.ts | 15 +--- .../chat/browser/chatStatus/chatStatus.ts | 16 ---- .../browser/chatStatus/chatStatusDashboard.ts | 3 +- .../browser/chatStatus/chatStatusEntry.ts | 3 +- 6 files changed, 87 insertions(+), 32 deletions(-) create mode 100644 src/vs/editor/common/services/completionsEnablement.ts diff --git a/src/vs/editor/browser/services/editorWorkerService.ts b/src/vs/editor/browser/services/editorWorkerService.ts index c5d160cb899..3f93ce10ea2 100644 --- a/src/vs/editor/browser/services/editorWorkerService.ts +++ b/src/vs/editor/browser/services/editorWorkerService.ts @@ -37,6 +37,7 @@ import { EditorWorkerHost } from '../../common/services/editorWorkerHost.js'; import { StringEdit } from '../../common/core/edits/stringEdit.js'; import { OffsetRange } from '../../common/core/ranges/offsetRange.js'; import { FileAccess } from '../../../base/common/network.js'; +import { isCompletionsEnabledWithTextResourceConfig } from '../../common/services/completionsEnablement.js'; /** * Stop the worker if it was not needed for 5 min. @@ -280,7 +281,9 @@ class WordBasedCompletionItemProvider implements languages.CompletionItemProvide return undefined; } - if (config.wordBasedSuggestions === 'offWithInlineSuggestions' && this.languageFeaturesService.inlineCompletionsProvider.has(model)) { + if (config.wordBasedSuggestions === 'offWithInlineSuggestions' + && this.languageFeaturesService.inlineCompletionsProvider.has(model) + && isCompletionsEnabledWithTextResourceConfig(this._configurationService, model.getLanguageId())) { return undefined; } diff --git a/src/vs/editor/common/services/completionsEnablement.ts b/src/vs/editor/common/services/completionsEnablement.ts new file mode 100644 index 00000000000..45152190ae1 --- /dev/null +++ b/src/vs/editor/common/services/completionsEnablement.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import product from '../../../platform/product/common/product.js'; +import { isObject } from '../../../base/common/types.js'; +import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; +import { ITextResourceConfigurationService } from './textResourceConfiguration.js'; + +/** + * Get the completions enablement setting name from product configuration. + */ +function getCompletionsEnablementSettingName(): string | undefined { + return product.defaultChatAgent?.completionsEnablementSetting; +} + +/** + * Checks if completions (e.g., Copilot) are enabled for a given language ID + * using `IConfigurationService`. + * + * @param configurationService The configuration service to read settings from. + * @param modeId The language ID to check. Defaults to '*' which checks the global setting. + * @returns `true` if completions are enabled for the language, `false` otherwise. + */ +export function isCompletionsEnabled(configurationService: IConfigurationService, modeId: string = '*'): boolean { + const settingName = getCompletionsEnablementSettingName(); + if (!settingName) { + return false; + } + + return isCompletionsEnabledFromObject( + configurationService.getValue>(settingName), + modeId + ); +} + +/** + * Checks if completions (e.g., Copilot) are enabled for a given language ID + * using `ITextResourceConfigurationService`. + * + * @param configurationService The text resource configuration service to read settings from. + * @param modeId The language ID to check. Defaults to '*' which checks the global setting. + * @returns `true` if completions are enabled for the language, `false` otherwise. + */ +export function isCompletionsEnabledWithTextResourceConfig(configurationService: ITextResourceConfigurationService, modeId: string = '*'): boolean { + const settingName = getCompletionsEnablementSettingName(); + if (!settingName) { + return false; + } + + // Pass undefined as resource to get the global setting + return isCompletionsEnabledFromObject( + configurationService.getValue>(undefined, settingName), + modeId + ); +} + +/** + * Checks if completions are enabled for a given language ID using a pre-fetched + * completions enablement object. + * + * @param completionsEnablementObject The object containing per-language enablement settings. + * @param modeId The language ID to check. Defaults to '*' which checks the global setting. + * @returns `true` if completions are enabled for the language, `false` otherwise. + */ +export function isCompletionsEnabledFromObject(completionsEnablementObject: Record | undefined, modeId: string = '*'): boolean { + if (!isObject(completionsEnablementObject)) { + return false; // default to disabled if setting is not available + } + + if (typeof completionsEnablementObject[modeId] !== 'undefined') { + return Boolean(completionsEnablementObject[modeId]); // go with setting if explicitly defined + } + + return Boolean(completionsEnablementObject['*']); // fallback to global setting otherwise +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index b936f4d216d..83d4831495d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -28,6 +28,7 @@ import { Command, InlineCompletionEndOfLifeReasonKind, InlineCompletionTriggerKi import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; import { ITextModel } from '../../../../common/model.js'; import { offsetEditFromContentChanges } from '../../../../common/model/textModelStringEdit.js'; +import { isCompletionsEnabledFromObject } from '../../../../common/services/completionsEnablement.js'; import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js'; import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js'; import { formatRecordableLogEntry, IRecordableEditorLogEntry, IRecordableLogEntry, StructuredLogger } from '../structuredLogger.js'; @@ -445,7 +446,7 @@ export class InlineCompletionsSource extends Disposable { } - if (!isCompletionsEnabled(this._completionsEnabled, this._textModel.getLanguageId())) { + if (!isCompletionsEnabledFromObject(this._completionsEnabled, this._textModel.getLanguageId())) { return; } @@ -571,18 +572,6 @@ function isSubset(set1: Set, set2: Set): boolean { return [...set1].every(item => set2.has(item)); } -function isCompletionsEnabled(completionsEnablementObject: Record | undefined, modeId: string = '*'): boolean { - if (completionsEnablementObject === undefined) { - return false; // default to disabled if setting is not available - } - - if (typeof completionsEnablementObject[modeId] !== 'undefined') { - return Boolean(completionsEnablementObject[modeId]); // go with setting if explicitly defined - } - - return Boolean(completionsEnablementObject['*']); // fallback to global setting otherwise -} - class UpdateOperation implements IDisposable { constructor( public readonly request: UpdateRequest, diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatus.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatus.ts index ef3e5989e8c..ad964b4534d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatus.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatus.ts @@ -4,24 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { ChatEntitlement, IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import product from '../../../../../platform/product/common/product.js'; -import { isObject } from '../../../../../base/common/types.js'; export function isNewUser(chatEntitlementService: IChatEntitlementService): boolean { return !chatEntitlementService.sentiment.installed || // chat not installed chatEntitlementService.entitlement === ChatEntitlement.Available; // not yet signed up to chat } - -export function isCompletionsEnabled(configurationService: IConfigurationService, modeId: string = '*'): boolean { - const result = configurationService.getValue>(product.defaultChatAgent.completionsEnablementSetting); - if (!isObject(result)) { - return false; - } - - if (typeof result[modeId] !== 'undefined') { - return Boolean(result[modeId]); // go with setting if explicitly defined - } - - return Boolean(result['*']); // fallback to global setting otherwise -} diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts index 7d94ce384e4..7ddbb06cc8c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts @@ -40,13 +40,14 @@ import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/edi import { IChatEntitlementService, ChatEntitlementService, ChatEntitlement, IQuotaSnapshot, getChatPlanName } from '../../../../services/chat/common/chatEntitlementService.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IChatSessionsService } from '../../common/chatSessionsService.js'; -import { isNewUser, isCompletionsEnabled } from './chatStatus.js'; +import { isNewUser } from './chatStatus.js'; import { IChatStatusItemService, ChatStatusEntry } from './chatStatusItemService.js'; import product from '../../../../../platform/product/common/product.js'; import { contrastBorder, inputValidationErrorBorder, inputValidationInfoBorder, inputValidationWarningBorder, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js'; import { Color } from '../../../../../base/common/color.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { ChatViewId } from '../chat.js'; +import { isCompletionsEnabled } from '../../../../../editor/common/services/completionsEnablement.js'; const defaultChat = product.defaultChatAgent; diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusEntry.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusEntry.ts index f465335f45e..0e1160d71e4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusEntry.ts @@ -19,8 +19,9 @@ import { IChatSessionsService } from '../../common/chatSessionsService.js'; import { ChatStatusDashboard } from './chatStatusDashboard.js'; import { mainWindow } from '../../../../../base/browser/window.js'; import { disposableWindowInterval } from '../../../../../base/browser/dom.js'; -import { isNewUser, isCompletionsEnabled } from './chatStatus.js'; +import { isNewUser } from './chatStatus.js'; import product from '../../../../../platform/product/common/product.js'; +import { isCompletionsEnabled } from '../../../../../editor/common/services/completionsEnablement.js'; export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribution { From f9b45cc9d5f1fd60c55b277281dc9b9be223bd9c Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 29 Jan 2026 12:45:39 +0100 Subject: [PATCH 095/152] fix #290769 (#291566) --- src/vs/workbench/browser/parts/titlebar/titlebarPart.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 5b17b596220..743f9e6ee8b 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -821,7 +821,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { private get activityActionsEnabled(): boolean { const activityBarPosition = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); - return !this.isCompact && !this.isAuxiliary && (activityBarPosition === ActivityBarPosition.TOP || activityBarPosition === ActivityBarPosition.BOTTOM || activityBarPosition === ActivityBarPosition.HIDDEN); + return !this.isCompact && !this.isAuxiliary && (activityBarPosition === ActivityBarPosition.TOP || activityBarPosition === ActivityBarPosition.BOTTOM); } private get globalActionsEnabled(): boolean { From 1b9472fdb8afbe3ef5b798d27d5804b5c060347f Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 29 Jan 2026 12:49:08 +0100 Subject: [PATCH 096/152] Move layout out of if --- .../welcomeAgentSessions/browser/agentSessionsWelcome.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index e3d9eb4f3fc..3edf21e7f57 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -208,8 +208,8 @@ export class AgentSessionsWelcomePage extends EditorPane { originalSessions = hasSessions; clearNode(sessionsSection); this.buildSessionsOrPrompts(sessionsSection); - this.layoutSessionsControl(); } + this.layoutSessionsControl(); })); this.scrollableElement?.scanDomNode(); From 9bd3edc6be2d5bac7eab6515d609e3cf4b087c48 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 29 Jan 2026 14:34:25 +0100 Subject: [PATCH 097/152] inline chat: fix placeholder text and remove unused parameter (#291568) - Change selection placeholder to 'Modify selected code' - Remove unused _hover parameter from InlineChatEditorAffordance - Fix input height reset in show() to use _updateInputHeight() Fixes #291070, fixes #291032 --- .../inlineChat/browser/inlineChatAffordance.ts | 3 +-- .../inlineChat/browser/inlineChatEditorAffordance.ts | 3 +-- .../inlineChat/browser/inlineChatOverlayWidget.ts | 11 ++++------- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts index 736bf414c2d..f0ec896f0bb 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -87,8 +87,7 @@ export class InlineChatAffordance extends Disposable { this._store.add(this._instantiationService.createInstance( InlineChatEditorAffordance, this._editor, - derived(r => affordance.read(r) === 'editor' ? selectionData.read(r) : undefined), - this._menuData + derived(r => affordance.read(r) === 'editor' ? selectionData.read(r) : undefined) )); this._store.add(autorun(r => { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts index a46d6af54c6..568fb591e54 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts @@ -10,7 +10,7 @@ import { Disposable, DisposableStore, MutableDisposable } from '../../../../base import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; -import { autorun, IObservable, ISettableObservable } from '../../../../base/common/observable.js'; +import { autorun, IObservable } from '../../../../base/common/observable.js'; import { MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -99,7 +99,6 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi constructor( private readonly _editor: ICodeEditor, selection: IObservable, - _hover: ISettableObservable<{ rect: DOMRect; above: boolean; lineNumber: number } | undefined>, @IInstantiationService instantiationService: IInstantiationService, ) { super(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 9585c9972d0..5094ab2e70d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -106,7 +106,6 @@ export class InlineChatInputWidget extends Disposable { const model = this._store.add(modelService.createModel('', null, URI.parse(`gutter-input:${Date.now()}`), true)); this._input.setModel(model); - this._input.layout({ width: 200, height: 18 }); // Initialize sticky scroll height observable const stickyScrollController = StickyScrollController.get(this._editorObs.editor); @@ -117,11 +116,10 @@ export class InlineChatInputWidget extends Disposable { const selection = this._editorObs.cursorSelection.read(r); const hasSelection = selection && !selection.isEmpty(); const placeholderText = hasSelection - ? localize('placeholderWithSelection', "Edit selection") + ? localize('placeholderWithSelection', "Modify selected code") : localize('placeholderNoSelection', "Generate code"); - this._input.updateOptions({ - placeholder: this._keybindingService.appendKeybinding(placeholderText, ACTION_START) - }); + + this._input.updateOptions({ placeholder: this._keybindingService.appendKeybinding(placeholderText, ACTION_START) }); })); // Listen to content size changes and resize the input editor (max 3 lines) @@ -202,8 +200,7 @@ export class InlineChatInputWidget extends Disposable { // Clear input state this._input.getModel().setValue(''); - this._inputContainer.style.height = '26px'; - this._input.layout({ width: 200, height: 18 }); + this._updateInputHeight(this._input.getContentHeight()); // Refresh actions from menu this._refreshActions(); From acbd49bcaa58cd822b27cc1aab8441328621e381 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 29 Jan 2026 14:57:19 +0100 Subject: [PATCH 098/152] agent status and unified agents bar should respect command center setting --- src/vs/workbench/browser/layout.ts | 6 +++--- .../experiments/agentSessionProjectionActions.ts | 6 ++++-- .../experiments/agentTitleBarStatusWidget.ts | 12 ++++-------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index abd34a528b5..da337f0d06e 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -117,6 +117,7 @@ interface IInitialEditorsState { const COMMAND_CENTER_SETTINGS = [ 'chat.agentsControl.enabled', + 'chat.unifiedAgentsBar.enabled', 'workbench.navigationControl.enabled', 'workbench.experimental.share.enabled', ]; @@ -396,10 +397,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi ].some(setting => e.affectsConfiguration(setting))) { // Show Command Center if command center actions enabled - const shareEnabled = e.affectsConfiguration('workbench.experimental.share.enabled') && this.configurationService.getValue('workbench.experimental.share.enabled'); - const navigationControlEnabled = e.affectsConfiguration('workbench.navigationControl.enabled') && this.configurationService.getValue('workbench.navigationControl.enabled'); + const enabledCommandCenterAction = COMMAND_CENTER_SETTINGS.some(setting => e.affectsConfiguration(setting) && this.configurationService.getValue(setting) === true); - if (shareEnabled || navigationControlEnabled) { + if (enabledCommandCenterAction) { if (this.configurationService.getValue(LayoutSettings.COMMAND_CENTER) === false) { this.configurationService.updateValue(LayoutSettings.COMMAND_CENTER, true); return; // onDidChangeConfiguration will be triggered again diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionActions.ts index 165804cf11e..a5e80a2aa79 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionActions.ts @@ -101,7 +101,8 @@ export class ToggleAgentStatusAction extends ToggleTitleBarConfigAction { ContextKeyExpr.and( ChatContextKeys.enabled, IsCompactTitleBarContext.negate(), - ChatContextKeys.supported + ChatContextKeys.supported, + ContextKeyExpr.has('config.window.commandCenter') ) ); } @@ -120,7 +121,8 @@ export class ToggleUnifiedAgentsBarAction extends ToggleTitleBarConfigAction { ContextKeyExpr.and( ChatContextKeys.enabled, IsCompactTitleBarContext.negate(), - ChatContextKeys.supported + ChatContextKeys.supported, + ContextKeyExpr.has('config.window.commandCenter'), ) ); } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts index 9ec4f162d4f..65ea313a09b 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts @@ -1244,20 +1244,16 @@ export class AgentTitleBarStatusRendering extends Disposable implements IWorkben // Add/remove CSS classes on workbench based on settings // Force enable command center and disable chat controls when agent status or unified agents bar is enabled const updateClass = () => { - const enabled = configurationService.getValue(ChatConfiguration.AgentStatusEnabled) === true; - const enhanced = configurationService.getValue(ChatConfiguration.UnifiedAgentsBar) === true; + const commandCenterEnabled = configurationService.getValue(LayoutSettings.COMMAND_CENTER) === true; + const enabled = configurationService.getValue(ChatConfiguration.AgentStatusEnabled) === true && commandCenterEnabled; + const enhanced = configurationService.getValue(ChatConfiguration.UnifiedAgentsBar) === true && commandCenterEnabled; mainWindow.document.body.classList.toggle('agent-status-enabled', enabled); mainWindow.document.body.classList.toggle('unified-agents-bar', enhanced); - - // Force enable command center when agent status or unified agents bar is enabled - if ((enabled || enhanced) && configurationService.getValue(LayoutSettings.COMMAND_CENTER) !== true) { - configurationService.updateValue(LayoutSettings.COMMAND_CENTER, true); - } }; updateClass(); this._register(configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(ChatConfiguration.AgentStatusEnabled) || e.affectsConfiguration(ChatConfiguration.UnifiedAgentsBar)) { + if (e.affectsConfiguration(ChatConfiguration.AgentStatusEnabled) || e.affectsConfiguration(ChatConfiguration.UnifiedAgentsBar) || e.affectsConfiguration(LayoutSettings.COMMAND_CENTER)) { updateClass(); } })); From 221810991f378dd81ff1226c427687cb29c3a1e9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 29 Jan 2026 06:04:32 -0800 Subject: [PATCH 099/152] Make auto approve rules for flags more speciifc Fixes #288589 --- .../terminalChatAgentToolsConfiguration.ts | 16 ++++++++-------- .../electron-browser/runInTerminalTool.test.ts | 2 ++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index 53853155826..3db27d7e85b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -220,7 +220,7 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary { store.add(fileService.registerProvider(Schemas.file, fileSystemProvider)); setConfig(TerminalChatAgentToolsSettingId.EnableAutoApprove, true); + setConfig(TerminalChatAgentToolsSettingId.BlockDetectedFileWrites, 'outsideWorkspace'); terminalServiceDisposeEmitter = new Emitter(); chatServiceDisposeEmitter = new Emitter<{ sessionResource: URI[]; reason: 'cleared' }>(); @@ -263,6 +264,7 @@ suite('RunInTerminalTool', () => { 'sed "s/foo/bar/g"', 'sed -n "1,10p" file.txt', 'sed -n \'45,80p\' /foo/bar/Example.java', + 'sed -n \'45,80p\' extensions/markdown-language-features/src/test/copyFile.test.ts', 'sort file.txt', 'tree directory', From 2a8b28ab9a623b289764771fa267863d0147913e Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 29 Jan 2026 15:07:19 +0100 Subject: [PATCH 100/152] better hover color for action list toolbar action --- src/vs/platform/actionWidget/browser/actionWidget.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index cd7ec8bab16..a5f92d85919 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -153,6 +153,10 @@ box-shadow: inset 0 -1px 0 var(--vscode-widget-shadow); } +.action-widget .monaco-list-row .action-list-item-toolbar .monaco-action-bar:not(.vertical) .action-label:not(.disabled):hover{ + background-color: var(--vscode-list-activeSelectionBackground); +} + /* Action bar */ .action-widget .action-widget-action-bar { From f1308562f45bc8b6873414cdc445863aae445f5d Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 29 Jan 2026 15:08:29 +0100 Subject: [PATCH 101/152] :lipstick: --- src/vs/platform/actionWidget/browser/actionWidget.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index a5f92d85919..0c63d247286 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -153,10 +153,6 @@ box-shadow: inset 0 -1px 0 var(--vscode-widget-shadow); } -.action-widget .monaco-list-row .action-list-item-toolbar .monaco-action-bar:not(.vertical) .action-label:not(.disabled):hover{ - background-color: var(--vscode-list-activeSelectionBackground); -} - /* Action bar */ .action-widget .action-widget-action-bar { @@ -213,6 +209,10 @@ display: flex; } +.action-widget .monaco-list-row .action-list-item-toolbar .monaco-action-bar:not(.vertical) .action-label:not(.disabled):hover{ + background-color: var(--vscode-list-activeSelectionBackground); +} + .action-widget-delegate-label { display: flex; align-items: center; From 2f13c3247487187846e0941d9280013e71cc9f56 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 29 Jan 2026 15:15:17 +0100 Subject: [PATCH 102/152] :listick: --- src/vs/editor/browser/services/editorWorkerService.ts | 2 +- src/vs/editor/common/services/completionsEnablement.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/services/editorWorkerService.ts b/src/vs/editor/browser/services/editorWorkerService.ts index 3f93ce10ea2..d5c878af3b3 100644 --- a/src/vs/editor/browser/services/editorWorkerService.ts +++ b/src/vs/editor/browser/services/editorWorkerService.ts @@ -283,7 +283,7 @@ class WordBasedCompletionItemProvider implements languages.CompletionItemProvide if (config.wordBasedSuggestions === 'offWithInlineSuggestions' && this.languageFeaturesService.inlineCompletionsProvider.has(model) - && isCompletionsEnabledWithTextResourceConfig(this._configurationService, model.getLanguageId())) { + && isCompletionsEnabledWithTextResourceConfig(this._configurationService, model.uri, model.getLanguageId())) { return undefined; } diff --git a/src/vs/editor/common/services/completionsEnablement.ts b/src/vs/editor/common/services/completionsEnablement.ts index 45152190ae1..b113f24da41 100644 --- a/src/vs/editor/common/services/completionsEnablement.ts +++ b/src/vs/editor/common/services/completionsEnablement.ts @@ -7,6 +7,7 @@ import product from '../../../platform/product/common/product.js'; import { isObject } from '../../../base/common/types.js'; import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; import { ITextResourceConfigurationService } from './textResourceConfiguration.js'; +import { URI } from '../../../base/common/uri.js'; /** * Get the completions enablement setting name from product configuration. @@ -43,7 +44,7 @@ export function isCompletionsEnabled(configurationService: IConfigurationService * @param modeId The language ID to check. Defaults to '*' which checks the global setting. * @returns `true` if completions are enabled for the language, `false` otherwise. */ -export function isCompletionsEnabledWithTextResourceConfig(configurationService: ITextResourceConfigurationService, modeId: string = '*'): boolean { +export function isCompletionsEnabledWithTextResourceConfig(configurationService: ITextResourceConfigurationService, resource: URI, modeId: string = '*'): boolean { const settingName = getCompletionsEnablementSettingName(); if (!settingName) { return false; @@ -51,7 +52,7 @@ export function isCompletionsEnabledWithTextResourceConfig(configurationService: // Pass undefined as resource to get the global setting return isCompletionsEnabledFromObject( - configurationService.getValue>(undefined, settingName), + configurationService.getValue>(resource, settingName), modeId ); } From 7624fc34ef7fd0b4e7e2b1e912736d68ad513b00 Mon Sep 17 00:00:00 2001 From: Anthony Stewart <150152+a-stewart@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:45:50 +0100 Subject: [PATCH 103/152] Fix bug where a format document command results in an unknown detailed reason --- src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index 10dd3e6db9f..174eea21c6a 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -1300,7 +1300,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE reason = source; sourceStr = source.metadata.source; } else { - reason = EditSources.unknown({ name: sourceStr }); + reason = EditSources.unknown({ name: source }); sourceStr = source; } From b488ab464f1d2a966954adc7bd87c9dac2015280 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:20:25 +0000 Subject: [PATCH 104/152] Fix command palette auto-close when focus was in inline chat (#291560) * Initial plan * Fix command palette auto-close when focus was in inline chat Move editor focus logic from InlineChatAffordance into InlineChatInputWidget._hide() and only focus editor when focus is still within the editor's DOM. This prevents stealing focus from command palette when it opens. Co-authored-by: jrieken <1794099+jrieken@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jrieken <1794099+jrieken@users.noreply.github.com> --- .../contrib/inlineChat/browser/inlineChatAffordance.ts | 1 - .../contrib/inlineChat/browser/inlineChatOverlayWidget.ts | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts index f0ec896f0bb..58db6c0bd36 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -111,7 +111,6 @@ export class InlineChatAffordance extends Disposable { const pos = this._inputWidget.position.read(r); if (pos === null) { this._menuData.set(undefined, undefined); - this._editor.focus(); } })); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 5094ab2e70d..0284206c2bf 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -278,6 +278,11 @@ export class InlineChatInputWidget extends Disposable { * Hide the widget (removes from editor but does not dispose). */ private _hide(): void { + // Focus editor if focus is still within the editor's DOM + const editorDomNode = this._editorObs.editor.getDomNode(); + if (editorDomNode && dom.isAncestorOfActiveElement(editorDomNode)) { + this._editorObs.editor.focus(); + } this._position.set(null, undefined); this._showStore.clear(); } From 653daa00112c012dedae9f83a27c1fbab5e99a43 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:34:23 +0100 Subject: [PATCH 105/152] Chat - implement session picker in the chat panel title (#281051) --- .../browser/ui/contextview/contextview.ts | 186 ++++-------------- src/vs/base/browser/ui/menu/menu.ts | 6 +- src/vs/base/common/layout.ts | 166 ++++++++++++++++ .../layout.test.ts} | 31 ++- .../quickinput/browser/media/quickInput.css | 5 + .../browser/quickInputController.ts | 69 ++++++- .../platform/quickinput/common/quickInput.ts | 10 + .../agentSessions/agentSessionsActions.ts | 2 +- .../agentSessions/agentSessionsPicker.ts | 2 + .../viewPane/chatViewTitleControl.ts | 4 +- 10 files changed, 310 insertions(+), 171 deletions(-) create mode 100644 src/vs/base/common/layout.ts rename src/vs/base/test/{browser/ui/contextview/contextview.test.ts => common/layout.test.ts} (60%) diff --git a/src/vs/base/browser/ui/contextview/contextview.ts b/src/vs/base/browser/ui/contextview/contextview.ts index 44c3c080e24..b3bfc63cb79 100644 --- a/src/vs/base/browser/ui/contextview/contextview.ts +++ b/src/vs/base/browser/ui/contextview/contextview.ts @@ -7,11 +7,13 @@ import { BrowserFeatures } from '../../canIUse.js'; import * as DOM from '../../dom.js'; import { StandardMouseEvent } from '../../mouseEvent.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../common/lifecycle.js'; +import { AnchorAlignment, AnchorAxisAlignment, AnchorPosition, IRect, layout2d } from '../../../common/layout.js'; import * as platform from '../../../common/platform.js'; -import { Range } from '../../../common/range.js'; import { OmitOptional } from '../../../common/types.js'; import './contextview.css'; +export { AnchorAlignment, AnchorAxisAlignment, AnchorPosition } from '../../../common/layout.js'; + export const enum ContextViewDOMPosition { ABSOLUTE = 1, FIXED, @@ -31,18 +33,6 @@ export function isAnchor(obj: unknown): obj is IAnchor | OmitOptional { return !!anchor && typeof anchor.x === 'number' && typeof anchor.y === 'number'; } -export const enum AnchorAlignment { - LEFT, RIGHT -} - -export const enum AnchorPosition { - BELOW, ABOVE -} - -export const enum AnchorAxisAlignment { - VERTICAL, HORIZONTAL -} - export interface IDelegate { /** * The anchor where to position the context view. @@ -73,66 +63,40 @@ export interface IContextViewProvider { layout(): void; } -export interface IPosition { - top: number; - left: number; -} +export function getAnchorRect(anchor: HTMLElement | StandardMouseEvent | IAnchor): IRect { + // Get the element's position and size (to anchor the view) + if (DOM.isHTMLElement(anchor)) { + const elementPosition = DOM.getDomNodePagePosition(anchor); -export interface ISize { - width: number; - height: number; -} + // In areas where zoom is applied to the element or its ancestors, we need to adjust the size of the element + // e.g. The title bar has counter zoom behavior meaning it applies the inverse of zoom level. + // Window Zoom Level: 1.5, Title Bar Zoom: 1/1.5, Size Multiplier: 1.5 + const zoom = DOM.getDomNodeZoomLevel(anchor); -export interface IView extends IPosition, ISize { } - -export const enum LayoutAnchorPosition { - Before, - After -} - -export enum LayoutAnchorMode { - AVOID, - ALIGN -} - -export interface ILayoutAnchor { - offset: number; - size: number; - mode?: LayoutAnchorMode; // default: AVOID - position: LayoutAnchorPosition; -} - -/** - * Lays out a one dimensional view next to an anchor in a viewport. - * - * @returns The view offset within the viewport. - */ -export function layout(viewportSize: number, viewSize: number, anchor: ILayoutAnchor): number { - const layoutAfterAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset : anchor.offset + anchor.size; - const layoutBeforeAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset + anchor.size : anchor.offset; - - if (anchor.position === LayoutAnchorPosition.Before) { - if (viewSize <= viewportSize - layoutAfterAnchorBoundary) { - return layoutAfterAnchorBoundary; // happy case, lay it out after the anchor - } - - if (viewSize <= layoutBeforeAnchorBoundary) { - return layoutBeforeAnchorBoundary - viewSize; // ok case, lay it out before the anchor - } - - return Math.max(viewportSize - viewSize, 0); // sad case, lay it over the anchor + return { + top: elementPosition.top * zoom, + left: elementPosition.left * zoom, + width: elementPosition.width * zoom, + height: elementPosition.height * zoom + }; + } else if (isAnchor(anchor)) { + return { + top: anchor.y, + left: anchor.x, + width: anchor.width || 1, + height: anchor.height || 2 + }; } else { - if (viewSize <= layoutBeforeAnchorBoundary) { - return layoutBeforeAnchorBoundary - viewSize; // happy case, lay it out before the anchor - } - - - if (viewSize <= viewportSize - layoutAfterAnchorBoundary && layoutBeforeAnchorBoundary < viewSize / 2) { - return layoutAfterAnchorBoundary; // ok case, lay it out after the anchor - } - - - return 0; // sad case, lay it over the anchor + return { + top: anchor.posy, + left: anchor.posx, + // We are about to position the context view where the mouse + // cursor is. To prevent the view being exactly under the mouse + // when showing and thus potentially triggering an action within, + // we treat the mouse location like a small sized block element. + width: 2, + height: 2 + }; } } @@ -270,82 +234,14 @@ export class ContextView extends Disposable { } // Get anchor - const anchor = this.delegate!.getAnchor(); - - // Compute around - let around: IView; - - // Get the element's position and size (to anchor the view) - if (DOM.isHTMLElement(anchor)) { - const elementPosition = DOM.getDomNodePagePosition(anchor); - - // In areas where zoom is applied to the element or its ancestors, we need to adjust the size of the element - // e.g. The title bar has counter zoom behavior meaning it applies the inverse of zoom level. - // Window Zoom Level: 1.5, Title Bar Zoom: 1/1.5, Size Multiplier: 1.5 - const zoom = DOM.getDomNodeZoomLevel(anchor); - - around = { - top: elementPosition.top * zoom, - left: elementPosition.left * zoom, - width: elementPosition.width * zoom, - height: elementPosition.height * zoom - }; - } else if (isAnchor(anchor)) { - around = { - top: anchor.y, - left: anchor.x, - width: anchor.width || 1, - height: anchor.height || 2 - }; - } else { - around = { - top: anchor.posy, - left: anchor.posx, - // We are about to position the context view where the mouse - // cursor is. To prevent the view being exactly under the mouse - // when showing and thus potentially triggering an action within, - // we treat the mouse location like a small sized block element. - width: 2, - height: 2 - }; - } - - const viewSizeWidth = DOM.getTotalWidth(this.view); - const viewSizeHeight = DOM.getTotalHeight(this.view); - - const anchorPosition = this.delegate!.anchorPosition ?? AnchorPosition.BELOW; - const anchorAlignment = this.delegate!.anchorAlignment ?? AnchorAlignment.LEFT; - const anchorAxisAlignment = this.delegate!.anchorAxisAlignment ?? AnchorAxisAlignment.VERTICAL; - - let top: number; - let left: number; - + const anchor = getAnchorRect(this.delegate!.getAnchor()); const activeWindow = DOM.getActiveWindow(); - if (anchorAxisAlignment === AnchorAxisAlignment.VERTICAL) { - const verticalAnchor: ILayoutAnchor = { offset: around.top - activeWindow.pageYOffset, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After }; - const horizontalAnchor: ILayoutAnchor = { offset: around.left, size: around.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN }; - - top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset; - - // if view intersects vertically with anchor, we must avoid the anchor - if (Range.intersects({ start: top, end: top + viewSizeHeight }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) { - horizontalAnchor.mode = LayoutAnchorMode.AVOID; - } - - left = layout(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor); - } else { - const horizontalAnchor: ILayoutAnchor = { offset: around.left, size: around.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After }; - const verticalAnchor: ILayoutAnchor = { offset: around.top, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN }; - - left = layout(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor); - - // if view intersects horizontally with anchor, we must avoid the anchor - if (Range.intersects({ start: left, end: left + viewSizeWidth }, { start: horizontalAnchor.offset, end: horizontalAnchor.offset + horizontalAnchor.size })) { - verticalAnchor.mode = LayoutAnchorMode.AVOID; - } - - top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset; - } + const viewport = { top: activeWindow.pageYOffset, left: activeWindow.pageXOffset, width: activeWindow.innerWidth, height: activeWindow.innerHeight }; + const view = { width: DOM.getTotalWidth(this.view), height: DOM.getTotalHeight(this.view) }; + const anchorPosition = this.delegate!.anchorPosition; + const anchorAlignment = this.delegate!.anchorAlignment; + const anchorAxisAlignment = this.delegate!.anchorAxisAlignment; + const { top, left } = layout2d(viewport, view, anchor, { anchorAlignment, anchorPosition, anchorAxisAlignment }); this.view.classList.remove('top', 'bottom', 'left', 'right'); this.view.classList.add(anchorPosition === AnchorPosition.BELOW ? 'bottom' : 'top'); diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index f48c4488073..c747ea1cd87 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -11,7 +11,6 @@ import { StandardKeyboardEvent } from '../../keyboardEvent.js'; import { StandardMouseEvent } from '../../mouseEvent.js'; import { ActionBar, ActionsOrientation, IActionViewItemProvider } from '../actionbar/actionbar.js'; import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions } from '../actionbar/actionViewItems.js'; -import { AnchorAlignment, layout, LayoutAnchorPosition } from '../contextview/contextview.js'; import { DomScrollableElement } from '../scrollbar/scrollableElement.js'; import { EmptySubmenuAction, IAction, IActionRunner, Separator, SubmenuAction } from '../../../common/actions.js'; import { RunOnceScheduler } from '../../../common/async.js'; @@ -26,6 +25,7 @@ import { DisposableStore } from '../../../common/lifecycle.js'; import { isLinux, isMacintosh } from '../../../common/platform.js'; import { ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js'; import * as strings from '../../../common/strings.js'; +import { AnchorAlignment, layout, LayoutAnchorPosition } from '../../../common/layout.js'; export const MENU_MNEMONIC_REGEX = /\(&([^\s&])\)|(^|[^&])&([^\s&])/; export const MENU_ESCAPED_MNEMONIC_REGEX = /(&)?(&)([^\s&])/g; @@ -859,7 +859,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem { const ret = { top: 0, left: 0 }; // Start with horizontal - ret.left = layout(windowDimensions.width, submenu.width, { position: expandDirection.horizontal === HorizontalDirection.Right ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, offset: entry.left, size: entry.width }); + ret.left = layout(windowDimensions.width, submenu.width, { position: expandDirection.horizontal === HorizontalDirection.Right ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, offset: entry.left, size: entry.width }).position; // We don't have enough room to layout the menu fully, so we are overlapping the menu if (ret.left >= entry.left && ret.left < entry.left + entry.width) { @@ -872,7 +872,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem { } // Now that we have a horizontal position, try layout vertically - ret.top = layout(windowDimensions.height, submenu.height, { position: LayoutAnchorPosition.Before, offset: entry.top, size: 0 }); + ret.top = layout(windowDimensions.height, submenu.height, { position: LayoutAnchorPosition.Before, offset: entry.top, size: 0 }).position; // We didn't have enough room below, but we did above, so we shift down to align the menu if (ret.top + submenu.height === entry.top && ret.top + entry.height + submenu.height <= windowDimensions.height) { diff --git a/src/vs/base/common/layout.ts b/src/vs/base/common/layout.ts new file mode 100644 index 00000000000..fdfcba9695b --- /dev/null +++ b/src/vs/base/common/layout.ts @@ -0,0 +1,166 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from './range.js'; + +export interface IAnchor { + x: number; + y: number; + width?: number; + height?: number; +} + +export const enum AnchorAlignment { + LEFT, RIGHT +} + +export const enum AnchorPosition { + BELOW, ABOVE +} + +export const enum AnchorAxisAlignment { + VERTICAL, HORIZONTAL +} + +interface IPosition { + readonly top: number; + readonly left: number; +} + +interface ISize { + readonly width: number; + readonly height: number; +} + +export interface IRect extends IPosition, ISize { } + +export const enum LayoutAnchorPosition { + Before, + After +} + +export enum LayoutAnchorMode { + AVOID, + ALIGN +} + +export interface ILayoutAnchor { + offset: number; + size: number; + mode?: LayoutAnchorMode; // default: AVOID + position: LayoutAnchorPosition; +} + +export interface ILayoutResult { + position: number; + result: 'ok' | 'flipped' | 'overlap'; +} + +/** + * Lays out a one dimensional view next to an anchor in a viewport. + * + * @returns The view offset within the viewport. + */ +export function layout(viewportSize: number, viewSize: number, anchor: ILayoutAnchor): ILayoutResult { + const layoutAfterAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset : anchor.offset + anchor.size; + const layoutBeforeAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset + anchor.size : anchor.offset; + + if (anchor.position === LayoutAnchorPosition.Before) { + if (viewSize <= viewportSize - layoutAfterAnchorBoundary) { + return { position: layoutAfterAnchorBoundary, result: 'ok' }; // happy case, lay it out after the anchor + } + + if (viewSize <= layoutBeforeAnchorBoundary) { + return { position: layoutBeforeAnchorBoundary - viewSize, result: 'flipped' }; // ok case, lay it out before the anchor + } + + return { position: Math.max(viewportSize - viewSize, 0), result: 'overlap' }; // sad case, lay it over the anchor + } else { + if (viewSize <= layoutBeforeAnchorBoundary) { + return { position: layoutBeforeAnchorBoundary - viewSize, result: 'ok' }; // happy case, lay it out before the anchor + } + + if (viewSize <= viewportSize - layoutAfterAnchorBoundary && layoutBeforeAnchorBoundary < viewSize / 2) { + return { position: layoutAfterAnchorBoundary, result: 'flipped' }; // ok case, lay it out after the anchor + } + + return { position: 0, result: 'overlap' }; // sad case, lay it over the anchor + } +} + +interface ILayout2DOptions { + readonly anchorAlignment?: AnchorAlignment; // default: left + readonly anchorPosition?: AnchorPosition; // default: below + readonly anchorAxisAlignment?: AnchorAxisAlignment; // default: vertical +} + +export interface ILayout2DResult { + top: number; + left: number; + bottom: number; + right: number; + anchorAlignment: AnchorAlignment; + anchorPosition: AnchorPosition; +} + +export function layout2d(viewport: IRect, view: ISize, anchor: IRect, options?: ILayout2DOptions): ILayout2DResult { + let anchorAlignment = options?.anchorAlignment ?? AnchorAlignment.LEFT; + let anchorPosition = options?.anchorPosition ?? AnchorPosition.ABOVE; + const anchorAxisAlignment = options?.anchorAxisAlignment ?? AnchorAxisAlignment.VERTICAL; + + let top: number; + let left: number; + + if (anchorAxisAlignment === AnchorAxisAlignment.VERTICAL) { + const verticalAnchor: ILayoutAnchor = { offset: anchor.top - viewport.top, size: anchor.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.After : LayoutAnchorPosition.Before }; + const horizontalAnchor: ILayoutAnchor = { offset: anchor.left, size: anchor.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN }; + + const verticalLayoutResult = layout(viewport.height, view.height, verticalAnchor); + top = verticalLayoutResult.position + viewport.top; + + if (verticalLayoutResult.result === 'flipped') { + anchorPosition = anchorPosition === AnchorPosition.BELOW ? AnchorPosition.ABOVE : AnchorPosition.BELOW; + } + + // if view intersects vertically with anchor, we must avoid the anchor + if (Range.intersects({ start: top, end: top + view.height }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) { + horizontalAnchor.mode = LayoutAnchorMode.AVOID; + } + + const horizontalLayoutResult = layout(viewport.width, view.width, horizontalAnchor); + left = horizontalLayoutResult.position; + + if (horizontalLayoutResult.result === 'flipped') { + anchorAlignment = anchorAlignment === AnchorAlignment.LEFT ? AnchorAlignment.RIGHT : AnchorAlignment.LEFT; + } + } else { + const horizontalAnchor: ILayoutAnchor = { offset: anchor.left, size: anchor.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After }; + const verticalAnchor: ILayoutAnchor = { offset: anchor.top, size: anchor.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.After : LayoutAnchorPosition.Before, mode: LayoutAnchorMode.ALIGN }; + + const horizontalLayoutResult = layout(viewport.width, view.width, horizontalAnchor); + left = horizontalLayoutResult.position; + + if (horizontalLayoutResult.result === 'flipped') { + anchorAlignment = anchorAlignment === AnchorAlignment.LEFT ? AnchorAlignment.RIGHT : AnchorAlignment.LEFT; + } + + // if view intersects horizontally with anchor, we must avoid the anchor + if (Range.intersects({ start: left, end: left + view.width }, { start: horizontalAnchor.offset, end: horizontalAnchor.offset + horizontalAnchor.size })) { + verticalAnchor.mode = LayoutAnchorMode.AVOID; + } + + const verticalLayoutResult = layout(viewport.height, view.height, verticalAnchor); + top = verticalLayoutResult.position + viewport.top; + + if (verticalLayoutResult.result === 'flipped') { + anchorPosition = anchorPosition === AnchorPosition.BELOW ? AnchorPosition.ABOVE : AnchorPosition.BELOW; + } + } + + const right = viewport.width - (left + view.width); + const bottom = viewport.height - (top + view.height); + + return { top, left, bottom, right, anchorAlignment, anchorPosition }; +} diff --git a/src/vs/base/test/browser/ui/contextview/contextview.test.ts b/src/vs/base/test/common/layout.test.ts similarity index 60% rename from src/vs/base/test/browser/ui/contextview/contextview.test.ts rename to src/vs/base/test/common/layout.test.ts index 4058d33f4a9..a6be1ea8ed2 100644 --- a/src/vs/base/test/browser/ui/contextview/contextview.test.ts +++ b/src/vs/base/test/common/layout.test.ts @@ -4,27 +4,26 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { layout, LayoutAnchorPosition } from '../../../../browser/ui/contextview/contextview.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../common/utils.js'; +import { layout, LayoutAnchorPosition } from '../../common/layout.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; -suite('Contextview', function () { +suite('Layout', function () { test('layout', () => { - assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.Before }), 0); - assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.Before }), 50); - assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.Before }), 180); + assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.Before }).position, 0); + assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.Before }).position, 50); + assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.Before }).position, 180); - assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.After }), 0); - assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.After }), 30); - assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.After }), 180); + assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.After }).position, 0); + assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.After }).position, 30); + assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.After }).position, 180); + assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.Before }).position, 50); + assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.Before }).position, 100); + assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.Before }).position, 130); - assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.Before }), 50); - assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.Before }), 100); - assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.Before }), 130); - - assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.After }), 50); - assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.After }), 30); - assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.After }), 130); + assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.After }).position, 50); + assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.After }).position, 30); + assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.After }).position, 130); }); ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 0636687742d..bd509719a3c 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -20,6 +20,11 @@ border-top-left-radius: 5px; } +.quick-input-widget.no-drag .quick-input-titlebar, +.quick-input-widget.no-drag .quick-input-title, +.quick-input-widget.no-drag .quick-input-header { + cursor: default; +} .quick-input-widget .monaco-inputbox .monaco-action-bar { top: 0; } diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index a3a85e36a35..419f83a03e3 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -37,6 +37,8 @@ import { TriStateCheckbox, createToggleActionViewItemProvider } from '../../../b import { defaultCheckboxStyles } from '../../theme/browser/defaultStyles.js'; import { QuickInputTreeController } from './tree/quickInputTreeController.js'; import { QuickTree } from './tree/quickTree.js'; +import { AnchorAlignment, AnchorPosition, layout2d } from '../../../base/common/layout.js'; +import { getAnchorRect } from '../../../base/browser/ui/contextview/contextview.js'; const $ = dom.$; @@ -535,6 +537,7 @@ export class QuickInputController extends Disposable { input.quickNavigate = options.quickNavigate; input.hideInput = !!options.hideInput; input.contextKey = options.contextKey; + input.anchor = options.anchor; input.busy = true; Promise.all([picks, options.activeItem]) .then(([items, _activeItem]) => { @@ -704,6 +707,7 @@ export class QuickInputController extends Disposable { ui.container.style.display = ''; this.updateLayout(); + this.dndController?.setEnabled(!controller.anchor); this.dndController?.layoutContainer(); ui.inputBox.setFocus(); this.quickInputTypeContext.set(controller.type); @@ -855,16 +859,52 @@ export class QuickInputController extends Disposable { private updateLayout() { if (this.ui && this.isVisible()) { const style = this.ui.container.style; - const width = Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH); + let width = Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH); style.width = width + 'px'; + let listHeight = this.dimension && this.dimension.height * 0.4; + // Position - style.top = `${this.viewState?.top ? Math.round(this.dimension!.height * this.viewState.top) : this.titleBarOffset}px`; - style.left = `${Math.round((this.dimension!.width * (this.viewState?.left ?? 0.5 /* center */)) - (width / 2))}px`; + if (this.controller?.anchor) { + const container = this.layoutService.getContainer(dom.getActiveWindow()).getBoundingClientRect(); + const anchor = getAnchorRect(this.controller.anchor); + width = 380; + listHeight = this.dimension ? Math.min(this.dimension.height * 0.2, 200) : 200; + + // Beware: + // We need to add some extra pixels to the height to account for the input and padding. + const containerHeight = Math.floor(listHeight) + 6 + 26 + 16; + const { top, left, right, bottom, anchorAlignment, anchorPosition } = layout2d(container, { width, height: containerHeight }, anchor); + + if (anchorAlignment === AnchorAlignment.RIGHT) { + style.right = `${right}px`; + style.left = 'initial'; + } else { + style.left = `${left}px`; + style.right = 'initial'; + } + + if (anchorPosition === AnchorPosition.BELOW) { + style.bottom = `${bottom}px`; + style.top = 'initial'; + } else { + style.top = `${top}px`; + style.bottom = 'initial'; + } + + style.width = `${width}px`; + style.height = ''; + } else { + style.top = `${this.viewState?.top ? Math.round(this.dimension!.height * this.viewState.top) : this.titleBarOffset}px`; + style.left = `${Math.round((this.dimension!.width * (this.viewState?.left ?? 0.5 /* center */)) - (width / 2))}px`; + style.right = ''; + style.bottom = ''; + style.height = ''; + } this.ui.inputBox.layout(); - this.ui.list.layout(this.dimension && this.dimension.height * 0.4); - this.ui.tree.layout(this.dimension && this.dimension.height * 0.4); + this.ui.list.layout(listHeight); + this.ui.tree.layout(listHeight); } } @@ -959,6 +999,8 @@ export interface IQuickInputControllerHost extends ILayoutService { } class QuickInputDragAndDropController extends Disposable { readonly dndViewState = observableValue<{ top?: number; left?: number; done: boolean } | undefined>(this, undefined); + private _enabled = true; + private readonly _snapThreshold = 20; private readonly _snapLineHorizontalRatio = 0.25; @@ -994,6 +1036,10 @@ class QuickInputDragAndDropController extends Disposable { } layoutContainer(dimension = this._layoutService.activeContainerDimension): void { + if (!this._enabled) { + return; + } + const state = this.dndViewState.get(); const dragAreaRect = this._quickInputContainer.getBoundingClientRect(); if (state?.top && state?.left) { @@ -1005,6 +1051,11 @@ class QuickInputDragAndDropController extends Disposable { } } + setEnabled(enabled: boolean): void { + this._enabled = enabled; + this._quickInputContainer.classList.toggle('no-drag', !enabled); + } + setAlignment(alignment: 'top' | 'center' | { top: number; left: number }, done = true): void { if (alignment === 'top') { this.dndViewState.set({ @@ -1035,6 +1086,10 @@ class QuickInputDragAndDropController extends Disposable { // Double click this._register(dom.addDisposableGenericMouseUpListener(dragArea, (event: MouseEvent) => { + if (!this._enabled) { + return; + } + const originEvent = new StandardMouseEvent(dom.getWindow(dragArea), event); if (originEvent.detail !== 2) { return; @@ -1051,6 +1106,10 @@ class QuickInputDragAndDropController extends Disposable { // Mouse down this._register(dom.addDisposableGenericMouseDownListener(dragArea, (e: MouseEvent) => { + if (!this._enabled) { + return; + } + const activeWindow = dom.getWindow(this._layoutService.activeContainer); const originEvent = new StandardMouseEvent(activeWindow, e); diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index 9ff9d71fe6c..9426be48e2f 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -197,6 +197,11 @@ export interface IPickOptions { */ activeItem?: Promise | T; + /** + * an optional anchor for the picker + */ + anchor?: HTMLElement | { x: number; y: number }; + onKeyMods?: (keyMods: IKeyMods) => void; onDidFocus?: (entry: T) => void; onDidTriggerItemButton?: (context: IQuickPickItemButtonContext) => void; @@ -353,6 +358,11 @@ export interface IQuickInput extends IDisposable { */ ignoreFocusOut: boolean; + /** + * An optional anchor for the quick input. + */ + anchor?: HTMLElement | { x: number; y: number }; + /** * Shows the quick input. */ diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts index 3af559ab6c8..f22d0690712 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts @@ -155,7 +155,7 @@ export class PickAgentSessionAction extends Action2 { async run(accessor: ServicesAccessor): Promise { const instantiationService = accessor.get(IInstantiationService); - const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker); + const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker, undefined); await agentSessionsPicker.pickAgentSession(); } } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsPicker.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsPicker.ts index 2550c4b0184..7700808a868 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsPicker.ts @@ -67,6 +67,7 @@ export class AgentSessionsPicker { private readonly sorter = new AgentSessionsSorter(); constructor( + private readonly anchor: HTMLElement | undefined, @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @IQuickInputService private readonly quickInputService: IQuickInputService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -77,6 +78,7 @@ export class AgentSessionsPicker { const disposables = new DisposableStore(); const picker = disposables.add(this.quickInputService.createQuickPick({ useSeparators: true })); + picker.anchor = this.anchor; picker.items = this.createPickerItems(); picker.canAcceptInBackground = true; picker.placeholder = localize('chatAgentPickerPlaceholder', "Search agent sessions by name"); diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts index 8cd698568b8..22f6b218a23 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts @@ -59,6 +59,8 @@ export class ChatViewTitleControl extends Disposable { } private registerActions(): void { + const that = this; + this._register(registerAction2(class extends Action2 { constructor() { super({ @@ -76,7 +78,7 @@ export class ChatViewTitleControl extends Disposable { async run(accessor: ServicesAccessor): Promise { const instantiationService = accessor.get(IInstantiationService); - const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker); + const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker, that.titleLabel.value?.element); await agentSessionsPicker.pickAgentSession(); } })); From 144c2e8f6f0da28cb9887d3cb1926b2c6f25c9ba Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 29 Jan 2026 15:35:09 +0100 Subject: [PATCH 106/152] disable session type picker when editing input --- .../inlineCompletions/browser/view/inlineEdits/theme.ts | 7 ++++++- .../contrib/chat/browser/actions/chatExecuteActions.ts | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts index a39de3bf3a8..88d8b681733 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts @@ -90,7 +90,12 @@ export const inlineEditIndicatorSecondaryForeground = registerColor( ); export const inlineEditIndicatorSecondaryBorder = registerColor( 'inlineEdit.gutterIndicator.secondaryBorder', - buttonSecondaryBackground, + { + light: buttonSecondaryBackground, + dark: buttonSecondaryBackground, + hcDark: buttonSecondaryBackground, + hcLight: buttonSecondaryBackground, + }, localize('inlineEdit.gutterIndicator.secondaryBorder', 'Border color for the secondary inline edit gutter indicator.') ); export const inlineEditIndicatorSecondaryBackground = registerColor( diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 548deb481d5..3e4f7655a1e 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -491,7 +491,7 @@ export class OpenSessionTargetPickerAction extends Action2 { tooltip: localize('setSessionTarget', "Set Session Target"), category: CHAT_CATEGORY, f1: false, - precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or(ChatContextKeys.chatSessionIsEmpty, ChatContextKeys.inAgentSessionsWelcome)), + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or(ChatContextKeys.chatSessionIsEmpty, ChatContextKeys.inAgentSessionsWelcome), ChatContextKeys.currentlyEditingInput.negate(), ChatContextKeys.currentlyEditing.negate()), menu: [ { id: MenuId.ChatInput, @@ -526,7 +526,7 @@ export class OpenDelegationPickerAction extends Action2 { tooltip: localize('delegateSession', "Delegate Session"), category: CHAT_CATEGORY, f1: false, - precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.chatSessionIsEmpty.negate()), + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.chatSessionIsEmpty.negate(), ChatContextKeys.currentlyEditingInput.negate(), ChatContextKeys.currentlyEditing.negate()), menu: [ { id: MenuId.ChatInput, From e2a9f36c8800bb77b54a3f5da2731337cbd02fce Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Thu, 29 Jan 2026 10:24:02 -0500 Subject: [PATCH 107/152] Fix empty hovers due to dom reuse (#291617) --- .../viewPane/chatContextUsageWidget.ts | 47 +++++++------------ 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts index d50a6efd846..3a336dc2111 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts @@ -140,46 +140,31 @@ export class ChatContextUsageWidget extends Disposable { const store = new DisposableStore(); this._hoverDisposable.value = store; - const getOrCreateDetails = (): ChatContextUsageDetails => { - if (!this._contextUsageDetails.value) { - this._contextUsageDetails.value = this.instantiationService.createInstance(ChatContextUsageDetails); - } - if (this.currentData) { - this._contextUsageDetails.value.update(this.currentData); + const createDetails = (): ChatContextUsageDetails | undefined => { + if (!this._isVisible.get() || !this.currentData) { + return undefined; } + this._contextUsageDetails.value = this.instantiationService.createInstance(ChatContextUsageDetails); + this._contextUsageDetails.value.update(this.currentData); return this._contextUsageDetails.value; }; - const resolveHoverOptions = (): IDelayedHoverOptions => { - const details = getOrCreateDetails(); - return { - content: details.domNode, - appearance: { showPointer: true, compact: true }, - persistence: { hideOnHover: false }, - trapFocus: true - }; + const hoverOptions: Omit = { + appearance: { showPointer: true, compact: true }, + persistence: { hideOnHover: false }, + trapFocus: true }; - store.add(this.hoverService.setupDelayedHover( - this.domNode, - resolveHoverOptions - )); + store.add(this.hoverService.setupDelayedHover(this.domNode, () => ({ + ...hoverOptions, + content: createDetails()?.domNode ?? '' + }))); - // Helper to show sticky hover with focus const showStickyHover = () => { - if (this.currentData) { - // Force hide any existing hover to ensure we can show our sticky one - this.hoverService.hideHover(true); - - const details = getOrCreateDetails(); + const details = createDetails(); + if (details) { this.hoverService.showInstantHover( - { - content: details.domNode, - target: this.domNode, - appearance: { showPointer: true, compact: true }, - persistence: { hideOnHover: false, sticky: true }, - trapFocus: true, - }, + { ...hoverOptions, content: details.domNode, target: this.domNode, persistence: { hideOnHover: false, sticky: true } }, true ); } From 6924e8019ad4eb33009e58f2dac21a49b19e678a Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 29 Jan 2026 16:39:47 +0100 Subject: [PATCH 108/152] change gutter color to be based on editor hover color --- .../inlineCompletions/browser/view/inlineEdits/theme.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts index a39de3bf3a8..af04a4122d9 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts @@ -8,7 +8,7 @@ import { Color } from '../../../../../../base/common/color.js'; import { BugIndicatingError } from '../../../../../../base/common/errors.js'; import { IObservable, observableFromEventOpts } from '../../../../../../base/common/observable.js'; import { localize } from '../../../../../../nls.js'; -import { buttonBackground, buttonForeground, buttonSecondaryBackground, buttonSecondaryForeground, diffInserted, diffInsertedLine, diffRemoved, editorBackground } from '../../../../../../platform/theme/common/colorRegistry.js'; +import { buttonBackground, buttonForeground, diffInserted, diffInsertedLine, diffRemoved, editorBackground, editorHoverBackground, editorHoverBorder, editorHoverForeground } from '../../../../../../platform/theme/common/colorRegistry.js'; import { asCssVariable, ColorIdentifier, darken, registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; import { InlineCompletionEditorType } from '../../model/provideInlineCompletions.js'; @@ -85,17 +85,17 @@ export const inlineEditIndicatorPrimaryBackground = registerColor( export const inlineEditIndicatorSecondaryForeground = registerColor( 'inlineEdit.gutterIndicator.secondaryForeground', - buttonSecondaryForeground, + editorHoverForeground, localize('inlineEdit.gutterIndicator.secondaryForeground', 'Foreground color for the secondary inline edit gutter indicator.') ); export const inlineEditIndicatorSecondaryBorder = registerColor( 'inlineEdit.gutterIndicator.secondaryBorder', - buttonSecondaryBackground, + editorHoverBorder, localize('inlineEdit.gutterIndicator.secondaryBorder', 'Border color for the secondary inline edit gutter indicator.') ); export const inlineEditIndicatorSecondaryBackground = registerColor( 'inlineEdit.gutterIndicator.secondaryBackground', - inlineEditIndicatorSecondaryBorder, + editorHoverBackground, localize('inlineEdit.gutterIndicator.secondaryBackground', 'Background color for the secondary inline edit gutter indicator.') ); From a103a120fdb45b6c84b756fec3d611bba9cbd5e6 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 29 Jan 2026 16:44:21 +0100 Subject: [PATCH 109/152] fix: enhance selection validation in InlineChatAffordance (#291622) fix https://github.com/microsoft/vscode/issues/290832 --- .../contrib/inlineChat/browser/inlineChatAffordance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts index 58db6c0bd36..5d53da9301b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -52,7 +52,7 @@ export class InlineChatAffordance extends Disposable { this._store.add(autorun(r => { const value = debouncedSelection.read(r); - if (!value || value.isEmpty() || !explicitSelection) { + if (!value || value.isEmpty() || !explicitSelection || _editor.getModel()?.getValueInRange(value).match(/^\s+$/)) { selectionData.set(undefined, undefined); return; } From ea32526b0d3bacf917e7624077180a8b8b34bf56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 29 Jan 2026 17:06:33 +0100 Subject: [PATCH 110/152] Revert "Chat - implement session picker in the chat panel title" (#291619) Revert "Chat - implement session picker in the chat panel title (#281051)" This reverts commit 653daa00112c012dedae9f83a27c1fbab5e99a43. --- .../browser/ui/contextview/contextview.ts | 186 ++++++++++++++---- src/vs/base/browser/ui/menu/menu.ts | 6 +- src/vs/base/common/layout.ts | 166 ---------------- .../ui/contextview/contextview.test.ts} | 31 +-- .../quickinput/browser/media/quickInput.css | 5 - .../browser/quickInputController.ts | 69 +------ .../platform/quickinput/common/quickInput.ts | 10 - .../agentSessions/agentSessionsActions.ts | 2 +- .../agentSessions/agentSessionsPicker.ts | 2 - .../viewPane/chatViewTitleControl.ts | 4 +- 10 files changed, 171 insertions(+), 310 deletions(-) delete mode 100644 src/vs/base/common/layout.ts rename src/vs/base/test/{common/layout.test.ts => browser/ui/contextview/contextview.test.ts} (60%) diff --git a/src/vs/base/browser/ui/contextview/contextview.ts b/src/vs/base/browser/ui/contextview/contextview.ts index b3bfc63cb79..44c3c080e24 100644 --- a/src/vs/base/browser/ui/contextview/contextview.ts +++ b/src/vs/base/browser/ui/contextview/contextview.ts @@ -7,13 +7,11 @@ import { BrowserFeatures } from '../../canIUse.js'; import * as DOM from '../../dom.js'; import { StandardMouseEvent } from '../../mouseEvent.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../common/lifecycle.js'; -import { AnchorAlignment, AnchorAxisAlignment, AnchorPosition, IRect, layout2d } from '../../../common/layout.js'; import * as platform from '../../../common/platform.js'; +import { Range } from '../../../common/range.js'; import { OmitOptional } from '../../../common/types.js'; import './contextview.css'; -export { AnchorAlignment, AnchorAxisAlignment, AnchorPosition } from '../../../common/layout.js'; - export const enum ContextViewDOMPosition { ABSOLUTE = 1, FIXED, @@ -33,6 +31,18 @@ export function isAnchor(obj: unknown): obj is IAnchor | OmitOptional { return !!anchor && typeof anchor.x === 'number' && typeof anchor.y === 'number'; } +export const enum AnchorAlignment { + LEFT, RIGHT +} + +export const enum AnchorPosition { + BELOW, ABOVE +} + +export const enum AnchorAxisAlignment { + VERTICAL, HORIZONTAL +} + export interface IDelegate { /** * The anchor where to position the context view. @@ -63,40 +73,66 @@ export interface IContextViewProvider { layout(): void; } -export function getAnchorRect(anchor: HTMLElement | StandardMouseEvent | IAnchor): IRect { - // Get the element's position and size (to anchor the view) - if (DOM.isHTMLElement(anchor)) { - const elementPosition = DOM.getDomNodePagePosition(anchor); +export interface IPosition { + top: number; + left: number; +} - // In areas where zoom is applied to the element or its ancestors, we need to adjust the size of the element - // e.g. The title bar has counter zoom behavior meaning it applies the inverse of zoom level. - // Window Zoom Level: 1.5, Title Bar Zoom: 1/1.5, Size Multiplier: 1.5 - const zoom = DOM.getDomNodeZoomLevel(anchor); +export interface ISize { + width: number; + height: number; +} - return { - top: elementPosition.top * zoom, - left: elementPosition.left * zoom, - width: elementPosition.width * zoom, - height: elementPosition.height * zoom - }; - } else if (isAnchor(anchor)) { - return { - top: anchor.y, - left: anchor.x, - width: anchor.width || 1, - height: anchor.height || 2 - }; +export interface IView extends IPosition, ISize { } + +export const enum LayoutAnchorPosition { + Before, + After +} + +export enum LayoutAnchorMode { + AVOID, + ALIGN +} + +export interface ILayoutAnchor { + offset: number; + size: number; + mode?: LayoutAnchorMode; // default: AVOID + position: LayoutAnchorPosition; +} + +/** + * Lays out a one dimensional view next to an anchor in a viewport. + * + * @returns The view offset within the viewport. + */ +export function layout(viewportSize: number, viewSize: number, anchor: ILayoutAnchor): number { + const layoutAfterAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset : anchor.offset + anchor.size; + const layoutBeforeAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset + anchor.size : anchor.offset; + + if (anchor.position === LayoutAnchorPosition.Before) { + if (viewSize <= viewportSize - layoutAfterAnchorBoundary) { + return layoutAfterAnchorBoundary; // happy case, lay it out after the anchor + } + + if (viewSize <= layoutBeforeAnchorBoundary) { + return layoutBeforeAnchorBoundary - viewSize; // ok case, lay it out before the anchor + } + + return Math.max(viewportSize - viewSize, 0); // sad case, lay it over the anchor } else { - return { - top: anchor.posy, - left: anchor.posx, - // We are about to position the context view where the mouse - // cursor is. To prevent the view being exactly under the mouse - // when showing and thus potentially triggering an action within, - // we treat the mouse location like a small sized block element. - width: 2, - height: 2 - }; + if (viewSize <= layoutBeforeAnchorBoundary) { + return layoutBeforeAnchorBoundary - viewSize; // happy case, lay it out before the anchor + } + + + if (viewSize <= viewportSize - layoutAfterAnchorBoundary && layoutBeforeAnchorBoundary < viewSize / 2) { + return layoutAfterAnchorBoundary; // ok case, lay it out after the anchor + } + + + return 0; // sad case, lay it over the anchor } } @@ -234,14 +270,82 @@ export class ContextView extends Disposable { } // Get anchor - const anchor = getAnchorRect(this.delegate!.getAnchor()); + const anchor = this.delegate!.getAnchor(); + + // Compute around + let around: IView; + + // Get the element's position and size (to anchor the view) + if (DOM.isHTMLElement(anchor)) { + const elementPosition = DOM.getDomNodePagePosition(anchor); + + // In areas where zoom is applied to the element or its ancestors, we need to adjust the size of the element + // e.g. The title bar has counter zoom behavior meaning it applies the inverse of zoom level. + // Window Zoom Level: 1.5, Title Bar Zoom: 1/1.5, Size Multiplier: 1.5 + const zoom = DOM.getDomNodeZoomLevel(anchor); + + around = { + top: elementPosition.top * zoom, + left: elementPosition.left * zoom, + width: elementPosition.width * zoom, + height: elementPosition.height * zoom + }; + } else if (isAnchor(anchor)) { + around = { + top: anchor.y, + left: anchor.x, + width: anchor.width || 1, + height: anchor.height || 2 + }; + } else { + around = { + top: anchor.posy, + left: anchor.posx, + // We are about to position the context view where the mouse + // cursor is. To prevent the view being exactly under the mouse + // when showing and thus potentially triggering an action within, + // we treat the mouse location like a small sized block element. + width: 2, + height: 2 + }; + } + + const viewSizeWidth = DOM.getTotalWidth(this.view); + const viewSizeHeight = DOM.getTotalHeight(this.view); + + const anchorPosition = this.delegate!.anchorPosition ?? AnchorPosition.BELOW; + const anchorAlignment = this.delegate!.anchorAlignment ?? AnchorAlignment.LEFT; + const anchorAxisAlignment = this.delegate!.anchorAxisAlignment ?? AnchorAxisAlignment.VERTICAL; + + let top: number; + let left: number; + const activeWindow = DOM.getActiveWindow(); - const viewport = { top: activeWindow.pageYOffset, left: activeWindow.pageXOffset, width: activeWindow.innerWidth, height: activeWindow.innerHeight }; - const view = { width: DOM.getTotalWidth(this.view), height: DOM.getTotalHeight(this.view) }; - const anchorPosition = this.delegate!.anchorPosition; - const anchorAlignment = this.delegate!.anchorAlignment; - const anchorAxisAlignment = this.delegate!.anchorAxisAlignment; - const { top, left } = layout2d(viewport, view, anchor, { anchorAlignment, anchorPosition, anchorAxisAlignment }); + if (anchorAxisAlignment === AnchorAxisAlignment.VERTICAL) { + const verticalAnchor: ILayoutAnchor = { offset: around.top - activeWindow.pageYOffset, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After }; + const horizontalAnchor: ILayoutAnchor = { offset: around.left, size: around.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN }; + + top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset; + + // if view intersects vertically with anchor, we must avoid the anchor + if (Range.intersects({ start: top, end: top + viewSizeHeight }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) { + horizontalAnchor.mode = LayoutAnchorMode.AVOID; + } + + left = layout(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor); + } else { + const horizontalAnchor: ILayoutAnchor = { offset: around.left, size: around.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After }; + const verticalAnchor: ILayoutAnchor = { offset: around.top, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN }; + + left = layout(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor); + + // if view intersects horizontally with anchor, we must avoid the anchor + if (Range.intersects({ start: left, end: left + viewSizeWidth }, { start: horizontalAnchor.offset, end: horizontalAnchor.offset + horizontalAnchor.size })) { + verticalAnchor.mode = LayoutAnchorMode.AVOID; + } + + top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset; + } this.view.classList.remove('top', 'bottom', 'left', 'right'); this.view.classList.add(anchorPosition === AnchorPosition.BELOW ? 'bottom' : 'top'); diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index c747ea1cd87..f48c4488073 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -11,6 +11,7 @@ import { StandardKeyboardEvent } from '../../keyboardEvent.js'; import { StandardMouseEvent } from '../../mouseEvent.js'; import { ActionBar, ActionsOrientation, IActionViewItemProvider } from '../actionbar/actionbar.js'; import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions } from '../actionbar/actionViewItems.js'; +import { AnchorAlignment, layout, LayoutAnchorPosition } from '../contextview/contextview.js'; import { DomScrollableElement } from '../scrollbar/scrollableElement.js'; import { EmptySubmenuAction, IAction, IActionRunner, Separator, SubmenuAction } from '../../../common/actions.js'; import { RunOnceScheduler } from '../../../common/async.js'; @@ -25,7 +26,6 @@ import { DisposableStore } from '../../../common/lifecycle.js'; import { isLinux, isMacintosh } from '../../../common/platform.js'; import { ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js'; import * as strings from '../../../common/strings.js'; -import { AnchorAlignment, layout, LayoutAnchorPosition } from '../../../common/layout.js'; export const MENU_MNEMONIC_REGEX = /\(&([^\s&])\)|(^|[^&])&([^\s&])/; export const MENU_ESCAPED_MNEMONIC_REGEX = /(&)?(&)([^\s&])/g; @@ -859,7 +859,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem { const ret = { top: 0, left: 0 }; // Start with horizontal - ret.left = layout(windowDimensions.width, submenu.width, { position: expandDirection.horizontal === HorizontalDirection.Right ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, offset: entry.left, size: entry.width }).position; + ret.left = layout(windowDimensions.width, submenu.width, { position: expandDirection.horizontal === HorizontalDirection.Right ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, offset: entry.left, size: entry.width }); // We don't have enough room to layout the menu fully, so we are overlapping the menu if (ret.left >= entry.left && ret.left < entry.left + entry.width) { @@ -872,7 +872,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem { } // Now that we have a horizontal position, try layout vertically - ret.top = layout(windowDimensions.height, submenu.height, { position: LayoutAnchorPosition.Before, offset: entry.top, size: 0 }).position; + ret.top = layout(windowDimensions.height, submenu.height, { position: LayoutAnchorPosition.Before, offset: entry.top, size: 0 }); // We didn't have enough room below, but we did above, so we shift down to align the menu if (ret.top + submenu.height === entry.top && ret.top + entry.height + submenu.height <= windowDimensions.height) { diff --git a/src/vs/base/common/layout.ts b/src/vs/base/common/layout.ts deleted file mode 100644 index fdfcba9695b..00000000000 --- a/src/vs/base/common/layout.ts +++ /dev/null @@ -1,166 +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 { Range } from './range.js'; - -export interface IAnchor { - x: number; - y: number; - width?: number; - height?: number; -} - -export const enum AnchorAlignment { - LEFT, RIGHT -} - -export const enum AnchorPosition { - BELOW, ABOVE -} - -export const enum AnchorAxisAlignment { - VERTICAL, HORIZONTAL -} - -interface IPosition { - readonly top: number; - readonly left: number; -} - -interface ISize { - readonly width: number; - readonly height: number; -} - -export interface IRect extends IPosition, ISize { } - -export const enum LayoutAnchorPosition { - Before, - After -} - -export enum LayoutAnchorMode { - AVOID, - ALIGN -} - -export interface ILayoutAnchor { - offset: number; - size: number; - mode?: LayoutAnchorMode; // default: AVOID - position: LayoutAnchorPosition; -} - -export interface ILayoutResult { - position: number; - result: 'ok' | 'flipped' | 'overlap'; -} - -/** - * Lays out a one dimensional view next to an anchor in a viewport. - * - * @returns The view offset within the viewport. - */ -export function layout(viewportSize: number, viewSize: number, anchor: ILayoutAnchor): ILayoutResult { - const layoutAfterAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset : anchor.offset + anchor.size; - const layoutBeforeAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset + anchor.size : anchor.offset; - - if (anchor.position === LayoutAnchorPosition.Before) { - if (viewSize <= viewportSize - layoutAfterAnchorBoundary) { - return { position: layoutAfterAnchorBoundary, result: 'ok' }; // happy case, lay it out after the anchor - } - - if (viewSize <= layoutBeforeAnchorBoundary) { - return { position: layoutBeforeAnchorBoundary - viewSize, result: 'flipped' }; // ok case, lay it out before the anchor - } - - return { position: Math.max(viewportSize - viewSize, 0), result: 'overlap' }; // sad case, lay it over the anchor - } else { - if (viewSize <= layoutBeforeAnchorBoundary) { - return { position: layoutBeforeAnchorBoundary - viewSize, result: 'ok' }; // happy case, lay it out before the anchor - } - - if (viewSize <= viewportSize - layoutAfterAnchorBoundary && layoutBeforeAnchorBoundary < viewSize / 2) { - return { position: layoutAfterAnchorBoundary, result: 'flipped' }; // ok case, lay it out after the anchor - } - - return { position: 0, result: 'overlap' }; // sad case, lay it over the anchor - } -} - -interface ILayout2DOptions { - readonly anchorAlignment?: AnchorAlignment; // default: left - readonly anchorPosition?: AnchorPosition; // default: below - readonly anchorAxisAlignment?: AnchorAxisAlignment; // default: vertical -} - -export interface ILayout2DResult { - top: number; - left: number; - bottom: number; - right: number; - anchorAlignment: AnchorAlignment; - anchorPosition: AnchorPosition; -} - -export function layout2d(viewport: IRect, view: ISize, anchor: IRect, options?: ILayout2DOptions): ILayout2DResult { - let anchorAlignment = options?.anchorAlignment ?? AnchorAlignment.LEFT; - let anchorPosition = options?.anchorPosition ?? AnchorPosition.ABOVE; - const anchorAxisAlignment = options?.anchorAxisAlignment ?? AnchorAxisAlignment.VERTICAL; - - let top: number; - let left: number; - - if (anchorAxisAlignment === AnchorAxisAlignment.VERTICAL) { - const verticalAnchor: ILayoutAnchor = { offset: anchor.top - viewport.top, size: anchor.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.After : LayoutAnchorPosition.Before }; - const horizontalAnchor: ILayoutAnchor = { offset: anchor.left, size: anchor.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN }; - - const verticalLayoutResult = layout(viewport.height, view.height, verticalAnchor); - top = verticalLayoutResult.position + viewport.top; - - if (verticalLayoutResult.result === 'flipped') { - anchorPosition = anchorPosition === AnchorPosition.BELOW ? AnchorPosition.ABOVE : AnchorPosition.BELOW; - } - - // if view intersects vertically with anchor, we must avoid the anchor - if (Range.intersects({ start: top, end: top + view.height }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) { - horizontalAnchor.mode = LayoutAnchorMode.AVOID; - } - - const horizontalLayoutResult = layout(viewport.width, view.width, horizontalAnchor); - left = horizontalLayoutResult.position; - - if (horizontalLayoutResult.result === 'flipped') { - anchorAlignment = anchorAlignment === AnchorAlignment.LEFT ? AnchorAlignment.RIGHT : AnchorAlignment.LEFT; - } - } else { - const horizontalAnchor: ILayoutAnchor = { offset: anchor.left, size: anchor.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After }; - const verticalAnchor: ILayoutAnchor = { offset: anchor.top, size: anchor.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.After : LayoutAnchorPosition.Before, mode: LayoutAnchorMode.ALIGN }; - - const horizontalLayoutResult = layout(viewport.width, view.width, horizontalAnchor); - left = horizontalLayoutResult.position; - - if (horizontalLayoutResult.result === 'flipped') { - anchorAlignment = anchorAlignment === AnchorAlignment.LEFT ? AnchorAlignment.RIGHT : AnchorAlignment.LEFT; - } - - // if view intersects horizontally with anchor, we must avoid the anchor - if (Range.intersects({ start: left, end: left + view.width }, { start: horizontalAnchor.offset, end: horizontalAnchor.offset + horizontalAnchor.size })) { - verticalAnchor.mode = LayoutAnchorMode.AVOID; - } - - const verticalLayoutResult = layout(viewport.height, view.height, verticalAnchor); - top = verticalLayoutResult.position + viewport.top; - - if (verticalLayoutResult.result === 'flipped') { - anchorPosition = anchorPosition === AnchorPosition.BELOW ? AnchorPosition.ABOVE : AnchorPosition.BELOW; - } - } - - const right = viewport.width - (left + view.width); - const bottom = viewport.height - (top + view.height); - - return { top, left, bottom, right, anchorAlignment, anchorPosition }; -} diff --git a/src/vs/base/test/common/layout.test.ts b/src/vs/base/test/browser/ui/contextview/contextview.test.ts similarity index 60% rename from src/vs/base/test/common/layout.test.ts rename to src/vs/base/test/browser/ui/contextview/contextview.test.ts index a6be1ea8ed2..4058d33f4a9 100644 --- a/src/vs/base/test/common/layout.test.ts +++ b/src/vs/base/test/browser/ui/contextview/contextview.test.ts @@ -4,26 +4,27 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { layout, LayoutAnchorPosition } from '../../common/layout.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; +import { layout, LayoutAnchorPosition } from '../../../../browser/ui/contextview/contextview.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../common/utils.js'; -suite('Layout', function () { +suite('Contextview', function () { test('layout', () => { - assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.Before }).position, 0); - assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.Before }).position, 50); - assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.Before }).position, 180); + assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.Before }), 0); + assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.Before }), 50); + assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.Before }), 180); - assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.After }).position, 0); - assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.After }).position, 30); - assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.After }).position, 180); - assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.Before }).position, 50); - assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.Before }).position, 100); - assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.Before }).position, 130); + assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.After }), 0); + assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.After }), 30); + assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.After }), 180); - assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.After }).position, 50); - assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.After }).position, 30); - assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.After }).position, 130); + assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.Before }), 50); + assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.Before }), 100); + assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.Before }), 130); + + assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.After }), 50); + assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.After }), 30); + assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.After }), 130); }); ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index bd509719a3c..0636687742d 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -20,11 +20,6 @@ border-top-left-radius: 5px; } -.quick-input-widget.no-drag .quick-input-titlebar, -.quick-input-widget.no-drag .quick-input-title, -.quick-input-widget.no-drag .quick-input-header { - cursor: default; -} .quick-input-widget .monaco-inputbox .monaco-action-bar { top: 0; } diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 419f83a03e3..a3a85e36a35 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -37,8 +37,6 @@ import { TriStateCheckbox, createToggleActionViewItemProvider } from '../../../b import { defaultCheckboxStyles } from '../../theme/browser/defaultStyles.js'; import { QuickInputTreeController } from './tree/quickInputTreeController.js'; import { QuickTree } from './tree/quickTree.js'; -import { AnchorAlignment, AnchorPosition, layout2d } from '../../../base/common/layout.js'; -import { getAnchorRect } from '../../../base/browser/ui/contextview/contextview.js'; const $ = dom.$; @@ -537,7 +535,6 @@ export class QuickInputController extends Disposable { input.quickNavigate = options.quickNavigate; input.hideInput = !!options.hideInput; input.contextKey = options.contextKey; - input.anchor = options.anchor; input.busy = true; Promise.all([picks, options.activeItem]) .then(([items, _activeItem]) => { @@ -707,7 +704,6 @@ export class QuickInputController extends Disposable { ui.container.style.display = ''; this.updateLayout(); - this.dndController?.setEnabled(!controller.anchor); this.dndController?.layoutContainer(); ui.inputBox.setFocus(); this.quickInputTypeContext.set(controller.type); @@ -859,52 +855,16 @@ export class QuickInputController extends Disposable { private updateLayout() { if (this.ui && this.isVisible()) { const style = this.ui.container.style; - let width = Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH); + const width = Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH); style.width = width + 'px'; - let listHeight = this.dimension && this.dimension.height * 0.4; - // Position - if (this.controller?.anchor) { - const container = this.layoutService.getContainer(dom.getActiveWindow()).getBoundingClientRect(); - const anchor = getAnchorRect(this.controller.anchor); - width = 380; - listHeight = this.dimension ? Math.min(this.dimension.height * 0.2, 200) : 200; - - // Beware: - // We need to add some extra pixels to the height to account for the input and padding. - const containerHeight = Math.floor(listHeight) + 6 + 26 + 16; - const { top, left, right, bottom, anchorAlignment, anchorPosition } = layout2d(container, { width, height: containerHeight }, anchor); - - if (anchorAlignment === AnchorAlignment.RIGHT) { - style.right = `${right}px`; - style.left = 'initial'; - } else { - style.left = `${left}px`; - style.right = 'initial'; - } - - if (anchorPosition === AnchorPosition.BELOW) { - style.bottom = `${bottom}px`; - style.top = 'initial'; - } else { - style.top = `${top}px`; - style.bottom = 'initial'; - } - - style.width = `${width}px`; - style.height = ''; - } else { - style.top = `${this.viewState?.top ? Math.round(this.dimension!.height * this.viewState.top) : this.titleBarOffset}px`; - style.left = `${Math.round((this.dimension!.width * (this.viewState?.left ?? 0.5 /* center */)) - (width / 2))}px`; - style.right = ''; - style.bottom = ''; - style.height = ''; - } + style.top = `${this.viewState?.top ? Math.round(this.dimension!.height * this.viewState.top) : this.titleBarOffset}px`; + style.left = `${Math.round((this.dimension!.width * (this.viewState?.left ?? 0.5 /* center */)) - (width / 2))}px`; this.ui.inputBox.layout(); - this.ui.list.layout(listHeight); - this.ui.tree.layout(listHeight); + this.ui.list.layout(this.dimension && this.dimension.height * 0.4); + this.ui.tree.layout(this.dimension && this.dimension.height * 0.4); } } @@ -999,8 +959,6 @@ export interface IQuickInputControllerHost extends ILayoutService { } class QuickInputDragAndDropController extends Disposable { readonly dndViewState = observableValue<{ top?: number; left?: number; done: boolean } | undefined>(this, undefined); - private _enabled = true; - private readonly _snapThreshold = 20; private readonly _snapLineHorizontalRatio = 0.25; @@ -1036,10 +994,6 @@ class QuickInputDragAndDropController extends Disposable { } layoutContainer(dimension = this._layoutService.activeContainerDimension): void { - if (!this._enabled) { - return; - } - const state = this.dndViewState.get(); const dragAreaRect = this._quickInputContainer.getBoundingClientRect(); if (state?.top && state?.left) { @@ -1051,11 +1005,6 @@ class QuickInputDragAndDropController extends Disposable { } } - setEnabled(enabled: boolean): void { - this._enabled = enabled; - this._quickInputContainer.classList.toggle('no-drag', !enabled); - } - setAlignment(alignment: 'top' | 'center' | { top: number; left: number }, done = true): void { if (alignment === 'top') { this.dndViewState.set({ @@ -1086,10 +1035,6 @@ class QuickInputDragAndDropController extends Disposable { // Double click this._register(dom.addDisposableGenericMouseUpListener(dragArea, (event: MouseEvent) => { - if (!this._enabled) { - return; - } - const originEvent = new StandardMouseEvent(dom.getWindow(dragArea), event); if (originEvent.detail !== 2) { return; @@ -1106,10 +1051,6 @@ class QuickInputDragAndDropController extends Disposable { // Mouse down this._register(dom.addDisposableGenericMouseDownListener(dragArea, (e: MouseEvent) => { - if (!this._enabled) { - return; - } - const activeWindow = dom.getWindow(this._layoutService.activeContainer); const originEvent = new StandardMouseEvent(activeWindow, e); diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index 9426be48e2f..9ff9d71fe6c 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -197,11 +197,6 @@ export interface IPickOptions { */ activeItem?: Promise | T; - /** - * an optional anchor for the picker - */ - anchor?: HTMLElement | { x: number; y: number }; - onKeyMods?: (keyMods: IKeyMods) => void; onDidFocus?: (entry: T) => void; onDidTriggerItemButton?: (context: IQuickPickItemButtonContext) => void; @@ -358,11 +353,6 @@ export interface IQuickInput extends IDisposable { */ ignoreFocusOut: boolean; - /** - * An optional anchor for the quick input. - */ - anchor?: HTMLElement | { x: number; y: number }; - /** * Shows the quick input. */ diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts index f22d0690712..3af559ab6c8 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts @@ -155,7 +155,7 @@ export class PickAgentSessionAction extends Action2 { async run(accessor: ServicesAccessor): Promise { const instantiationService = accessor.get(IInstantiationService); - const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker, undefined); + const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker); await agentSessionsPicker.pickAgentSession(); } } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsPicker.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsPicker.ts index 7700808a868..2550c4b0184 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsPicker.ts @@ -67,7 +67,6 @@ export class AgentSessionsPicker { private readonly sorter = new AgentSessionsSorter(); constructor( - private readonly anchor: HTMLElement | undefined, @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @IQuickInputService private readonly quickInputService: IQuickInputService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -78,7 +77,6 @@ export class AgentSessionsPicker { const disposables = new DisposableStore(); const picker = disposables.add(this.quickInputService.createQuickPick({ useSeparators: true })); - picker.anchor = this.anchor; picker.items = this.createPickerItems(); picker.canAcceptInBackground = true; picker.placeholder = localize('chatAgentPickerPlaceholder', "Search agent sessions by name"); diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts index 22f6b218a23..8cd698568b8 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts @@ -59,8 +59,6 @@ export class ChatViewTitleControl extends Disposable { } private registerActions(): void { - const that = this; - this._register(registerAction2(class extends Action2 { constructor() { super({ @@ -78,7 +76,7 @@ export class ChatViewTitleControl extends Disposable { async run(accessor: ServicesAccessor): Promise { const instantiationService = accessor.get(IInstantiationService); - const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker, that.titleLabel.value?.element); + const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker); await agentSessionsPicker.pickAgentSession(); } })); From a6f5b3e9a6bf1f087456691e16d7ea97f7ee3f0e Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 29 Jan 2026 16:07:23 +0000 Subject: [PATCH 111/152] Update @vscode/codicons to version 0.0.45-2 and add 'openai' icon to codiconsLibrary --- package-lock.json | 8 ++++---- package.json | 2 +- remote/web/package-lock.json | 8 ++++---- remote/web/package.json | 2 +- src/vs/base/common/codiconsLibrary.ts | 1 + 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index e486587a3af..7d4af81b621 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.45-0", + "@vscode/codicons": "^0.0.45-2", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", @@ -2947,9 +2947,9 @@ ] }, "node_modules/@vscode/codicons": { - "version": "0.0.45-0", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-0.tgz", - "integrity": "sha512-ixvw4auQobMOnMX9cOk8/3GfEgkTKCchsab2O6QvyL6+x4FJegOrK3Wgn4Y+Qua51LqnAsgpB5n74q8HEPh1pA==", + "version": "0.0.45-2", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-2.tgz", + "integrity": "sha512-Z5uZd8E2f84Jf4jv6ozSjIU/cnHn7F1REBGUtzdqJufWoLYauH/nwpVn8fWtvXNtR1QwEyh6x3WAeR2l5rnnyg==", "license": "CC-BY-4.0" }, "node_modules/@vscode/deviceid": { diff --git a/package.json b/package.json index 5023a7daebb..520360f71af 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.45-0", + "@vscode/codicons": "^0.0.45-2", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index a970a788d3a..b2d86a2b17e 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.45-0", + "@vscode/codicons": "^0.0.45-2", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", @@ -73,9 +73,9 @@ "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, "node_modules/@vscode/codicons": { - "version": "0.0.45-0", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-0.tgz", - "integrity": "sha512-ixvw4auQobMOnMX9cOk8/3GfEgkTKCchsab2O6QvyL6+x4FJegOrK3Wgn4Y+Qua51LqnAsgpB5n74q8HEPh1pA==", + "version": "0.0.45-2", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-2.tgz", + "integrity": "sha512-Z5uZd8E2f84Jf4jv6ozSjIU/cnHn7F1REBGUtzdqJufWoLYauH/nwpVn8fWtvXNtR1QwEyh6x3WAeR2l5rnnyg==", "license": "CC-BY-4.0" }, "node_modules/@vscode/iconv-lite-umd": { diff --git a/remote/web/package.json b/remote/web/package.json index db9eed29f88..3c5f2e46943 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -5,7 +5,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.45-0", + "@vscode/codicons": "^0.0.45-2", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index 90cd1bab10f..dde42f3be9e 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -652,4 +652,5 @@ export const codiconsLibrary = { worktree: register('worktree', 0xec7e), screenCut: register('screen-cut', 0xec7f), ask: register('ask', 0xec80), + openai: register('openai', 0xec81), } as const; From a823d7d21c85bd633de62acf86ed6a54cfdafb38 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 29 Jan 2026 17:28:04 +0100 Subject: [PATCH 112/152] update distro (#291635) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5023a7daebb..4c450e79c73 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.109.0", - "distro": "84c8b9580d546487fee8ff25a29c5f3f49d33799", + "distro": "6c9f72a1ba8565301b303ec4314f5a24d585f012", "author": { "name": "Microsoft Corporation" }, @@ -240,4 +240,4 @@ "optionalDependencies": { "windows-foreground-love": "0.6.1" } -} +} \ No newline at end of file From 5271ac01f0653fcb5e9ce9ff4a230e6798f19106 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 29 Jan 2026 17:33:17 +0100 Subject: [PATCH 113/152] read cached policy data even before fetching the default account (#291631) --- src/vs/base/common/defaultAccount.ts | 1 - src/vs/base/common/policy.ts | 4 +- .../inlineCompletions/test/browser/utils.ts | 2 + .../standalone/browser/standaloneServices.ts | 4 +- .../defaultAccount/common/defaultAccount.ts | 6 +- src/vs/platform/policy/common/policy.ts | 4 +- .../contrib/chat/browser/chat.contribution.ts | 12 +- .../accounts/browser/defaultAccount.ts | 115 ++++++++++++------ .../policies/common/accountPolicyService.ts | 19 +-- .../test/browser/accountPolicyService.test.ts | 24 ++-- .../browser/multiplexPolicyService.test.ts | 21 ++-- 11 files changed, 126 insertions(+), 86 deletions(-) diff --git a/src/vs/base/common/defaultAccount.ts b/src/vs/base/common/defaultAccount.ts index 6b7f5d76d50..352fa5e4b34 100644 --- a/src/vs/base/common/defaultAccount.ts +++ b/src/vs/base/common/defaultAccount.ts @@ -59,5 +59,4 @@ export interface IDefaultAccount { readonly sessionId: string; readonly enterprise: boolean; readonly entitlementsData?: IEntitlementsData | null; - readonly policyData?: IPolicyData; } diff --git a/src/vs/base/common/policy.ts b/src/vs/base/common/policy.ts index 8141b0f9b5d..c27030fe03a 100644 --- a/src/vs/base/common/policy.ts +++ b/src/vs/base/common/policy.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../nls.js'; -import { IDefaultAccount } from './defaultAccount.js'; +import { IPolicyData } from './defaultAccount.js'; /** * System-wide policy file path for Linux systems. @@ -96,5 +96,5 @@ export interface IPolicy { * * If `undefined`, the feature's setting is not locked and can be overridden by other means. */ - readonly value?: (account: IDefaultAccount) => string | number | boolean | undefined; + readonly value?: (policyData: IPolicyData) => string | number | boolean | undefined; } diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts index 5b61a15bfb8..d3b3856cef3 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts @@ -267,6 +267,8 @@ export async function withAsyncTestCodeEditorAndInlineCompletionsModel( options.serviceCollection.set(IDefaultAccountService, { _serviceBrand: undefined, onDidChangeDefaultAccount: Event.None, + onDidChangePolicyData: Event.None, + policyData: null, getDefaultAccount: async () => null, setDefaultAccountProvider: () => { }, getDefaultAccountAuthenticationProvider: () => { return { id: 'mockProvider', name: 'Mock Provider', enterprise: false }; }, diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 61c629eda52..61aa37410e2 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -100,7 +100,7 @@ import { IDataChannelService, NullDataChannelService } from '../../../platform/d import { IWebWorkerService } from '../../../platform/webWorker/browser/webWorkerService.js'; import { StandaloneWebWorkerService } from './services/standaloneWebWorkerService.js'; import { IDefaultAccountService } from '../../../platform/defaultAccount/common/defaultAccount.js'; -import { IDefaultAccount, IDefaultAccountAuthenticationProvider } from '../../../base/common/defaultAccount.js'; +import { IDefaultAccount, IDefaultAccountAuthenticationProvider, IPolicyData } from '../../../base/common/defaultAccount.js'; class SimpleModel implements IResolvedTextEditorModel { @@ -1115,6 +1115,8 @@ class StandaloneDefaultAccountService implements IDefaultAccountService { declare readonly _serviceBrand: undefined; readonly onDidChangeDefaultAccount: Event = Event.None; + readonly onDidChangePolicyData: Event = Event.None; + readonly policyData: IPolicyData | null = null; async getDefaultAccount(): Promise { return null; diff --git a/src/vs/platform/defaultAccount/common/defaultAccount.ts b/src/vs/platform/defaultAccount/common/defaultAccount.ts index d3bee567a79..63a9b956608 100644 --- a/src/vs/platform/defaultAccount/common/defaultAccount.ts +++ b/src/vs/platform/defaultAccount/common/defaultAccount.ts @@ -5,11 +5,13 @@ import { createDecorator } from '../../instantiation/common/instantiation.js'; import { Event } from '../../../base/common/event.js'; -import { IDefaultAccount, IDefaultAccountAuthenticationProvider } from '../../../base/common/defaultAccount.js'; +import { IDefaultAccount, IDefaultAccountAuthenticationProvider, IPolicyData } from '../../../base/common/defaultAccount.js'; export interface IDefaultAccountProvider { readonly defaultAccount: IDefaultAccount | null; readonly onDidChangeDefaultAccount: Event; + readonly policyData: IPolicyData | null; + readonly onDidChangePolicyData: Event; getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider; refresh(): Promise; signIn(options?: { additionalScopes?: readonly string[];[key: string]: unknown }): Promise; @@ -20,6 +22,8 @@ export const IDefaultAccountService = createDecorator('d export interface IDefaultAccountService { readonly _serviceBrand: undefined; readonly onDidChangeDefaultAccount: Event; + readonly onDidChangePolicyData: Event; + readonly policyData: IPolicyData | null; getDefaultAccount(): Promise; getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider; setDefaultAccountProvider(provider: IDefaultAccountProvider): void; diff --git a/src/vs/platform/policy/common/policy.ts b/src/vs/platform/policy/common/policy.ts index a02d49e28d9..f8b86c80704 100644 --- a/src/vs/platform/policy/common/policy.ts +++ b/src/vs/platform/policy/common/policy.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IStringDictionary } from '../../../base/common/collections.js'; -import { IDefaultAccount } from '../../../base/common/defaultAccount.js'; +import { IPolicyData } from '../../../base/common/defaultAccount.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { Iterable } from '../../../base/common/iterator.js'; import { Disposable } from '../../../base/common/lifecycle.js'; @@ -14,7 +14,7 @@ import { createDecorator } from '../../instantiation/common/instantiation.js'; export type PolicyValue = string | number | boolean; export type PolicyDefinition = { type: 'string' | 'number' | 'boolean'; - value?: (account: IDefaultAccount) => string | number | boolean | undefined; + value?: (policyData: IPolicyData) => string | number | boolean | undefined; }; export const IPolicyService = createDecorator('policy'); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index f1ca19dc37d..d211a1d0929 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -320,7 +320,7 @@ configurationRegistry.registerConfiguration({ name: 'ChatToolsAutoApprove', category: PolicyCategory.InteractiveSession, minimumVersion: '1.99', - value: (account) => account.policyData?.chat_preview_features_enabled === false ? false : undefined, + value: (policyData) => policyData.chat_preview_features_enabled === false ? false : undefined, localization: { description: { key: 'autoApprove2.description', @@ -473,11 +473,11 @@ configurationRegistry.registerConfiguration({ name: 'ChatMCP', category: PolicyCategory.InteractiveSession, minimumVersion: '1.99', - value: (account) => { - if (account.policyData?.mcp === false) { + value: (policyData) => { + if (policyData.mcp === false) { return McpAccessValue.None; } - if (account.policyData?.mcpAccess === 'registry_only') { + if (policyData.mcpAccess === 'registry_only') { return McpAccessValue.Registry; } return undefined; @@ -588,7 +588,7 @@ configurationRegistry.registerConfiguration({ name: 'ChatAgentMode', category: PolicyCategory.InteractiveSession, minimumVersion: '1.99', - value: (account) => account.policyData?.chat_agent_enabled === false ? false : undefined, + value: (policyData) => policyData.chat_agent_enabled === false ? false : undefined, localization: { description: { key: 'chat.agent.enabled.description', @@ -665,7 +665,7 @@ configurationRegistry.registerConfiguration({ name: 'McpGalleryServiceUrl', category: PolicyCategory.InteractiveSession, minimumVersion: '1.101', - value: (account) => account.policyData?.mcpRegistryUrl, + value: (policyData) => policyData.mcpRegistryUrl, localization: { description: { key: 'mcp.gallery.serviceUrl', diff --git a/src/vs/workbench/services/accounts/browser/defaultAccount.ts b/src/vs/workbench/services/accounts/browser/defaultAccount.ts index e45b4e10c3c..f0a645aea45 100644 --- a/src/vs/workbench/services/accounts/browser/defaultAccount.ts +++ b/src/vs/workbench/services/accounts/browser/defaultAccount.ts @@ -111,12 +111,16 @@ export class DefaultAccountService extends Disposable implements IDefaultAccount declare _serviceBrand: undefined; private defaultAccount: IDefaultAccount | null = null; + get policyData(): IPolicyData | null { return this.defaultAccountProvider?.policyData ?? null; } private readonly initBarrier = new Barrier(); private readonly _onDidChangeDefaultAccount = this._register(new Emitter()); readonly onDidChangeDefaultAccount = this._onDidChangeDefaultAccount.event; + private readonly _onDidChangePolicyData = this._register(new Emitter()); + readonly onDidChangePolicyData = this._onDidChangePolicyData.event; + private readonly defaultAccountConfig: IDefaultAccountConfig; private defaultAccountProvider: IDefaultAccountProvider | null = null; @@ -148,11 +152,15 @@ export class DefaultAccountService extends Disposable implements IDefaultAccount } this.defaultAccountProvider = provider; + if (this.defaultAccountProvider.policyData) { + this._onDidChangePolicyData.fire(this.defaultAccountProvider.policyData); + } provider.refresh().then(account => { this.defaultAccount = account; }).finally(() => { this.initBarrier.open(); this._register(provider.onDidChangeDefaultAccount(account => this.setDefaultAccount(account))); + this._register(provider.onDidChangePolicyData(policyData => this._onDidChangePolicyData.fire(policyData))); }); } @@ -178,6 +186,16 @@ export class DefaultAccountService extends Disposable implements IDefaultAccount } } +interface IAccountPolicyData { + readonly accountId: string; + readonly policyData: IPolicyData; +} + +interface IDefaultAccountData { + defaultAccount: IDefaultAccount; + policyData: IAccountPolicyData | null; +} + type DefaultAccountStatusTelemetry = { status: string; initial: boolean; @@ -192,12 +210,18 @@ type DefaultAccountStatusTelemetryClassification = { class DefaultAccountProvider extends Disposable implements IDefaultAccountProvider { - private _defaultAccount: IDefaultAccount | null = null; - get defaultAccount(): IDefaultAccount | null { return this._defaultAccount ?? null; } + private _defaultAccount: IDefaultAccountData | null = null; + get defaultAccount(): IDefaultAccount | null { return this._defaultAccount?.defaultAccount ?? null; } + + private _policyData: IAccountPolicyData | null = null; + get policyData(): IPolicyData | null { return this._policyData?.policyData ?? null; } private readonly _onDidChangeDefaultAccount = this._register(new Emitter()); readonly onDidChangeDefaultAccount = this._onDidChangeDefaultAccount.event; + private readonly _onDidChangePolicyData = this._register(new Emitter()); + readonly onDidChangePolicyData = this._onDidChangePolicyData.event; + private readonly accountStatusContext: IContextKey; private initialized = false; private readonly initPromise: Promise; @@ -220,6 +244,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid ) { super(); this.accountStatusContext = CONTEXT_DEFAULT_ACCOUNT_STATE.bindTo(contextKeyService); + this._policyData = this.getCachedPolicyData(); this.initPromise = this.init() .finally(() => { this.telemetryService.publicLog2('defaultaccount:status', { status: this.defaultAccount ? 'available' : 'unavailable', initial: true }); @@ -227,6 +252,22 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid }); } + private getCachedPolicyData(): IAccountPolicyData | null { + const cached = this.storageService.get(CACHED_POLICY_DATA_KEY, StorageScope.APPLICATION); + if (cached) { + try { + const { accountId, policyData } = JSON.parse(cached); + if (accountId && policyData) { + this.logService.debug('[DefaultAccount] Initializing with cached policy data'); + return { accountId, policyData }; + } + } catch (error) { + this.logService.error('[DefaultAccount] Failed to parse cached policy data', getErrorMessage(error)); + } + } + return null; + } + private async init(): Promise { if (isWeb && !this.environmentService.remoteAuthority) { this.logService.debug('[DefaultAccount] Running in web without remote, skipping initialization'); @@ -323,7 +364,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid } } - private async fetchDefaultAccount(): Promise { + private async fetchDefaultAccount(): Promise { const defaultAccountProvider = this.getDefaultAccountAuthenticationProvider(); this.logService.debug('[DefaultAccount] Default account provider ID:', defaultAccountProvider.id); @@ -336,24 +377,47 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid return await this.getDefaultAccountForAuthenticationProvider(defaultAccountProvider); } - private setDefaultAccount(account: IDefaultAccount | null): void { + private setDefaultAccount(account: IDefaultAccountData | null): void { if (equals(this._defaultAccount, account)) { return; } this.logService.trace('[DefaultAccount] Updating default account:', account); - this._defaultAccount = account; - this._onDidChangeDefaultAccount.fire(this._defaultAccount); - if (this._defaultAccount) { + if (account) { + this._defaultAccount = account; + this.setPolicyData(account.policyData); + this._onDidChangeDefaultAccount.fire(this._defaultAccount.defaultAccount); this.accountStatusContext.set(DefaultAccountStatus.Available); this.logService.debug('[DefaultAccount] Account status set to Available'); } else { + this._defaultAccount = null; + this.setPolicyData(null); + this._onDidChangeDefaultAccount.fire(null); this.accountDataPollScheduler.cancel(); this.accountStatusContext.set(DefaultAccountStatus.Unavailable); this.logService.debug('[DefaultAccount] Account status set to Unavailable'); } } + private setPolicyData(accountPolicyData: IAccountPolicyData | null): void { + if (equals(this._policyData, accountPolicyData)) { + return; + } + this._policyData = accountPolicyData; + this.cachePolicyData(accountPolicyData); + this._onDidChangePolicyData.fire(this._policyData?.policyData ?? null); + } + + private cachePolicyData(accountPolicyData: IAccountPolicyData | null): void { + if (accountPolicyData) { + this.logService.debug('[DefaultAccount] Caching policy data for account:', accountPolicyData.accountId); + this.storageService.store(CACHED_POLICY_DATA_KEY, JSON.stringify(accountPolicyData), StorageScope.APPLICATION, StorageTarget.MACHINE); + } else { + this.logService.debug('[DefaultAccount] Removing cached policy data'); + this.storageService.remove(CACHED_POLICY_DATA_KEY, StorageScope.APPLICATION); + } + } + private scheduleAccountDataPoll(): void { if (!this._defaultAccount) { return; @@ -373,7 +437,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid return result; } - private async getDefaultAccountForAuthenticationProvider(authenticationProvider: IDefaultAccountAuthenticationProvider): Promise { + private async getDefaultAccountForAuthenticationProvider(authenticationProvider: IDefaultAccountAuthenticationProvider): Promise { try { this.logService.debug('[DefaultAccount] Getting Default Account from authenticated sessions for provider:', authenticationProvider.id); const sessions = await this.findMatchingProviderSession(authenticationProvider.id, this.defaultAccountConfig.authenticationProvider.scopes); @@ -390,7 +454,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid } } - private async getDefaultAccountFromAuthenticatedSessions(authenticationProvider: IDefaultAccountAuthenticationProvider, sessions: AuthenticationSession[]): Promise { + private async getDefaultAccountFromAuthenticatedSessions(authenticationProvider: IDefaultAccountAuthenticationProvider, sessions: AuthenticationSession[]): Promise { try { const accountId = sessions[0].account.id; const [entitlementsData, tokenEntitlementsData] = await Promise.all([ @@ -398,7 +462,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid this.getTokenEntitlements(sessions), ]); - let policyData = this.getCachedPolicyData(accountId); + let policyData: Mutable | undefined = this._policyData?.accountId === accountId ? { ...this._policyData.policyData } : undefined; if (tokenEntitlementsData) { policyData = policyData ?? {}; policyData.chat_agent_enabled = tokenEntitlementsData.chat_agent_enabled; @@ -411,18 +475,16 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid policyData.mcpAccess = mcpRegistryProvider.registry_access; } } - this.cachePolicyData(accountId, policyData); } - const account: IDefaultAccount = { + const defaultAccount: IDefaultAccount = { authenticationProvider, sessionId: sessions[0].id, enterprise: authenticationProvider.enterprise || sessions[0].account.label.includes('_'), entitlementsData, - policyData, }; this.logService.debug('[DefaultAccount] Successfully created default account for provider:', authenticationProvider.id); - return account; + return { defaultAccount, policyData: policyData ? { accountId, policyData } : null }; } catch (error) { this.logService.error('[DefaultAccount] Failed to create default account for provider:', authenticationProvider.id, getErrorMessage(error)); return null; @@ -515,28 +577,6 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid return undefined; } - private cachePolicyData(accountId: string, policyData: IPolicyData): void { - this.logService.debug('[DefaultAccount] Caching policy data for account:', accountId); - this.storageService.store(CACHED_POLICY_DATA_KEY, JSON.stringify({ accountId, policyData }), StorageScope.APPLICATION, StorageTarget.MACHINE); - } - - private getCachedPolicyData(accountId: string): Mutable | undefined { - const cached = this.storageService.get(CACHED_POLICY_DATA_KEY, StorageScope.APPLICATION); - if (cached) { - try { - const { accountId: cachedAccountId, policyData } = JSON.parse(cached); - if (cachedAccountId === accountId) { - this.logService.debug('[DefaultAccount] Using cached policy data for account:', accountId); - return policyData; - } - this.logService.debug('[DefaultAccount] Cached policy data is for different account, ignoring'); - } catch (error) { - this.logService.error('[DefaultAccount] Failed to parse cached policy data', getErrorMessage(error)); - } - } - return undefined; - } - private async getEntitlements(sessions: AuthenticationSession[]): Promise { const entitlementUrl = this.getEntitlementUrl(); if (!entitlementUrl) { @@ -744,7 +784,6 @@ class DefaultAccountProviderContribution extends Disposable implements IWorkbenc @IProductService productService: IProductService, @IInstantiationService instantiationService: IInstantiationService, @IDefaultAccountService defaultAccountService: IDefaultAccountService, - @ILogService logService: ILogService, ) { super(); const defaultAccountProvider = this._register(instantiationService.createInstance(DefaultAccountProvider, toDefaultAccountConfig(productService.defaultChatAgent))); @@ -752,4 +791,4 @@ class DefaultAccountProviderContribution extends Disposable implements IWorkbenc } } -registerWorkbenchContribution2(DefaultAccountProviderContribution.ID, DefaultAccountProviderContribution, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(DefaultAccountProviderContribution.ID, DefaultAccountProviderContribution, WorkbenchPhase.BlockStartup); diff --git a/src/vs/workbench/services/policies/common/accountPolicyService.ts b/src/vs/workbench/services/policies/common/accountPolicyService.ts index e84e364e7d8..ffd32cb2fa6 100644 --- a/src/vs/workbench/services/policies/common/accountPolicyService.ts +++ b/src/vs/workbench/services/policies/common/accountPolicyService.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { IStringDictionary } from '../../../../base/common/collections.js'; -import { IDefaultAccount } from '../../../../base/common/defaultAccount.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { AbstractPolicyService, IPolicyService, PolicyDefinition } from '../../../../platform/policy/common/policy.js'; import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js'; @@ -12,32 +11,26 @@ import { IDefaultAccountService } from '../../../../platform/defaultAccount/comm export class AccountPolicyService extends AbstractPolicyService implements IPolicyService { - private account: IDefaultAccount | null = null; - constructor( @ILogService private readonly logService: ILogService, @IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService ) { super(); - this.defaultAccountService.getDefaultAccount() - .then(account => { - this.account = account; - this._updatePolicyDefinitions(this.policyDefinitions); - this._register(this.defaultAccountService.onDidChangeDefaultAccount(account => { - this.account = account; - this._updatePolicyDefinitions(this.policyDefinitions); - })); - }); + this._updatePolicyDefinitions(this.policyDefinitions); + this._register(this.defaultAccountService.onDidChangePolicyData(() => { + this._updatePolicyDefinitions(this.policyDefinitions); + })); } protected async _updatePolicyDefinitions(policyDefinitions: IStringDictionary): Promise { this.logService.trace(`AccountPolicyService#_updatePolicyDefinitions: Got ${Object.keys(policyDefinitions).length} policy definitions`); const updated: string[] = []; + const policyData = this.defaultAccountService.policyData; for (const key in policyDefinitions) { const policy = policyDefinitions[key]; - const policyValue = this.account && policy.value ? policy.value(this.account) : undefined; + const policyValue = policyData && policy.value ? policy.value(policyData) : undefined; if (policyValue !== undefined) { if (this.policies.get(key) !== policyValue) { this.policies.set(key, policyValue); diff --git a/src/vs/workbench/services/policies/test/browser/accountPolicyService.test.ts b/src/vs/workbench/services/policies/test/browser/accountPolicyService.test.ts index 555a62494c0..d157a664c3b 100644 --- a/src/vs/workbench/services/policies/test/browser/accountPolicyService.test.ts +++ b/src/vs/workbench/services/policies/test/browser/accountPolicyService.test.ts @@ -13,7 +13,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/tes import { Registry } from '../../../../../platform/registry/common/platform.js'; import { Extensions, IConfigurationNode, IConfigurationRegistry } from '../../../../../platform/configuration/common/configurationRegistry.js'; import { DefaultConfiguration, PolicyConfiguration } from '../../../../../platform/configuration/common/configurations.js'; -import { IDefaultAccount, IDefaultAccountAuthenticationProvider } from '../../../../../base/common/defaultAccount.js'; +import { IDefaultAccount, IDefaultAccountAuthenticationProvider, IPolicyData } from '../../../../../base/common/defaultAccount.js'; import { PolicyCategory } from '../../../../../base/common/policy.js'; import { TestProductService } from '../../../../test/common/workbenchTestServices.js'; @@ -30,9 +30,11 @@ const BASE_DEFAULT_ACCOUNT: IDefaultAccount = { class DefaultAccountProvider implements IDefaultAccountProvider { readonly onDidChangeDefaultAccount = Event.None; + readonly onDidChangePolicyData = Event.None; constructor( readonly defaultAccount: IDefaultAccount, + readonly policyData: IPolicyData = {}, ) { } getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider { @@ -81,7 +83,7 @@ suite('AccountPolicyService', () => { category: PolicyCategory.Extensions, minimumVersion: '1.0.0', localization: { description: { key: '', value: '' } }, - value: account => account.policyData?.chat_preview_features_enabled === false ? 'policyValueB' : undefined, + value: policyData => policyData.chat_preview_features_enabled === false ? 'policyValueB' : undefined, } }, 'setting.C': { @@ -92,7 +94,7 @@ suite('AccountPolicyService', () => { category: PolicyCategory.Extensions, minimumVersion: '1.0.0', localization: { description: { key: '', value: '' } }, - value: account => account.policyData?.chat_preview_features_enabled === false ? JSON.stringify(['policyValueC1', 'policyValueC2']) : undefined, + value: policyData => policyData.chat_preview_features_enabled === false ? JSON.stringify(['policyValueC1', 'policyValueC2']) : undefined, } }, 'setting.D': { @@ -103,7 +105,7 @@ suite('AccountPolicyService', () => { category: PolicyCategory.Extensions, minimumVersion: '1.0.0', localization: { description: { key: '', value: '' } }, - value: account => account.policyData?.chat_preview_features_enabled === false ? false : undefined, + value: policyData => policyData.chat_preview_features_enabled === false ? false : undefined, } }, 'setting.E': { @@ -127,8 +129,8 @@ suite('AccountPolicyService', () => { }); - async function assertDefaultBehavior(defaultAccount: IDefaultAccount) { - defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(defaultAccount)); + async function assertDefaultBehavior(policyData: IPolicyData | undefined) { + defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(BASE_DEFAULT_ACCOUNT, policyData)); await defaultAccountService.refresh(); await policyConfiguration.initialize(); @@ -159,18 +161,16 @@ suite('AccountPolicyService', () => { test('should initialize with default account', async () => { - const defaultAccount = { ...BASE_DEFAULT_ACCOUNT }; - await assertDefaultBehavior(defaultAccount); + await assertDefaultBehavior(undefined); }); test('should initialize with default account and preview features enabled', async () => { - const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, policyData: { chat_preview_features_enabled: true } }; - await assertDefaultBehavior(defaultAccount); + await assertDefaultBehavior({ chat_preview_features_enabled: true }); }); test('should initialize with default account and preview features disabled', async () => { - const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, policyData: { chat_preview_features_enabled: false } }; - defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(defaultAccount)); + const policyData: IPolicyData = { chat_preview_features_enabled: false }; + defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(BASE_DEFAULT_ACCOUNT, policyData)); await defaultAccountService.refresh(); await policyConfiguration.initialize(); diff --git a/src/vs/workbench/services/policies/test/browser/multiplexPolicyService.test.ts b/src/vs/workbench/services/policies/test/browser/multiplexPolicyService.test.ts index 49c631ca49b..2da5b33f162 100644 --- a/src/vs/workbench/services/policies/test/browser/multiplexPolicyService.test.ts +++ b/src/vs/workbench/services/policies/test/browser/multiplexPolicyService.test.ts @@ -20,7 +20,7 @@ import { IFileService } from '../../../../../platform/files/common/files.js'; import { InMemoryFileSystemProvider } from '../../../../../platform/files/common/inMemoryFilesystemProvider.js'; import { FileService } from '../../../../../platform/files/common/fileService.js'; import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { IDefaultAccount, IDefaultAccountAuthenticationProvider } from '../../../../../base/common/defaultAccount.js'; +import { IDefaultAccount, IDefaultAccountAuthenticationProvider, IPolicyData } from '../../../../../base/common/defaultAccount.js'; import { PolicyCategory } from '../../../../../base/common/policy.js'; import { TestProductService } from '../../../../test/common/workbenchTestServices.js'; @@ -37,9 +37,11 @@ const BASE_DEFAULT_ACCOUNT: IDefaultAccount = { class DefaultAccountProvider implements IDefaultAccountProvider { readonly onDidChangeDefaultAccount = Event.None; + readonly onDidChangePolicyData = Event.None; constructor( readonly defaultAccount: IDefaultAccount, + readonly policyData: IPolicyData = {}, ) { } getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider { @@ -90,7 +92,7 @@ suite('MultiplexPolicyService', () => { category: PolicyCategory.Extensions, minimumVersion: '1.0.0', localization: { description: { key: '', value: '' } }, - value: account => account.policyData?.chat_preview_features_enabled === false ? 'policyValueB' : undefined, + value: policyData => policyData.chat_preview_features_enabled === false ? 'policyValueB' : undefined, } }, 'setting.C': { @@ -101,7 +103,7 @@ suite('MultiplexPolicyService', () => { category: PolicyCategory.Extensions, minimumVersion: '1.0.0', localization: { description: { key: '', value: '' } }, - value: account => account.policyData?.chat_preview_features_enabled === false ? JSON.stringify(['policyValueC1', 'policyValueC2']) : undefined, + value: policyData => policyData.chat_preview_features_enabled === false ? JSON.stringify(['policyValueC1', 'policyValueC2']) : undefined, } }, 'setting.D': { @@ -112,7 +114,7 @@ suite('MultiplexPolicyService', () => { category: PolicyCategory.Extensions, minimumVersion: '1.0.0', localization: { description: { key: '', value: '' } }, - value: account => account.policyData?.chat_preview_features_enabled === false ? false : undefined, + value: policyData => policyData.chat_preview_features_enabled === false ? false : undefined, } }, 'setting.E': { @@ -186,8 +188,7 @@ suite('MultiplexPolicyService', () => { test('policy from file only', async () => { await clear(); - const defaultAccount = { ...BASE_DEFAULT_ACCOUNT }; - defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(defaultAccount)); + defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(BASE_DEFAULT_ACCOUNT)); await defaultAccountService.refresh(); await fileService.writeFile(policyFile, @@ -228,8 +229,8 @@ suite('MultiplexPolicyService', () => { test('policy from default account only', async () => { await clear(); - const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, policyData: { chat_preview_features_enabled: false } }; - defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(defaultAccount)); + const policyData: IPolicyData = { chat_preview_features_enabled: false }; + defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(BASE_DEFAULT_ACCOUNT, policyData)); await defaultAccountService.refresh(); await fileService.writeFile(policyFile, @@ -269,8 +270,8 @@ suite('MultiplexPolicyService', () => { test('policy from file and default account', async () => { await clear(); - const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, policyData: { chat_preview_features_enabled: false } }; - defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(defaultAccount)); + const policyData: IPolicyData = { chat_preview_features_enabled: false }; + defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(BASE_DEFAULT_ACCOUNT, policyData)); await defaultAccountService.refresh(); await fileService.writeFile(policyFile, From d944566c4db6f2159e2b4309a0282a948e99fe83 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 29 Jan 2026 17:34:57 +0100 Subject: [PATCH 114/152] Show chat agents/prompts in extension features list (#291643) --- .../platform/extensions/common/extensions.ts | 1 + .../chatPromptFilesContribution.ts | 84 ++++++++++++++++++- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index e8470285f7f..021ad016e02 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -235,6 +235,7 @@ export interface IExtensionContributions { readonly chatPromptFiles?: ReadonlyArray; readonly chatInstructions?: ReadonlyArray; readonly chatAgents?: ReadonlyArray; + readonly chatSkills?: ReadonlyArray; readonly languageModelTools?: ReadonlyArray; readonly languageModelToolSets?: ReadonlyArray; readonly mcpServerDefinitionProviders?: ReadonlyArray; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts index 8e9d41c3db7..6468c60cdfa 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ -import { DisposableMap } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js'; import { joinPath, isEqualOrParent } from '../../../../../base/common/resources.js'; import { localize } from '../../../../../nls.js'; -import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; +import { ExtensionIdentifier, IExtensionManifest } from '../../../../../platform/extensions/common/extensions.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js'; import { IPromptsService, PromptsStorage } from './service/promptsService.js'; @@ -15,6 +15,9 @@ import { PromptsType } from './promptTypes.js'; import { UriComponents } from '../../../../../base/common/uri.js'; import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; +import { Registry } from '../../../../../platform/registry/common/platform.js'; +import { Extensions, IExtensionFeaturesRegistry, IExtensionFeatureTableRenderer, IRenderedData, IRowData, ITableData } from '../../../../services/extensionManagement/common/extensionFeatures.js'; interface IRawChatFileContribution { readonly path: string; @@ -162,3 +165,80 @@ CommandsRegistry.registerCommand('_listExtensionPromptFiles', async (accessor): return result; }); + +class ChatPromptFilesDataRenderer extends Disposable implements IExtensionFeatureTableRenderer { + readonly type = 'table'; + + constructor(private readonly contributionPoint: ChatContributionPoint) { + super(); + } + + shouldRender(manifest: IExtensionManifest): boolean { + return !!manifest.contributes?.[this.contributionPoint]; + } + + render(manifest: IExtensionManifest): IRenderedData { + const contributions = manifest.contributes?.[this.contributionPoint] ?? []; + if (!contributions.length) { + return { data: { headers: [], rows: [] }, dispose: () => { } }; + } + + const headers = [ + localize('chatFilesName', "Name"), + localize('chatFilesDescription', "Description"), + localize('chatFilesPath', "Path"), + ]; + + const rows: IRowData[][] = contributions.map(d => { + return [ + d.name ?? '-', + d.description ?? '-', + d.path, + ]; + }); + + return { + data: { + headers, + rows + }, + dispose: () => { } + }; + } +} + +Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: ChatContributionPoint.chatPromptFiles, + label: localize('chatPromptFiles', "Chat Prompt Files"), + access: { + canToggle: false + }, + renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatPromptFiles]), +}); + +Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: ChatContributionPoint.chatInstructions, + label: localize('chatInstructions', "Chat Instructions"), + access: { + canToggle: false + }, + renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatInstructions]), +}); + +Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: ChatContributionPoint.chatAgents, + label: localize('chatAgents', "Chat Agents"), + access: { + canToggle: false + }, + renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatAgents]), +}); + +Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: ChatContributionPoint.chatSkills, + label: localize('chatSkills', "Chat Skills"), + access: { + canToggle: false + }, + renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatSkills]), +}); From 7ba11de8cfad58e43a38fd64fca7daf6dd92d16c Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 29 Jan 2026 16:35:41 +0000 Subject: [PATCH 115/152] fix: update theme IDs and labels for consistency --- extensions/theme-2026/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/theme-2026/package.json b/extensions/theme-2026/package.json index b425a019e2f..305cc066c89 100644 --- a/extensions/theme-2026/package.json +++ b/extensions/theme-2026/package.json @@ -17,14 +17,14 @@ "contributes": { "themes": [ { - "id": "2026-light-experimental", - "label": "2026 Light", + "id": "Experimental Light", + "label": "VS Code Light", "uiTheme": "vs", "path": "./themes/2026-light.json" }, { - "id": "2026-dark-experimental", - "label": "2026 Dark", + "id": "Experimental Dark", + "label": "VS Code Dark", "uiTheme": "vs-dark", "path": "./themes/2026-dark.json" } From d9e4d027ebe5c4cbe9f775101996c70917e7eeff Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 29 Jan 2026 17:36:55 +0100 Subject: [PATCH 116/152] https://github.com/microsoft/vscode/issues/289753 (#291644) --- src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 094ee8995fa..ba678499fec 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -547,7 +547,7 @@ export class BreadcrumbsControl { const pickerArrowSize = 8; let pickerArrowOffset: number; - const data = dom.getDomNodePagePosition(event.node.firstChild as HTMLElement); + const data = dom.getDomNodePagePosition(event.node); const y = data.top + data.height + pickerArrowSize; if (y + maxHeight >= window.innerHeight) { maxHeight = window.innerHeight - y - 30 /* room for shadow and status bar*/; From 00b43c826d9b07523068d2b122600d9b74828fce Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 29 Jan 2026 08:49:53 -0800 Subject: [PATCH 117/152] mcp: update draft typings and announce mcp apps support (#291654) Implements https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx#client-host-capabilities Updates MCP typings to the latest draft to be able to specify that field. --- .../toolInvocationParts/chatMcpAppModel.ts | 2 +- .../mcp/common/mcpServerRequestHandler.ts | 31 +- .../contrib/mcp/common/mcpTaskManager.ts | 3 + .../mcp/common/modelContextProtocol.ts | 1015 ++++++++++++++--- .../common/mcpServerRequestHandler.test.ts | 5 +- 5 files changed, 875 insertions(+), 181 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts index 34fbe127cd9..7541799ba29 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts @@ -723,7 +723,7 @@ export class ChatMcpAppModel extends Disposable { jsonrpc: '2.0', id, error: { code, message }, - } satisfies MCP.JSONRPCError); + } satisfies MCP.JSONRPCErrorResponse); } private async _sendNotification(message: McpApps.HostNotification): Promise { diff --git a/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts b/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts index d7e0ed232f7..b71f9791274 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts @@ -133,6 +133,11 @@ export class McpServerRequestHandler extends Disposable { elicitation: opts.elicitationRequestHandler ? { create: {} } : undefined, }, }, + extensions: { + 'io.modelcontextprotocol/ui': { + mimeTypes: ['text/html;profile=mcp-app'] + } + } }, clientInfo: { name: productService.nameLong, @@ -321,22 +326,26 @@ export class McpServerRequestHandler extends Disposable { /** * Handle successful responses */ - private handleResult(response: MCP.JSONRPCResponse): void { - const request = this._pendingRequests.get(response.id); - if (request) { - this._pendingRequests.delete(response.id); - request.promise.complete(response.result); + private handleResult(response: MCP.JSONRPCResultResponse): void { + if (response.id !== undefined) { + const request = this._pendingRequests.get(response.id); + if (request) { + this._pendingRequests.delete(response.id); + request.promise.complete(response.result); + } } } /** * Handle error responses */ - private handleError(response: MCP.JSONRPCError): void { - const request = this._pendingRequests.get(response.id); - if (request) { - this._pendingRequests.delete(response.id); - request.promise.error(new MpcResponseError(response.error.message, response.error.code, response.error.data)); + private handleError(response: MCP.JSONRPCErrorResponse): void { + if (response.id !== undefined) { + const request = this._pendingRequests.get(response.id); + if (request) { + this._pendingRequests.delete(response.id); + request.promise.error(new MpcResponseError(response.error.message, response.error.code, response.error.data)); + } } } @@ -394,7 +403,7 @@ export class McpServerRequestHandler extends Disposable { e = McpError.unknown(e); } - const errorResponse: MCP.JSONRPCError = { + const errorResponse: MCP.JSONRPCErrorResponse = { jsonrpc: MCP.JSONRPC_VERSION, id: request.id, error: { diff --git a/src/vs/workbench/contrib/mcp/common/mcpTaskManager.ts b/src/vs/workbench/contrib/mcp/common/mcpTaskManager.ts index 75dac9f1002..689e457469c 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTaskManager.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTaskManager.ts @@ -89,6 +89,7 @@ export class McpTaskManager extends Disposable { status: 'working', createdAt, ttl, + lastUpdatedAt: new Date().toISOString(), pollInterval: 1000, // Suggest 1 second polling interval }; @@ -171,6 +172,8 @@ export class McpTaskManager extends Disposable { } entry.task.status = status; + entry.task.lastUpdatedAt = new Date().toISOString(); + if (statusMessage !== undefined) { entry.task.statusMessage = statusMessage; } diff --git a/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts b/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts index a095b35bb9a..db33783efc6 100644 --- a/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts +++ b/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts @@ -37,14 +37,48 @@ export namespace MCP { export type JSONRPCMessage = | JSONRPCRequest | JSONRPCNotification - | JSONRPCResponse - | JSONRPCError; + | JSONRPCResponse; /** @internal */ export const LATEST_PROTOCOL_VERSION = "2025-11-25"; /** @internal */ export const JSONRPC_VERSION = "2.0"; + /** + * Represents the contents of a `_meta` field, which clients and servers use to attach additional metadata to their interactions. + * + * Certain key names are reserved by MCP for protocol-level metadata; implementations MUST NOT make assumptions about values at these keys. Additionally, specific schema definitions may reserve particular names for purpose-specific metadata, as declared in those definitions. + * + * Valid keys have two segments: + * + * **Prefix:** + * - Optional - if specified, MUST be a series of _labels_ separated by dots (`.`), followed by a slash (`/`). + * - Labels MUST start with a letter and end with a letter or digit. Interior characters may be letters, digits, or hyphens (`-`). + * - Any prefix consisting of zero or more labels, followed by `modelcontextprotocol` or `mcp`, followed by any label, is **reserved** for MCP use. For example: `modelcontextprotocol.io/`, `mcp.dev/`, `api.modelcontextprotocol.org/`, and `tools.mcp.com/` are all reserved. + * + * **Name:** + * - Unless empty, MUST start and end with an alphanumeric character (`[a-z0-9A-Z]`). + * - Interior characters may be alphanumeric, hyphens (`-`), underscores (`_`), or dots (`.`). + * + * @see [General fields: `_meta`](/specification/draft/basic/index#meta) for more details. + * @category Common Types + */ + export type MetaObject = Record; + + /** + * Extends {@link MetaObject} with additional request-specific fields. All key naming rules from `MetaObject` apply. + * + * @see {@link MetaObject} for key naming rules and reserved prefixes. + * @see [General fields: `_meta`](/specification/draft/basic/index#meta) for more details. + * @category Common Types + */ + export interface RequestMetaObject extends MetaObject { + /** + * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by {@link ProgressNotification | notifications/progress}). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. + */ + progressToken?: ProgressToken; + } + /** * A progress token, used to associate progress notifications with the original request. * @@ -67,30 +101,22 @@ export namespace MCP { export interface TaskAugmentedRequestParams extends RequestParams { /** * If specified, the caller is requesting task-augmented execution for this request. - * The request will return a CreateTaskResult immediately, and the actual result can be - * retrieved later via tasks/result. + * The request will return a {@link CreateTaskResult} immediately, and the actual result can be + * retrieved later via {@link GetTaskPayloadRequest | tasks/result}. * * Task augmentation is subject to capability negotiation - receivers MUST declare support * for task augmentation of specific request types in their capabilities. */ task?: TaskMetadata; } + /** * Common params for any request. * - * @internal + * @category Common Types */ export interface RequestParams { - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { - /** - * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. - */ - progressToken?: ProgressToken; - [key: string]: unknown; - }; + _meta?: RequestMetaObject; } /** @internal */ @@ -101,12 +127,13 @@ export namespace MCP { params?: { [key: string]: any }; } - /** @internal */ + /** + * Common params for any notification. + * + * @category Common Types + */ export interface NotificationParams { - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** @internal */ @@ -118,18 +145,17 @@ export namespace MCP { } /** + * Common result fields. + * * @category Common Types */ export interface Result { - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; [key: string]: unknown; } /** - * @category Common Types + * @category Errors */ export interface Error { /** @@ -177,12 +203,30 @@ export namespace MCP { * * @category JSON-RPC */ - export interface JSONRPCResponse { + export interface JSONRPCResultResponse { jsonrpc: typeof JSONRPC_VERSION; id: RequestId; result: Result; } + /** + * A response to a request that indicates an error occurred. + * + * @category JSON-RPC + */ + export interface JSONRPCErrorResponse { + jsonrpc: typeof JSONRPC_VERSION; + id?: RequestId; + error: Error; + } + + /** + * A response to a request, containing either the result or error. + * + * @category JSON-RPC + */ + export type JSONRPCResponse = JSONRPCResultResponse | JSONRPCErrorResponse; + // Standard JSON-RPC error codes export const PARSE_ERROR = -32700; export const INVALID_REQUEST = -32600; @@ -190,28 +234,113 @@ export namespace MCP { export const INVALID_PARAMS = -32602; export const INTERNAL_ERROR = -32603; + /** + * A JSON-RPC error indicating that invalid JSON was received by the server. This error is returned when the server cannot parse the JSON text of a message. + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @example Invalid JSON + * {@includeCode ./examples/ParseError/invalid-json.json} + * + * @category Errors + */ + export interface ParseError extends Error { + code: typeof PARSE_ERROR; + } + + /** + * A JSON-RPC error indicating that the request is not a valid request object. This error is returned when the message structure does not conform to the JSON-RPC 2.0 specification requirements for a request (e.g., missing required fields like `jsonrpc` or `method`, or using invalid types for these fields). + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @category Errors + */ + export interface InvalidRequestError extends Error { + code: typeof INVALID_REQUEST; + } + + /** + * A JSON-RPC error indicating that the requested method does not exist or is not available. + * + * In MCP, this error is returned when a request is made for a method that requires a capability that has not been declared. This can occur in either direction: + * + * - A server returning this error when the client requests a capability it doesn't support (e.g., requesting completions when the `completions` capability was not advertised) + * - A client returning this error when the server requests a capability it doesn't support (e.g., requesting roots when the client did not declare the `roots` capability) + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @example Roots not supported + * {@includeCode ./examples/MethodNotFoundError/roots-not-supported.json} + * + * @category Errors + */ + export interface MethodNotFoundError extends Error { + code: typeof METHOD_NOT_FOUND; + } + + /** + * A JSON-RPC error indicating that the method parameters are invalid or malformed. + * + * In MCP, this error is returned in various contexts when request parameters fail validation: + * + * - **Tools**: Unknown tool name or invalid tool arguments + * - **Prompts**: Unknown prompt name or missing required arguments + * - **Pagination**: Invalid or expired cursor values + * - **Logging**: Invalid log level + * - **Tasks**: Invalid or nonexistent task ID, invalid cursor, or attempting to cancel a task already in a terminal status + * - **Elicitation**: Server requests an elicitation mode not declared in client capabilities + * - **Sampling**: Missing tool result or tool results mixed with other content + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @example Unknown tool + * {@includeCode ./examples/InvalidParamsError/unknown-tool.json} + * + * @example Invalid tool arguments + * {@includeCode ./examples/InvalidParamsError/invalid-tool-arguments.json} + * + * @example Unknown prompt + * {@includeCode ./examples/InvalidParamsError/unknown-prompt.json} + * + * @example Invalid cursor + * {@includeCode ./examples/InvalidParamsError/invalid-cursor.json} + * + * @category Errors + */ + export interface InvalidParamsError extends Error { + code: typeof INVALID_PARAMS; + } + + /** + * A JSON-RPC error indicating that an internal error occurred on the receiver. This error is returned when the receiver encounters an unexpected condition that prevents it from fulfilling the request. + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @example Unexpected error + * {@includeCode ./examples/InternalError/unexpected-error.json} + * + * @category Errors + */ + export interface InternalError extends Error { + code: typeof INTERNAL_ERROR; + } + // Implementation-specific JSON-RPC error codes [-32000, -32099] /** @internal */ export const URL_ELICITATION_REQUIRED = -32042; - /** - * A response to a request that indicates an error occurred. - * - * @category JSON-RPC - */ - export interface JSONRPCError { - jsonrpc: typeof JSONRPC_VERSION; - id: RequestId; - error: Error; - } - /** * An error response that indicates that the server requires the client to provide additional information via an elicitation request. * + * @example Authorization required + * {@includeCode ./examples/URLElicitationRequiredError/authorization-required.json} + * * @internal */ - export interface URLElicitationRequiredError - extends Omit { + export interface URLElicitationRequiredError extends Omit< + JSONRPCErrorResponse, + "error" + > { error: Error & { code: typeof URL_ELICITATION_REQUIRED; data: { @@ -223,7 +352,7 @@ export namespace MCP { /* Empty result */ /** - * A response that indicates success but carries no data. + * A result that indicates success but carries no data. * * @category Common Types */ @@ -233,6 +362,9 @@ export namespace MCP { /** * Parameters for a `notifications/cancelled` notification. * + * @example User-requested cancellation + * {@includeCode ./examples/CancelledNotificationParams/user-requested-cancellation.json} + * * @category `notifications/cancelled` */ export interface CancelledNotificationParams extends NotificationParams { @@ -241,7 +373,7 @@ export namespace MCP { * * This MUST correspond to the ID of a request previously issued in the same direction. * This MUST be provided for cancelling non-task requests. - * This MUST NOT be used for cancelling tasks (use the `tasks/cancel` request instead). + * This MUST NOT be used for cancelling tasks (use the {@link CancelTaskRequest | tasks/cancel} request instead). */ requestId?: RequestId; @@ -260,7 +392,10 @@ export namespace MCP { * * A client MUST NOT attempt to cancel its `initialize` request. * - * For task cancellation, use the `tasks/cancel` request instead of this notification. + * For task cancellation, use the {@link CancelTaskRequest | tasks/cancel} request instead of this notification. + * + * @example User-requested cancellation + * {@includeCode ./examples/CancelledNotification/user-requested-cancellation.json} * * @category `notifications/cancelled` */ @@ -273,6 +408,9 @@ export namespace MCP { /** * Parameters for an `initialize` request. * + * @example Full client capabilities + * {@includeCode ./examples/InitializeRequestParams/full-client-capabilities.json} + * * @category `initialize` */ export interface InitializeRequestParams extends RequestParams { @@ -287,6 +425,9 @@ export namespace MCP { /** * This request is sent from the client to the server when it first connects, asking it to begin initialization. * + * @example Initialize request + * {@includeCode ./examples/InitializeRequest/initialize-request.json} + * * @category `initialize` */ export interface InitializeRequest extends JSONRPCRequest { @@ -295,7 +436,10 @@ export namespace MCP { } /** - * After receiving an initialize request from the client, the server sends this response. + * The result returned by the server for an {@link InitializeRequest | initialize} request. + * + * @example Full server capabilities + * {@includeCode ./examples/InitializeResult/full-server-capabilities.json} * * @category `initialize` */ @@ -315,9 +459,24 @@ export namespace MCP { instructions?: string; } + /** + * A successful response from the server for a {@link InitializeRequest | initialize} request. + * + * @example Initialize result response + * {@includeCode ./examples/InitializeResultResponse/initialize-result-response.json} + * + * @category `initialize` + */ + export interface InitializeResultResponse extends JSONRPCResultResponse { + result: InitializeResult; + } + /** * This notification is sent from the client to the server after initialization has finished. * + * @example Initialized notification + * {@includeCode ./examples/InitializedNotification/initialized-notification.json} + * * @category `notifications/initialized` */ export interface InitializedNotification extends JSONRPCNotification { @@ -337,6 +496,12 @@ export namespace MCP { experimental?: { [key: string]: object }; /** * Present if the client supports listing roots. + * + * @example Roots - minimum baseline support + * {@includeCode ./examples/ClientCapabilities/roots-minimum-baseline-support.json} + * + * @example Roots - list changed notifications + * {@includeCode ./examples/ClientCapabilities/roots-list-changed-notifications.json} */ roots?: { /** @@ -346,20 +511,35 @@ export namespace MCP { }; /** * Present if the client supports sampling from an LLM. + * + * @example Sampling - minimum baseline support + * {@includeCode ./examples/ClientCapabilities/sampling-minimum-baseline-support.json} + * + * @example Sampling - tool use support + * {@includeCode ./examples/ClientCapabilities/sampling-tool-use-support.json} + * + * @example Sampling - context inclusion support (soft-deprecated) + * {@includeCode ./examples/ClientCapabilities/sampling-context-inclusion-support-soft-deprecated.json} */ sampling?: { /** - * Whether the client supports context inclusion via includeContext parameter. + * Whether the client supports context inclusion via `includeContext` parameter. * If not declared, servers SHOULD only use `includeContext: "none"` (or omit it). */ context?: object; /** - * Whether the client supports tool use via tools and toolChoice parameters. + * Whether the client supports tool use via `tools` and `toolChoice` parameters. */ tools?: object; }; /** * Present if the client supports elicitation from the server. + * + * @example Elicitation - form and URL mode support + * {@includeCode ./examples/ClientCapabilities/elicitation-form-and-url-mode-support.json} + * + * @example Elicitation - form mode only (implicit) + * {@includeCode ./examples/ClientCapabilities/elicitation-form-only-implicit.json} */ elicitation?: { form?: object; url?: object }; @@ -368,11 +548,11 @@ export namespace MCP { */ tasks?: { /** - * Whether this client supports tasks/list. + * Whether this client supports {@link ListTasksRequest | tasks/list}. */ list?: object; /** - * Whether this client supports tasks/cancel. + * Whether this client supports {@link CancelTaskRequest | tasks/cancel}. */ cancel?: object; /** @@ -384,7 +564,7 @@ export namespace MCP { */ sampling?: { /** - * Whether the client supports task-augmented sampling/createMessage requests. + * Whether the client supports task-augmented `sampling/createMessage` requests. */ createMessage?: object; }; @@ -393,12 +573,21 @@ export namespace MCP { */ elicitation?: { /** - * Whether the client supports task-augmented elicitation/create requests. + * Whether the client supports task-augmented {@link ElicitRequest | elicitation/create} requests. */ create?: object; }; }; }; + /** + * Optional MCP extensions that the client supports. Keys are extension identifiers + * (e.g., "io.modelcontextprotocol/oauth-client-credentials"), and values are + * per-extension settings objects. An empty object indicates support with no settings. + * + * @example Extensions - UI extension with MIME type support + * {@includeCode ./examples/ClientCapabilities/extensions-ui-mime-types.json} + */ + extensions?: { [key: string]: object }; } /** @@ -413,14 +602,26 @@ export namespace MCP { experimental?: { [key: string]: object }; /** * Present if the server supports sending log messages to the client. + * + * @example Logging - minimum baseline support + * {@includeCode ./examples/ServerCapabilities/logging-minimum-baseline-support.json} */ logging?: object; /** * Present if the server supports argument autocompletion suggestions. + * + * @example Completions - minimum baseline support + * {@includeCode ./examples/ServerCapabilities/completions-minimum-baseline-support.json} */ completions?: object; /** * Present if the server offers any prompt templates. + * + * @example Prompts - minimum baseline support + * {@includeCode ./examples/ServerCapabilities/prompts-minimum-baseline-support.json} + * + * @example Prompts - list changed notifications + * {@includeCode ./examples/ServerCapabilities/prompts-list-changed-notifications.json} */ prompts?: { /** @@ -430,6 +631,18 @@ export namespace MCP { }; /** * Present if the server offers any resources to read. + * + * @example Resources - minimum baseline support + * {@includeCode ./examples/ServerCapabilities/resources-minimum-baseline-support.json} + * + * @example Resources - subscription to individual resource updates (only) + * {@includeCode ./examples/ServerCapabilities/resources-subscription-to-individual-resource-updates-only.json} + * + * @example Resources - list changed notifications (only) + * {@includeCode ./examples/ServerCapabilities/resources-list-changed-notifications-only.json} + * + * @example Resources - all notifications + * {@includeCode ./examples/ServerCapabilities/resources-all-notifications.json} */ resources?: { /** @@ -443,6 +656,12 @@ export namespace MCP { }; /** * Present if the server offers any tools to call. + * + * @example Tools - minimum baseline support + * {@includeCode ./examples/ServerCapabilities/tools-minimum-baseline-support.json} + * + * @example Tools - list changed notifications + * {@includeCode ./examples/ServerCapabilities/tools-list-changed-notifications.json} */ tools?: { /** @@ -455,11 +674,11 @@ export namespace MCP { */ tasks?: { /** - * Whether this server supports tasks/list. + * Whether this server supports {@link ListTasksRequest | tasks/list}. */ list?: object; /** - * Whether this server supports tasks/cancel. + * Whether this server supports {@link CancelTaskRequest | tasks/cancel}. */ cancel?: object; /** @@ -471,12 +690,21 @@ export namespace MCP { */ tools?: { /** - * Whether the server supports task-augmented tools/call requests. + * Whether the server supports task-augmented {@link CallToolRequest | tools/call} requests. */ call?: object; }; }; }; + /** + * Optional MCP extensions that the server supports. Keys are extension identifiers + * (e.g., "io.modelcontextprotocol/apps"), and values are per-extension settings + * objects. An empty object indicates support with no settings. + * + * @example Extensions - UI extension support + * {@includeCode ./examples/ServerCapabilities/extensions-ui.json} + */ + extensions?: { [key: string]: object }; } /** @@ -489,7 +717,7 @@ export namespace MCP { * A standard URI pointing to an icon resource. May be an HTTP/HTTPS URL or a * `data:` URI with Base64-encoded image data. * - * Consumers SHOULD takes steps to ensure URLs serving icons are from the + * Consumers SHOULD take steps to ensure URLs serving icons are from the * same domain as the client/server or a trusted domain. * * Consumers SHOULD take appropriate precautions when consuming SVGs as they can contain @@ -514,8 +742,8 @@ export namespace MCP { sizes?: string[]; /** - * Optional specifier for the theme this icon is designed for. `light` indicates - * the icon is designed to be used with a light background, and `dark` indicates + * Optional specifier for the theme this icon is designed for. `"light"` indicates + * the icon is designed to be used with a light background, and `"dark"` indicates * the icon is designed to be used with a dark background. * * If not provided, the client should assume the icon can be used with any theme. @@ -558,7 +786,7 @@ export namespace MCP { * Intended for UI and end-user contexts - optimized to be human-readable and easily understood, * even by those unfamiliar with domain-specific terminology. * - * If not provided, the name should be used for display (except for Tool, + * If not provided, the name should be used for display (except for {@link Tool}, * where `annotations.title` should be given precedence over using `name`, * if present). */ @@ -571,6 +799,9 @@ export namespace MCP { * @category `initialize` */ export interface Implementation extends BaseMetadata, Icons { + /** + * The version of this implementation. + */ version: string; /** @@ -594,6 +825,9 @@ export namespace MCP { /** * A ping, issued by either the server or the client, to check that the other party is still alive. The receiver must promptly respond, or else may be disconnected. * + * @example Ping request + * {@includeCode ./examples/PingRequest/ping-request.json} + * * @category `ping` */ export interface PingRequest extends JSONRPCRequest { @@ -601,10 +835,25 @@ export namespace MCP { params?: RequestParams; } + /** + * A successful response for a {@link PingRequest | ping} request. + * + * @example Ping result response + * {@includeCode ./examples/PingResultResponse/ping-result-response.json} + * + * @category `ping` + */ + export interface PingResultResponse extends JSONRPCResultResponse { + result: EmptyResult; + } + /* Progress notifications */ /** - * Parameters for a `notifications/progress` notification. + * Parameters for a {@link ProgressNotification | notifications/progress} notification. + * + * @example Progress message + * {@includeCode ./examples/ProgressNotificationParams/progress-message.json} * * @category `notifications/progress` */ @@ -634,6 +883,9 @@ export namespace MCP { /** * An out-of-band notification used to inform the receiver of a progress update for a long-running request. * + * @example Progress message + * {@includeCode ./examples/ProgressNotification/progress-message.json} + * * @category `notifications/progress` */ export interface ProgressNotification extends JSONRPCNotification { @@ -643,9 +895,12 @@ export namespace MCP { /* Pagination */ /** - * Common parameters for paginated requests. + * Common params for paginated requests. * - * @internal + * @example List request with cursor + * {@includeCode ./examples/PaginatedRequestParams/list-with-cursor.json} + * + * @category Common Types */ export interface PaginatedRequestParams extends RequestParams { /** @@ -673,6 +928,9 @@ export namespace MCP { /** * Sent from the client to request a list of resources the server has. * + * @example List resources request + * {@includeCode ./examples/ListResourcesRequest/list-resources-request.json} + * * @category `resources/list` */ export interface ListResourcesRequest extends PaginatedRequest { @@ -680,7 +938,10 @@ export namespace MCP { } /** - * The server's response to a resources/list request from the client. + * The result returned by the server for a {@link ListResourcesRequest | resources/list} request. + * + * @example Resources list with cursor + * {@includeCode ./examples/ListResourcesResult/resources-list-with-cursor.json} * * @category `resources/list` */ @@ -688,9 +949,24 @@ export namespace MCP { resources: Resource[]; } + /** + * A successful response from the server for a {@link ListResourcesRequest | resources/list} request. + * + * @example List resources result response + * {@includeCode ./examples/ListResourcesResultResponse/list-resources-result-response.json} + * + * @category `resources/list` + */ + export interface ListResourcesResultResponse extends JSONRPCResultResponse { + result: ListResourcesResult; + } + /** * Sent from the client to request a list of resource templates the server has. * + * @example List resource templates request + * {@includeCode ./examples/ListResourceTemplatesRequest/list-resource-templates-request.json} + * * @category `resources/templates/list` */ export interface ListResourceTemplatesRequest extends PaginatedRequest { @@ -698,7 +974,10 @@ export namespace MCP { } /** - * The server's response to a resources/templates/list request from the client. + * The result returned by the server for a {@link ListResourceTemplatesRequest | resources/templates/list} request. + * + * @example Resource templates list + * {@includeCode ./examples/ListResourceTemplatesResult/resource-templates-list.json} * * @category `resources/templates/list` */ @@ -707,7 +986,19 @@ export namespace MCP { } /** - * Common parameters when working with resources. + * A successful response from the server for a {@link ListResourceTemplatesRequest | resources/templates/list} request. + * + * @example List resource templates result response + * {@includeCode ./examples/ListResourceTemplatesResultResponse/list-resource-templates-result-response.json} + * + * @category `resources/templates/list` + */ + export interface ListResourceTemplatesResultResponse extends JSONRPCResultResponse { + result: ListResourceTemplatesResult; + } + + /** + * Common params for resource-related requests. * * @internal */ @@ -730,6 +1021,9 @@ export namespace MCP { /** * Sent from the client to the server, to read a specific resource URI. * + * @example Read resource request + * {@includeCode ./examples/ReadResourceRequest/read-resource-request.json} + * * @category `resources/read` */ export interface ReadResourceRequest extends JSONRPCRequest { @@ -738,7 +1032,10 @@ export namespace MCP { } /** - * The server's response to a resources/read request from the client. + * The result returned by the server for a {@link ReadResourceRequest | resources/read} request. + * + * @example File resource contents + * {@includeCode ./examples/ReadResourceResult/file-resource-contents.json} * * @category `resources/read` */ @@ -746,9 +1043,24 @@ export namespace MCP { contents: (TextResourceContents | BlobResourceContents)[]; } + /** + * A successful response from the server for a {@link ReadResourceRequest | resources/read} request. + * + * @example Read resource result response + * {@includeCode ./examples/ReadResourceResultResponse/read-resource-result-response.json} + * + * @category `resources/read` + */ + export interface ReadResourceResultResponse extends JSONRPCResultResponse { + result: ReadResourceResult; + } + /** * An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This may be issued by servers without any previous subscription from the client. * + * @example Resources list changed + * {@includeCode ./examples/ResourceListChangedNotification/resources-list-changed.json} + * * @category `notifications/resources/list_changed` */ export interface ResourceListChangedNotification extends JSONRPCNotification { @@ -759,12 +1071,18 @@ export namespace MCP { /** * Parameters for a `resources/subscribe` request. * + * @example Subscribe to file resource + * {@includeCode ./examples/SubscribeRequestParams/subscribe-to-file-resource.json} + * * @category `resources/subscribe` */ export interface SubscribeRequestParams extends ResourceRequestParams { } /** - * Sent from the client to request resources/updated notifications from the server whenever a particular resource changes. + * Sent from the client to request {@link ResourceUpdatedNotification | resources/updated} notifications from the server whenever a particular resource changes. + * + * @example Subscribe request + * {@includeCode ./examples/SubscribeRequest/subscribe-request.json} * * @category `resources/subscribe` */ @@ -773,6 +1091,18 @@ export namespace MCP { params: SubscribeRequestParams; } + /** + * A successful response from the server for a {@link SubscribeRequest | resources/subscribe} request. + * + * @example Subscribe result response + * {@includeCode ./examples/SubscribeResultResponse/subscribe-result-response.json} + * + * @category `resources/subscribe` + */ + export interface SubscribeResultResponse extends JSONRPCResultResponse { + result: EmptyResult; + } + /** * Parameters for a `resources/unsubscribe` request. * @@ -781,7 +1111,10 @@ export namespace MCP { export interface UnsubscribeRequestParams extends ResourceRequestParams { } /** - * Sent from the client to request cancellation of resources/updated notifications from the server. This should follow a previous resources/subscribe request. + * Sent from the client to request cancellation of {@link ResourceUpdatedNotification | resources/updated} notifications from the server. This should follow a previous {@link SubscribeRequest | resources/subscribe} request. + * + * @example Unsubscribe request + * {@includeCode ./examples/UnsubscribeRequest/unsubscribe-request.json} * * @category `resources/unsubscribe` */ @@ -790,9 +1123,24 @@ export namespace MCP { params: UnsubscribeRequestParams; } + /** + * A successful response from the server for a {@link UnsubscribeRequest | resources/unsubscribe} request. + * + * @example Unsubscribe result response + * {@includeCode ./examples/UnsubscribeResultResponse/unsubscribe-result-response.json} + * + * @category `resources/unsubscribe` + */ + export interface UnsubscribeResultResponse extends JSONRPCResultResponse { + result: EmptyResult; + } + /** * Parameters for a `notifications/resources/updated` notification. * + * @example File resource updated + * {@includeCode ./examples/ResourceUpdatedNotificationParams/file-resource-updated.json} + * * @category `notifications/resources/updated` */ export interface ResourceUpdatedNotificationParams extends NotificationParams { @@ -805,7 +1153,10 @@ export namespace MCP { } /** - * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This should only be sent if the client previously sent a resources/subscribe request. + * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This should only be sent if the client previously sent a {@link SubscribeRequest | resources/subscribe} request. + * + * @example File resource updated notification + * {@includeCode ./examples/ResourceUpdatedNotification/file-resource-updated-notification.json} * * @category `notifications/resources/updated` */ @@ -817,6 +1168,9 @@ export namespace MCP { /** * A known resource that the server is capable of reading. * + * @example File resource with annotations + * {@includeCode ./examples/Resource/file-resource-with-annotations.json} + * * @category `resources/list` */ export interface Resource extends BaseMetadata, Icons { @@ -851,10 +1205,7 @@ export namespace MCP { */ size?: number; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** @@ -887,10 +1238,7 @@ export namespace MCP { */ annotations?: Annotations; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** @@ -910,13 +1258,13 @@ export namespace MCP { */ mimeType?: string; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** + * @example Text file contents + * {@includeCode ./examples/TextResourceContents/text-file-contents.json} + * * @category Content */ export interface TextResourceContents extends ResourceContents { @@ -927,6 +1275,9 @@ export namespace MCP { } /** + * @example Image file contents + * {@includeCode ./examples/BlobResourceContents/image-file-contents.json} + * * @category Content */ export interface BlobResourceContents extends ResourceContents { @@ -942,6 +1293,9 @@ export namespace MCP { /** * Sent from the client to request a list of prompts and prompt templates the server has. * + * @example List prompts request + * {@includeCode ./examples/ListPromptsRequest/list-prompts-request.json} + * * @category `prompts/list` */ export interface ListPromptsRequest extends PaginatedRequest { @@ -949,7 +1303,10 @@ export namespace MCP { } /** - * The server's response to a prompts/list request from the client. + * The result returned by the server for a {@link ListPromptsRequest | prompts/list} request. + * + * @example Prompts list with cursor + * {@includeCode ./examples/ListPromptsResult/prompts-list-with-cursor.json} * * @category `prompts/list` */ @@ -957,9 +1314,24 @@ export namespace MCP { prompts: Prompt[]; } + /** + * A successful response from the server for a {@link ListPromptsRequest | prompts/list} request. + * + * @example List prompts result response + * {@includeCode ./examples/ListPromptsResultResponse/list-prompts-result-response.json} + * + * @category `prompts/list` + */ + export interface ListPromptsResultResponse extends JSONRPCResultResponse { + result: ListPromptsResult; + } + /** * Parameters for a `prompts/get` request. * + * @example Get code review prompt + * {@includeCode ./examples/GetPromptRequestParams/get-code-review-prompt.json} + * * @category `prompts/get` */ export interface GetPromptRequestParams extends RequestParams { @@ -976,6 +1348,9 @@ export namespace MCP { /** * Used by the client to get a prompt provided by the server. * + * @example Get prompt request + * {@includeCode ./examples/GetPromptRequest/get-prompt-request.json} + * * @category `prompts/get` */ export interface GetPromptRequest extends JSONRPCRequest { @@ -984,7 +1359,10 @@ export namespace MCP { } /** - * The server's response to a prompts/get request from the client. + * The result returned by the server for a {@link GetPromptRequest | prompts/get} request. + * + * @example Code review prompt + * {@includeCode ./examples/GetPromptResult/code-review-prompt.json} * * @category `prompts/get` */ @@ -996,6 +1374,18 @@ export namespace MCP { messages: PromptMessage[]; } + /** + * A successful response from the server for a {@link GetPromptRequest | prompts/get} request. + * + * @example Get prompt result response + * {@includeCode ./examples/GetPromptResultResponse/get-prompt-result-response.json} + * + * @category `prompts/get` + */ + export interface GetPromptResultResponse extends JSONRPCResultResponse { + result: GetPromptResult; + } + /** * A prompt or prompt template that the server offers. * @@ -1012,10 +1402,7 @@ export namespace MCP { */ arguments?: PromptArgument[]; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** @@ -1044,7 +1431,7 @@ export namespace MCP { /** * Describes a message returned as part of a prompt. * - * This is similar to `SamplingMessage`, but also supports the embedding of + * This is similar to {@link SamplingMessage}, but also supports the embedding of * resources from the MCP server. * * @category `prompts/get` @@ -1057,7 +1444,10 @@ export namespace MCP { /** * A resource that the server is capable of reading, included in a prompt or tool call result. * - * Note: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests. + * Note: resource links returned by tools are not guaranteed to appear in the results of {@link ListResourcesRequest | resources/list} requests. + * + * @example File resource link + * {@includeCode ./examples/ResourceLink/file-resource-link.json} * * @category Content */ @@ -1071,6 +1461,9 @@ export namespace MCP { * It is up to the client how best to render embedded resources for the benefit * of the LLM and/or the user. * + * @example Embedded file resource with annotations + * {@includeCode ./examples/EmbeddedResource/embedded-file-resource-with-annotations.json} + * * @category Content */ export interface EmbeddedResource { @@ -1082,14 +1475,14 @@ export namespace MCP { */ annotations?: Annotations; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This may be issued by servers without any previous subscription from the client. * + * @example Prompts list changed + * {@includeCode ./examples/PromptListChangedNotification/prompts-list-changed.json} + * * @category `notifications/prompts/list_changed` */ export interface PromptListChangedNotification extends JSONRPCNotification { @@ -1101,6 +1494,9 @@ export namespace MCP { /** * Sent from the client to request a list of tools the server has. * + * @example List tools request + * {@includeCode ./examples/ListToolsRequest/list-tools-request.json} + * * @category `tools/list` */ export interface ListToolsRequest extends PaginatedRequest { @@ -1108,7 +1504,10 @@ export namespace MCP { } /** - * The server's response to a tools/list request from the client. + * The result returned by the server for a {@link ListToolsRequest | tools/list} request. + * + * @example Tools list with cursor + * {@includeCode ./examples/ListToolsResult/tools-list-with-cursor.json} * * @category `tools/list` */ @@ -1117,7 +1516,28 @@ export namespace MCP { } /** - * The server's response to a tool call. + * A successful response from the server for a {@link ListToolsRequest | tools/list} request. + * + * @example List tools result response + * {@includeCode ./examples/ListToolsResultResponse/list-tools-result-response.json} + * + * @category `tools/list` + */ + export interface ListToolsResultResponse extends JSONRPCResultResponse { + result: ListToolsResult; + } + + /** + * The result returned by the server for a {@link CallToolRequest | tools/call} request. + * + * @example Result with unstructured text + * {@includeCode ./examples/CallToolResult/result-with-unstructured-text.json} + * + * @example Result with structured content + * {@includeCode ./examples/CallToolResult/result-with-structured-content.json} + * + * @example Invalid tool input error + * {@includeCode ./examples/CallToolResult/invalid-tool-input-error.json} * * @category `tools/call` */ @@ -1149,9 +1569,27 @@ export namespace MCP { isError?: boolean; } + /** + * A successful response from the server for a {@link CallToolRequest | tools/call} request. + * + * @example Call tool result response + * {@includeCode ./examples/CallToolResultResponse/call-tool-result-response.json} + * + * @category `tools/call` + */ + export interface CallToolResultResponse extends JSONRPCResultResponse { + result: CallToolResult; + } + /** * Parameters for a `tools/call` request. * + * @example `get_weather` tool call params + * {@includeCode ./examples/CallToolRequestParams/get-weather-tool-call-params.json} + * + * @example Tool call params with progress token + * {@includeCode ./examples/CallToolRequestParams/tool-call-params-with-progress-token.json} + * * @category `tools/call` */ export interface CallToolRequestParams extends TaskAugmentedRequestParams { @@ -1168,6 +1606,9 @@ export namespace MCP { /** * Used by the client to invoke a tool provided by the server. * + * @example Call tool request + * {@includeCode ./examples/CallToolRequest/call-tool-request.json} + * * @category `tools/call` */ export interface CallToolRequest extends JSONRPCRequest { @@ -1178,6 +1619,9 @@ export namespace MCP { /** * An optional notification from the server to the client, informing it that the list of tools it offers has changed. This may be issued by servers without any previous subscription from the client. * + * @example Tools list changed + * {@includeCode ./examples/ToolListChangedNotification/tools-list-changed.json} + * * @category `notifications/tools/list_changed` */ export interface ToolListChangedNotification extends JSONRPCNotification { @@ -1186,13 +1630,13 @@ export namespace MCP { } /** - * Additional properties describing a Tool to clients. + * Additional properties describing a {@link Tool} to clients. * - * NOTE: all properties in ToolAnnotations are **hints**. + * NOTE: all properties in `ToolAnnotations` are **hints**. * They are not guaranteed to provide a faithful description of * tool behavior (including descriptive properties like `title`). * - * Clients should never make tool use decisions based on ToolAnnotations + * Clients should never make tool use decisions based on `ToolAnnotations` * received from untrusted servers. * * @category `tools/list` @@ -1252,11 +1696,11 @@ export namespace MCP { * This allows clients to handle long-running operations through polling * the task system. * - * - "forbidden": Tool does not support task-augmented execution (default when absent) - * - "optional": Tool may support task-augmented execution - * - "required": Tool requires task-augmented execution + * - `"forbidden"`: Tool does not support task-augmented execution (default when absent) + * - `"optional"`: Tool may support task-augmented execution + * - `"required"`: Tool requires task-augmented execution * - * Default: "forbidden" + * Default: `"forbidden"` */ taskSupport?: "forbidden" | "optional" | "required"; } @@ -1264,6 +1708,18 @@ export namespace MCP { /** * Definition for a tool the client can call. * + * @example With default 2020-12 input schema + * {@includeCode ./examples/Tool/with-default-2020-12-input-schema.json} + * + * @example With explicit draft-07 input schema + * {@includeCode ./examples/Tool/with-explicit-draft-07-input-schema.json} + * + * @example With no parameters + * {@includeCode ./examples/Tool/with-no-parameters.json} + * + * @example With output schema for structured content + * {@includeCode ./examples/Tool/with-output-schema-for-structured-content.json} + * * @category `tools/list` */ export interface Tool extends BaseMetadata, Icons { @@ -1291,10 +1747,10 @@ export namespace MCP { /** * An optional JSON Schema object defining the structure of the tool's output returned in - * the structuredContent field of a CallToolResult. + * the structuredContent field of a {@link CallToolResult}. * - * Defaults to JSON Schema 2020-12 when no explicit $schema is provided. - * Currently restricted to type: "object" at the root level. + * Defaults to JSON Schema 2020-12 when no explicit `$schema` is provided. + * Currently restricted to `type: "object"` at the root level. */ outputSchema?: { $schema?: string; @@ -1306,14 +1762,11 @@ export namespace MCP { /** * Optional additional tool information. * - * Display name precedence order is: title, annotations.title, then name. + * Display name precedence order is: `title`, `annotations.title`, then `name`. */ annotations?: ToolAnnotations; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /* Tasks */ @@ -1386,6 +1839,11 @@ export namespace MCP { */ createdAt: string; + /** + * ISO 8601 timestamp when the task was last updated. + */ + lastUpdatedAt: string; + /** * Actual retention duration from creation in milliseconds, null for unlimited. */ @@ -1398,7 +1856,7 @@ export namespace MCP { } /** - * A response to a task-augmented request. + * The result returned for a task-augmented request. * * @category `tasks` */ @@ -1406,6 +1864,15 @@ export namespace MCP { task: Task; } + /** + * A successful response for a task-augmented request. + * + * @category `tasks` + */ + export interface CreateTaskResultResponse extends JSONRPCResultResponse { + result: CreateTaskResult; + } + /** * A request to retrieve the state of a task. * @@ -1422,12 +1889,21 @@ export namespace MCP { } /** - * The response to a tasks/get request. + * The result returned for a {@link GetTaskRequest | tasks/get} request. * * @category `tasks/get` */ export type GetTaskResult = Result & Task; + /** + * A successful response for a {@link GetTaskRequest | tasks/get} request. + * + * @category `tasks/get` + */ + export interface GetTaskResultResponse extends JSONRPCResultResponse { + result: GetTaskResult; + } + /** * A request to retrieve the result of a completed task. * @@ -1444,9 +1920,9 @@ export namespace MCP { } /** - * The response to a tasks/result request. + * The result returned for a {@link GetTaskPayloadRequest | tasks/result} request. * The structure matches the result type of the original request. - * For example, a tools/call task would return the CallToolResult structure. + * For example, a {@link CallToolRequest | tools/call} task would return the {@link CallToolResult} structure. * * @category `tasks/result` */ @@ -1454,6 +1930,15 @@ export namespace MCP { [key: string]: unknown; } + /** + * A successful response for a {@link GetTaskPayloadRequest | tasks/result} request. + * + * @category `tasks/result` + */ + export interface GetTaskPayloadResultResponse extends JSONRPCResultResponse { + result: GetTaskPayloadResult; + } + /** * A request to cancel a task. * @@ -1470,12 +1955,21 @@ export namespace MCP { } /** - * The response to a tasks/cancel request. + * The result returned for a {@link CancelTaskRequest | tasks/cancel} request. * * @category `tasks/cancel` */ export type CancelTaskResult = Result & Task; + /** + * A successful response for a {@link CancelTaskRequest | tasks/cancel} request. + * + * @category `tasks/cancel` + */ + export interface CancelTaskResultResponse extends JSONRPCResultResponse { + result: CancelTaskResult; + } + /** * A request to retrieve a list of tasks. * @@ -1486,7 +1980,7 @@ export namespace MCP { } /** - * The response to a tasks/list request. + * The result returned for a {@link ListTasksRequest | tasks/list} request. * * @category `tasks/list` */ @@ -1494,6 +1988,15 @@ export namespace MCP { tasks: Task[]; } + /** + * A successful response for a {@link ListTasksRequest | tasks/list} request. + * + * @category `tasks/list` + */ + export interface ListTasksResultResponse extends JSONRPCResultResponse { + result: ListTasksResult; + } + /** * Parameters for a `notifications/tasks/status` notification. * @@ -1516,11 +2019,14 @@ export namespace MCP { /** * Parameters for a `logging/setLevel` request. * + * @example Set log level to "info" + * {@includeCode ./examples/SetLevelRequestParams/set-log-level-to-info.json} + * * @category `logging/setLevel` */ export interface SetLevelRequestParams extends RequestParams { /** - * The level of logging that the client wants to receive from the server. The server should send all logs at this level and higher (i.e., more severe) to the client as notifications/message. + * The level of logging that the client wants to receive from the server. The server should send all logs at this level and higher (i.e., more severe) to the client as {@link LoggingMessageNotification | notifications/message}. */ level: LoggingLevel; } @@ -1528,6 +2034,9 @@ export namespace MCP { /** * A request from the client to the server, to enable or adjust logging. * + * @example Set logging level request + * {@includeCode ./examples/SetLevelRequest/set-logging-level-request.json} + * * @category `logging/setLevel` */ export interface SetLevelRequest extends JSONRPCRequest { @@ -1535,9 +2044,24 @@ export namespace MCP { params: SetLevelRequestParams; } + /** + * A successful response from the server for a {@link SetLevelRequest | logging/setLevel} request. + * + * @example Set logging level result response + * {@includeCode ./examples/SetLevelResultResponse/set-logging-level-result-response.json} + * + * @category `logging/setLevel` + */ + export interface SetLevelResultResponse extends JSONRPCResultResponse { + result: EmptyResult; + } + /** * Parameters for a `notifications/message` notification. * + * @example Log database connection failed + * {@includeCode ./examples/LoggingMessageNotificationParams/log-database-connection-failed.json} + * * @category `notifications/message` */ export interface LoggingMessageNotificationParams extends NotificationParams { @@ -1556,7 +2080,10 @@ export namespace MCP { } /** - * JSONRPCNotification of a log message passed from server to client. If no logging/setLevel request has been sent from the client, the server MAY decide which messages to send automatically. + * JSONRPCNotification of a log message passed from server to client. If no `logging/setLevel` request has been sent from the client, the server MAY decide which messages to send automatically. + * + * @example Log database connection failed + * {@includeCode ./examples/LoggingMessageNotification/log-database-connection-failed.json} * * @category `notifications/message` */ @@ -1587,6 +2114,15 @@ export namespace MCP { /** * Parameters for a `sampling/createMessage` request. * + * @example Basic request + * {@includeCode ./examples/CreateMessageRequestParams/basic-request.json} + * + * @example Request with tools + * {@includeCode ./examples/CreateMessageRequestParams/request-with-tools.json} + * + * @example Follow-up request with tool results + * {@includeCode ./examples/CreateMessageRequestParams/follow-up-with-tool-results.json} + * * @category `sampling/createMessage` */ export interface CreateMessageRequestParams extends TaskAugmentedRequestParams { @@ -1603,8 +2139,8 @@ export namespace MCP { * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. * The client MAY ignore this request. * - * Default is "none". Values "thisServer" and "allServers" are soft-deprecated. Servers SHOULD only use these values if the client - * declares ClientCapabilities.sampling.context. These values may be removed in future spec releases. + * Default is `"none"`. Values `"thisServer"` and `"allServers"` are soft-deprecated. Servers SHOULD only use these values if the client + * declares {@link ClientCapabilities.sampling.context}. These values may be removed in future spec releases. */ includeContext?: "none" | "thisServer" | "allServers"; /** @@ -1624,12 +2160,12 @@ export namespace MCP { metadata?: object; /** * Tools that the model may use during generation. - * The client MUST return an error if this field is provided but ClientCapabilities.sampling.tools is not declared. + * The client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared. */ tools?: Tool[]; /** * Controls how the model uses tools. - * The client MUST return an error if this field is provided but ClientCapabilities.sampling.tools is not declared. + * The client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared. * Default is `{ mode: "auto" }`. */ toolChoice?: ToolChoice; @@ -1643,9 +2179,9 @@ export namespace MCP { export interface ToolChoice { /** * Controls the tool use ability of the model: - * - "auto": Model decides whether to use tools (default) - * - "required": Model MUST use at least one tool before completing - * - "none": Model MUST NOT use any tools + * - `"auto"`: Model decides whether to use tools (default) + * - `"required"`: Model MUST use at least one tool before completing + * - `"none"`: Model MUST NOT use any tools */ mode?: "auto" | "required" | "none"; } @@ -1653,6 +2189,9 @@ export namespace MCP { /** * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. * + * @example Sampling request + * {@includeCode ./examples/CreateMessageRequest/sampling-request.json} + * * @category `sampling/createMessage` */ export interface CreateMessageRequest extends JSONRPCRequest { @@ -1661,10 +2200,19 @@ export namespace MCP { } /** - * The client's response to a sampling/createMessage request from the server. + * The result returned by the client for a {@link CreateMessageRequest | sampling/createMessage} request. * The client should inform the user before returning the sampled message, to allow them * to inspect the response (human in the loop) and decide whether to allow the server to see it. * + * @example Text response + * {@includeCode ./examples/CreateMessageResult/text-response.json} + * + * @example Tool use response + * {@includeCode ./examples/CreateMessageResult/tool-use-response.json} + * + * @example Final response after tool use + * {@includeCode ./examples/CreateMessageResult/final-response.json} + * * @category `sampling/createMessage` */ export interface CreateMessageResult extends Result, SamplingMessage { @@ -1677,29 +2225,48 @@ export namespace MCP { * The reason why sampling stopped, if known. * * Standard values: - * - "endTurn": Natural end of the assistant's turn - * - "stopSequence": A stop sequence was encountered - * - "maxTokens": Maximum token limit was reached - * - "toolUse": The model wants to use one or more tools + * - `"endTurn"`: Natural end of the assistant's turn + * - `"stopSequence"`: A stop sequence was encountered + * - `"maxTokens"`: Maximum token limit was reached + * - `"toolUse"`: The model wants to use one or more tools * * This field is an open string to allow for provider-specific stop reasons. */ stopReason?: "endTurn" | "stopSequence" | "maxTokens" | "toolUse" | string; } + /** + * A successful response from the client for a {@link CreateMessageRequest | sampling/createMessage} request. + * + * @example Sampling result response + * {@includeCode ./examples/CreateMessageResultResponse/sampling-result-response.json} + * + * @category `sampling/createMessage` + */ + export interface CreateMessageResultResponse extends JSONRPCResultResponse { + result: CreateMessageResult; + } + /** * Describes a message issued to or received from an LLM API. * + * @example Single content block + * {@includeCode ./examples/SamplingMessage/single-content-block.json} + * + * @example Multiple content blocks + * {@includeCode ./examples/SamplingMessage/multiple-content-blocks.json} + * * @category `sampling/createMessage` */ export interface SamplingMessage { role: Role; content: SamplingMessageContentBlock | SamplingMessageContentBlock[]; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } + + /** + * @category `sampling/createMessage` + */ export type SamplingMessageContentBlock = | TextContent | ImageContent @@ -1757,6 +2324,9 @@ export namespace MCP { /** * Text provided to or from an LLM. * + * @example Text content + * {@includeCode ./examples/TextContent/text-content.json} + * * @category Content */ export interface TextContent { @@ -1772,15 +2342,15 @@ export namespace MCP { */ annotations?: Annotations; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** * An image provided to or from an LLM. * + * @example `image/png` content with annotations + * {@includeCode ./examples/ImageContent/image-png-content-with-annotations.json} + * * @category Content */ export interface ImageContent { @@ -1803,15 +2373,15 @@ export namespace MCP { */ annotations?: Annotations; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** * Audio provided to or from an LLM. * + * @example `audio/wav` content + * {@includeCode ./examples/AudioContent/audio-wav-content.json} + * * @category Content */ export interface AudioContent { @@ -1834,15 +2404,15 @@ export namespace MCP { */ annotations?: Annotations; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** * A request from the assistant to call a tool. * + * @example `get_weather` tool use + * {@includeCode ./examples/ToolUseContent/get-weather-tool-use.json} + * * @category `sampling/createMessage` */ export interface ToolUseContent { @@ -1868,15 +2438,16 @@ export namespace MCP { /** * Optional metadata about the tool use. Clients SHOULD preserve this field when * including tool uses in subsequent sampling requests to enable caching optimizations. - * - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** * The result of a tool use, provided by the user back to the assistant. * + * @example `get_weather` tool result + * {@includeCode ./examples/ToolResultContent/get-weather-tool-result.json} + * * @category `sampling/createMessage` */ export interface ToolResultContent { @@ -1885,14 +2456,14 @@ export namespace MCP { /** * The ID of the tool use this result corresponds to. * - * This MUST match the ID from a previous ToolUseContent. + * This MUST match the ID from a previous {@link ToolUseContent}. */ toolUseId: string; /** * The unstructured result content of the tool use. * - * This has the same format as CallToolResult.content and can include text, images, + * This has the same format as {@link CallToolResult.content} and can include text, images, * audio, resource links, and embedded resources. */ content: ContentBlock[]; @@ -1900,7 +2471,7 @@ export namespace MCP { /** * An optional structured result object. * - * If the tool defined an outputSchema, this SHOULD conform to that schema. + * If the tool defined an {@link Tool.outputSchema}, this SHOULD conform to that schema. */ structuredContent?: { [key: string]: unknown }; @@ -1915,10 +2486,8 @@ export namespace MCP { /** * Optional metadata about the tool result. Clients SHOULD preserve this field when * including tool results in subsequent sampling requests to enable caching optimizations. - * - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** @@ -1934,6 +2503,9 @@ export namespace MCP { * up to the client to decide how to interpret these preferences and how to * balance them against other considerations. * + * @example With hints and priorities + * {@includeCode ./examples/ModelPreferences/with-hints-and-priorities.json} + * * @category `sampling/createMessage` */ export interface ModelPreferences { @@ -2010,6 +2582,12 @@ export namespace MCP { * Parameters for a `completion/complete` request. * * @category `completion/complete` + * + * @example Prompt argument completion + * {@includeCode ./examples/CompleteRequestParams/prompt-argument-completion.json} + * + * @example Prompt argument completion with context + * {@includeCode ./examples/CompleteRequestParams/prompt-argument-completion-with-context.json} */ export interface CompleteRequestParams extends RequestParams { ref: PromptReference | ResourceTemplateReference; @@ -2041,6 +2619,9 @@ export namespace MCP { /** * A request from the client to the server, to ask for completion options. * + * @example Completion request + * {@includeCode ./examples/CompleteRequest/completion-request.json} + * * @category `completion/complete` */ export interface CompleteRequest extends JSONRPCRequest { @@ -2049,9 +2630,15 @@ export namespace MCP { } /** - * The server's response to a completion/complete request + * The result returned by the server for a {@link CompleteRequest | completion/complete} request. * * @category `completion/complete` + * + * @example Single completion value + * {@includeCode ./examples/CompleteResult/single-completion-value.json} + * + * @example Multiple completion values with more available + * {@includeCode ./examples/CompleteResult/multiple-completion-values-with-more-available.json} */ export interface CompleteResult extends Result { completion: { @@ -2070,6 +2657,18 @@ export namespace MCP { }; } + /** + * A successful response from the server for a {@link CompleteRequest | completion/complete} request. + * + * @example Completion result response + * {@includeCode ./examples/CompleteResultResponse/completion-result-response.json} + * + * @category `completion/complete` + */ + export interface CompleteResultResponse extends JSONRPCResultResponse { + result: CompleteResult; + } + /** * A reference to a resource or resource template definition. * @@ -2104,6 +2703,9 @@ export namespace MCP { * This request is typically used when the server needs to understand the file system * structure or access specific locations that the client has permission to read from. * + * @example List roots request + * {@includeCode ./examples/ListRootsRequest/list-roots-request.json} + * * @category `roots/list` */ export interface ListRootsRequest extends JSONRPCRequest { @@ -2112,24 +2714,45 @@ export namespace MCP { } /** - * The client's response to a roots/list request from the server. - * This result contains an array of Root objects, each representing a root directory + * The result returned by the client for a {@link ListRootsRequest | roots/list} request. + * This result contains an array of {@link Root} objects, each representing a root directory * or file that the server can operate on. * + * @example Single root directory + * {@includeCode ./examples/ListRootsResult/single-root-directory.json} + * + * @example Multiple root directories + * {@includeCode ./examples/ListRootsResult/multiple-root-directories.json} + * * @category `roots/list` */ export interface ListRootsResult extends Result { roots: Root[]; } + /** + * A successful response from the client for a {@link ListRootsRequest | roots/list} request. + * + * @example List roots result response + * {@includeCode ./examples/ListRootsResultResponse/list-roots-result-response.json} + * + * @category `roots/list` + */ + export interface ListRootsResultResponse extends JSONRPCResultResponse { + result: ListRootsResult; + } + /** * Represents a root directory or file that the server can operate on. * + * @example Project directory root + * {@includeCode ./examples/Root/project-directory.json} + * * @category `roots/list` */ export interface Root { /** - * The URI identifying the root. This *must* start with file:// for now. + * The URI identifying the root. This *must* start with `file://` for now. * This restriction may be relaxed in future versions of the protocol to allow * other URI schemes. * @@ -2143,16 +2766,16 @@ export namespace MCP { */ name?: string; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** * A notification from the client to the server, informing it that the list of roots has changed. * This notification should be sent whenever the client adds, removes, or modifies any root. - * The server should then request an updated list of roots using the ListRootsRequest. + * The server should then request an updated list of roots using the {@link ListRootsRequest}. + * + * @example Roots list changed + * {@includeCode ./examples/RootsListChangedNotification/roots-list-changed.json} * * @category `notifications/roots/list_changed` */ @@ -2164,6 +2787,12 @@ export namespace MCP { /** * The parameters for a request to elicit non-sensitive information from the user via a form in the client. * + * @example Elicit single field + * {@includeCode ./examples/ElicitRequestFormParams/elicit-single-field.json} + * + * @example Elicit multiple fields + * {@includeCode ./examples/ElicitRequestFormParams/elicit-multiple-fields.json} + * * @category `elicitation/create` */ export interface ElicitRequestFormParams extends TaskAugmentedRequestParams { @@ -2194,6 +2823,9 @@ export namespace MCP { /** * The parameters for a request to elicit information from the user via a URL in the client. * + * @example Elicit sensitive data + * {@includeCode ./examples/ElicitRequestURLParams/elicit-sensitive-data.json} + * * @category `elicitation/create` */ export interface ElicitRequestURLParams extends TaskAugmentedRequestParams { @@ -2233,6 +2865,9 @@ export namespace MCP { /** * A request from the server to elicit additional information from the user via the client. * + * @example Elicitation request + * {@includeCode ./examples/ElicitRequest/elicitation-request.json} + * * @category `elicitation/create` */ export interface ElicitRequest extends JSONRPCRequest { @@ -2253,6 +2888,9 @@ export namespace MCP { | EnumSchema; /** + * @example Email input schema + * {@includeCode ./examples/StringSchema/email-input-schema.json} + * * @category `elicitation/create` */ export interface StringSchema { @@ -2266,6 +2904,9 @@ export namespace MCP { } /** + * @example Number input schema + * {@includeCode ./examples/NumberSchema/number-input-schema.json} + * * @category `elicitation/create` */ export interface NumberSchema { @@ -2278,6 +2919,9 @@ export namespace MCP { } /** + * @example Boolean input schema + * {@includeCode ./examples/BooleanSchema/boolean-input-schema.json} + * * @category `elicitation/create` */ export interface BooleanSchema { @@ -2290,6 +2934,9 @@ export namespace MCP { /** * Schema for single-selection enumeration without display titles for options. * + * @example Color select schema + * {@includeCode ./examples/UntitledSingleSelectEnumSchema/color-select-schema.json} + * * @category `elicitation/create` */ export interface UntitledSingleSelectEnumSchema { @@ -2315,6 +2962,9 @@ export namespace MCP { /** * Schema for single-selection enumeration with display titles for each option. * + * @example Titled color select schema + * {@includeCode ./examples/TitledSingleSelectEnumSchema/titled-color-select-schema.json} + * * @category `elicitation/create` */ export interface TitledSingleSelectEnumSchema { @@ -2357,6 +3007,9 @@ export namespace MCP { /** * Schema for multiple-selection enumeration without display titles for options. * + * @example Color multi-select schema + * {@includeCode ./examples/UntitledMultiSelectEnumSchema/color-multi-select-schema.json} + * * @category `elicitation/create` */ export interface UntitledMultiSelectEnumSchema { @@ -2396,6 +3049,9 @@ export namespace MCP { /** * Schema for multiple-selection enumeration with display titles for each option. * + * @example Titled color multi-select schema + * {@includeCode ./examples/TitledMultiSelectEnumSchema/titled-color-multi-select-schema.json} + * * @category `elicitation/create` */ export interface TitledMultiSelectEnumSchema { @@ -2449,7 +3105,7 @@ export namespace MCP { | TitledMultiSelectEnumSchema; /** - * Use TitledSingleSelectEnumSchema instead. + * Use {@link TitledSingleSelectEnumSchema} instead. * This interface will be removed in a future version. * * @category `elicitation/create` @@ -2477,30 +3133,54 @@ export namespace MCP { | LegacyTitledEnumSchema; /** - * The client's response to an elicitation request. + * The result returned by the client for an {@link ElicitRequest | elicitation/create} request. + * + * @example Input single field + * {@includeCode ./examples/ElicitResult/input-single-field.json} + * + * @example Input multiple fields + * {@includeCode ./examples/ElicitResult/input-multiple-fields.json} + * + * @example Accept URL mode (no content) + * {@includeCode ./examples/ElicitResult/accept-url-mode-no-content.json} * * @category `elicitation/create` */ export interface ElicitResult extends Result { /** * The user action in response to the elicitation. - * - "accept": User submitted the form/confirmed the action - * - "decline": User explicitly decline the action - * - "cancel": User dismissed without making an explicit choice + * - `"accept"`: User submitted the form/confirmed the action + * - `"decline"`: User explicitly declined the action + * - `"cancel"`: User dismissed without making an explicit choice */ action: "accept" | "decline" | "cancel"; /** - * The submitted form data, only present when action is "accept" and mode was "form". + * The submitted form data, only present when action is `"accept"` and mode was `"form"`. * Contains values matching the requested schema. * Omitted for out-of-band mode responses. */ content?: { [key: string]: string | number | boolean | string[] }; } + /** + * A successful response from the client for a {@link ElicitRequest | elicitation/create} request. + * + * @example Elicitation result response + * {@includeCode ./examples/ElicitResultResponse/elicitation-result-response.json} + * + * @category `elicitation/create` + */ + export interface ElicitResultResponse extends JSONRPCResultResponse { + result: ElicitResult; + } + /** * An optional notification from the server to the client, informing it of a completion of a out-of-band elicitation request. * + * @example Elicitation complete + * {@includeCode ./examples/ElicitationCompleteNotification/elicitation-complete.json} + * * @category `notifications/elicitation/complete` */ export interface ElicitationCompleteNotification extends JSONRPCNotification { @@ -2588,6 +3268,7 @@ export namespace MCP { | ListResourcesResult | ReadResourceResult | CallToolResult + | CreateTaskResult | ListToolsResult | GetTaskResult | GetTaskPayloadResult diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts index 3f268d17355..b051bac13ef 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts @@ -220,7 +220,7 @@ suite('Workbench - MCP - ServerRequestHandler', () => { const sentMessages = transport.getSentMessages(); const pingResponse = sentMessages.find(m => 'id' in m && m.id === pingRequest.id && 'result' in m - ) as MCP.JSONRPCResponse; + ) as MCP.JSONRPCResultResponse; assert.ok(pingResponse, 'No ping response was sent'); assert.deepStrictEqual(pingResponse.result, {}); @@ -246,7 +246,7 @@ suite('Workbench - MCP - ServerRequestHandler', () => { const sentMessages = transport.getSentMessages(); const rootsResponse = sentMessages.find(m => 'id' in m && m.id === rootsRequest.id && 'result' in m - ) as MCP.JSONRPCResponse; + ) as MCP.JSONRPCResultResponse; assert.ok(rootsResponse, 'No roots/list response was sent'); assert.strictEqual((rootsResponse.result as MCP.ListRootsResult).roots.length, 2); @@ -400,6 +400,7 @@ suite.skip('Workbench - MCP - McpTask', () => { // TODO@connor4312 https://githu taskId: 'task1', status: 'working', createdAt: new Date().toISOString(), + lastUpdatedAt: new Date().toISOString(), ttl: null, ...overrides }; From 46308bc433df41308eb1254c1a55f137c0a3cb10 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 29 Jan 2026 17:51:13 +0100 Subject: [PATCH 118/152] Remove unnecessary log statement in JSON client (#291645) [json] remove unnecessary og statement --- extensions/json-language-features/client/src/jsonClient.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index 95d0a131b7c..eb336e8f89b 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -650,8 +650,6 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP async function getSchemaAssociations(forceRefresh: boolean): Promise { if (!schemaAssociationsCache || forceRefresh) { schemaAssociationsCache = computeSchemaAssociations(); - runtime.logOutputChannel.info(`Computed schema associations: ${(await schemaAssociationsCache).map(a => `${a.uri} -> [${a.fileMatch.join(', ')}]`).join('\n')}`); - } return schemaAssociationsCache; } From fe90dba7c18a2245acc5ba10a715861f8499f813 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 29 Jan 2026 17:55:27 +0100 Subject: [PATCH 119/152] improve new agent.md default empty tool selection (#291647) --- .../contrib/chat/browser/promptSyntax/newPromptFileActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts index 4aa9eea127e..3b1f279ef88 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts @@ -163,7 +163,7 @@ function getDefaultContentSnippet(promptType: PromptsType, name: string | undefi `name: ${name ?? '${1:agent-name}'}`, `description: \${2:Describe what this custom agent does and when to use it.}`, `argument-hint: \${3:The inputs this agent expects, e.g., "a task to implement" or "a question to answer".}`, - `# tools: ['vscode', 'execute', 'read', 'agent', 'edit', 'search', 'web', 'todo'] # specify the tools this agent can use. if not set, all enabled tools are allowed`, + `# tools: ['vscode', 'execute', 'read', 'agent', 'edit', 'search', 'web', 'todo'] # specify the tools this agent can use. If not set, all enabled tools are allowed.`, `---`, `\${4:Define what this custom agent does, including its behavior, capabilities, and any specific instructions for its operation.}`, ].join('\n'); From fe1d4cfca0fd1580ee7c657d865727def71ac435 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 29 Jan 2026 18:24:06 +0100 Subject: [PATCH 120/152] chore: update known CSS variabled (#291663) --- build/lib/stylelint/vscode-known-variables.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 51dc85f5574..5dee2dcd04f 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -386,8 +386,8 @@ "--vscode-inlineChat-background", "--vscode-inlineChat-border", "--vscode-inlineChat-foreground", - "--vscode-inlineChat-regionHighlight", "--vscode-inlineChat-shadow", + "--vscode-inlineChat-regionHighlight", "--vscode-inlineChatDiff-inserted", "--vscode-inlineChatDiff-removed", "--vscode-inlineChatInput-background", From e0e634e326c64ad29edce120b10b5aab03ceacc9 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:25:35 +0100 Subject: [PATCH 121/152] Bump macOS test timeout to 90 minutes (#291664) The owners of this have asked us to try a longer timeout to help them determine if the issues we're seeing are perf degradation or a hang --- build/azure-pipelines/darwin/product-build-darwin-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines/darwin/product-build-darwin-ci.yml b/build/azure-pipelines/darwin/product-build-darwin-ci.yml index 3920c4ec799..45af3707590 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-ci.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-ci.yml @@ -7,7 +7,7 @@ parameters: jobs: - job: macOS${{ parameters.VSCODE_TEST_SUITE }} displayName: ${{ parameters.VSCODE_TEST_SUITE }} Tests - timeoutInMinutes: 30 + timeoutInMinutes: 90 variables: VSCODE_ARCH: arm64 templateContext: From 12ce890b0be963c88d84ff981d9ac942cbed7253 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 29 Jan 2026 17:26:06 +0000 Subject: [PATCH 122/152] Update @vscode/codicons to version 0.0.45-4 and add 'claude' icon to codiconsLibrary --- package-lock.json | 8 ++++---- package.json | 4 ++-- remote/web/package-lock.json | 8 ++++---- remote/web/package.json | 2 +- src/vs/base/common/codiconsLibrary.ts | 1 + 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7d4af81b621..b198a0d53d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.45-2", + "@vscode/codicons": "^0.0.45-4", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", @@ -2947,9 +2947,9 @@ ] }, "node_modules/@vscode/codicons": { - "version": "0.0.45-2", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-2.tgz", - "integrity": "sha512-Z5uZd8E2f84Jf4jv6ozSjIU/cnHn7F1REBGUtzdqJufWoLYauH/nwpVn8fWtvXNtR1QwEyh6x3WAeR2l5rnnyg==", + "version": "0.0.45-4", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-4.tgz", + "integrity": "sha512-uuWqpry+FcHAw1JDkXwEW0YIuTtX3n6KqSshNlvLUjuP92PSrfq99jW52AWJ7qeunmPvgKCaZOeSSLUqHRHjmw==", "license": "CC-BY-4.0" }, "node_modules/@vscode/deviceid": { diff --git a/package.json b/package.json index 7678b49e9b6..6c0299c23ec 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.45-2", + "@vscode/codicons": "^0.0.45-4", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", @@ -240,4 +240,4 @@ "optionalDependencies": { "windows-foreground-love": "0.6.1" } -} \ No newline at end of file +} diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index b2d86a2b17e..9ebe85bde28 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.45-2", + "@vscode/codicons": "^0.0.45-4", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", @@ -73,9 +73,9 @@ "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, "node_modules/@vscode/codicons": { - "version": "0.0.45-2", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-2.tgz", - "integrity": "sha512-Z5uZd8E2f84Jf4jv6ozSjIU/cnHn7F1REBGUtzdqJufWoLYauH/nwpVn8fWtvXNtR1QwEyh6x3WAeR2l5rnnyg==", + "version": "0.0.45-4", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-4.tgz", + "integrity": "sha512-uuWqpry+FcHAw1JDkXwEW0YIuTtX3n6KqSshNlvLUjuP92PSrfq99jW52AWJ7qeunmPvgKCaZOeSSLUqHRHjmw==", "license": "CC-BY-4.0" }, "node_modules/@vscode/iconv-lite-umd": { diff --git a/remote/web/package.json b/remote/web/package.json index 3c5f2e46943..f940ea309ad 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -5,7 +5,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.45-2", + "@vscode/codicons": "^0.0.45-4", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index dde42f3be9e..0ff9490a058 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -653,4 +653,5 @@ export const codiconsLibrary = { screenCut: register('screen-cut', 0xec7f), ask: register('ask', 0xec80), openai: register('openai', 0xec81), + claude: register('claude', 0xec82), } as const; From 57508a17696404774d9a97f3d3a8a861d710765d Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:47:47 -0800 Subject: [PATCH 123/152] add missing team members --- .vscode/notebooks/my-endgame.github-issues | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index e880ae2e23d..b37705c3c6c 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -12,7 +12,7 @@ { "kind": 2, "language": "github-issues", - "value": "$NOT_TEAM_MEMBERS=-author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:bamurtaugh -author:bpasero -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:ntrogh -author:hediet -author:isidorn -author:joaomoreno -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:TylerLeonhardt -author:Tyriar -author:weinand -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:ulugbekna -author:aiday-mar -author:bhavyaus -author:justschen -author:benibenj -author:luabud -author:anthonykim1 -author:joshspicer -author:osortega -author:hawkticehurst -author:pierceboggan -author:benvillalobos -author:dileepyavan -author:dineshc-msft -author:dmitrivMS -author:eli-w-king -author:jo-oikawa -author:jruales -author:jytjyt05 -author:kycutler -author:mrleemurray -author:pwang347 -author:vijayupadya -author:bryanchen-d -author:cwebster-99" + "value": "$NOT_TEAM_MEMBERS=-author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:bamurtaugh -author:bpasero -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:ntrogh -author:hediet -author:isidorn -author:joaomoreno -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:TylerLeonhardt -author:Tyriar -author:weinand -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:ulugbekna -author:aiday-mar -author:bhavyaus -author:justschen -author:benibenj -author:luabud -author:anthonykim1 -author:joshspicer -author:osortega -author:hawkticehurst -author:pierceboggan -author:benvillalobos -author:dileepyavan -author:dineshc-msft -author:dmitrivMS -author:eli-w-king -author:jo-oikawa -author:jruales -author:jytjyt05 -author:kycutler -author:mrleemurray -author:pwang347 -author:vijayupadya -author:bryanchen-d -author:cwebster-99 -author:rwoll -author:lostintanget -author:jukasper -author:zhichli\n" }, { "kind": 1, From ede4c001b29c0b8ab94f71ef94fdad8f47c6c220 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:48:56 -0800 Subject: [PATCH 124/152] fix --- .vscode/notebooks/my-endgame.github-issues | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index b37705c3c6c..117139c5920 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -12,7 +12,7 @@ { "kind": 2, "language": "github-issues", - "value": "$NOT_TEAM_MEMBERS=-author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:bamurtaugh -author:bpasero -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:ntrogh -author:hediet -author:isidorn -author:joaomoreno -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:TylerLeonhardt -author:Tyriar -author:weinand -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:ulugbekna -author:aiday-mar -author:bhavyaus -author:justschen -author:benibenj -author:luabud -author:anthonykim1 -author:joshspicer -author:osortega -author:hawkticehurst -author:pierceboggan -author:benvillalobos -author:dileepyavan -author:dineshc-msft -author:dmitrivMS -author:eli-w-king -author:jo-oikawa -author:jruales -author:jytjyt05 -author:kycutler -author:mrleemurray -author:pwang347 -author:vijayupadya -author:bryanchen-d -author:cwebster-99 -author:rwoll -author:lostintanget -author:jukasper -author:zhichli\n" + "value": "$NOT_TEAM_MEMBERS=-author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:bamurtaugh -author:bpasero -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:ntrogh -author:hediet -author:isidorn -author:joaomoreno -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:TylerLeonhardt -author:Tyriar -author:weinand -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:ulugbekna -author:aiday-mar -author:bhavyaus -author:justschen -author:benibenj -author:luabud -author:anthonykim1 -author:joshspicer -author:osortega -author:hawkticehurst -author:pierceboggan -author:benvillalobos -author:dileepyavan -author:dineshc-msft -author:dmitrivMS -author:eli-w-king -author:jo-oikawa -author:jruales -author:jytjyt05 -author:kycutler -author:mrleemurray -author:pwang347 -author:vijayupadya -author:bryanchen-d -author:cwebster-99 -author:rwoll -author:lostintanget -author:jukasper -author:zhichli" }, { "kind": 1, From 6f458a11a99d6d8c881edd31b1ac378ff295f750 Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:52:38 -0800 Subject: [PATCH 125/152] Browser: make paused state less jarring (#291637) * Browser: make paused state less jarring * Update src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * 15% * fix --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../browserView/common/browserView.ts | 6 ++ .../browserView/electron-main/browserView.ts | 11 ++- .../electron-main/browserViewMainService.ts | 4 + .../contrib/browserView/common/browserView.ts | 17 +++- .../electron-browser/browserEditor.ts | 86 +++++++++++++------ .../electron-browser/media/browser.css | 8 +- 6 files changed, 101 insertions(+), 31 deletions(-) diff --git a/src/vs/platform/browserView/common/browserView.ts b/src/vs/platform/browserView/common/browserView.ts index 3d86bf537a1..5e76962350b 100644 --- a/src/vs/platform/browserView/common/browserView.ts +++ b/src/vs/platform/browserView/common/browserView.ts @@ -27,6 +27,7 @@ export interface IBrowserViewState { canGoForward: boolean; loading: boolean; focused: boolean; + visible: boolean; isDevToolsOpen: boolean; lastScreenshot: VSBuffer | undefined; lastFavicon: string | undefined; @@ -55,6 +56,10 @@ export interface IBrowserViewFocusEvent { focused: boolean; } +export interface IBrowserViewVisibilityEvent { + visible: boolean; +} + export interface IBrowserViewDevToolsStateEvent { isDevToolsOpen: boolean; } @@ -112,6 +117,7 @@ export interface IBrowserViewService { onDynamicDidNavigate(id: string): Event; onDynamicDidChangeLoadingState(id: string): Event; onDynamicDidChangeFocus(id: string): Event; + onDynamicDidChangeVisibility(id: string): Event; onDynamicDidChangeDevToolsState(id: string): Event; onDynamicDidKeyCommand(id: string): Event; onDynamicDidChangeTitle(id: string): Event; diff --git a/src/vs/platform/browserView/electron-main/browserView.ts b/src/vs/platform/browserView/electron-main/browserView.ts index ed86bb2762a..33717cb54c2 100644 --- a/src/vs/platform/browserView/electron-main/browserView.ts +++ b/src/vs/platform/browserView/electron-main/browserView.ts @@ -7,7 +7,7 @@ import { WebContentsView, webContents } from 'electron'; import { Disposable } from '../../../base/common/lifecycle.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { VSBuffer } from '../../../base/common/buffer.js'; -import { IBrowserViewBounds, IBrowserViewDevToolsStateEvent, IBrowserViewFocusEvent, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewNavigationEvent, IBrowserViewLoadingEvent, IBrowserViewLoadError, IBrowserViewTitleChangeEvent, IBrowserViewFaviconChangeEvent, IBrowserViewNewPageRequest, BrowserViewStorageScope, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, IBrowserViewFindInPageResult } from '../common/browserView.js'; +import { IBrowserViewBounds, IBrowserViewDevToolsStateEvent, IBrowserViewFocusEvent, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewNavigationEvent, IBrowserViewLoadingEvent, IBrowserViewLoadError, IBrowserViewTitleChangeEvent, IBrowserViewFaviconChangeEvent, IBrowserViewNewPageRequest, BrowserViewStorageScope, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, IBrowserViewFindInPageResult, IBrowserViewVisibilityEvent } from '../common/browserView.js'; import { EVENT_KEY_CODE_MAP, KeyCode, KeyMod, SCAN_CODE_STR_TO_EVENT_KEY_CODE } from '../../../base/common/keyCodes.js'; import { IWindowsMainService } from '../../windows/electron-main/windows.js'; import { IBaseWindow, ICodeWindow } from '../../window/electron-main/window.js'; @@ -51,6 +51,9 @@ export class BrowserView extends Disposable { private readonly _onDidChangeFocus = this._register(new Emitter()); readonly onDidChangeFocus: Event = this._onDidChangeFocus.event; + private readonly _onDidChangeVisibility = this._register(new Emitter()); + readonly onDidChangeVisibility: Event = this._onDidChangeVisibility.event; + private readonly _onDidChangeDevToolsState = this._register(new Emitter()); readonly onDidChangeDevToolsState: Event = this._onDidChangeDevToolsState.event; @@ -281,6 +284,7 @@ export class BrowserView extends Disposable { canGoForward: webContents.navigationHistory.canGoForward(), loading: webContents.isLoading(), focused: webContents.isFocused(), + visible: this._view.getVisible(), isDevToolsOpen: webContents.isDevToolsOpened(), lastScreenshot: this._lastScreenshot, lastFavicon: this._lastFavicon, @@ -322,12 +326,17 @@ export class BrowserView extends Disposable { * Set the visibility of this view */ setVisible(visible: boolean): void { + if (this._view.getVisible() === visible) { + return; + } + // If the view is focused, pass focus back to the window when hiding if (!visible && this._view.webContents.isFocused()) { this._window?.win?.webContents.focus(); } this._view.setVisible(visible); + this._onDidChangeVisibility.fire({ visible }); } /** diff --git a/src/vs/platform/browserView/electron-main/browserViewMainService.ts b/src/vs/platform/browserView/electron-main/browserViewMainService.ts index 2a13abf70e0..a462d108ca0 100644 --- a/src/vs/platform/browserView/electron-main/browserViewMainService.ts +++ b/src/vs/platform/browserView/electron-main/browserViewMainService.ts @@ -123,6 +123,10 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa return this._getBrowserView(id).onDidChangeFocus; } + onDynamicDidChangeVisibility(id: string) { + return this._getBrowserView(id).onDidChangeVisibility; + } + onDynamicDidChangeDevToolsState(id: string) { return this._getBrowserView(id).onDidChangeDevToolsState; } diff --git a/src/vs/workbench/contrib/browserView/common/browserView.ts b/src/vs/workbench/contrib/browserView/common/browserView.ts index e532690676e..a292a3a1ba2 100644 --- a/src/vs/workbench/contrib/browserView/common/browserView.ts +++ b/src/vs/workbench/contrib/browserView/common/browserView.ts @@ -22,7 +22,8 @@ import { BrowserViewStorageScope, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, - IBrowserViewFindInPageResult + IBrowserViewFindInPageResult, + IBrowserViewVisibilityEvent } from '../../../../platform/browserView/common/browserView.js'; import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; @@ -82,6 +83,7 @@ export interface IBrowserViewModel extends IDisposable { readonly screenshot: VSBuffer | undefined; readonly loading: boolean; readonly focused: boolean; + readonly visible: boolean; readonly canGoBack: boolean; readonly isDevToolsOpen: boolean; readonly canGoForward: boolean; @@ -98,6 +100,7 @@ export interface IBrowserViewModel extends IDisposable { readonly onDidChangeFavicon: Event; readonly onDidRequestNewPage: Event; readonly onDidFindInPage: Event; + readonly onDidChangeVisibility: Event; readonly onDidClose: Event; readonly onWillDispose: Event; @@ -125,6 +128,7 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { private _screenshot: VSBuffer | undefined = undefined; private _loading: boolean = false; private _focused: boolean = false; + private _visible: boolean = false; private _isDevToolsOpen: boolean = false; private _canGoBack: boolean = false; private _canGoForward: boolean = false; @@ -150,6 +154,7 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { get favicon(): string | undefined { return this._favicon; } get loading(): boolean { return this._loading; } get focused(): boolean { return this._focused; } + get visible(): boolean { return this._visible; } get isDevToolsOpen(): boolean { return this._isDevToolsOpen; } get canGoBack(): boolean { return this._canGoBack; } get canGoForward(): boolean { return this._canGoForward; } @@ -193,6 +198,10 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { return this.browserViewService.onDynamicDidFindInPage(this.id); } + get onDidChangeVisibility(): Event { + return this.browserViewService.onDynamicDidChangeVisibility(this.id); + } + get onDidClose(): Event { return this.browserViewService.onDynamicDidClose(this.id); } @@ -221,6 +230,7 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { this._title = state.title; this._loading = state.loading; this._focused = state.focused; + this._visible = state.visible; this._isDevToolsOpen = state.isDevToolsOpen; this._canGoBack = state.canGoBack; this._canGoForward = state.canGoForward; @@ -262,6 +272,10 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { this._register(this.onDidChangeFocus(({ focused }) => { this._focused = focused; })); + + this._register(this.onDidChangeVisibility(({ visible }) => { + this._visible = visible; + })); } async layout(bounds: IBrowserViewBounds): Promise { @@ -269,6 +283,7 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { } async setVisible(visible: boolean): Promise { + this._visible = visible; // Set optimistically so model is in sync immediately return this.browserViewService.setVisible(this.id, visible); } diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts index ed80694577d..2d79adc5c8d 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts @@ -5,7 +5,7 @@ import './media/browser.css'; import { localize } from '../../../../nls.js'; -import { $, addDisposableListener, Dimension, disposableWindowInterval, EventType, IDomPosition, registerExternalFocusChecker } from '../../../../base/browser/dom.js'; +import { $, addDisposableListener, Dimension, EventType, IDomPosition, registerExternalFocusChecker } from '../../../../base/browser/dom.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { RawContextKey, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -192,6 +192,7 @@ export class BrowserEditor extends EditorPane { private readonly _inputDisposables = this._register(new DisposableStore()); private overlayManager: BrowserOverlayManager | undefined; private _elementSelectionCts: CancellationTokenSource | undefined; + private _screenshotTimeout: ReturnType | undefined; constructor( group: IEditorGroup, @@ -344,6 +345,9 @@ export class BrowserEditor extends EditorPane { this.focusUrlInput(); } + // Start / stop screenshots when the model visibility changes + this._inputDisposables.add(this._model.onDidChangeVisibility(() => this.doScreenshot())); + // Listen to model events for UI updates this._inputDisposables.add(this._model.onDidKeyCommand(keyEvent => { // Handle like webview does - convert to webview KeyEvent format @@ -398,16 +402,11 @@ export class BrowserEditor extends EditorPane { this.layoutBrowserContainer(); } })); - // Capture screenshot periodically (once per second) to keep background updated - this._inputDisposables.add(disposableWindowInterval( - this.window, - () => this.capturePlaceholderSnapshot(), - 1000 - )); this.updateErrorDisplay(); this.layoutBrowserContainer(); - await this._model.setVisible(this.shouldShowView); + this.updateVisibility(); + this.doScreenshot(); } protected override setEditorVisible(visible: boolean): void { @@ -442,14 +441,26 @@ export class BrowserEditor extends EditorPane { if (this._model) { const show = this.shouldShowView; - void this._model.setVisible(show); - if ( - show && - this._browserContainer.ownerDocument.hasFocus() && - this._browserContainer.ownerDocument.activeElement === this._browserContainer - ) { - // If the editor is focused, ensure the browser view also gets focus - void this._model.focus(); + if (show === this._model.visible) { + return; + } + + if (show) { + this._model.setVisible(true); + if ( + this._browserContainer.ownerDocument.hasFocus() && + this._browserContainer.ownerDocument.activeElement === this._browserContainer + ) { + // If the editor is focused, ensure the browser view also gets focus + void this._model.focus(); + } + } else { + this.doScreenshot(); + + // Hide the browser view just before the next render. + // This attempts to give the screenshot some time to be captured and displayed. + // If we hide immediately it is more likely to flicker while the old screenshot is still visible. + this.window.requestAnimationFrame(() => this._model?.setVisible(false)); } } } @@ -780,17 +791,35 @@ export class BrowserEditor extends EditorPane { } } - /** - * Capture a screenshot of the current browser view to use as placeholder background - */ - private async capturePlaceholderSnapshot(): Promise { - if (this._model && !this._overlayVisible) { - try { - const buffer = await this._model.captureScreenshot({ quality: 80 }); - this.setBackgroundImage(buffer); - } catch (error) { - this.logService.error('BrowserEditor.capturePlaceholderSnapshot: Failed to capture screenshot', error); - } + private async doScreenshot(): Promise { + if (!this._model) { + return; + } + + // Cancel any existing timeout + this.cancelScheduledScreenshot(); + + // Only take screenshots if the model is visible + if (!this._model.visible) { + return; + } + + try { + // Capture screenshot and set as background image + const screenshot = await this._model.captureScreenshot({ quality: 80 }); + this.setBackgroundImage(screenshot); + } catch (error) { + this.logService.error('Failed to capture browser view screenshot', error); + } + + // Schedule next screenshot in 1 second + this._screenshotTimeout = setTimeout(() => this.doScreenshot(), 1000); + } + + private cancelScheduledScreenshot(): void { + if (this._screenshotTimeout) { + clearTimeout(this._screenshotTimeout); + this._screenshotTimeout = undefined; } } @@ -859,6 +888,9 @@ export class BrowserEditor extends EditorPane { this._elementSelectionCts = undefined; } + // Cancel any scheduled screenshots + this.cancelScheduledScreenshot(); + // Clear find widget model this._findWidget.rawValue?.setModel(undefined); this._findWidget.rawValue?.hide(); diff --git a/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css b/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css index 89087be7beb..f4fafcbf3af 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css +++ b/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css @@ -72,15 +72,19 @@ justify-content: center; pointer-events: none; color: var(--vscode-foreground); - background-color: color-mix(in srgb, var(--vscode-editor-background) 60%, transparent); + background-color: color-mix(in srgb, var(--vscode-editor-background) 15%, transparent); opacity: 0; visibility: hidden; - transition: opacity 200ms ease-out; + transition: opacity 200ms ease-in; &.visible { opacity: 1; visibility: visible; } + + &.show-message { + background-color: color-mix(in srgb, var(--vscode-editor-background) 60%, transparent); + } } .browser-overlay-paused-message { From 80c02caeb02633bddb716cac8d480d7454ad6b5f Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:56:04 -0800 Subject: [PATCH 126/152] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .vscode/notebooks/my-endgame.github-issues | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index 117139c5920..d17509b7758 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -12,7 +12,7 @@ { "kind": 2, "language": "github-issues", - "value": "$NOT_TEAM_MEMBERS=-author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:bamurtaugh -author:bpasero -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:ntrogh -author:hediet -author:isidorn -author:joaomoreno -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:TylerLeonhardt -author:Tyriar -author:weinand -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:ulugbekna -author:aiday-mar -author:bhavyaus -author:justschen -author:benibenj -author:luabud -author:anthonykim1 -author:joshspicer -author:osortega -author:hawkticehurst -author:pierceboggan -author:benvillalobos -author:dileepyavan -author:dineshc-msft -author:dmitrivMS -author:eli-w-king -author:jo-oikawa -author:jruales -author:jytjyt05 -author:kycutler -author:mrleemurray -author:pwang347 -author:vijayupadya -author:bryanchen-d -author:cwebster-99 -author:rwoll -author:lostintanget -author:jukasper -author:zhichli" + "value": "$NOT_TEAM_MEMBERS=-author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:bamurtaugh -author:bpasero -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:ntrogh -author:hediet -author:isidorn -author:joaomoreno -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:TylerLeonhardt -author:Tyriar -author:weinand -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:ulugbekna -author:aiday-mar -author:bhavyaus -author:justschen -author:benibenj -author:luabud -author:anthonykim1 -author:joshspicer -author:osortega -author:hawkticehurst -author:pierceboggan -author:benvillalobos -author:dileepyavan -author:dineshc-msft -author:dmitrivMS -author:eli-w-king -author:jo-oikawa -author:jruales -author:jytjyt05 -author:kycutler -author:mrleemurray -author:pwang347 -author:vijayupadya -author:bryanchen-d -author:cwebster-99 -author:rwoll -author:lostintangent -author:jukasper -author:zhichli" }, { "kind": 1, From e710e342972700fe338e80d48af89c3f668fbed7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 29 Jan 2026 19:19:19 +0100 Subject: [PATCH 127/152] refactor - remove unused chat issue reporting command (#291684) --- .../browser/chatSetup/chatSetupProviders.ts | 54 +------------------ 1 file changed, 1 insertion(+), 53 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts index 308b543253d..8e2fa8220f5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts @@ -48,9 +48,6 @@ import { ChatSetupController } from './chatSetupController.js'; import { ChatSetupAnonymous, ChatSetupStep, IChatSetupResult } from './chatSetup.js'; import { ChatSetup } from './chatSetupRunner.js'; import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; -import { IOutputService } from '../../../../services/output/common/output.js'; -import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; -import { IWorkbenchIssueService } from '../../../issue/common/issue.js'; import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; import { IHostService } from '../../../../services/host/browser/host.js'; @@ -175,7 +172,6 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { private static readonly TRUST_NEEDED_MESSAGE = new MarkdownString(localize('trustNeeded', "You need to trust this workspace to use Chat.")); private static readonly CHAT_RETRY_COMMAND_ID = 'workbench.action.chat.retrySetup'; - private static readonly CHAT_REPORT_ISSUE_WITH_OUTPUT_COMMAND_ID = 'workbench.action.chat.reportIssueWithOutput'; private readonly _onUnresolvableError = this._register(new Emitter()); readonly onUnresolvableError = this._onUnresolvableError.event; @@ -200,50 +196,6 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { private registerCommands(): void { - // Report issue with output command - this._register(CommandsRegistry.registerCommand(SetupAgent.CHAT_REPORT_ISSUE_WITH_OUTPUT_COMMAND_ID, async accessor => { - const outputService = accessor.get(IOutputService); - const textModelService = accessor.get(ITextModelService); - const issueService = accessor.get(IWorkbenchIssueService); - const logService = accessor.get(ILogService); - - let outputData = ''; - let channelName = ''; - - let channel = outputService.getChannel(defaultChat.outputChannelId); - if (channel) { - channelName = defaultChat.outputChannelId; - } else { - logService.warn(`[chat setup] Output channel '${defaultChat.outputChannelId}' not found, falling back to Window output channel`); - channel = outputService.getChannel('rendererLog'); - channelName = 'Window'; - } - - if (channel) { - try { - const model = await textModelService.createModelReference(channel.uri); - try { - const rawOutput = model.object.textEditorModel.getValue(); - outputData = `
\nGitHub Copilot Chat Output (${channelName})\n\n\`\`\`\n${rawOutput}\n\`\`\`\n
`; - logService.info(`[chat setup] Retrieved ${rawOutput.length} characters from ${channelName} output channel`); - } finally { - model.dispose(); - } - } catch (error) { - logService.error(`[chat setup] Failed to retrieve output channel content: ${error}`); - } - } else { - logService.warn(`[chat setup] No output channel available`); - } - - await issueService.openReporter({ - extensionId: defaultChat.chatExtensionId, - issueTitle: 'Chat took too long to get ready', - issueBody: 'Chat took too long to get ready', - data: outputData || localize('chatOutputChannelUnavailable', "GitHub Copilot Chat output channel not available. Please ensure the GitHub Copilot Chat extension is active and try again. If the issue persists, you can manually include relevant information from the Output panel (View > Output > GitHub Copilot Chat).") - }); - })); - // Retry chat command this._register(CommandsRegistry.registerCommand(SetupAgent.CHAT_RETRY_COMMAND_ID, async (accessor, sessionResource: URI) => { const hostService = accessor.get(IHostService); @@ -430,11 +382,7 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { id: SetupAgent.CHAT_RETRY_COMMAND_ID, title: localize('retryChat', "Restart"), arguments: [requestModel.session.sessionResource] - }, - additionalCommands: [{ - id: SetupAgent.CHAT_REPORT_ISSUE_WITH_OUTPUT_COMMAND_ID, - title: localize('reportChatIssue', "Report Issue"), - }] + } }); // This means Chat is unhealthy and we cannot retry the From 148bbfdb57a32e666bf8d3254f8989a7cbd8a818 Mon Sep 17 00:00:00 2001 From: ulugbekna Date: Thu, 29 Jan 2026 17:23:25 +0100 Subject: [PATCH 128/152] fix(stringEdit): prevent _tryRebase from producing non-disjoint edits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The `_tryRebase` method could produce edits that violate the sorted/disjoint invariant required by `StringEdit`, causing a `BugIndicatingError` to be thrown with the message: `Edits must be disjoint and sorted. Found [X, X) -> "..." after Y` ## Root Cause When a base edit deletes significantly more characters than it inserts (creating a negative offset), subsequent "our" edits can get transformed to positions that conflict with previously-added edits. Example scenario: 1. `ourEdit1`: [100, 110) → "A" — added to result, ends at position 110 2. `baseEdit`: [110, 125) → "" — deletes 15 chars, offset becomes -15 3. `ourEdit2`: [120, 120) → "B" — transforms to [105, 105) after offset Problem: Position 105 is BEFORE 110 (the end of `ourEdit1`), violating the invariant that edits must be sorted and disjoint. The original code only checked for direct intersections with base edits, but missed this case where the cumulative offset transformation causes an "our" edit to land before the end of a previously-added "our" edit. ## Fix Track the exclusive end position of the last added edit (`lastEndEx`) and before adding each transformed edit, verify that its start position is not before `lastEndEx`. If it is: - When `noOverlap=true` (tryRebase): return `undefined` - When `noOverlap=false` (rebaseSkipConflicting): skip the conflicting edit ## Behavior Changes - `tryRebase()`: Now returns `undefined` for cases that previously crashed - `rebaseSkipConflicting()`: Skips conflicting edits instead of crashing - `trySwap()`: Uses `tryRebase` internally, handles edge cases gracefully These are safe changes since the previous behavior was to throw an error. Callers of `tryRebase()` already handle `undefined` returns, and `rebaseSkipConflicting()` is expected to drop conflicting edits. --- src/vs/editor/common/core/edits/stringEdit.ts | 31 +++++++--- .../test/common/core/stringEdit.test.ts | 56 +++++++++++++++++++ 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/common/core/edits/stringEdit.ts b/src/vs/editor/common/core/edits/stringEdit.ts index 9f6c9800f41..83d40b5e8c2 100644 --- a/src/vs/editor/common/core/edits/stringEdit.ts +++ b/src/vs/editor/common/core/edits/stringEdit.ts @@ -97,6 +97,7 @@ export abstract class BaseStringEdit = BaseSt let baseIdx = 0; let ourIdx = 0; let offset = 0; + let lastEndEx = -1; // Track end of last added edit to ensure sorted/disjoint invariant while (ourIdx < this.replacements.length || baseIdx < base.replacements.length) { // take the edit that starts first @@ -108,10 +109,17 @@ export abstract class BaseStringEdit = BaseSt break; } else if (!baseEdit) { // no more edits from base - newEdits.push(new StringReplacement( - ourEdit.replaceRange.delta(offset), - ourEdit.newText - )); + const transformedRange = ourEdit.replaceRange.delta(offset); + // Check if the transformed edit would violate the sorted/disjoint invariant + if (transformedRange.start < lastEndEx) { + if (noOverlap) { + return undefined; + } + ourIdx++; // Skip this edit as it conflicts with a previously added edit + continue; + } + newEdits.push(new StringReplacement(transformedRange, ourEdit.newText)); + lastEndEx = transformedRange.endExclusive; ourIdx++; } else if (ourEdit.replaceRange.intersects(baseEdit.replaceRange) || areConcurrentInserts(ourEdit.replaceRange, baseEdit.replaceRange)) { ourIdx++; // Don't take our edit, as it is conflicting -> skip @@ -120,10 +128,17 @@ export abstract class BaseStringEdit = BaseSt } } else if (ourEdit.replaceRange.start < baseEdit.replaceRange.start) { // Our edit starts first - newEdits.push(new StringReplacement( - ourEdit.replaceRange.delta(offset), - ourEdit.newText - )); + const transformedRange = ourEdit.replaceRange.delta(offset); + // Check if the transformed edit would violate the sorted/disjoint invariant + if (transformedRange.start < lastEndEx) { + if (noOverlap) { + return undefined; + } + ourIdx++; // Skip this edit as it conflicts with a previously added edit + continue; + } + newEdits.push(new StringReplacement(transformedRange, ourEdit.newText)); + lastEndEx = transformedRange.endExclusive; ourIdx++; } else { baseIdx++; diff --git a/src/vs/editor/test/common/core/stringEdit.test.ts b/src/vs/editor/test/common/core/stringEdit.test.ts index c54e132580e..9189dd62be3 100644 --- a/src/vs/editor/test/common/core/stringEdit.test.ts +++ b/src/vs/editor/test/common/core/stringEdit.test.ts @@ -154,6 +154,62 @@ suite('Edit', () => { // This should return undefined because both are inserts at the same position assert.strictEqual(rebasedEdit, undefined); }); + + test('tryRebase should return undefined when rebasing would produce non-disjoint edits (negative offset case)', () => { + // ourEdit1: [100, 110) -> "A" + // ourEdit2: [120, 120) -> "B" + // baseEdit: [110, 125) -> "" (delete 15 chars, offset = -15) + // After transformation, ourEdit2 at [105, 105) < ourEdit1 end (110) + + const ourEdit = StringEdit.create([ + new StringReplacement(new OffsetRange(100, 110), 'A'), + new StringReplacement(OffsetRange.emptyAt(120), 'B'), + ]); + + const baseEdit = StringEdit.create([ + new StringReplacement(new OffsetRange(110, 125), ''), + ]); + + const result = ourEdit.tryRebase(baseEdit); + assert.strictEqual(result, undefined); + }); + + test('tryRebase should succeed when edits remain disjoint after rebasing', () => { + // ourEdit1: [100, 110) -> "A" + // ourEdit2: [200, 210) -> "B" + // baseEdit: [50, 60) -> "" (delete 10 chars, offset = -10) + // After: ourEdit1 at [90, 100), ourEdit2 at [190, 200) - still disjoint + + const ourEdit = StringEdit.create([ + new StringReplacement(new OffsetRange(100, 110), 'A'), + new StringReplacement(new OffsetRange(200, 210), 'B'), + ]); + + const baseEdit = StringEdit.create([ + new StringReplacement(new OffsetRange(50, 60), ''), + ]); + + const result = ourEdit.tryRebase(baseEdit); + assert.ok(result); + assert.strictEqual(result?.replacements[0].replaceRange.start, 90); + assert.strictEqual(result?.replacements[1].replaceRange.start, 190); + }); + + test('rebaseSkipConflicting should skip edits that would produce non-disjoint results', () => { + const ourEdit = StringEdit.create([ + new StringReplacement(new OffsetRange(100, 110), 'A'), + new StringReplacement(OffsetRange.emptyAt(120), 'B'), + ]); + + const baseEdit = StringEdit.create([ + new StringReplacement(new OffsetRange(110, 125), ''), + ]); + + // Should not throw, and should skip the conflicting edit + const result = ourEdit.rebaseSkipConflicting(baseEdit); + assert.strictEqual(result.replacements.length, 1); + assert.strictEqual(result.replacements[0].replaceRange.start, 100); + }); }); suite('ArrayEdit', () => { From a72cd7b63e06ee2bad86679806a56bfff3da30b7 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 29 Jan 2026 13:23:33 -0500 Subject: [PATCH 129/152] add wait for command function (#291653) --- .../toolInvocationParts/chatTerminalToolProgressPart.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts index 2ed4afbf599..c5eae902688 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts @@ -371,7 +371,9 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart this.domNode = progressPart.domNode; } - if (expandedStateByInvocation.get(toolInvocation) || (this._isInThinkingContainer && IChatToolInvocation.isComplete(toolInvocation))) { + // Only auto-expand in thinking containers if there's actual output to show + const hasStoredOutput = !!terminalData.terminalCommandOutput; + if (expandedStateByInvocation.get(toolInvocation) || (this._isInThinkingContainer && IChatToolInvocation.isComplete(toolInvocation) && hasStoredOutput)) { void this._toggleOutput(true); } this._register(this._terminalChatService.registerProgressPart(this)); From 90d24f7d3424df566adb7cc6a3d758ca7f169964 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 29 Jan 2026 19:24:33 +0100 Subject: [PATCH 130/152] Chat Sessions: Toggling filter collapses More section (fix #291544) (#291683) --- .../browser/agentSessions/agentSessionsControl.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 6c4eb042e8a..bc13d51d74f 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -325,13 +325,13 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo break; } case AgentSessionSection.More: { - const shouldCollapseMore = - !this.sessionsListFindIsOpen && // always expand when find is open - !this.options.filter.getExcludes().read; // always expand when only showing unread - - if (shouldCollapseMore && !child.collapsed) { - this.sessionsList.collapse(child.element); - } else if (!shouldCollapseMore && child.collapsed) { + if ( + child.collapsed && + ( + this.sessionsListFindIsOpen || // always expand when find is open + this.options.filter.getExcludes().read // always expand when only showing unread + ) + ) { this.sessionsList.expand(child.element); } break; From bf729fa50adbc422b858ba2dcb138357b6a5f7f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 29 Jan 2026 19:27:20 +0100 Subject: [PATCH 131/152] strip out git askpass sourcemap footer (#291673) fixes #282020 --- build/lib/extensions.ts | 23 +++++++++++++++++----- extensions/git/extension.webpack.config.js | 2 ++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 24462a3b26e..e06f1510a66 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -98,14 +98,22 @@ function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string, const result = es.through(); const packagedDependencies: string[] = []; + const stripOutSourceMaps: string[] = []; const packageJsonConfig = require(path.join(extensionPath, 'package.json')); if (packageJsonConfig.dependencies) { - const webpackRootConfig = require(path.join(extensionPath, webpackConfigFileName)).default; + const webpackConfig = require(path.join(extensionPath, webpackConfigFileName)); + const webpackRootConfig = webpackConfig.default; for (const key in webpackRootConfig.externals) { if (key in packageJsonConfig.dependencies) { packagedDependencies.push(key); } } + + if (webpackConfig.StripOutSourceMaps) { + for (const filePath of webpackConfig.StripOutSourceMaps) { + stripOutSourceMaps.push(filePath); + } + } } // TODO: add prune support based on packagedDependencies to vsce.PackageManager.Npm similar @@ -177,10 +185,15 @@ function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string, // * rewrite sourceMappingURL // * save to disk so that upload-task picks this up if (path.extname(data.basename) === '.js') { - const contents = (data.contents as Buffer).toString('utf8'); - data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { - return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; - }), 'utf8'); + if (stripOutSourceMaps.indexOf(data.relative) >= 0) { // remove source map + const contents = (data.contents as Buffer).toString('utf8'); + data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, ''), 'utf8'); + } else { + const contents = (data.contents as Buffer).toString('utf8'); + data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { + return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; + }), 'utf8'); + } } this.emit('data', data); diff --git a/extensions/git/extension.webpack.config.js b/extensions/git/extension.webpack.config.js index 15cf273015b..34f801e2eca 100644 --- a/extensions/git/extension.webpack.config.js +++ b/extensions/git/extension.webpack.config.js @@ -13,3 +13,5 @@ export default withDefaults({ ['git-editor-main']: './src/git-editor-main.ts' } }); + +export const StripOutSourceMaps = ['dist/askpass-main.js']; From 424641585af5c22401e862fd06e8d1c1822a1283 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 29 Jan 2026 13:32:56 -0500 Subject: [PATCH 132/152] don't prompt if user explicitly set automatic tasks to `off` (#291671) --- src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts index 7063ae8be2b..080d0432909 100644 --- a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts +++ b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts @@ -47,9 +47,9 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut if (!this._workspaceTrustManagementService.isWorkspaceTrusted()) { return; } - const hasShownPromptForAutomaticTasks = this._storageService.getBoolean(HAS_PROMPTED_FOR_AUTOMATIC_TASKS, StorageScope.WORKSPACE, false); - if (this._hasRunTasks || - (this._configurationService.getValue(ALLOW_AUTOMATIC_TASKS) === 'off' && hasShownPromptForAutomaticTasks)) { + const { value, userValue } = this._configurationService.inspect(ALLOW_AUTOMATIC_TASKS); + // If user explicitly set it to 'off', don't run or prompt + if (this._hasRunTasks || (value === 'off' && userValue !== undefined)) { return; } this._hasRunTasks = true; From 9081bdd491f344c4312b362bb069dbff8d673eb9 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 29 Jan 2026 13:45:54 -0500 Subject: [PATCH 133/152] check output before appending (#291658) --- .../browser/chatTerminalCommandMirror.ts | 51 ++++- .../browser/chatTerminalCommandMirror.test.ts | 182 +++++++++++++++++- 2 files changed, 229 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts b/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts index 60308557753..2f3dc25de5e 100644 --- a/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts +++ b/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts @@ -68,6 +68,28 @@ export function computeMaxBufferColumnWidth(buffer: { readonly length: number; g return maxWidth; } +/** + * Checks if two VT strings match around a boundary where we would slice. + * This is an efficient O(1) check that verifies a small window of characters + * before the slice point to detect if the VT sequences have diverged (common on Windows). + * + * @param newVT The new VT text to compare. + * @param oldVT The old VT text to compare against. + * @param slicePoint The point where we would slice. Must be <= both string lengths. + * @param windowSize The number of characters before slicePoint to check (default 50). + * @returns True if the boundary matches, false if VT sequences have diverged. + */ +export function vtBoundaryMatches(newVT: string, oldVT: string, slicePoint: number, windowSize: number = 50): boolean { + const start = Math.max(0, slicePoint - windowSize); + const end = slicePoint; + for (let i = start; i < end; i++) { + if (newVT.charCodeAt(i) !== oldVT.charCodeAt(i)) { + return false; + } + } + return true; +} + export interface IDetachedTerminalCommandMirrorRenderResult { lineCount?: number; maxColumnWidth?: number; @@ -280,7 +302,16 @@ export class DetachedTerminalCommandMirror extends Disposable implements IDetach } await new Promise(resolve => { - if (!this._lastVT) { + // Only append if the boundary around the slice point matches; otherwise rewrite. + // This is an efficient constant-time check (checking up to 50 characters) instead of comparing the entire prefix. + // On Windows, VT sequences can differ even for equivalent content, causing corruption + // if we blindly append. + const canAppend = !!this._lastVT && vt.text.length >= this._lastVT.length && this._vtBoundaryMatches(vt.text, this._lastVT.length); + if (!canAppend) { + // Reset the terminal if we had previous content (can't append, need full rewrite) + if (this._lastVT) { + detached.xterm.clearBuffer(); + } if (vt.text) { detached.xterm.write(vt.text, resolve); } else { @@ -505,9 +536,16 @@ export class DetachedTerminalCommandMirror extends Disposable implements IDetach return; } - const canAppend = !!this._lastVT && startLine >= previousCursor; + // Only append if: (1) cursor hasn't moved backwards, and (2) boundary around slice point matches. + // This is an efficient O(1) check instead of comparing the entire prefix. + // On Windows, VT sequences can differ even for equivalent content, so we must verify. + const canAppend = !!this._lastVT && startLine >= previousCursor && vt.text.length >= this._lastVT.length && this._vtBoundaryMatches(vt.text, this._lastVT.length); await new Promise(resolve => { - if (!this._lastVT || !canAppend) { + if (!canAppend) { + // Reset the terminal if we had previous content (can't append, need full rewrite) + if (this._lastVT) { + detachedRaw.clearBuffer(); + } if (vt.text) { detachedRaw.write(vt.text, resolve); } else { @@ -542,6 +580,13 @@ export class DetachedTerminalCommandMirror extends Disposable implements IDetach private _getAbsoluteCursorY(raw: RawXtermTerminal): number { return raw.buffer.active.baseY + raw.buffer.active.cursorY; } + + /** + * Checks if the new VT text matches the old VT around the boundary where we would slice. + */ + private _vtBoundaryMatches(newVT: string, slicePoint: number): boolean { + return vtBoundaryMatches(newVT, this._lastVT, slicePoint); + } } /** diff --git a/src/vs/workbench/contrib/terminal/test/browser/chatTerminalCommandMirror.test.ts b/src/vs/workbench/contrib/terminal/test/browser/chatTerminalCommandMirror.test.ts index e5fd2c6a08b..851c3d5f702 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/chatTerminalCommandMirror.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/chatTerminalCommandMirror.test.ts @@ -14,7 +14,7 @@ import { TerminalCapabilityStore } from '../../../../../platform/terminal/common import { XtermTerminal } from '../../browser/xterm/xtermTerminal.js'; import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; import { TestXtermAddonImporter } from './xterm/xtermTestUtils.js'; -import { computeMaxBufferColumnWidth } from '../../browser/chatTerminalCommandMirror.js'; +import { computeMaxBufferColumnWidth, vtBoundaryMatches } from '../../browser/chatTerminalCommandMirror.js'; const defaultTerminalConfig = { fontFamily: 'monospace', @@ -231,6 +231,123 @@ suite('Workbench - ChatTerminalCommandMirror', () => { // Incremental mirror should match fresh mirror strictEqual(getBufferText(mirror), getBufferText(freshMirror)); }); + + test('VT divergence detection prevents corruption (Windows scenario)', async () => { + // This test simulates the Windows issue where VT sequences can differ + // between calls even for equivalent visual content. On Windows, the + // serializer can produce different escape sequences (e.g., different + // line endings or cursor positioning) causing the prefix to diverge. + // + // Without boundary checking, blindly slicing would corrupt output: + // - vt1: "Line1\r\nLine2" (length 13) + // - vt2: "Line1\nLine2\nLine3" (different format, but starts similarly) + // - slice(13) on vt2 would give "ine3" instead of the full new content + + const mirror = await createXterm(); + + // Simulate first VT snapshot + const vt1 = 'Line1\r\nLine2'; + await write(mirror, vt1); + strictEqual(getBufferText(mirror), 'Line1\nLine2'); + + // Simulate divergent VT snapshot (different escape sequences for same content) + // This mimics what can happen on Windows where the VT serializer + // produces different output between calls + const vt2 = 'DifferentPrefix' + 'Line3'; + + // Use the actual utility function to test boundary checking + const boundaryMatches = vtBoundaryMatches(vt2, vt1, vt1.length); + + // Boundary should NOT match because the prefix diverged + strictEqual(boundaryMatches, false, 'Boundary check should detect divergence'); + + // When boundary doesn't match, the fix does a full reset + rewrite + // instead of corrupting the output by blind slicing + mirror.raw.reset(); + await write(mirror, vt2); + + // Final content should be the complete new VT, not corrupted + strictEqual(getBufferText(mirror), 'DifferentPrefixLine3'); + }); + + test('boundary check allows append when VT prefix matches', async () => { + const mirror = await createXterm(); + + // First VT snapshot + const vt1 = 'Line1\r\nLine2\r\n'; + await write(mirror, vt1); + + // Second VT snapshot that properly extends the first + const vt2 = vt1 + 'Line3\r\n'; + + // Use the actual utility function to test boundary checking + const boundaryMatches = vtBoundaryMatches(vt2, vt1, vt1.length); + + strictEqual(boundaryMatches, true, 'Boundary check should pass when prefix matches'); + + // Append should work correctly + const appended = vt2.slice(vt1.length); + await write(mirror, appended); + + strictEqual(getBufferText(mirror), 'Line1\nLine2\nLine3'); + }); + + test('incremental updates use append path (not full rewrite) in normal operation', async () => { + // This test verifies that in normal operation (VT prefix matches), + // we use the efficient append path rather than full rewrite. + + const source = await createXterm(); + const marker = source.raw.registerMarker(0)!; + + // Build up content incrementally, simulating streaming output + const writes: string[] = []; + + // Step 1: Initial content + await write(source, 'output line 1\r\n'); + const vt1 = await source.getRangeAsVT(marker, undefined, true) ?? ''; + + const mirror = await createXterm(); + await write(mirror, vt1); + writes.push(vt1); + + // Step 2: Add more content - should use append path + await write(source, 'output line 2\r\n'); + const vt2 = await source.getRangeAsVT(marker, undefined, true) ?? ''; + + // Verify VT extends properly (prefix matches) + strictEqual(vt2.startsWith(vt1), true, 'VT2 should start with VT1'); + + // Append only the new part (this is what the append path does) + const appended2 = vt2.slice(vt1.length); + strictEqual(appended2.length > 0, true, 'Should have new content to append'); + strictEqual(appended2.length < vt2.length, true, 'Append should be smaller than full rewrite'); + await write(mirror, appended2); + writes.push(appended2); + + // Step 3: Add more content - should continue using append path + await write(source, 'output line 3\r\n'); + const vt3 = await source.getRangeAsVT(marker, undefined, true) ?? ''; + + strictEqual(vt3.startsWith(vt2), true, 'VT3 should start with VT2'); + + const appended3 = vt3.slice(vt2.length); + strictEqual(appended3.length > 0, true, 'Should have new content to append'); + strictEqual(appended3.length < vt3.length, true, 'Append should be smaller than full rewrite'); + await write(mirror, appended3); + writes.push(appended3); + + marker.dispose(); + + // Verify final content is correct + strictEqual(getBufferText(mirror), 'output line 1\noutput line 2\noutput line 3'); + + // Verify we used the append path (total bytes written should be roughly + // equal to total VT, not 3x the total due to full rewrites) + const totalWritten = writes.reduce((sum, w) => sum + w.length, 0); + const fullRewriteWouldBe = vt1.length + vt2.length + vt3.length; + strictEqual(totalWritten < fullRewriteWouldBe, true, + `Append path should write less (${totalWritten}) than full rewrites would (${fullRewriteWouldBe})`); + }); }); suite('computeMaxBufferColumnWidth', () => { @@ -375,4 +492,67 @@ suite('Workbench - ChatTerminalCommandMirror', () => { strictEqual(computeMaxBufferColumnWidth(buffer, 150), 120); }); }); + + suite('vtBoundaryMatches', () => { + + test('returns true when strings match at boundary', () => { + const oldVT = 'Line1\r\nLine2\r\n'; + const newVT = oldVT + 'Line3\r\n'; + strictEqual(vtBoundaryMatches(newVT, oldVT, oldVT.length), true); + }); + + test('returns false when strings diverge at boundary', () => { + const oldVT = 'Line1\r\nLine2'; + const newVT = 'DifferentPrefixLine3'; + strictEqual(vtBoundaryMatches(newVT, oldVT, oldVT.length), false); + }); + + test('returns false when single character differs in window', () => { + const oldVT = 'AAAAAAAAAA'; + const newVT = 'AAAAABAAAA' + 'NewContent'; + strictEqual(vtBoundaryMatches(newVT, oldVT, oldVT.length), false); + }); + + test('returns true for empty strings', () => { + strictEqual(vtBoundaryMatches('', '', 0), true); + }); + + test('returns true when slicePoint is 0', () => { + const oldVT = ''; + const newVT = 'SomeContent'; + strictEqual(vtBoundaryMatches(newVT, oldVT, 0), true); + }); + + test('handles strings shorter than window size', () => { + const oldVT = 'Short'; + const newVT = 'Short' + 'Added'; + strictEqual(vtBoundaryMatches(newVT, oldVT, oldVT.length), true); + }); + + test('respects custom window size parameter', () => { + // With default window (50), this would match since the diff is at position 70 + const prefix = 'A'.repeat(80); + const oldVT = prefix; + const newVT = 'X' + 'A'.repeat(79) + 'NewContent'; // differs at position 0 + + // With window of 50, only checks chars 30-80, which would match + strictEqual(vtBoundaryMatches(newVT, oldVT, oldVT.length, 50), true); + + // With window of 100, would check chars 0-80, which would NOT match + strictEqual(vtBoundaryMatches(newVT, oldVT, oldVT.length, 100), false); + }); + + test('detects divergence in escape sequences (Windows scenario)', () => { + // Simulates Windows issue where VT escape sequences differ + const oldVT = '\x1b[0m\x1b[1mBold\x1b[0m\r\n'; + const newVT = '\x1b[0m\x1b[22mBold\x1b[0m\r\nMore'; // Different escape code for bold + strictEqual(vtBoundaryMatches(newVT, oldVT, oldVT.length), false); + }); + + test('handles matching escape sequences', () => { + const oldVT = '\x1b[31mRed\x1b[0m\r\n'; + const newVT = '\x1b[31mRed\x1b[0m\r\nGreen'; + strictEqual(vtBoundaryMatches(newVT, oldVT, oldVT.length), true); + }); + }); }); From d6d5aec8e8612ad78560a6dfea26b08846f47def Mon Sep 17 00:00:00 2001 From: dileepyavan <52841896+dileepyavan@users.noreply.github.com> Date: Thu, 29 Jan 2026 10:51:58 -0800 Subject: [PATCH 134/152] Adding sandbox pre-reqs to the setting UI for linux (#291670) changes in settings UI --- .../common/terminalChatAgentToolsConfiguration.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index 3db27d7e85b..e2ad2ad76bc 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -543,7 +543,7 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary Date: Thu, 29 Jan 2026 20:25:51 +0100 Subject: [PATCH 135/152] refactor - simplify auto-expand logic in `AgentSessionsControl` (#291705) --- .../agentSessions/agentSessionsControl.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index bc13d51d74f..5312ecd1529 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -167,7 +167,6 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo overrideStyles: this.options.overrideStyles, twistieAdditionalCssClass: () => 'force-no-twistie', collapseByDefault: (element: unknown) => collapseByDefault(element), - expandOnlyOnTwistieClick: element => !collapseByDefault(element), renderIndentGuides: RenderIndentGuides.None, } )) as WorkbenchCompressibleAsyncDataTree; @@ -325,14 +324,17 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo break; } case AgentSessionSection.More: { - if ( - child.collapsed && - ( - this.sessionsListFindIsOpen || // always expand when find is open - this.options.filter.getExcludes().read // always expand when only showing unread - ) - ) { - this.sessionsList.expand(child.element); + if (child.collapsed) { + let autoExpandMore = false; + if (this.sessionsListFindIsOpen) { + autoExpandMore = true; // always expand when find is open + } else if (this.options.filter.getExcludes().read && child.element.sessions.some(session => !session.isRead())) { + autoExpandMore = true; // expand when showing only unread and this section includes unread + } + + if (autoExpandMore) { + this.sessionsList.expand(child.element); + } } break; } From d1fa3d656d3b0ca42fe6f2efc210d9a15f51cf31 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 29 Jan 2026 20:29:51 +0100 Subject: [PATCH 136/152] feat - update icon for `Codex` and `Claude` providers (#291706) --- .../contrib/chat/browser/agentSessions/agentSessions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts index d0dec2b7280..0100bbb1b49 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts @@ -64,8 +64,9 @@ export function getAgentSessionProviderIcon(provider: AgentSessionProviders): Th case AgentSessionProviders.Cloud: return Codicon.cloud; case AgentSessionProviders.Codex: + return Codicon.openai; case AgentSessionProviders.Claude: - return Codicon.code; + return Codicon.claude; } } From 6579a01f28dc7c30af49715699e7a52a911bfe3e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 29 Jan 2026 20:36:14 +0100 Subject: [PATCH 137/152] refactor - remove unused diff files indicators (#291707) --- .../agentSessions/agentSessionsViewer.ts | 17 ----------------- .../agentSessions/media/agentsessionsviewer.css | 9 --------- 2 files changed, 26 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 0e6c259d0da..a207d53d3dd 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -60,7 +60,6 @@ interface IAgentSessionItemTemplate { // Column 2 Row 2 readonly diffContainer: HTMLElement; - readonly diffFilesSpan: HTMLSpanElement; readonly diffAddedSpan: HTMLSpanElement; readonly diffRemovedSpan: HTMLSpanElement; @@ -80,10 +79,6 @@ export interface IAgentSessionRendererOptions { getHoverPosition(): HoverPosition; } -// TODO@bpasero figure out these defaults going forward -const SESSION_BADGE_ENABLED = false; -const SESSION_DIFF_FILES_INDICATOR = false; - export class AgentSessionRenderer extends Disposable implements ICompressibleTreeRenderer { static readonly TEMPLATE_ID = 'agent-session'; @@ -121,7 +116,6 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre h('div.agent-session-details-row', [ h('div.agent-session-diff-container@diffContainer', [ - h('span.agent-session-diff-files@filesSpan'), h('span.agent-session-diff-added@addedSpan'), h('span.agent-session-diff-removed@removedSpan') ]), @@ -150,7 +144,6 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre title: disposables.add(new IconLabel(elements.title, { supportHighlights: true, supportIcons: true })), titleToolbar, diffContainer: elements.diffContainer, - diffFilesSpan: elements.filesSpan, diffAddedSpan: elements.addedSpan, diffRemovedSpan: elements.removedSpan, badge: elements.badge, @@ -168,7 +161,6 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre // Clear old state template.elementDisposable.clear(); - template.diffFilesSpan.textContent = ''; template.diffAddedSpan.textContent = ''; template.diffRemovedSpan.textContent = ''; template.badge.textContent = ''; @@ -199,7 +191,6 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre } } template.diffContainer.classList.toggle('has-diff', hasDiff); - template.diffFilesSpan.classList.toggle('has-diff-file-indicator', hasDiff && SESSION_DIFF_FILES_INDICATOR); let hasAgentSessionChanges = false; if ( @@ -236,10 +227,6 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre } private renderBadge(session: ITreeNode, template: IAgentSessionItemTemplate): boolean { - if (!SESSION_BADGE_ENABLED) { - return false; - } - const badge = session.element.badge; if (badge) { this.renderMarkdownOrText(badge, template.badge, template.elementDisposable); @@ -270,10 +257,6 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre return false; } - if (diff.files > 0 && SESSION_DIFF_FILES_INDICATOR) { - template.diffFilesSpan.textContent = diff.files === 1 ? localize('diffFile', "1 file") : localize('diffFiles', "{0} files", diff.files); - } - if (diff.insertions >= 0 /* render even `0` for more homogeneity */) { template.diffAddedSpan.textContent = `+${diff.insertions}`; } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css index 2d5ce428880..a4a142f69e7 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css @@ -26,7 +26,6 @@ background-color: unset; outline: 1px solid var(--vscode-agentSessionSelectedBadge-border); - .agent-session-diff-files, .agent-session-diff-added, .agent-session-diff-removed { color: unset; @@ -167,14 +166,6 @@ display: none; } - .agent-session-diff-files { - color: var(--vscode-descriptionForeground); - - &:not(.has-diff-file-indicator) { - display: none; - } - } - .agent-session-diff-added { color: var(--vscode-chat-linesAddedForeground); } From b3f114a5067917dc11415edbe2536035d718b435 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 29 Jan 2026 11:49:39 -0800 Subject: [PATCH 138/152] feat: implement auto-resize for freeform textarea in ask questions (#291680) * feat: implement auto-resize for freeform textarea in chat question carousel * fix: use auto exapnding textarea for all freeform input * review comments * fix: tests --- .../chatQuestionCarouselPart.ts | 76 +++++++++++++++---- .../media/chatQuestionCarousel.css | 13 +++- .../chatQuestionCarouselPart.test.ts | 4 +- 3 files changed, 72 insertions(+), 21 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 2986268d84f..90ab82a4b99 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts @@ -13,9 +13,8 @@ import { hasKey } from '../../../../../../base/common/types.js'; import { localize } from '../../../../../../nls.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IMarkdownRendererService } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; -import { defaultButtonStyles, defaultInputBoxStyles } from '../../../../../../platform/theme/browser/defaultStyles.js'; +import { defaultButtonStyles } from '../../../../../../platform/theme/browser/defaultStyles.js'; import { Button } from '../../../../../../base/browser/ui/button/button.js'; -import { InputBox } from '../../../../../../base/browser/ui/inputbox/inputBox.js'; import { IChatQuestion, IChatQuestionCarousel } from '../../../common/chatService/chatService.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; import { ChatQueryTitlePart } from './chatConfirmationWidget.js'; @@ -47,7 +46,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent private _isSkipped = false; - private readonly _textInputBoxes: Map = new Map(); + private readonly _textInputTextareas: Map = new Map(); private readonly _radioInputs: Map = new Map(); private readonly _checkboxInputs: Map = new Map(); private readonly _freeformTextareas: Map = new Map(); @@ -233,7 +232,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent // Dispose interactive UI disposables (header, nav buttons, etc.) this._interactiveUIStore.clear(); this._inputBoxes.clear(); - this._textInputBoxes.clear(); + this._textInputTextareas.clear(); this._radioInputs.clear(); this._checkboxInputs.clear(); this._freeformTextareas.clear(); @@ -352,7 +351,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent // Clear previous input boxes and stale references this._inputBoxes.clear(); - this._textInputBoxes.clear(); + this._textInputTextareas.clear(); this._radioInputs.clear(); this._checkboxInputs.clear(); this._freeformTextareas.clear(); @@ -423,24 +422,55 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent } } + /** + * Sets up auto-resize behavior for a textarea element. + * @returns A function that triggers the resize manually (useful for initial sizing). + */ + private setupTextareaAutoResize(textarea: HTMLTextAreaElement): () => void { + const autoResize = () => { + textarea.style.height = 'auto'; + textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`; + this._onDidChangeHeight.fire(); + }; + this._inputBoxes.add(dom.addDisposableListener(textarea, dom.EventType.INPUT, autoResize)); + return autoResize; + } + private renderTextInput(container: HTMLElement, question: IChatQuestion): void { - const inputBox = this._inputBoxes.add(new InputBox(container, undefined, { - placeholder: localize('chat.questionCarousel.enterText', 'Enter your answer'), - inputBoxStyles: defaultInputBoxStyles, - })); + const textarea = dom.$('textarea.chat-question-text-textarea'); + textarea.placeholder = localize('chat.questionCarousel.enterText', 'Enter your answer'); + textarea.rows = 1; + textarea.setAttribute('aria-label', question.title); // Restore previous answer if exists const previousAnswer = this._answers.get(question.id); if (previousAnswer !== undefined) { - inputBox.value = String(previousAnswer); + textarea.value = String(previousAnswer); } else if (question.defaultValue !== undefined) { - inputBox.value = String(question.defaultValue); + textarea.value = String(question.defaultValue); } - this._textInputBoxes.set(question.id, inputBox); + // Setup auto-resize behavior + const autoResize = this.setupTextareaAutoResize(textarea); + + // Handle Enter to submit (Shift+Enter for newline) + this._inputBoxes.add(dom.addDisposableListener(textarea, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { + const event = new StandardKeyboardEvent(e); + if (event.keyCode === KeyCode.Enter && !event.shiftKey && textarea.value.trim()) { + e.preventDefault(); + e.stopPropagation(); + this.handleNext(); + } + })); + + container.appendChild(textarea); + this._textInputTextareas.set(question.id, textarea); // Focus on input when rendered using proper DOM scheduling - this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(inputBox.element), () => inputBox.focus())); + this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(textarea), () => { + textarea.focus(); + autoResize(); + })); } private renderSingleSelect(container: HTMLElement, question: IChatQuestion): void { @@ -520,6 +550,9 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent } })); + // Setup auto-resize behavior + const autoResize = this.setupTextareaAutoResize(freeformTextarea); + // uncheck radio when there is text this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.INPUT, () => { if (freeformTextarea.value.trim()) { @@ -532,6 +565,11 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent freeformContainer.appendChild(freeformTextarea); container.appendChild(freeformContainer); this._freeformTextareas.set(question.id, freeformTextarea); + + // Resize textarea if it has restored content + if (previousFreeform !== undefined) { + this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(freeformTextarea), () => autoResize())); + } } } @@ -613,11 +651,19 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent } })); + // Setup auto-resize behavior + const autoResize = this.setupTextareaAutoResize(freeformTextarea); + // For multiSelect, both checkboxes and freeform input are combined, so don't uncheck on input freeformContainer.appendChild(freeformTextarea); container.appendChild(freeformContainer); this._freeformTextareas.set(question.id, freeformTextarea); + + // Resize textarea if it has restored content + if (previousFreeform !== undefined) { + this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(freeformTextarea), () => autoResize())); + } } } @@ -629,8 +675,8 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent switch (question.type) { case 'text': { - const inputBox = this._textInputBoxes.get(question.id); - return inputBox?.value ?? question.defaultValue; + const textarea = this._textInputTextareas.get(question.id); + return textarea?.value ?? question.defaultValue; } case 'singleSelect': { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css index 772b09d77c7..9d2697de0f7 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css @@ -222,25 +222,30 @@ color: var(--vscode-descriptionForeground); } -.chat-question-freeform-textarea { +.chat-question-freeform-textarea, +.chat-question-text-textarea { width: 100%; + min-height: 32px; max-height: 200px; padding: 6px 8px; border: 1px solid var(--vscode-input-border, var(--vscode-chat-requestBorder)); background-color: var(--vscode-input-background); color: var(--vscode-input-foreground); border-radius: 4px; - resize: vertical; + resize: none; font-family: var(--vscode-chat-font-family, inherit); font-size: var(--vscode-chat-font-size-body-s); box-sizing: border-box; + overflow-y: hidden; } -.chat-question-freeform-textarea:focus { +.chat-question-freeform-textarea:focus, +.chat-question-text-textarea:focus { outline: 1px solid var(--vscode-focusBorder); border-color: var(--vscode-focusBorder); } -.chat-question-freeform-textarea::placeholder { +.chat-question-freeform-textarea::placeholder, +.chat-question-text-textarea::placeholder { color: var(--vscode-input-placeholderForeground); } diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts index 552a3b3daf2..304216ea7f6 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts @@ -96,8 +96,8 @@ suite('ChatQuestionCarouselPart', () => { const inputContainer = widget.domNode.querySelector('.chat-question-input-container'); assert.ok(inputContainer); - const inputBox = inputContainer?.querySelector('.monaco-inputbox'); - assert.ok(inputBox, 'Should have an input box for text questions'); + const textarea = inputContainer?.querySelector('textarea.chat-question-text-textarea'); + assert.ok(textarea, 'Should have a textarea for text questions'); }); test('renders radio buttons for singleSelect type questions', () => { From c026062cfddbf74c3150a8ae5ae57d371ed02370 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 29 Jan 2026 11:52:57 -0800 Subject: [PATCH 139/152] chat: add post-approval feedback message (#291712) Adds display of a localized 'Approve tool result?' message when a chat response is waiting for post-approval confirmation on a tool invocation. - Updates ChatResponseModel to detect WaitingForPostApproval state - Displays appropriate confirmation message to user during tool approval - Improves UX by providing clear feedback on what action is needed (Commit message generated by Copilot) --- src/vs/workbench/contrib/chat/common/model/chatModel.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index 3c25e3b1f59..b9b87b8a0ea 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -1030,6 +1030,9 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel const title = state.confirmationMessages?.title; return title ? (isMarkdownString(title) ? title.value : title) : undefined; } + if (state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { + return localize('waitingForPostApproval', "Approve tool result?"); + } } if (part.kind === 'confirmation' && !part.isUsed) { return part.title; From 8936d7e785a0efd4f6b9d928f227a7bdb05f9cda Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:01:31 -0800 Subject: [PATCH 140/152] Bump tar from 7.5.6 to 7.5.7 in /build/npm/gyp (#291362) Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.6 to 7.5.7. - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v7.5.6...v7.5.7) --- updated-dependencies: - dependency-name: tar dependency-version: 7.5.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build/npm/gyp/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/npm/gyp/package-lock.json b/build/npm/gyp/package-lock.json index 1ca858e42d2..a4ef0b2fada 100644 --- a/build/npm/gyp/package-lock.json +++ b/build/npm/gyp/package-lock.json @@ -1069,9 +1069,9 @@ } }, "node_modules/tar": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.6.tgz", - "integrity": "sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { From 70529729e608d50528ca02990523b409a9713daa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 29 Jan 2026 21:16:43 +0100 Subject: [PATCH 141/152] engineering: use tar+zstd for win32 node_modules cache (#291624) * engineering: use tar+zstd for win32 node_modules cache * bump cache * Update .github/workflows/pr-node-modules.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update build/azure-pipelines/win32/product-build-win32-node-modules.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update build/azure-pipelines/win32/steps/product-build-win32-compile.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update .github/workflows/pr-win32-test.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * bump cache salt * more fixes --------- Co-authored-by: Aman Karmani Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/pr-node-modules.yml | 2 +- .github/workflows/pr-win32-test.yml | 4 ++-- build/.cachesalt | 2 +- .../win32/product-build-win32-node-modules.yml | 2 +- .../win32/steps/product-build-win32-compile.yml | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr-node-modules.yml b/.github/workflows/pr-node-modules.yml index 68e65fd1298..4ccefc8435b 100644 --- a/.github/workflows/pr-node-modules.yml +++ b/.github/workflows/pr-node-modules.yml @@ -282,4 +282,4 @@ jobs: $ErrorActionPreference = "Stop" exec { node build/azure-pipelines/common/listNodeModules.ts .build/node_modules_list.txt } exec { mkdir -Force .build/node_modules_cache } - exec { 7z.exe a .build/node_modules_cache/cache.7z -mx3 `@.build/node_modules_list.txt } + exec { & "C:\Program Files\Git\usr\bin\tar.exe" --posix -cf .build/node_modules_cache/cache.tzst --exclude cache.tzst -C . --files-from .build/node_modules_list.txt --force-local --use-compress-program "zstd -T0" } diff --git a/.github/workflows/pr-win32-test.yml b/.github/workflows/pr-win32-test.yml index bd4a62d42fa..ceda82f998a 100644 --- a/.github/workflows/pr-win32-test.yml +++ b/.github/workflows/pr-win32-test.yml @@ -47,7 +47,7 @@ jobs: - name: Extract node_modules cache if: steps.node-modules-cache.outputs.cache-hit == 'true' shell: pwsh - run: 7z.exe x .build/node_modules_cache/cache.7z -aoa + run: '& "C:\Program Files\Git\usr\bin\tar.exe" -xf .build/node_modules_cache/cache.tzst -C . --force-local --use-compress-program "zstd -d"' - name: Install dependencies if: steps.node-modules-cache.outputs.cache-hit != 'true' @@ -86,7 +86,7 @@ jobs: $ErrorActionPreference = "Stop" exec { node build/azure-pipelines/common/listNodeModules.ts .build/node_modules_list.txt } exec { mkdir -Force .build/node_modules_cache } - exec { 7z.exe a .build/node_modules_cache/cache.7z -mx3 `@.build/node_modules_list.txt } + exec { & "C:\Program Files\Git\usr\bin\tar.exe" --posix -cf .build/node_modules_cache/cache.tzst --exclude cache.tzst -C . --files-from .build/node_modules_list.txt --force-local --use-compress-program "zstd -T0" } - name: Create .build folder shell: pwsh diff --git a/build/.cachesalt b/build/.cachesalt index a90b8e833cb..1c96e74a984 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2026-01-23T20:55:53.631Z +2026-01-29T15:20:27.797Z diff --git a/build/azure-pipelines/win32/product-build-win32-node-modules.yml b/build/azure-pipelines/win32/product-build-win32-node-modules.yml index 6780073f57a..bcb0704d7f2 100644 --- a/build/azure-pipelines/win32/product-build-win32-node-modules.yml +++ b/build/azure-pipelines/win32/product-build-win32-node-modules.yml @@ -90,6 +90,6 @@ jobs: $ErrorActionPreference = "Stop" exec { node build/azure-pipelines/common/listNodeModules.ts .build/node_modules_list.txt } exec { mkdir -Force .build/node_modules_cache } - exec { 7z.exe a .build/node_modules_cache/cache.7z -mx3 `@.build/node_modules_list.txt } + exec { & "C:\Program Files\Git\usr\bin\tar.exe" --posix -cf .build/node_modules_cache/cache.tzst --exclude cache.tzst -C . --files-from .build/node_modules_list.txt --force-local --use-compress-program "zstd -T0" } condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Create node_modules archive diff --git a/build/azure-pipelines/win32/steps/product-build-win32-compile.yml b/build/azure-pipelines/win32/steps/product-build-win32-compile.yml index d6412c23420..39880ce7840 100644 --- a/build/azure-pipelines/win32/steps/product-build-win32-compile.yml +++ b/build/azure-pipelines/win32/steps/product-build-win32-compile.yml @@ -65,7 +65,7 @@ steps: cacheHitVar: NODE_MODULES_RESTORED displayName: Restore node_modules cache - - powershell: 7z.exe x .build/node_modules_cache/cache.7z -aoa + - powershell: '& "C:\Program Files\Git\usr\bin\tar.exe" -xf .build/node_modules_cache/cache.tzst -C . --force-local --use-compress-program "zstd -d"' condition: and(succeeded(), eq(variables.NODE_MODULES_RESTORED, 'true')) displayName: Extract node_modules cache @@ -110,7 +110,7 @@ steps: $ErrorActionPreference = "Stop" exec { node build/azure-pipelines/common/listNodeModules.ts .build/node_modules_list.txt } exec { mkdir -Force .build/node_modules_cache } - exec { 7z.exe a .build/node_modules_cache/cache.7z -mx3 `@.build/node_modules_list.txt } + exec { & "C:\Program Files\Git\usr\bin\tar.exe" --posix -cf .build/node_modules_cache/cache.tzst --exclude cache.tzst -C . --files-from .build/node_modules_list.txt --force-local --use-compress-program "zstd -T0" } condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Create node_modules archive From c534163aad084167299bb642b51576138891a553 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 29 Jan 2026 21:19:58 +0100 Subject: [PATCH 142/152] update instructions (#291715) * update instructions * . --- .github/copilot-instructions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b5c5a087962..3ec839df4ef 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -139,6 +139,7 @@ function f(x: number, y: string): void { } - When adding tooltips to UI elements, prefer the use of IHoverService service. - Do not duplicate code. Always look for existing utility functions, helpers, or patterns in the codebase before implementing new functionality. Reuse and extend existing code whenever possible. - You MUST deal with disposables by registering them immediately after creation for later disposal. Use helpers such as `DisposableStore`, `MutableDisposable` or `DisposableMap`. Do NOT register a disposable to the containing class if the object is created within a method that is called repeadedly to avoid leaks. Instead, return a `IDisposable` from such method and let the caller register it. +- You MUST NOT use storage keys of another component only to make changes to that component. You MUST come up with proper API to change another component. ## Learnings - Minimize the amount of assertions in tests. Prefer one snapshot-style `assert.deepStrictEqual` over multiple precise assertions, as they are much more difficult to understand and to update. From fd3565eb6bd06e688a98cabd029088c16390e62e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 20:25:06 +0000 Subject: [PATCH 143/152] Bump tar and dmg-builder in /build (#291331) Bumps [tar](https://github.com/isaacs/node-tar) to 7.5.7 and updates ancestor dependency [dmg-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/dmg-builder). These dependencies need to be updated together. Updates `tar` from 6.2.1 to 7.5.7 - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v6.2.1...v7.5.7) Updates `dmg-builder` from 26.5.0 to 26.6.0 - [Release notes](https://github.com/electron-userland/electron-builder/releases) - [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/dmg-builder/CHANGELOG.md) - [Commits](https://github.com/electron-userland/electron-builder/commits/electron-builder@26.6.0/packages/dmg-builder) --- updated-dependencies: - dependency-name: tar dependency-version: 7.5.7 dependency-type: indirect - dependency-name: dmg-builder dependency-version: 26.6.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build/package-lock.json | 365 ++++++++++++++-------------------------- build/package.json | 2 +- 2 files changed, 126 insertions(+), 241 deletions(-) diff --git a/build/package-lock.json b/build/package-lock.json index 1a544ba854f..542c66d69f4 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -53,7 +53,7 @@ "ansi-colors": "^3.2.3", "byline": "^5.0.0", "debug": "^4.3.2", - "dmg-builder": "^26.5.0", + "dmg-builder": "^26.6.0", "esbuild": "0.27.2", "extract-zip": "^2.0.1", "gulp-merge-json": "^2.1.1", @@ -813,14 +813,13 @@ } }, "node_modules/@electron/rebuild": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.1.tgz", - "integrity": "sha512-iMGXb6Ib7H/Q3v+BKZJoETgF9g6KMNZVbsO4b7Dmpgb5qTFqyFTzqW9F3TOSHdybv2vKYKzSS9OiZL+dcJb+1Q==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", + "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", "dev": true, "license": "MIT", "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", - "chalk": "^4.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "got": "^11.7.0", @@ -831,7 +830,7 @@ "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", - "tar": "^6.0.5", + "tar": "^7.5.6", "yargs": "^17.0.1" }, "bin": { @@ -841,142 +840,6 @@ "node": ">=22.12.0" } }, - "node_modules/@electron/rebuild/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@electron/rebuild/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@electron/rebuild/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/rebuild/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@electron/rebuild/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@electron/rebuild/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@electron/rebuild/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/rebuild/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/rebuild/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/rebuild/node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@electron/rebuild/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@electron/rebuild/node_modules/node-abi": { "version": "4.26.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.26.0.tgz", @@ -1003,38 +866,6 @@ "node": ">=10" } }, - "node_modules/@electron/rebuild/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/rebuild/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@electron/universal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", @@ -3136,18 +2967,19 @@ "license": "MIT" }, "node_modules/app-builder-lib": { - "version": "26.5.0", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.5.0.tgz", - "integrity": "sha512-iRRiJhM0uFMauDeIuv8ESHZSn+LESbdDEuHi7rKdeETjrvBObecXnWJx1f3vs3KtoGcd3hCk1zURKypyvZOtFQ==", + "version": "26.6.0", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.6.0.tgz", + "integrity": "sha512-P2naoSaGOqJY54cqTceO9lms2M790UM7BA8AlOuaolQhRp/LOshAVc4vzVlYFw4YNPtiuBJqdAhWALuoEKnayQ==", "dev": true, "license": "MIT", "dependencies": { "@develar/schema-utils": "~2.6.5", "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", - "@electron/rebuild": "4.0.1", + "@electron/rebuild": "^4.0.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", @@ -3160,7 +2992,7 @@ "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", - "electron-publish": "26.4.1", + "electron-publish": "26.6.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", @@ -3170,9 +3002,10 @@ "lazy-val": "^1.0.5", "minimatch": "^10.0.3", "plist": "3.1.0", + "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", - "tar": "7.5.3", + "tar": "^7.5.6", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "which": "^5.0.0" @@ -3181,8 +3014,55 @@ "node": ">=14.0.0" }, "peerDependencies": { - "dmg-builder": "26.5.0", - "electron-builder-squirrel-windows": "26.5.0" + "dmg-builder": "26.6.0", + "electron-builder-squirrel-windows": "26.6.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, "node_modules/app-builder-lib/node_modules/@electron/osx-sign": { @@ -3242,6 +3122,29 @@ "node": ">=12" } }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/app-builder-lib/node_modules/isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", @@ -3265,19 +3168,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/app-builder-lib/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/app-builder-lib/node_modules/minimatch": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", @@ -3307,16 +3197,6 @@ "node": ">=10" } }, - "node_modules/app-builder-lib/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/app-builder-lib/node_modules/which": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", @@ -3398,6 +3278,13 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, "node_modules/async-done": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", @@ -4595,13 +4482,13 @@ } }, "node_modules/dmg-builder": { - "version": "26.5.0", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.5.0.tgz", - "integrity": "sha512-AyOCzpS1TCxDkSWxAzpfw5l7jBX4C8jKCucmT/6y6/24H5VKSHpjcVJD0W8o5BrFi+skC7Z7+F4aNyHmvn4AAw==", + "version": "26.6.0", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.6.0.tgz", + "integrity": "sha512-IkGlOLfJ3q7y9iaDMnNSArDdPg3Ntx8Ps6aL7yTEIpL6znA+t5L/LRTAGFz1J/12hM/NiNEYg0LoBEheqGdZXw==", "dev": true, "license": "MIT", "dependencies": { - "app-builder-lib": "26.5.0", + "app-builder-lib": "26.6.0", "builder-util": "26.4.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", @@ -4883,9 +4770,9 @@ } }, "node_modules/electron-publish": { - "version": "26.4.1", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.4.1.tgz", - "integrity": "sha512-nByal9K5Ar3BNJUfCSglXltpKUhJqpwivNpKVHnkwxTET9LKl+NxoojpGF1dSXVFcoBKVm+OhsVa28ZsoshEPA==", + "version": "26.6.0", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.6.0.tgz", + "integrity": "sha512-LsyHMMqbvJ2vsOvuWJ19OezgF2ANdCiHpIucDHNiLhuI+/F3eW98ouzWSRmXXi82ZOPZXC07jnIravY4YYwCLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4893,7 +4780,7 @@ "builder-util": "26.4.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", - "form-data": "^4.0.0", + "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" @@ -5513,9 +5400,9 @@ "dev": true }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, "license": "MIT", "dependencies": { @@ -6332,13 +6219,6 @@ "node": ">=10" } }, - "node_modules/jake/node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -7011,19 +6891,6 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -7907,6 +7774,25 @@ "node": ">=10" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -9054,10 +8940,9 @@ } }, "node_modules/tar": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.3.tgz", - "integrity": "sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { diff --git a/build/package.json b/build/package.json index e45161dc2c3..82a9974ba79 100644 --- a/build/package.json +++ b/build/package.json @@ -47,7 +47,7 @@ "ansi-colors": "^3.2.3", "byline": "^5.0.0", "debug": "^4.3.2", - "dmg-builder": "^26.5.0", + "dmg-builder": "^26.6.0", "esbuild": "0.27.2", "extract-zip": "^2.0.1", "gulp-merge-json": "^2.1.1", From acfc3629374a386ac1090b4155a229c250436854 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 20:51:53 +0000 Subject: [PATCH 144/152] Bump tar from 7.5.6 to 7.5.7 (#291723) Bumps tar from 7.5.6 to 7.5.7. --- updated-dependencies: - dependency-name: tar dependency-version: 7.5.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index b198a0d53d6..801e87b04c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -149,7 +149,7 @@ "source-map": "0.6.1", "source-map-support": "^0.3.2", "style-loader": "^3.3.2", - "tar": "^7.5.4", + "tar": "^7.5.7", "ts-loader": "^9.5.1", "tsec": "0.2.7", "tslib": "^2.6.3", @@ -16377,9 +16377,9 @@ } }, "node_modules/tar": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.6.tgz", - "integrity": "sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", diff --git a/package.json b/package.json index 6c0299c23ec..9997a328a33 100644 --- a/package.json +++ b/package.json @@ -211,7 +211,7 @@ "source-map": "0.6.1", "source-map-support": "^0.3.2", "style-loader": "^3.3.2", - "tar": "^7.5.4", + "tar": "^7.5.7", "ts-loader": "^9.5.1", "tsec": "0.2.7", "tslib": "^2.6.3", From 9fbb7b870847322097301203a1ba7aa318c1decf Mon Sep 17 00:00:00 2001 From: dileepyavan <52841896+dileepyavan@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:14:59 -0800 Subject: [PATCH 145/152] In sandbox mode for chatUI, the command should be displayed without sandbox command and environment variables (#291395) changes --- .../toolInvocationParts/chatTerminalToolProgressPart.ts | 2 +- .../workbench/contrib/chat/common/chatService/chatService.ts | 2 ++ .../browser/tools/commandLineRewriter/commandLineRewriter.ts | 2 ++ .../tools/commandLineRewriter/commandLineSandboxRewriter.ts | 3 ++- .../chatAgentTools/browser/tools/runInTerminalTool.ts | 3 +++ 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts index c5eae902688..47580c82591 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts @@ -274,7 +274,7 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart ]); this._titleElement = elements.title; - const command = (terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original).trimStart(); + const command = (terminalData.commandLine.forDisplay ?? terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original).trimStart(); this._commandText = command; this._terminalOutputContextKey = ChatContextKeys.inChatTerminalToolOutput.bindTo(this._contextKeyService); diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 313657bbe42..75b4f81832a 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -413,6 +413,8 @@ export interface IChatTerminalToolInvocationData { original: string; userEdited?: string; toolEdited?: string; + // command to show in the chat UI (potentially different from what is actually run in the terminal) + forDisplay?: string; }; /** The working directory URI for the terminal */ cwd?: UriComponents; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineRewriter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineRewriter.ts index 1ba417cd3fe..3a3c4a3c955 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineRewriter.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineRewriter.ts @@ -22,4 +22,6 @@ export interface ICommandLineRewriterOptions { export interface ICommandLineRewriterResult { rewritten: string; reasoning: string; + //for scenarios where we want to show a different command in the chat UI than what is actually run in the terminal + forDisplay?: string; } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineSandboxRewriter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineSandboxRewriter.ts index a85cb243ba9..030afe5a76a 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineSandboxRewriter.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineSandboxRewriter.ts @@ -29,7 +29,8 @@ export class CommandLineSandboxRewriter extends Disposable implements ICommandLi const wrappedCommand = this._sandboxService.wrapCommand(options.commandLine); return { rewritten: wrappedCommand, - reasoning: 'Wrapped command for sandbox execution' + reasoning: 'Wrapped command for sandbox execution', + forDisplay: options.commandLine, // show the command that is passed as input. In this case, the output from CommandLinePreventHistoryRewriter }; } } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index 3f07cbaadeb..7a986811d1e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -456,6 +456,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { const terminalCommandId = `tool-${generateUuid()}`; let rewrittenCommand: string | undefined = args.command; + let forDisplayCommand: string | undefined = undefined; for (const rewriter of this._commandLineRewriters) { const rewriteResult = await rewriter.rewrite({ commandLine: rewrittenCommand, @@ -465,6 +466,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { }); if (rewriteResult) { rewrittenCommand = rewriteResult.rewritten; + forDisplayCommand = rewriteResult.forDisplay; this._logService.info(`RunInTerminalTool: Command rewritten by ${rewriter.constructor.name}: ${rewriteResult.reasoning}`); } } @@ -476,6 +478,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { commandLine: { original: args.command, toolEdited: rewrittenCommand === args.command ? undefined : rewrittenCommand, + forDisplay: forDisplayCommand, }, cwd, language, From 218de2609fa95470b639b7490b26d2cff3d8b983 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:16:57 -0800 Subject: [PATCH 146/152] thinking header fix: styling and better generic text (#291462) * thinking header fix: styling and better generic text * only increment in constructor if there is thinking text * reset the timer, accidentally set to 1 for testing --- .../chatThinkingContentPart.ts | 32 ++++++++++++++++--- .../media/chatThinkingContent.css | 14 ++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index 89a4d81bbf0..0d29e172d50 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -27,7 +27,7 @@ import { Lazy } from '../../../../../../base/common/lazy.js'; import { Emitter } from '../../../../../../base/common/event.js'; import { DisposableMap, DisposableStore, IDisposable } from '../../../../../../base/common/lifecycle.js'; import { autorun } from '../../../../../../base/common/observable.js'; -import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { CancellationTokenSource } from '../../../../../../base/common/cancellation.js'; import { IChatMarkdownAnchorService } from './chatMarkdownAnchorService.js'; import { ChatMessageRole, ILanguageModelsService } from '../../../common/languageModels.js'; import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js'; @@ -189,6 +189,10 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen } this.currentThinkingValue = initialText; + if (initialText.trim()) { + this.appendedItemCount++; + } + // Alert screen reader users that thinking has started alert(localize('chat.thinking.started', 'Thinking')); @@ -618,6 +622,9 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen } private async generateTitleViaLLM(): Promise { + const cts = new CancellationTokenSource(); + const timeout = setTimeout(() => cts.cancel(), 5000); + try { let models = await this.languageModelsService.selectLanguageModels({ vendor: 'copilot', id: 'copilot-fast' }); if (!models.length) { @@ -628,6 +635,11 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen return; } + if (cts.token.isCancellationRequested) { + this.setFallbackTitle(); + return; + } + let context: string; if (this.extractedTitles.length > 0) { context = this.extractedTitles.join(', '); @@ -720,11 +732,14 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen new ExtensionIdentifier('core'), [{ role: ChatMessageRole.User, content: [{ type: 'text', value: prompt }] }], {}, - CancellationToken.None + cts.token ); let generatedTitle = ''; for await (const part of response.stream) { + if (cts.token.isCancellationRequested) { + break; + } if (Array.isArray(part)) { for (const p of part) { if (p.type === 'text') { @@ -736,6 +751,11 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen } } + if (cts.token.isCancellationRequested) { + this.setFallbackTitle(); + return; + } + await response.result; generatedTitle = generatedTitle.trim(); @@ -755,6 +775,9 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen } } catch (error) { // fall through to default title + } finally { + clearTimeout(timeout); + cts.dispose(); } this.setFallbackTitle(); @@ -784,8 +807,8 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen } private setFallbackTitle(): void { - const finalLabel = this.toolInvocationCount > 0 - ? localize('chat.thinking.finished.withTools', 'Finished working and invoked {0} tool{1}', this.toolInvocationCount, this.toolInvocationCount === 1 ? '' : 's') + const finalLabel = this.appendedItemCount > 0 + ? localize('chat.thinking.finished.withSteps', 'Finished with {0} step{1}', this.appendedItemCount, this.appendedItemCount === 1 ? '' : 's') : localize('chat.thinking.finished', 'Finished Working'); this.currentTitle = finalLabel; @@ -1150,6 +1173,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen if (this._store.isDisposed) { return; } + this.appendedItemCount++; this.textContainer = $('.chat-thinking-item.markdown-content'); if (content.value) { // Use lazy rendering when collapsed to preserve order with tool items diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css index 6b310d88306..2a250a99d7a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css @@ -17,6 +17,20 @@ margin: 0px; } + > .chat-used-context-label .monaco-button.monaco-icon-button { + line-height: 1.5em; + font-size: var(--vscode-chat-font-size-body-m); + margin-top: 1px; + + .codicon { + font-size: 13px; + } + + .codicon::before { + font-size: var(--vscode-chat-font-size-body-s); + } + } + /* shimmer animation stuffs */ .chat-thinking-spinner-item .chat-thinking-spinner-label { background: linear-gradient(90deg, From 000e601e5e27cd796fd1664e84f1206ab25a145c Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:48:29 -0800 Subject: [PATCH 147/152] Revert "engineering: use tar+zstd for win32 node_modules cache" (#291740) Revert "engineering: use tar+zstd for win32 node_modules cache (#291624)" This reverts commit 70529729e608d50528ca02990523b409a9713daa. --- .github/workflows/pr-node-modules.yml | 2 +- .github/workflows/pr-win32-test.yml | 4 ++-- build/.cachesalt | 2 +- .../win32/product-build-win32-node-modules.yml | 2 +- .../win32/steps/product-build-win32-compile.yml | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr-node-modules.yml b/.github/workflows/pr-node-modules.yml index 4ccefc8435b..68e65fd1298 100644 --- a/.github/workflows/pr-node-modules.yml +++ b/.github/workflows/pr-node-modules.yml @@ -282,4 +282,4 @@ jobs: $ErrorActionPreference = "Stop" exec { node build/azure-pipelines/common/listNodeModules.ts .build/node_modules_list.txt } exec { mkdir -Force .build/node_modules_cache } - exec { & "C:\Program Files\Git\usr\bin\tar.exe" --posix -cf .build/node_modules_cache/cache.tzst --exclude cache.tzst -C . --files-from .build/node_modules_list.txt --force-local --use-compress-program "zstd -T0" } + exec { 7z.exe a .build/node_modules_cache/cache.7z -mx3 `@.build/node_modules_list.txt } diff --git a/.github/workflows/pr-win32-test.yml b/.github/workflows/pr-win32-test.yml index ceda82f998a..bd4a62d42fa 100644 --- a/.github/workflows/pr-win32-test.yml +++ b/.github/workflows/pr-win32-test.yml @@ -47,7 +47,7 @@ jobs: - name: Extract node_modules cache if: steps.node-modules-cache.outputs.cache-hit == 'true' shell: pwsh - run: '& "C:\Program Files\Git\usr\bin\tar.exe" -xf .build/node_modules_cache/cache.tzst -C . --force-local --use-compress-program "zstd -d"' + run: 7z.exe x .build/node_modules_cache/cache.7z -aoa - name: Install dependencies if: steps.node-modules-cache.outputs.cache-hit != 'true' @@ -86,7 +86,7 @@ jobs: $ErrorActionPreference = "Stop" exec { node build/azure-pipelines/common/listNodeModules.ts .build/node_modules_list.txt } exec { mkdir -Force .build/node_modules_cache } - exec { & "C:\Program Files\Git\usr\bin\tar.exe" --posix -cf .build/node_modules_cache/cache.tzst --exclude cache.tzst -C . --files-from .build/node_modules_list.txt --force-local --use-compress-program "zstd -T0" } + exec { 7z.exe a .build/node_modules_cache/cache.7z -mx3 `@.build/node_modules_list.txt } - name: Create .build folder shell: pwsh diff --git a/build/.cachesalt b/build/.cachesalt index 1c96e74a984..a90b8e833cb 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2026-01-29T15:20:27.797Z +2026-01-23T20:55:53.631Z diff --git a/build/azure-pipelines/win32/product-build-win32-node-modules.yml b/build/azure-pipelines/win32/product-build-win32-node-modules.yml index bcb0704d7f2..6780073f57a 100644 --- a/build/azure-pipelines/win32/product-build-win32-node-modules.yml +++ b/build/azure-pipelines/win32/product-build-win32-node-modules.yml @@ -90,6 +90,6 @@ jobs: $ErrorActionPreference = "Stop" exec { node build/azure-pipelines/common/listNodeModules.ts .build/node_modules_list.txt } exec { mkdir -Force .build/node_modules_cache } - exec { & "C:\Program Files\Git\usr\bin\tar.exe" --posix -cf .build/node_modules_cache/cache.tzst --exclude cache.tzst -C . --files-from .build/node_modules_list.txt --force-local --use-compress-program "zstd -T0" } + exec { 7z.exe a .build/node_modules_cache/cache.7z -mx3 `@.build/node_modules_list.txt } condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Create node_modules archive diff --git a/build/azure-pipelines/win32/steps/product-build-win32-compile.yml b/build/azure-pipelines/win32/steps/product-build-win32-compile.yml index 39880ce7840..d6412c23420 100644 --- a/build/azure-pipelines/win32/steps/product-build-win32-compile.yml +++ b/build/azure-pipelines/win32/steps/product-build-win32-compile.yml @@ -65,7 +65,7 @@ steps: cacheHitVar: NODE_MODULES_RESTORED displayName: Restore node_modules cache - - powershell: '& "C:\Program Files\Git\usr\bin\tar.exe" -xf .build/node_modules_cache/cache.tzst -C . --force-local --use-compress-program "zstd -d"' + - powershell: 7z.exe x .build/node_modules_cache/cache.7z -aoa condition: and(succeeded(), eq(variables.NODE_MODULES_RESTORED, 'true')) displayName: Extract node_modules cache @@ -110,7 +110,7 @@ steps: $ErrorActionPreference = "Stop" exec { node build/azure-pipelines/common/listNodeModules.ts .build/node_modules_list.txt } exec { mkdir -Force .build/node_modules_cache } - exec { & "C:\Program Files\Git\usr\bin\tar.exe" --posix -cf .build/node_modules_cache/cache.tzst --exclude cache.tzst -C . --files-from .build/node_modules_list.txt --force-local --use-compress-program "zstd -T0" } + exec { 7z.exe a .build/node_modules_cache/cache.7z -mx3 `@.build/node_modules_list.txt } condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Create node_modules archive From 6b7d765cc06b227342b9f6727aca336defe1d12e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 22:02:09 +0000 Subject: [PATCH 148/152] Fix chat terminal streaming chevron vertical alignment (#291650) --- .../chatContentParts/media/chatTerminalToolProgressPart.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTerminalToolProgressPart.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTerminalToolProgressPart.css index 397f482c9f4..e93c799c012 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTerminalToolProgressPart.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTerminalToolProgressPart.css @@ -107,7 +107,7 @@ display: flex; gap: 4px; margin-left: auto; - align-self: flex-start; + align-self: center; } .chat-terminal-content-part .chat-terminal-content-message { From 07e6e58af5c7a4e031d5d8f924f6a96196dd28f5 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 22:23:14 +0000 Subject: [PATCH 149/152] Fix excessive padding on terminal tool progress part action bar icons (#291648) --- .../chatContentParts/media/chatTerminalToolProgressPart.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTerminalToolProgressPart.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTerminalToolProgressPart.css index e93c799c012..e5bb45e1b57 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTerminalToolProgressPart.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTerminalToolProgressPart.css @@ -110,6 +110,11 @@ align-self: center; } +/* Reset margin for action bar icons - they should have similar spacing to other action bars */ +.chat-terminal-content-part .chat-terminal-action-bar .monaco-action-bar .codicon { + margin: 0; +} + .chat-terminal-content-part .chat-terminal-content-message { .rendered-markdown { white-space: normal; From aa328e0fed07df01d7f0a0cf2c16e6fef17c5b76 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 29 Jan 2026 22:36:56 +0000 Subject: [PATCH 150/152] Ensure we read the observable on the right input part (#291748) Hoping to fix a layout issue reported by Harald but I don't think this is it. --- .../workbench/contrib/chat/browser/widget/chatWidget.ts | 8 +++++++- .../chat/browser/widgetHosts/viewPane/chatViewPane.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 11e38cc7612..c2d9c3e2392 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -561,11 +561,17 @@ export class ChatWidget extends Disposable implements IChatWidget { return this._attachmentCapabilities; } + /** + * Either the inline input (when editing) or the main input part + */ get input(): ChatInputPart { return this.viewModel?.editing && this.configurationService.getValue('chat.editRequests') !== 'input' ? this.inlineInputPart : this.inputPart; } - private get inputPart(): ChatInputPart { + /** + * The main input part at the buttom of the chat widget. Use `input` to get the active input (main or inline editing part). + */ + get inputPart(): ChatInputPart { return this.inputPartDisposable.value!; } diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index b8265052b4e..791b0da55c1 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -606,7 +606,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { // When showing sessions stacked, adjust the height of the sessions list to make room for chat input this._register(autorun(reader => { - chatWidget.input.height.read(reader); + chatWidget.inputPart.height.read(reader); if (this.sessionsViewerVisible && this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { this.relayout(); } From 9eaa9ff9b2b83eae4c99c914866124e8ec888004 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 29 Jan 2026 15:00:16 -0800 Subject: [PATCH 151/152] chat: fix control flow logic in tool invocation state transition (#291743) Fixes the incorrect use of 'if' instead of 'else if' in the tool invocation state transition logic. This ensures that the confirmation check is only performed when autoConfirmed is falsy, preventing logic errors during tool invocation processing. Fixes https://github.com/microsoft/vscode/issues/291453 Fixes https://github.com/microsoft/vscode/issues/290250 (Commit message generated by Copilot) --- .../chat/common/model/chatProgressTypes/chatToolInvocation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts index 3b3dc98eccd..9deddc5695b 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts @@ -190,7 +190,7 @@ export class ChatToolInvocation implements IChatToolInvocation { // Transition to the appropriate state if (autoConfirmed) { confirm(autoConfirmed); - } if (!this.confirmationMessages?.title) { + } else if (!this.confirmationMessages?.title) { this._state.set({ type: IChatToolInvocation.StateKind.Executing, confirmed: { type: ToolConfirmKind.ConfirmationNotNeeded, reason: this.confirmationMessages?.confirmationNotNeededReason }, From e83c168deff491a53bfe615777fab6b30abe6ff5 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 30 Jan 2026 00:07:27 +0100 Subject: [PATCH 152/152] fix #https://github.com/microsoft/vscode-dev/issues/1366 (#291731) --- .../common/extensionGalleryService.ts | 24 +++- .../common/extensionGalleryService.test.ts | 116 ++++++++++-------- 2 files changed, 86 insertions(+), 54 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 001e5d65b15..9772de66828 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -447,6 +447,24 @@ export function sortExtensionVersions(versions: IRawGalleryExtensionVersion[], p return versions; } +/** + * Filters extension versions to return only the relevant versions for a given target platform. + * + * This function processes a list of extension versions (expected to be sorted by version descending) + * and returns a filtered list containing: + * 1. All versions that are NOT compatible with the target platform (for other platforms) + * 2. At most one compatible release version (the first/latest one encountered) + * 3. At most one compatible pre-release version (the first/latest one encountered) + * + * When a platform-specific version (exactly matching targetPlatform) is encountered with the same + * version number as a previously stored universal/undefined version, it replaces that version. + * This ensures platform-specific builds are preferred over universal builds for the same version. + * + * @param versions - Array of extension versions, expected to be sorted by version number descending + * @param targetPlatform - The target platform to filter for (e.g., LINUX_X64, WIN32_X64) + * @param allTargetPlatforms - All target platforms the extension supports + * @returns Filtered array of versions relevant for the target platform + */ export function filterLatestExtensionVersionsForTargetPlatform(versions: IRawGalleryExtensionVersion[], targetPlatform: TargetPlatform, allTargetPlatforms: TargetPlatform[]): IRawGalleryExtensionVersion[] { const latestVersions: IRawGalleryExtensionVersion[] = []; @@ -463,19 +481,19 @@ export function filterLatestExtensionVersionsForTargetPlatform(versions: IRawGal } // For compatible versions, only include the first (latest) of each type - // Prefer specific target platform matches over undefined/universal platforms + // Prefer specific target platform matches over undefined/universal platforms only when version numbers are the same if (isPreReleaseVersion(version)) { if (preReleaseVersionIndex === -1) { preReleaseVersionIndex = latestVersions.length; latestVersions.push(version); - } else if (versionTargetPlatform === targetPlatform) { + } else if (versionTargetPlatform === targetPlatform && latestVersions[preReleaseVersionIndex].version === version.version) { latestVersions[preReleaseVersionIndex] = version; } } else { if (releaseVersionIndex === -1) { releaseVersionIndex = latestVersions.length; latestVersions.push(version); - } else if (versionTargetPlatform === targetPlatform) { + } else if (versionTargetPlatform === targetPlatform && latestVersions[releaseVersionIndex].version === version.version) { latestVersions[releaseVersionIndex] = version; } } diff --git a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts index 5dbc39a3205..415cdcb9775 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts @@ -133,19 +133,32 @@ suite('Extension Gallery Service', () => { assert.deepStrictEqual(result, versions); }); - test('should include both release and pre-release versions for same platform', () => { - const version1 = aExtensionVersion('1.0.0', TargetPlatform.WIN32_X64); - const version2 = aPreReleaseExtensionVersion('0.9.0', TargetPlatform.WIN32_X64); // Different version number - const versions = [version1, version2]; + test('should include latest release and latest pre-release versions for same platform', () => { + const release = aExtensionVersion('1.0.0', TargetPlatform.WIN32_X64); + const prerelease = aPreReleaseExtensionVersion('0.9.0', TargetPlatform.WIN32_X64); + const versions = [release, prerelease]; const allTargetPlatforms = [TargetPlatform.WIN32_X64]; const result = filterLatestExtensionVersionsForTargetPlatform(versions, TargetPlatform.WIN32_X64, allTargetPlatforms); // Should include both since they have different version numbers assert.strictEqual(result.length, 2); - assert.strictEqual(result[0], version1); - assert.strictEqual(result[1], version2); + assert.strictEqual(result[0], release); + assert.strictEqual(result[1], prerelease); + }); + test('should include latest prerelease and latest release versions for same platform', () => { + const prerelease = aPreReleaseExtensionVersion('1.1.0', TargetPlatform.WIN32_X64); + const release = aExtensionVersion('1.0.0', TargetPlatform.WIN32_X64); + const versions = [prerelease, release]; + const allTargetPlatforms = [TargetPlatform.WIN32_X64]; + + const result = filterLatestExtensionVersionsForTargetPlatform(versions, TargetPlatform.WIN32_X64, allTargetPlatforms); + + // Should include both since they have different version numbers + assert.strictEqual(result.length, 2); + assert.strictEqual(result[0], prerelease); + assert.strictEqual(result[1], release); }); test('should include one version per target platform for release versions', () => { @@ -164,33 +177,6 @@ suite('Extension Gallery Service', () => { assert.ok(result.includes(version3)); // Non-compatible, included }); - test('should separate release and pre-release versions', () => { - const releaseVersion = aExtensionVersion('1.0.0', TargetPlatform.WIN32_X64); - const preReleaseVersion = aPreReleaseExtensionVersion('1.1.0', TargetPlatform.WIN32_X64); - const versions = [releaseVersion, preReleaseVersion]; - const allTargetPlatforms = [TargetPlatform.WIN32_X64]; - - const result = filterLatestExtensionVersionsForTargetPlatform(versions, TargetPlatform.WIN32_X64, allTargetPlatforms); - - // Should include both since they are different types (release vs pre-release) - assert.strictEqual(result.length, 2); - assert.ok(result.includes(releaseVersion)); - assert.ok(result.includes(preReleaseVersion)); - }); - - test('should include both release and pre-release versions for same platform with different version numbers', () => { - const preRelease1 = aPreReleaseExtensionVersion('1.1.0', TargetPlatform.WIN32_X64); - const release2 = aExtensionVersion('1.0.0', TargetPlatform.WIN32_X64); // Different version number - const versions = [preRelease1, release2]; - const allTargetPlatforms = [TargetPlatform.WIN32_X64]; - - const result = filterLatestExtensionVersionsForTargetPlatform(versions, TargetPlatform.WIN32_X64, allTargetPlatforms); - - // Should include both since they have different version numbers - assert.strictEqual(result.length, 2); - assert.strictEqual(result[0], preRelease1); - assert.strictEqual(result[1], release2); - }); test('should handle versions without target platform (UNDEFINED)', () => { const version1 = aExtensionVersion('1.0.0'); // No target platform specified @@ -281,20 +267,21 @@ suite('Extension Gallery Service', () => { assert.ok(!result.includes(lowerVersionUniversal)); // Filtered (second compatible release) }); - test('should handle lower version with specific platform vs higher version with universal platform', () => { - // Reverse scenario: older version for specific platform vs newer version with universal compatibility - const lowerVersionSpecificPlatform = aExtensionVersion('1.0.0', TargetPlatform.WIN32_X64); + test('should handle higher version with universal platform vs lower version with specific platform', () => { + // Scenario: higher universal version comes first, then lower platform-specific version const higherVersionUniversal = aExtensionVersion('2.0.0'); // UNDEFINED/universal platform + const lowerVersionSpecificPlatform = aExtensionVersion('1.0.0', TargetPlatform.WIN32_X64); - const versions = [lowerVersionSpecificPlatform, higherVersionUniversal]; + const versions = [higherVersionUniversal, lowerVersionSpecificPlatform]; const allTargetPlatforms = [TargetPlatform.WIN32_X64, TargetPlatform.DARWIN_X64]; const result = filterLatestExtensionVersionsForTargetPlatform(versions, TargetPlatform.WIN32_X64, allTargetPlatforms); - // Both are compatible with WIN32_X64, but only the first release version should be included + // Both are compatible with WIN32_X64, the first (higher) version should be kept + // Platform-specific version should NOT replace since it has a different (lower) version number assert.strictEqual(result.length, 1); - assert.ok(result.includes(lowerVersionSpecificPlatform)); // First compatible release - assert.ok(!result.includes(higherVersionUniversal)); // Filtered (second compatible release) + assert.ok(result.includes(higherVersionUniversal)); // First compatible release (higher version) + assert.ok(!result.includes(lowerVersionSpecificPlatform)); // Filtered (lower version) }); test('should handle multiple specific platforms vs universal platform with version differences', () => { @@ -391,19 +378,20 @@ suite('Extension Gallery Service', () => { assert.ok(!result.includes(universalVersion)); }); - test('should handle both release and pre-release with replacement', () => { - // Both release and pre-release starting with undefined and then getting specific platform - const undefinedRelease = aExtensionVersion('1.0.0'); // UNDEFINED release - const specificRelease = aExtensionVersion('1.0.0', TargetPlatform.WIN32_X64); // Specific release + test('should handle both release and pre-release with same version replacement', () => { + // Both release and pre-release with undefined platform, then specific platform with same versions + // Versions sorted by version descending (pre-release 1.1.0, release 1.0.0, then same versions with specific platform) const undefinedPreRelease = aPreReleaseExtensionVersion('1.1.0'); // UNDEFINED pre-release - const specificPreRelease = aPreReleaseExtensionVersion('1.1.0', TargetPlatform.WIN32_X64); // Specific pre-release + const specificPreRelease = aPreReleaseExtensionVersion('1.1.0', TargetPlatform.WIN32_X64); // Specific pre-release (same version) + const undefinedRelease = aExtensionVersion('1.0.0'); // UNDEFINED release + const specificRelease = aExtensionVersion('1.0.0', TargetPlatform.WIN32_X64); // Specific release (same version) - const versions = [undefinedRelease, undefinedPreRelease, specificRelease, specificPreRelease]; + const versions = [undefinedPreRelease, specificPreRelease, undefinedRelease, specificRelease]; const allTargetPlatforms = [TargetPlatform.WIN32_X64]; const result = filterLatestExtensionVersionsForTargetPlatform(versions, TargetPlatform.WIN32_X64, allTargetPlatforms); - // Should return both specific platform versions + // Should return both specific platform versions (they replaced the undefined ones) assert.strictEqual(result.length, 2); assert.ok(result.includes(specificRelease)); assert.ok(result.includes(specificPreRelease)); @@ -427,21 +415,47 @@ suite('Extension Gallery Service', () => { }); test('should handle replacement with non-compatible versions in between', () => { + // Versions sorted by version descending const undefinedVersion = aExtensionVersion('1.0.0'); // UNDEFINED, compatible with WIN32_X64 - const nonCompatibleVersion = aExtensionVersion('0.9.0', TargetPlatform.LINUX_ARM64); // Non-compatible platform - const specificVersion = aExtensionVersion('1.0.0', TargetPlatform.WIN32_X64); // Specific for WIN32_X64 + const specificVersion = aExtensionVersion('1.0.0', TargetPlatform.WIN32_X64); // Specific for WIN32_X64 (same version) + const nonCompatibleVersion = aExtensionVersion('0.9.0', TargetPlatform.LINUX_ARM64); // Non-compatible platform (lower version) - const versions = [undefinedVersion, nonCompatibleVersion, specificVersion]; + const versions = [undefinedVersion, specificVersion, nonCompatibleVersion]; const allTargetPlatforms = [TargetPlatform.WIN32_X64, TargetPlatform.DARWIN_X64]; const result = filterLatestExtensionVersionsForTargetPlatform(versions, TargetPlatform.WIN32_X64, allTargetPlatforms); - // Should return specific WIN32_X64 version (replacing undefined) and non-compatible LINUX_ARM64 version + // Should return specific WIN32_X64 version (replacing undefined since same version) and non-compatible LINUX_ARM64 version assert.strictEqual(result.length, 2); assert.ok(result.includes(specificVersion)); assert.ok(result.includes(nonCompatibleVersion)); assert.ok(!result.includes(undefinedVersion)); }); + test('should filter versions for linux-x64 target platform with mixed universal and platform-specific versions', () => { + // Data from real extension versions (sorted by version descending, as returned by gallery API): + // 0.15.0 - pre-release, universal + // 0.14.0 - release, universal + // 0.6.0 - release, linux-x64 + // 0.5.1 - pre-release, linux-x64 + const versions = [ + aPreReleaseExtensionVersion('0.15.0'), // pre-release, universal (highest version) + aExtensionVersion('0.14.0'), // release, universal + aExtensionVersion('0.6.0', TargetPlatform.LINUX_X64), // release, linux-x64 + aPreReleaseExtensionVersion('0.5.1', TargetPlatform.LINUX_X64), // pre-release, linux-x64 (lowest version) + ]; + const allTargetPlatforms = [TargetPlatform.LINUX_X64]; + + const result = filterLatestExtensionVersionsForTargetPlatform(versions, TargetPlatform.LINUX_X64, allTargetPlatforms); + + // Expected: + // - 0.15.0 universal (first compatible pre-release, higher version than 0.5.1 linux-x64) + // - 0.14.0 universal (first compatible release, higher version than 0.6.0 linux-x64) + // Platform-specific versions are NOT preferred when they have lower version numbers + assert.strictEqual(result.length, 2); + assert.ok(result.includes(versions[0])); // 0.15.0 universal (pre-release) + assert.ok(result.includes(versions[1])); // 0.14.0 universal (release) + }); + }); });