From c666bd55e1f19c939373ad708e01f0d08994075e Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sun, 1 Mar 2026 13:48:14 -0800 Subject: [PATCH 01/15] Avoid more context key creation from chat inline anchor --- .../chat/browser/attachments/chatAttachmentWidgets.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts index eb154e0b96f6a..c1763a79436c1 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts @@ -51,7 +51,7 @@ import { IOpenerService, OpenInternalOptions } from '../../../../../platform/ope import { FolderThemeIcon, IThemeService } from '../../../../../platform/theme/common/themeService.js'; import { fillEditorsDragData } from '../../../../browser/dnd.js'; import { IFileLabelOptions, IResourceLabel, ResourceLabels } from '../../../../browser/labels.js'; -import { ResourceContextKey } from '../../../../common/contextkeys.js'; +import { StaticResourceContextKey } from '../../../../common/contextkeys.js'; import { IEditorService, SIDE_GROUP } from '../../../../services/editor/common/editorService.js'; import { IPreferencesService } from '../../../../services/preferences/common/preferences.js'; import { revealInSideBarCommand } from '../../../files/browser/fileActions.contribution.js'; @@ -1379,7 +1379,7 @@ export function hookUpResourceAttachmentDragAndContextMenu(accessor: ServicesAcc // Context const scopedContextKeyService = store.add(contextKeyService.createScoped(widget)); - store.add(setResourceContext(accessor, scopedContextKeyService, resource)); + setResourceContext(accessor, scopedContextKeyService, resource); // Drag and drop widget.draggable = true; @@ -1422,7 +1422,7 @@ export function hookUpSymbolAttachmentDragAndContextMenu(accessor: ServicesAcces // but resource context and provider contexts are initialized lazily on first use) const scopedContextKeyService = store.add(parentContextKeyService.createScoped(widget)); chatAttachmentResourceContextKey.bindTo(scopedContextKeyService).set(attachment.value.uri.toString()); - store.add(setResourceContext(accessor, scopedContextKeyService, attachment.value.uri)); + setResourceContext(accessor, scopedContextKeyService, attachment.value.uri); let providerContexts: ReadonlyArray<[IContextKey, LanguageFeatureRegistry]> | undefined; @@ -1473,14 +1473,13 @@ export function hookUpSymbolAttachmentDragAndContextMenu(accessor: ServicesAcces return store; } -function setResourceContext(accessor: ServicesAccessor, scopedContextKeyService: IScopedContextKeyService, resource: URI) { +function setResourceContext(accessor: ServicesAccessor, scopedContextKeyService: IScopedContextKeyService, resource: URI): void { const fileService = accessor.get(IFileService); const languageService = accessor.get(ILanguageService); const modelService = accessor.get(IModelService); - const resourceContextKey = new ResourceContextKey(scopedContextKeyService, fileService, languageService, modelService); + const resourceContextKey = new StaticResourceContextKey(scopedContextKeyService, fileService, languageService, modelService); resourceContextKey.set(resource); - return resourceContextKey; } function addBasicContextMenu(accessor: ServicesAccessor, widget: HTMLElement, scopedContextKeyService: IScopedContextKeyService, menuId: MenuId, arg: unknown, updateContextKeys?: () => Promise): IDisposable { From d63b8bde8067b948f935aea8da34c752573a9012 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sun, 1 Mar 2026 14:53:57 -0800 Subject: [PATCH 02/15] chat: wrap markdown tables with horizontal scrollbar (#298604) - Wrap each rendered markdown table in a DomScrollableElement so it scrolls horizontally with the VS Code custom scrollbar instead of the native one. - Apply per-column min-width (in ch units, capped at 3ch) based on max text-content length, so short values like "001" aren't squeezed to a single character wide. - Allow table headers to wrap at word boundaries (white-space: normal) rather than staying on a single line. - Extract the wrapping logic into chatMarkdownTableScrolling.ts with unit tests in chatMarkdownTableScrolling.test.ts. Fixes #265062 (Written by Copilot) --- .../chatMarkdownContentPart.ts | 3 + .../chatMarkdownTableScrolling.ts | 82 ++++++++++ .../chat/browser/widget/media/chat.css | 12 +- .../widget/chatMarkdownTableScrolling.test.ts | 144 ++++++++++++++++++ 4 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownTableScrolling.ts create mode 100644 src/vs/workbench/contrib/chat/test/browser/widget/chatMarkdownTableScrolling.test.ts diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts index 4255c82e1d1f0..8f388e7ebb56e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts @@ -10,6 +10,7 @@ import { status } from '../../../../../../base/browser/ui/aria/aria.js'; import { HoverStyle } from '../../../../../../base/browser/ui/hover/hover.js'; import { HoverPosition } from '../../../../../../base/browser/ui/hover/hoverWidget.js'; import { DomScrollableElement } from '../../../../../../base/browser/ui/scrollbar/scrollableElement.js'; +import { wrapTablesWithScrollable } from './chatMarkdownTableScrolling.js'; import { coalesce } from '../../../../../../base/common/arrays.js'; import { findLast } from '../../../../../../base/common/arraysFind.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; @@ -374,6 +375,8 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP scrollable.scanDomNode(); } + orderedDisposablesList.push(wrapTablesWithScrollable(this.domNode, layoutParticipants)); + orderedDisposablesList.reverse().forEach(d => store.add(d)); }; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownTableScrolling.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownTableScrolling.ts new file mode 100644 index 0000000000000..517fe538dc693 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownTableScrolling.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../base/browser/dom.js'; +import { DomScrollableElement } from '../../../../../../base/browser/ui/scrollbar/scrollableElement.js'; +import { Lazy } from '../../../../../../base/common/lazy.js'; +import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; + +import { ScrollbarVisibility } from '../../../../../../base/common/scrollable.js'; + +/** + * Finds all tables in `domNode` and wraps each in a {@link DomScrollableElement} + * so they scroll horizontally with the custom VS Code scrollbar instead of the + * native one. Each wrapped table is pushed onto `orderedDisposablesList` and a + * `scanDomNode` callback is registered on `layoutParticipants` so the scrollbar + * re-measures whenever the container is resized. + * + * Each column's `min-width` is also set to the maximum character count across + * all cells in that column (in `ch` units), preventing short-content columns + * like "001" from being squeezed to one character wide. Single-character columns + * are left unchanged. This is layout-free: only `textContent` lengths are read. + */ +export function wrapTablesWithScrollable(domNode: HTMLElement, layoutParticipants: Lazy void>>): DisposableStore { + const store = new DisposableStore(); + // eslint-disable-next-line no-restricted-syntax + for (const table of domNode.querySelectorAll('table')) { + if (!dom.isHTMLElement(table)) { + continue; + } + + applyTableColumnMinWidths(table); + + // Wrap the table in a div so DomScrollableElement can compare the div's + // constrained clientWidth against the table's natural scrollWidth. + // Passing the table directly doesn't work because a table always expands + // to its content width, so clientWidth == scrollWidth and no scrollbar appears. + const parent = table.parentElement; + const nextSibling = table.nextSibling; + const tableContainer = document.createElement('div'); + tableContainer.appendChild(table); // moves table out of DOM + const scrollable = store.add(new DomScrollableElement(tableContainer, { // moves tableContainer into scrollNode + vertical: ScrollbarVisibility.Hidden, + horizontal: ScrollbarVisibility.Auto, + })); + const scrollNode = scrollable.getDomNode(); + scrollNode.classList.add('rendered-markdown-table-scroll-wrapper'); + parent?.insertBefore(scrollNode, nextSibling); + + layoutParticipants.value.add(() => { scrollable.scanDomNode(); }); + scrollable.scanDomNode(); + } + return store; +} + +/** Maximum `min-width` (in `ch`) applied to any table column, regardless of its content length. */ +const TABLE_COLUMN_MIN_WIDTH_CAP_CH = 3; + +function applyTableColumnMinWidths(table: HTMLTableElement): void { + const rows = table.rows; + const colMaxChars: number[] = []; + for (const row of rows) { + for (let c = 0; c < row.cells.length; c++) { + const len = row.cells[c].textContent?.length ?? 0; + if (len > (colMaxChars[c] ?? 0)) { + colMaxChars[c] = len; + } + } + } + // Apply min-width only to the first row's cells so each column width + // constraint is set once rather than touching every cell in the table. + const firstRow = rows[0]; + if (firstRow) { + for (let c = 0; c < firstRow.cells.length; c++) { + const minCh = colMaxChars[c]; + if (minCh !== undefined && minCh > 1) { + firstRow.cells[c].style.minWidth = Math.min(minCh, TABLE_COLUMN_MIN_WIDTH_CAP_CH) + 'ch'; + } + } + } +} 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 c3ec962151a34..98a0e4aad6a65 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -313,10 +313,13 @@ font-weight: 600; } -.interactive-item-container .value .rendered-markdown table { +.interactive-item-container .value .rendered-markdown .rendered-markdown-table-scroll-wrapper { width: 100%; - text-align: left; margin-bottom: 16px; +} + +.interactive-item-container .value .rendered-markdown table { + text-align: left; border-radius: var(--vscode-cornerRadius-medium); overflow: hidden; border-collapse: separate; @@ -332,6 +335,11 @@ padding: 4px 6px; } +.interactive-item-container .value .rendered-markdown table th { + white-space: normal; + overflow-wrap: break-word; +} + .interactive-item-container .value .rendered-markdown table td:last-child, .interactive-item-container .value .rendered-markdown table th:last-child { border-right: none; diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatMarkdownTableScrolling.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatMarkdownTableScrolling.test.ts new file mode 100644 index 0000000000000..4a25f74f0b388 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatMarkdownTableScrolling.test.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Lazy } from '../../../../../../base/common/lazy.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { wrapTablesWithScrollable } from '../../../browser/widget/chatContentParts/chatMarkdownTableScrolling.js'; + +/** Builds an HTMLElement containing one or more tables from markdown-style 2-D arrays. */ +function buildContainer(tables: string[][][]): HTMLDivElement { + const container = document.createElement('div'); + for (const rows of tables) { + const table = document.createElement('table'); + rows.forEach((rowData, rowIndex) => { + const section = rowIndex === 0 + ? table.createTHead() + : (table.tBodies[0] ?? table.createTBody()); + const tr = section.insertRow(); + for (const text of rowData) { + const cell = rowIndex === 0 ? document.createElement('th') : tr.insertCell(); + cell.textContent = text; + if (rowIndex === 0) { + tr.appendChild(cell); + } + } + }); + container.appendChild(table); + } + return container; +} + +suite('wrapTablesWithScrollable', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + function wrap(container: HTMLDivElement): { layoutParticipants: Set<() => void> } { + const layoutParticipants = new Set<() => void>(); + store.add(wrapTablesWithScrollable(container, new Lazy(() => layoutParticipants))); + return { layoutParticipants }; + } + + test('replaces each table with a scroll wrapper in the DOM', () => { + const container = buildContainer([[ + ['ID', 'Name'], + ['001', 'Alice'], + ]]); + // Before: direct child is + assert.strictEqual(container.children[0].tagName, 'TABLE'); + + wrap(container); + + // After: direct child is the monaco-scrollable-element wrapper + const wrapper = container.children[0]; + assert.ok(wrapper.classList.contains('rendered-markdown-table-scroll-wrapper'), + 'outer node should have the scroll wrapper class'); + }); + + test('table is preserved inside the scroll wrapper', () => { + const container = buildContainer([[['A', 'BB'], ['C', 'DD']]]); + wrap(container); + + // The table must still be in the document, nested inside the wrapper + const table = container.querySelector('table'); + assert.ok(table, 'table should still exist in DOM'); + assert.ok(container.contains(table), 'table should be inside container'); + assert.ok(!container.children[0].isSameNode(table), 'table should not be a direct child anymore'); + }); + + test('registers a layout participant for each table', () => { + const container = buildContainer([ + [['H1', 'H2'], ['a', 'bb']], + [['X', 'YY'], ['c', 'dd']], + ]); + const { layoutParticipants } = wrap(container); + assert.strictEqual(layoutParticipants.size, 2, 'one layout participant registered per table'); + }); + + test('sets column min-width capped at 3ch', () => { + const container = buildContainer([[ + ['ID', 'Name'], + ['001', 'Alice'], + ['002', 'Longer Name'], + ]]); + wrap(container); + + const table = container.querySelector('table')!; + // min-width is set only on the first row; other rows are untouched + // col 0 max = 3 chars -> 3ch; col 1 max = 11 chars -> capped at 3ch + assert.deepStrictEqual( + Array.from(table.rows[0].cells).map(cell => cell.style.minWidth), + ['3ch', '3ch'] + ); + assert.deepStrictEqual( + Array.from(table.rows[1].cells).map(cell => cell.style.minWidth), + ['', ''] + ); + }); + + test('uses actual char count when below the 3ch cap', () => { + const container = buildContainer([[['AB', 'C'], ['DE', 'F']]]); + wrap(container); + + const table = container.querySelector('table')!; + // col 0 max=2 -> 2ch; col 1 max=1 -> no min-width + assert.strictEqual(table.rows[0].cells[0].style.minWidth, '2ch'); + assert.strictEqual(table.rows[0].cells[1].style.minWidth, ''); + }); + + test('does not set min-width on single-character columns', () => { + const container = buildContainer([[['X', 'hello'], ['Y', 'world']]]); + wrap(container); + + const table = container.querySelector('table')!; + assert.strictEqual(table.rows[0].cells[0].style.minWidth, '', 'single-char column should have no min-width'); + }); + + test('handles multiple tables independently', () => { + const container = buildContainer([ + [['AB', 'C'], ['DE', 'F']], + [['X', 'YYY'], ['Z', 'WWW']], + ]); + wrap(container); + + const tables = container.querySelectorAll('table'); + assert.strictEqual(tables.length, 2); + + // Table 1: col 0 max=2, col 1 max=1 -> only col 0 gets min-width + assert.strictEqual(tables[0].rows[0].cells[0].style.minWidth, '2ch'); + assert.strictEqual(tables[0].rows[0].cells[1].style.minWidth, ''); + + // Table 2: col 0 max=1, col 1 max=3 -> only col 1 gets min-width + assert.strictEqual(tables[1].rows[0].cells[0].style.minWidth, ''); + assert.strictEqual(tables[1].rows[0].cells[1].style.minWidth, '3ch'); + }); + + test('no-ops on a container with no tables', () => { + const container = document.createElement('div'); + container.innerHTML = '

hello

'; + const { layoutParticipants } = wrap(container); + assert.strictEqual(layoutParticipants.size, 0); + assert.strictEqual(container.querySelector('table'), null); + }); +}); From f85c0b3c833f8b2ab3a7c91feab33bc7a2993ebf Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Mon, 2 Mar 2026 03:35:08 +0100 Subject: [PATCH 03/15] improves npm caching (#298608) --- .../vscode-extras/src/npmUpToDateFeature.ts | 104 ++++++++++++++++-- build/npm/installStateHash.ts | 52 ++++++++- build/npm/postinstall.ts | 3 +- 3 files changed, 143 insertions(+), 16 deletions(-) diff --git a/.vscode/extensions/vscode-extras/src/npmUpToDateFeature.ts b/.vscode/extensions/vscode-extras/src/npmUpToDateFeature.ts index b3f172eca1dc6..df9abf863ae52 100644 --- a/.vscode/extensions/vscode-extras/src/npmUpToDateFeature.ts +++ b/.vscode/extensions/vscode-extras/src/npmUpToDateFeature.ts @@ -19,6 +19,7 @@ interface PostinstallState { interface InstallState { readonly root: string; + readonly stateContentsFile: string; readonly current: PostinstallState; readonly saved: PostinstallState | undefined; readonly files: readonly string[]; @@ -29,6 +30,10 @@ export class NpmUpToDateFeature extends vscode.Disposable { private readonly _disposables: vscode.Disposable[] = []; private _watchers: fs.FSWatcher[] = []; private _terminal: vscode.Terminal | undefined; + private _stateContentsFile: string | undefined; + private _root: string | undefined; + + private static readonly _scheme = 'npm-dep-state'; constructor(private readonly _output: vscode.LogOutputChannel) { const disposables: vscode.Disposable[] = []; @@ -48,10 +53,28 @@ export class NpmUpToDateFeature extends vscode.Disposable { this._statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); this._disposables.push(this._statusBarItem); + this._disposables.push( + vscode.workspace.registerTextDocumentContentProvider(NpmUpToDateFeature._scheme, { + provideTextDocumentContent: (uri) => { + const params = new URLSearchParams(uri.query); + const source = params.get('source'); + const file = uri.path.slice(1); // strip leading / + if (source === 'saved') { + return this._readSavedContent(file); + } + return this._readCurrentContent(file); + } + }) + ); + this._disposables.push( vscode.commands.registerCommand('vscode-extras.runNpmInstall', () => this._runNpmInstall()) ); + this._disposables.push( + vscode.commands.registerCommand('vscode-extras.showDependencyDiff', (file: string) => this._showDiff(file)) + ); + this._disposables.push( vscode.window.onDidCloseTerminal(t => { if (t === this._terminal) { @@ -66,8 +89,7 @@ export class NpmUpToDateFeature extends vscode.Disposable { private _runNpmInstall(): void { if (this._terminal) { - this._terminal.show(); - return; + this._terminal.dispose(); } const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri; if (!workspaceRoot) { @@ -113,6 +135,8 @@ export class NpmUpToDateFeature extends vscode.Disposable { return; } + this._stateContentsFile = state.stateContentsFile; + this._root = state.root; this._setupWatcher(state); const changedFiles = this._getChangedFiles(state); @@ -123,9 +147,16 @@ export class NpmUpToDateFeature extends vscode.Disposable { } else { this._statusBarItem.text = '$(warning) node_modules is stale - run npm i'; const tooltip = new vscode.MarkdownString(); - tooltip.appendText('Dependencies are out of date. Click to run npm install.\n\nChanged files:\n'); - for (const file of changedFiles) { - tooltip.appendText(` • ${file}\n`); + tooltip.isTrusted = true; + tooltip.supportHtml = true; + tooltip.appendMarkdown('**Dependencies are out of date.** Click to run npm install.\n\nChanged files:\n\n'); + for (const entry of changedFiles) { + if (entry.isFile) { + const args = encodeURIComponent(JSON.stringify(entry.label)); + tooltip.appendMarkdown(`- [${entry.label}](command:vscode-extras.showDependencyDiff?${args})\n`); + } else { + tooltip.appendMarkdown(`- ${entry.label}\n`); + } } this._statusBarItem.tooltip = tooltip; this._statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); @@ -133,18 +164,71 @@ export class NpmUpToDateFeature extends vscode.Disposable { } } - private _getChangedFiles(state: InstallState): string[] { + private _showDiff(file: string): void { + const cacheBuster = Date.now().toString(); + const savedUri = vscode.Uri.from({ + scheme: NpmUpToDateFeature._scheme, + path: `/${file}`, + query: new URLSearchParams({ source: 'saved', t: cacheBuster }).toString(), + }); + const currentUri = vscode.Uri.from({ + scheme: NpmUpToDateFeature._scheme, + path: `/${file}`, + query: new URLSearchParams({ source: 'current', t: cacheBuster }).toString(), + }); + + vscode.commands.executeCommand('vscode.diff', savedUri, currentUri, `${file} (last install ↔ current)`); + } + + private _readSavedContent(file: string): string { + if (!this._stateContentsFile) { + return ''; + } + try { + const contents: Record = JSON.parse(fs.readFileSync(this._stateContentsFile, 'utf8')); + return contents[file] ?? ''; + } catch { + return ''; + } + } + + private _readCurrentContent(file: string): string { + if (!this._root) { + return ''; + } + try { + return this._normalizeFileContent(path.join(this._root, file)); + } catch { + return ''; + } + } + + private _normalizeFileContent(filePath: string): string { + const raw = fs.readFileSync(filePath, 'utf8'); + if (path.basename(filePath) === 'package.json') { + const json = JSON.parse(raw); + for (const key of NpmUpToDateFeature._packageJsonIgnoredKeys) { + delete json[key]; + } + return JSON.stringify(json, null, '\t') + '\n'; + } + return raw; + } + + private static readonly _packageJsonIgnoredKeys = ['distro']; + + private _getChangedFiles(state: InstallState): { readonly label: string; readonly isFile: boolean }[] { if (!state.saved) { - return ['(no postinstall state found)']; + return [{ label: '(no postinstall state found)', isFile: false }]; } - const changed: string[] = []; + const changed: { readonly label: string; readonly isFile: boolean }[] = []; if (state.saved.nodeVersion !== state.current.nodeVersion) { - changed.push(`Node.js version (${state.saved.nodeVersion} → ${state.current.nodeVersion})`); + changed.push({ label: `Node.js version (${state.saved.nodeVersion} → ${state.current.nodeVersion})`, isFile: false }); } const allKeys = new Set([...Object.keys(state.current.fileHashes), ...Object.keys(state.saved.fileHashes)]); for (const key of allKeys) { if (state.current.fileHashes[key] !== state.saved.fileHashes[key]) { - changed.push(key); + changed.push({ label: key, isFile: true }); } } return changed; diff --git a/build/npm/installStateHash.ts b/build/npm/installStateHash.ts index 79c0c130f5e28..5674a1eaee377 100644 --- a/build/npm/installStateHash.ts +++ b/build/npm/installStateHash.ts @@ -10,6 +10,7 @@ import { dirs } from './dirs.ts'; export const root = fs.realpathSync.native(path.dirname(path.dirname(import.meta.dirname))); export const stateFile = path.join(root, 'node_modules', '.postinstall-state'); +export const stateContentsFile = path.join(root, 'node_modules', '.postinstall-state-contents'); export const forceInstallMessage = 'Run \x1b[36mnode build/npm/fast-install.ts --force\x1b[0m to force a full install.'; export function collectInputFiles(): string[] { @@ -17,7 +18,7 @@ export function collectInputFiles(): string[] { for (const dir of dirs) { const base = dir === '' ? root : path.join(root, dir); - for (const file of ['package.json', '.npmrc']) { + for (const file of ['package.json', 'package-lock.json', '.npmrc']) { const filePath = path.join(base, file); if (fs.existsSync(filePath)) { files.push(filePath); @@ -35,23 +36,55 @@ export interface PostinstallState { readonly fileHashes: Record; } -function hashFileContent(filePath: string): string { +const packageJsonIgnoredKeys = new Set(['distro']); + +function normalizeFileContent(filePath: string): string { + const raw = fs.readFileSync(filePath, 'utf8'); + if (path.basename(filePath) === 'package.json') { + const json = JSON.parse(raw); + for (const key of packageJsonIgnoredKeys) { + delete json[key]; + } + return JSON.stringify(json, null, '\t') + '\n'; + } + return raw; +} + +function hashContent(content: string): string { const hash = crypto.createHash('sha256'); - hash.update(fs.readFileSync(filePath)); + hash.update(content); return hash.digest('hex'); } export function computeState(): PostinstallState { const fileHashes: Record = {}; for (const filePath of collectInputFiles()) { - fileHashes[path.relative(root, filePath)] = hashFileContent(filePath); + const key = path.relative(root, filePath); + try { + fileHashes[key] = hashContent(normalizeFileContent(filePath)); + } catch { + // file may not be readable + } } return { nodeVersion: process.versions.node, fileHashes }; } +export function computeContents(): Record { + const fileContents: Record = {}; + for (const filePath of collectInputFiles()) { + try { + fileContents[path.relative(root, filePath)] = normalizeFileContent(filePath); + } catch { + // file may not be readable + } + } + return fileContents; +} + export function readSavedState(): PostinstallState | undefined { try { - return JSON.parse(fs.readFileSync(stateFile, 'utf8')); + const { nodeVersion, fileHashes } = JSON.parse(fs.readFileSync(stateFile, 'utf8')); + return { nodeVersion, fileHashes }; } catch { return undefined; } @@ -67,10 +100,19 @@ export function isUpToDate(): boolean { && JSON.stringify(saved.fileHashes) === JSON.stringify(current.fileHashes); } +export function readSavedContents(): Record | undefined { + try { + return JSON.parse(fs.readFileSync(stateContentsFile, 'utf8')); + } catch { + return undefined; + } +} + // When run directly, output state as JSON for tooling (e.g. the vscode-extras extension). if (import.meta.filename === process.argv[1]) { console.log(JSON.stringify({ root, + stateContentsFile, current: computeState(), saved: readSavedState(), files: [...collectInputFiles(), stateFile], diff --git a/build/npm/postinstall.ts b/build/npm/postinstall.ts index 6b61bbded5c84..ae2651cd188a1 100644 --- a/build/npm/postinstall.ts +++ b/build/npm/postinstall.ts @@ -8,7 +8,7 @@ import path from 'path'; import * as os from 'os'; import * as child_process from 'child_process'; import { dirs } from './dirs.ts'; -import { root, stateFile, computeState, isUpToDate } from './installStateHash.ts'; +import { root, stateFile, stateContentsFile, computeState, computeContents, isUpToDate } from './installStateHash.ts'; const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; const rootNpmrcConfigKeys = getNpmrcConfigKeys(path.join(root, '.npmrc')); @@ -287,6 +287,7 @@ async function main() { child_process.execSync('git config blame.ignoreRevsFile .git-blame-ignore-revs'); fs.writeFileSync(stateFile, JSON.stringify(_state)); + fs.writeFileSync(stateContentsFile, JSON.stringify(computeContents())); } main().catch(err => { From b753be80eb72dd88f5104f89226666e52520803f Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Sun, 1 Mar 2026 20:24:40 -0800 Subject: [PATCH 04/15] customizations: allow transient selection of the folder to explore customizations against (#298403) * customizations: allow transient selection of the folder to explore customizations against * address PR review comments - Fix no-op setOverrideProjectRoot to accept URI parameter - Fix JSDoc/comments to be environment-agnostic (session or workspace) - Account for folder picker footer height in sidebar layout - Use ILabelService for URI tooltip instead of fsPath --- .../aiCustomizationWorkspaceService.ts | 31 ++++++- .../contrib/chat/browser/promptsService.ts | 9 +- .../aiCustomizationListWidget.ts | 17 ++++ .../aiCustomizationManagementEditor.ts | 84 ++++++++++++++++++- .../aiCustomizationWorkspaceService.ts | 6 +- .../media/aiCustomizationManagement.css | 58 +++++++++++++ .../common/aiCustomizationWorkspaceService.ts | 18 ++++ 7 files changed, 215 insertions(+), 8 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts index 79dc93837c9e5..2f1b25038eef8 100644 --- a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts +++ b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { derived, IObservable } from '../../../../base/common/observable.js'; +import { derived, IObservable, observableValue, ISettableObservable } from '../../../../base/common/observable.js'; import { joinPath } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { IAICustomizationWorkspaceService, AICustomizationManagementSection, IStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; @@ -23,6 +23,13 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization declare readonly _serviceBrand: undefined; readonly activeProjectRoot: IObservable; + readonly hasOverrideProjectRoot: IObservable; + + /** + * Transient override for the project root. When set, `activeProjectRoot` + * returns this value instead of the session-derived root. + */ + private readonly _overrideRoot: ISettableObservable; /** * CLI-accessible user directories for customization file filtering and creation. @@ -50,17 +57,39 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization includedUserFileRoots: this._cliUserRoots, }; + this._overrideRoot = observableValue(this, undefined); + this.activeProjectRoot = derived(reader => { + const override = this._overrideRoot.read(reader); + if (override) { + return override; + } const session = this.sessionsService.activeSession.read(reader); return session?.worktree ?? session?.repository; }); + + this.hasOverrideProjectRoot = derived(reader => { + return this._overrideRoot.read(reader) !== undefined; + }); } getActiveProjectRoot(): URI | undefined { + const override = this._overrideRoot.get(); + if (override) { + return override; + } const session = this.sessionsService.getActiveSession(); return session?.worktree ?? session?.repository; } + setOverrideProjectRoot(root: URI): void { + this._overrideRoot.set(root, undefined); + } + + clearOverrideProjectRoot(): void { + this._overrideRoot.set(undefined, undefined); + } + readonly managementSections: readonly AICustomizationManagementSection[] = [ AICustomizationManagementSection.Agents, AICustomizationManagementSection.Skills, diff --git a/src/vs/sessions/contrib/chat/browser/promptsService.ts b/src/vs/sessions/contrib/chat/browser/promptsService.ts index 96596f2de0c3f..3a7af9380b202 100644 --- a/src/vs/sessions/contrib/chat/browser/promptsService.ts +++ b/src/vs/sessions/contrib/chat/browser/promptsService.ts @@ -19,7 +19,7 @@ import { IWorkbenchEnvironmentService } from '../../../../workbench/services/env import { IPathService } from '../../../../workbench/services/path/common/pathService.js'; import { ISearchService } from '../../../../workbench/services/search/common/search.js'; import { IUserDataProfileService } from '../../../../workbench/services/userDataProfile/common/userDataProfile.js'; -import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; +import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; export class AgenticPromptsService extends PromptsService { private _copilotRoot: URI | undefined; @@ -67,7 +67,7 @@ class AgenticPromptFilesLocator extends PromptFilesLocator { @IUserDataProfileService userDataService: IUserDataProfileService, @ILogService logService: ILogService, @IPathService pathService: IPathService, - @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, + @IAICustomizationWorkspaceService private readonly customizationWorkspaceService: IAICustomizationWorkspaceService, ) { super( fileService, @@ -95,7 +95,7 @@ class AgenticPromptFilesLocator extends PromptFilesLocator { } protected override onDidChangeWorkspaceFolders(): Event { - return Event.fromObservableLight(this.activeSessionService.activeSession); + return Event.fromObservableLight(this.customizationWorkspaceService.activeProjectRoot); } public override async getHookSourceFolders(): Promise { @@ -108,8 +108,7 @@ class AgenticPromptFilesLocator extends PromptFilesLocator { } private getActiveWorkspaceFolder(): IWorkspaceFolder | undefined { - const session = this.activeSessionService.getActiveSession(); - const root = session?.worktree ?? session?.repository; + const root = this.customizationWorkspaceService.getActiveProjectRoot(); if (!root) { return undefined; } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index f3f644dffa886..9f3f4492db7ef 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -201,6 +201,11 @@ class GroupHeaderRenderer implements IListRenderer { readonly templateId = 'aiCustomizationItem'; + constructor( + @IHoverService private readonly hoverService: IHoverService, + @ILabelService private readonly labelService: ILabelService, + ) { } + renderTemplate(container: HTMLElement): IAICustomizationItemTemplateData { const disposables = new DisposableStore(); const elementDisposables = new DisposableStore(); @@ -236,6 +241,18 @@ class AICustomizationItemRenderer implements IListRenderer { + const uriLabel = this.labelService.getUriLabel(element.uri, { relative: false }); + return { + content: `${element.name}\n${uriLabel}`, + appearance: { + compact: true, + skipFadeInAnimation: true, + } + }; + })); + // Name with highlights templateData.nameLabel.set(element.name, element.nameMatches); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index a18e242ba6972..7b3d437b0efe3 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -61,11 +61,14 @@ import { getSimpleEditorOptions } from '../../../codeEditor/browser/simpleEditor import { IWorkingCopyService } from '../../../../services/workingCopy/common/workingCopyService.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; +import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { VSBuffer } from '../../../../../base/common/buffer.js'; import { HOOKS_SOURCE_FOLDER } from '../../common/promptSyntax/config/promptFileLocations.js'; import { COPILOT_CLI_HOOK_TYPE_MAP } from '../../common/promptSyntax/hookSchema.js'; import { McpServerEditorInput } from '../../../mcp/browser/mcpServerEditorInput.js'; import { McpServerEditor } from '../../../mcp/browser/mcpServerEditor.js'; +import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IWorkbenchMcpServer } from '../../../mcp/common/mcpTypes.js'; const $ = DOM.$; @@ -168,6 +171,11 @@ export class AICustomizationManagementEditor extends EditorPane { private readonly editorDisposables = this._register(new DisposableStore()); private _editorContentChanged = false; + // Folder picker (sessions window only) + private folderPickerContainer: HTMLElement | undefined; + private folderPickerLabel: HTMLElement | undefined; + private folderPickerClearButton: HTMLElement | undefined; + private readonly inEditorContextKey: IContextKey; private readonly sectionContextKey: IContextKey; @@ -187,6 +195,8 @@ export class AICustomizationManagementEditor extends EditorPane { @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @ITextFileService private readonly textFileService: ITextFileService, @IFileService private readonly fileService: IFileService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @IHoverService private readonly hoverService: IHoverService, ) { super(AICustomizationManagementEditor.ID, group, telemetryService, themeService, storageService); @@ -264,7 +274,8 @@ export class AICustomizationManagementEditor extends EditorPane { layout: (width, _, height) => { this.sidebarContainer.style.width = `${width}px`; if (height !== undefined) { - const listHeight = height - 8; + const footerHeight = this.folderPickerContainer?.offsetHeight ?? 0; + const listHeight = height - 8 - footerHeight; this.sectionsList.layout(listHeight, width); } }, @@ -350,6 +361,72 @@ export class AICustomizationManagementEditor extends EditorPane { this.selectSection(e.elements[0].id); } })); + + // Folder picker (sessions window only) + if (this.workspaceService.isSessionsWindow) { + this.createFolderPicker(sidebarContent); + } + } + + private createFolderPicker(sidebarContent: HTMLElement): void { + const footer = this.folderPickerContainer = DOM.append(sidebarContent, $('.sidebar-folder-picker')); + + const button = DOM.append(footer, $('button.folder-picker-button')); + button.setAttribute('aria-label', localize('browseFolder', "Browse folder")); + + const folderIcon = DOM.append(button, $(`.codicon.codicon-${Codicon.folder.id}`)); + folderIcon.classList.add('folder-picker-icon'); + + this.folderPickerLabel = DOM.append(button, $('span.folder-picker-label')); + + this.folderPickerClearButton = DOM.append(footer, $('button.folder-picker-clear')); + this.folderPickerClearButton.setAttribute('aria-label', localize('clearFolderOverride', "Reset to session folder")); + DOM.append(this.folderPickerClearButton, $(`.codicon.codicon-${Codicon.close.id}`)); + + // Clicking the main button opens the folder dialog + this.editorDisposables.add(DOM.addDisposableListener(button, 'click', () => { + this.browseForFolder(); + })); + + // Clear button resets to session default + this.editorDisposables.add(DOM.addDisposableListener(this.folderPickerClearButton, 'click', () => { + this.workspaceService.clearOverrideProjectRoot(); + })); + + // Hover showing full path + this.editorDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), button, () => { + const root = this.workspaceService.getActiveProjectRoot(); + return root?.fsPath ?? ''; + })); + + // Keep label and clear button in sync with the active root + this.editorDisposables.add(autorun(reader => { + const root = this.workspaceService.activeProjectRoot.read(reader); + const hasOverride = this.workspaceService.hasOverrideProjectRoot.read(reader); + this.updateFolderPickerLabel(root, hasOverride); + })); + } + + private updateFolderPickerLabel(root: URI | undefined, hasOverride: boolean): void { + if (this.folderPickerLabel) { + this.folderPickerLabel.textContent = root ? basename(root) : localize('noFolder', "No folder"); + } + if (this.folderPickerClearButton) { + this.folderPickerClearButton.style.display = hasOverride ? '' : 'none'; + } + } + + private async browseForFolder(): Promise { + const result = await this.fileDialogService.showOpenDialog({ + canSelectFolders: true, + canSelectFiles: false, + canSelectMany: false, + title: localize('selectFolder', "Select Folder to Explore"), + defaultUri: this.workspaceService.getActiveProjectRoot(), + }); + if (result?.[0]) { + this.workspaceService.setOverrideProjectRoot(result[0]); + } } private createContent(): void { @@ -585,6 +662,9 @@ export class AICustomizationManagementEditor extends EditorPane { } override async setInput(input: AICustomizationManagementEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + // On (re)open, clear any override so the root comes from the default source + this.workspaceService.clearOverrideProjectRoot(); + this.inEditorContextKey.set(true); this.sectionContextKey.set(this.selectedSection); @@ -603,6 +683,8 @@ export class AICustomizationManagementEditor extends EditorPane { if (this.viewMode === 'mcpDetail') { this.goBackFromMcpDetail(); } + // Clear transient folder override on close + this.workspaceService.clearOverrideProjectRoot(); super.clearInput(); } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts index 0f22c638e180b..6d867728d3702 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { derived, IObservable, observableFromEventOpts } from '../../../../../base/common/observable.js'; +import { constObservable, derived, IObservable, observableFromEventOpts } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IAICustomizationWorkspaceService, AICustomizationManagementSection, IStorageSourceFilter } from '../../common/aiCustomizationWorkspaceService.js'; @@ -63,6 +63,10 @@ class AICustomizationWorkspaceService implements IAICustomizationWorkspaceServic readonly isSessionsWindow = false; + readonly hasOverrideProjectRoot = constObservable(false); + setOverrideProjectRoot(_root: URI): void { } + clearOverrideProjectRoot(): void { } + async commitFiles(_projectRoot: URI, _fileUris: URI[]): Promise { // No-op in core VS Code. } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css index eb634aa456255..af43b382e185b 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -31,7 +31,65 @@ overflow: hidden; } +/* Folder picker footer (sessions window only) */ +.ai-customization-management-editor .sidebar-folder-picker { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 2px; + padding: 6px 4px; + border-top: 1px solid var(--vscode-sideBarSectionHeader-border, transparent); +} + +.ai-customization-management-editor .folder-picker-button { + display: flex; + align-items: center; + gap: 6px; + flex: 1; + min-width: 0; + padding: 4px 6px; + border: none; + border-radius: 4px; + background: transparent; + color: var(--vscode-descriptionForeground); + cursor: pointer; + font-size: 11px; +} +.ai-customization-management-editor .folder-picker-button:hover { + background-color: var(--vscode-list-hoverBackground); +} + +.ai-customization-management-editor .folder-picker-icon { + flex-shrink: 0; + font-size: 14px; +} + +.ai-customization-management-editor .folder-picker-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ai-customization-management-editor .folder-picker-clear { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + padding: 0; + border: none; + border-radius: 4px; + background: transparent; + color: var(--vscode-descriptionForeground); + cursor: pointer; + font-size: 12px; +} + +.ai-customization-management-editor .folder-picker-clear:hover { + background-color: var(--vscode-list-hoverBackground); +} /* Section list items */ .ai-customization-management-editor .section-list-item { diff --git a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts index 28467684c8d93..26af14f656140 100644 --- a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts @@ -103,4 +103,22 @@ export interface IAICustomizationWorkspaceService { * Launches the AI-guided creation flow for the given customization type. */ generateCustomization(type: PromptsType): Promise; + + /** + * Whether a transient project root override is currently active. + */ + readonly hasOverrideProjectRoot: IObservable; + + /** + * Sets a transient override for the active project root. + * While set, `activeProjectRoot` returns this value instead of the + * session- or workspace-derived root. Call `clearOverrideProjectRoot()` to revert. + */ + setOverrideProjectRoot(root: URI): void; + + /** + * Clears the transient project root override, reverting to the + * session-derived (or workspace-derived) root. + */ + clearOverrideProjectRoot(): void; } From c0ba0411a9fc68e28be63a6deafe1746eb82e3c6 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sun, 1 Mar 2026 21:09:00 -0800 Subject: [PATCH 05/15] Add progress content part component fixture --- .../chatProgressContentPart.fixture.ts | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 src/vs/workbench/test/browser/componentFixtures/chatProgressContentPart.fixture.ts diff --git a/src/vs/workbench/test/browser/componentFixtures/chatProgressContentPart.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chatProgressContentPart.fixture.ts new file mode 100644 index 0000000000000..4136612fa9882 --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/chatProgressContentPart.fixture.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { Event } from '../../../../base/common/event.js'; +import { observableValue } from '../../../../base/common/observable.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { mock } from '../../../../base/test/common/mock.js'; +import { IMarkdownRendererService, MarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js'; +import { ChatProgressContentPart } from '../../../contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.js'; +import { ChatContentMarkdownRenderer } from '../../../contrib/chat/browser/widget/chatContentMarkdownRenderer.js'; +import { IChatContentPartRenderContext } from '../../../contrib/chat/browser/widget/chatContentParts/chatContentParts.js'; +import { IChatMarkdownAnchorService } from '../../../contrib/chat/browser/widget/chatContentParts/chatMarkdownAnchorService.js'; +import { IChatProgressMessage } from '../../../contrib/chat/common/chatService/chatService.js'; +import { IChatResponseViewModel } from '../../../contrib/chat/common/model/chatViewModel.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js'; + +import '../../../contrib/chat/browser/widget/media/chat.css'; + +function createMockContext(opts?: { isComplete?: boolean; hasFollowingContent?: boolean }): IChatContentPartRenderContext { + const element = new class extends mock() { + override readonly isComplete = opts?.isComplete ?? false; + }(); + return { + element, + elementIndex: 0, + container: document.createElement('div'), + content: opts?.hasFollowingContent ? [{ kind: 'progressMessage', content: new MarkdownString('test') }] : [], + contentIndex: 0, + editorPool: undefined!, + codeBlockStartIndex: 0, + treeStartIndex: 0, + diffEditorPool: undefined!, + codeBlockModelCollection: undefined!, + currentWidth: observableValue('currentWidth', 400), + onDidChangeVisibility: Event.None, + }; +} + +function createProgressMessage(text: string): IChatProgressMessage { + return { + kind: 'progressMessage', + content: new MarkdownString(text), + }; +} + +function renderProgressPart( + context: ComponentFixtureContext, + message: IChatProgressMessage, + renderContext: IChatContentPartRenderContext, + opts?: { + forceShowSpinner?: boolean; + forceShowMessage?: boolean; + icon?: ThemeIcon; + shimmer?: boolean; + }, +): void { + const { container, disposableStore } = context; + + const mockAnchorService = new class extends mock() { + override register() { return { dispose() { } }; } + }(); + + const instantiationService = createEditorServices(disposableStore, { + colorTheme: context.theme, + additionalServices: (reg) => { + reg.define(IMarkdownRendererService, MarkdownRendererService); + reg.defineInstance(IChatMarkdownAnchorService, mockAnchorService); + }, + }); + + const markdownRenderer = instantiationService.createInstance(ChatContentMarkdownRenderer); + + const part = disposableStore.add( + instantiationService.createInstance( + ChatProgressContentPart, + message, + markdownRenderer, + renderContext, + opts?.forceShowSpinner, + opts?.forceShowMessage, + opts?.icon, + undefined, // toolInvocation + opts?.shimmer, + ) + ); + + // .interactive-session provides CSS custom properties (--vscode-chat-font-size-body-s, etc.) + // .interactive-item-container .progress-container is the selector for layout styles + container.style.width = '400px'; + container.style.padding = '8px'; + container.classList.add('interactive-session'); + + const itemContainer = dom.$('.interactive-item-container'); + itemContainer.appendChild(part.domNode); + container.appendChild(itemContainer); +} + +export default defineThemedFixtureGroup({ + WithSpinner: defineComponentFixture({ + render: (ctx) => renderProgressPart( + ctx, + createProgressMessage('Searching workspace for relevant files...'), + createMockContext({ isComplete: false }), + { forceShowSpinner: true, forceShowMessage: true, shimmer: false }, + ), + }), + + Completed: defineComponentFixture({ + render: (ctx) => renderProgressPart( + ctx, + createProgressMessage('Found 12 relevant files'), + createMockContext({ isComplete: true }), + { forceShowSpinner: false, forceShowMessage: true }, + ), + }), + + WithCustomIcon: defineComponentFixture({ + render: (ctx) => renderProgressPart( + ctx, + createProgressMessage('Running tests...'), + createMockContext({ isComplete: false }), + { forceShowSpinner: true, forceShowMessage: true, icon: Codicon.beaker }, + ), + }), + + WithInlineCode: defineComponentFixture({ + render: (ctx) => renderProgressPart( + ctx, + createProgressMessage('Reading `src/vs/workbench/contrib/chat/browser/chatWidget.ts`'), + createMockContext({ isComplete: false }), + { forceShowSpinner: true, forceShowMessage: true, shimmer: false }, + ), + }), + + LongMessage: defineComponentFixture({ + render: (ctx) => renderProgressPart( + ctx, + createProgressMessage('Searching across multiple workspace folders for TypeScript files matching the pattern you described, including test files and configuration'), + createMockContext({ isComplete: false }), + { forceShowSpinner: true, forceShowMessage: true, shimmer: false }, + ), + }), +}); From 62788813e503424a03e2e40dd1d7012f33b7f119 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 2 Mar 2026 16:21:18 +1100 Subject: [PATCH 06/15] fix: enhance chat mode resolution by adding mode name lookup (#298225) --- .../workbench/contrib/chat/browser/widget/input/chatInputPart.ts | 1 + 1 file changed, 1 insertion(+) 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 d2e482f504a0f..74d2cd42b67c3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -1023,6 +1023,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } const mode2 = this.chatModeService.findModeById(mode) ?? + this.chatModeService.findModeByName(mode) ?? this.chatModeService.findModeById(ChatModeKind.Agent) ?? ChatMode.Ask; this.setChatMode2(mode2, storeSelection); From 190ed2998211d756f74346dbfd763b86d5e15d17 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sun, 1 Mar 2026 22:20:14 -0800 Subject: [PATCH 07/15] Remove edits2 setting (#298619) --- .../workbench/contrib/chat/browser/actions/chatActions.ts | 5 ++--- .../contrib/chat/browser/actions/chatExecuteActions.ts | 7 +++---- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 5 ----- src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts | 2 +- .../contrib/chat/browser/widget/input/chatInputPart.ts | 7 +------ .../contrib/chat/common/chatService/chatService.ts | 2 -- .../contrib/chat/common/chatService/chatServiceImpl.ts | 6 +----- src/vs/workbench/contrib/chat/common/constants.ts | 1 - .../agentSessions/localAgentSessionsController.test.ts | 1 - .../chat/test/common/chatService/mockChatService.ts | 1 - .../contrib/preferences/browser/settingsLayout.ts | 3 +-- 11 files changed, 9 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index b6ea731238562..7516c88f8edf3 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -1600,11 +1600,10 @@ export async function handleModeSwitch( return { needToClearSession: false }; } - const configurationService = accessor.get(IConfigurationService); const dialogService = accessor.get(IDialogService); - const needToClearEdits = (!configurationService.getValue(ChatConfiguration.Edits2Enabled) && (fromMode === ChatModeKind.Edit || toMode === ChatModeKind.Edit)) && requestCount > 0; + const needToClearEdits = (fromMode === ChatModeKind.Edit || toMode === ChatModeKind.Edit) && requestCount > 0; if (needToClearEdits) { - // If not using edits2 and switching into or out of edit mode, ask to discard the session + // Switching into or out of edit mode, ask to discard the session const phrase = localize('switchMode.confirmPhrase', "Switching agents will end your current edit session."); const currentEdits = model.editingSession.entries.get(); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index b2995f02d6b68..29fb309c4db48 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -306,7 +306,6 @@ class ToggleChatModeAction extends Action2 { async run(accessor: ServicesAccessor, ...args: unknown[]) { const commandService = accessor.get(ICommandService); - const configurationService = accessor.get(IConfigurationService); const instaService = accessor.get(IInstantiationService); const modeService = accessor.get(IChatModeService); const telemetryService = accessor.get(ITelemetryService); @@ -326,7 +325,7 @@ class ToggleChatModeAction extends Action2 { const chatSession = widget.viewModel?.model; const requestCount = chatSession?.getRequests().length ?? 0; - const switchToMode = (arg && (modeService.findModeById(arg.modeId) || modeService.findModeByName(arg.modeId))) ?? this.getNextMode(widget, requestCount, configurationService, modeService); + const switchToMode = (arg && (modeService.findModeById(arg.modeId) || modeService.findModeByName(arg.modeId))) ?? this.getNextMode(widget, requestCount, modeService); const currentMode = widget.input.currentModeObs.get(); if (switchToMode.id === currentMode.id) { @@ -365,11 +364,11 @@ class ToggleChatModeAction extends Action2 { } } - private getNextMode(chatWidget: IChatWidget, requestCount: number, configurationService: IConfigurationService, modeService: IChatModeService): IChatMode { + private getNextMode(chatWidget: IChatWidget, requestCount: number, modeService: IChatModeService): IChatMode { const modes = modeService.getModes(); const flat = [ ...modes.builtin.filter(mode => { - return mode.kind !== ChatModeKind.Edit || configurationService.getValue(ChatConfiguration.Edits2Enabled) || requestCount === 0; + return mode.kind !== ChatModeKind.Edit || requestCount === 0; }), ...(modes.custom ?? []), ]; diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 355094d8011ec..2c3b010411927 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -621,11 +621,6 @@ configurationRegistry.registerConfiguration({ mode: 'startup' } }, - [ChatConfiguration.Edits2Enabled]: { - type: 'boolean', - description: nls.localize('chat.edits2Enabled', "Enable the new Edits mode that is based on tool-calling. When this is enabled, models that don't support tool-calling are unavailable for Edits mode."), - default: false, - }, [ChatConfiguration.ExtensionToolsEnabled]: { type: 'boolean', description: nls.localize('chat.extensionToolsEnabled', "Enable using tools contributed by third-party extensions."), diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index ca969bf7da018..40e88107acad0 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -2244,7 +2244,7 @@ export class ChatWidget extends Disposable implements IChatWidget { await this._applyPromptFileIfSet(requestInputs); await this._autoAttachInstructions(requestInputs); - if (this.viewOptions.enableWorkingSet !== undefined && this.input.currentModeKind === ChatModeKind.Edit && !this.chatService.edits2Enabled) { + if (this.viewOptions.enableWorkingSet !== undefined && this.input.currentModeKind === ChatModeKind.Edit) { const uniqueWorkingSetEntries = new ResourceSet(); // NOTE: this is used for bookkeeping so the UI can avoid rendering references in the UI that are already shown in the working set const editingSessionAttachedContext: ChatRequestVariableSet = requestInputs.attachedContext; 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 74d2cd42b67c3..6fe3ad2ee47d3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -734,11 +734,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._register(this._onDidChangeCurrentChatMode.event(() => { this.checkModelSupported(); })); - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(ChatConfiguration.Edits2Enabled)) { - this.checkModelSupported(); - } - })); } public setEditing(enabled: boolean, editingSentRequest: ChatContextKeys.EditingRequestType | undefined) { @@ -1043,7 +1038,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private modelSupportedForDefaultAgent(model: ILanguageModelChatMetadataAndIdentifier): boolean { // Probably this logic could live in configuration on the agent, or somewhere else, if it gets more complex - if (this.currentModeKind === ChatModeKind.Agent || (this.currentModeKind === ChatModeKind.Edit && this.configurationService.getValue(ChatConfiguration.Edits2Enabled))) { + if (this.currentModeKind === ChatModeKind.Agent) { return ILanguageModelChatMetadata.suitableForAgentMode(model.metadata); } diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 3d31f408577b1..868b0be875f9b 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -1453,8 +1453,6 @@ export interface IChatService { activateDefaultAgent(location: ChatAgentLocation): Promise; - readonly edits2Enabled: boolean; - readonly requestInProgressObs: IObservable; /** diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 6b2d3a5aefcd4..c2393a4b9cd98 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -48,7 +48,7 @@ import { IChatSlashCommandService } from '../participants/chatSlashCommands.js'; import { IChatTransferService } from '../model/chatTransferService.js'; import { LocalChatSessionUri } from '../model/chatUri.js'; import { IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../constants.js'; +import { ChatAgentLocation, ChatModeKind } from '../constants.js'; import { ChatMessageRole, IChatMessage, ILanguageModelsService } from '../languageModels.js'; import { ILanguageModelToolsService } from '../tools/languageModelToolsService.js'; import { ChatSessionOperationLog } from '../model/chatSessionOperationLog.js'; @@ -143,10 +143,6 @@ export class ChatService extends Disposable implements IChatService { return this._sessionModels.waitForModelDisposals(); } - public get edits2Enabled(): boolean { - return this.configurationService.getValue(ChatConfiguration.Edits2Enabled); - } - private get isEmptyWindow(): boolean { const workspace = this.workspaceContextService.getWorkspace(); return !workspace.configuration && workspace.folders.length === 0; diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 57b1391e68f21..52fe226750675 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -22,7 +22,6 @@ export enum ChatConfiguration { UnifiedAgentsBar = 'chat.unifiedAgentsBar.enabled', AgentSessionProjectionEnabled = 'chat.agentSessionProjection.enabled', EditModeHidden = 'chat.editMode.hidden', - Edits2Enabled = 'chat.edits2.enabled', ExtensionToolsEnabled = 'chat.extensionTools.enabled', RepoInfoEnabled = 'chat.repoInfo.enabled', EditRequests = 'chat.editRequests', diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts index b15974182e42f..c1c62a1246edc 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts @@ -40,7 +40,6 @@ class MockChatService implements IChatService { private readonly _chatModels: ISettableObservable> = observableValue('chatModels', []); readonly chatModels = this._chatModels; requestInProgressObs = observableValue('name', false); - edits2Enabled: boolean = false; _serviceBrand: undefined; editingSessions = []; transferredSessionResource = undefined; diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts index 54579be64cb42..f10ffa492b6b2 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts @@ -16,7 +16,6 @@ import { ChatAgentLocation } from '../../../common/constants.js'; export class MockChatService implements IChatService { chatModels: IObservable> = observableValue('chatModels', []); requestInProgressObs = observableValue('name', false); - edits2Enabled: boolean = false; _serviceBrand: undefined; editingSessions = []; transferredSessionResource: URI | undefined; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index 6e6f22ce61a26..7c5d7df73e52b 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -232,8 +232,7 @@ export const tocData: ITOCEntry = { label: localize('chatTools', "Tools"), settings: [ 'chat.tools.*', - 'chat.extensionTools.*', - 'chat.edits2.enabled' + 'chat.extensionTools.*' ] }, { From f69012e7a3a2373cb546409ce374f557382f43bc Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 2 Mar 2026 09:22:29 +0100 Subject: [PATCH 08/15] fix: include error handling in inline chat session overlay logic (#298651) re https://github.com/microsoft/vscode/issues/298647 --- .../contrib/inlineChat/browser/inlineChatController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index ad180ba141c7b..76aa39da94237 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -362,12 +362,13 @@ export class InlineChatController implements IEditorContribution { const lastRequest = session.chatModel.lastRequestObs.read(r); const isInProgress = lastRequest?.response?.isInProgress.read(r); const isPendingConfirmation = !!lastRequest?.response?.isPendingConfirmation.read(r); + const isError = !!lastRequest?.response?.result?.errorDetails; ctxPendingConfirmation.set(isPendingConfirmation); const entry = session.editingSession.readEntry(session.uri, r); // 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 || isPendingConfirmation) { + if (isInProgress || isNotSettled || isPendingConfirmation || isError) { sessionOverlayWidget.show(session); } else { sessionOverlayWidget.hide(); From a432b6a01d136e0fc3eeefe6e2f04c68cc4c41af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 09:29:01 +0100 Subject: [PATCH 09/15] Bump minimatch from 9.0.5 to 9.0.9 in /build/npm/gyp (#298336) Bumps [minimatch](https://github.com/isaacs/minimatch) from 9.0.5 to 9.0.9. - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v9.0.5...v9.0.9) --- updated-dependencies: - dependency-name: minimatch dependency-version: 9.0.9 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 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build/npm/gyp/package-lock.json b/build/npm/gyp/package-lock.json index 6e28e550f4699..e2785131796d7 100644 --- a/build/npm/gyp/package-lock.json +++ b/build/npm/gyp/package-lock.json @@ -526,13 +526,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" From fd63d2a3867894ba688351cfa6fa34c00c92a7c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 09:38:15 +0100 Subject: [PATCH 10/15] Bump minimatch in /build (#298315) Bumps and [minimatch](https://github.com/isaacs/minimatch). These dependencies needed to be updated together. Updates `minimatch` from 10.1.1 to 10.2.4 - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v10.1.1...v10.2.4) Updates `minimatch` from 3.1.2 to 3.1.5 - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v10.1.1...v10.2.4) Updates `minimatch` from 9.0.5 to 9.0.9 - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v10.1.1...v10.2.4) --- updated-dependencies: - dependency-name: minimatch dependency-version: 10.2.4 dependency-type: indirect - dependency-name: minimatch dependency-version: 3.1.5 dependency-type: indirect - dependency-name: minimatch dependency-version: 9.0.9 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build/package-lock.json | 72 +++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/build/package-lock.json b/build/package-lock.json index b7890ceb3d420..ec46db00b08cd 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -1027,29 +1027,6 @@ "node": ">=18" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2170,6 +2147,29 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/@vscode/vsce/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@vscode/vsce/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/@vscode/vsce/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2232,16 +2232,16 @@ } }, "node_modules/@vscode/vsce/node_modules/glob/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4668,10 +4668,11 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6757,12 +6758,13 @@ } }, "node_modules/vscode-universal-bundler/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" From 35a6bb3858be84eb6bdfe03784a2e9ce927f6e82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 08:48:04 +0000 Subject: [PATCH 11/15] Bump minimatch (#298537) Bumps [minimatch](https://github.com/isaacs/minimatch) to 3.1.5 and updates ancestor dependencies and [minimatch](https://github.com/isaacs/minimatch). These dependencies need to be updated together. Updates `minimatch` from 3.1.2 to 3.1.5 - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5) Updates `minimatch` from 9.0.5 to 9.0.9 - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5) Updates `minimatch` from 5.1.6 to 5.1.9 - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5) --- updated-dependencies: - dependency-name: minimatch dependency-version: 3.1.5 dependency-type: direct:development - dependency-name: minimatch dependency-version: 9.0.9 dependency-type: indirect - dependency-name: minimatch dependency-version: 5.1.9 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 47 ++++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 34be077fa1f8a..cbf7d5d6d9c5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -136,7 +136,7 @@ "lazy.js": "^0.4.2", "merge-options": "^1.0.1", "mime": "^1.4.1", - "minimatch": "^3.0.4", + "minimatch": "^3.1.5", "mocha": "^10.8.2", "mocha-junit-reporter": "^2.2.1", "mocha-multi-reporters": "^1.5.1", @@ -2313,13 +2313,13 @@ } }, "node_modules/@ts-morph/common/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -2857,13 +2857,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -3450,13 +3450,13 @@ } }, "node_modules/@vscode/l10n-dev/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -3652,13 +3652,13 @@ } }, "node_modules/@vscode/test-cli/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -12820,10 +12820,11 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -13040,9 +13041,9 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 175cd7368eea4..0945d9edaa197 100644 --- a/package.json +++ b/package.json @@ -205,7 +205,7 @@ "lazy.js": "^0.4.2", "merge-options": "^1.0.1", "mime": "^1.4.1", - "minimatch": "^3.0.4", + "minimatch": "^3.1.5", "mocha": "^10.8.2", "mocha-junit-reporter": "^2.2.1", "mocha-multi-reporters": "^1.5.1", From d1d66da10af0b862f8528a283db7772454d2f8a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 08:53:34 +0000 Subject: [PATCH 12/15] Bump minimatch from 3.1.2 to 3.1.5 in /test/integration/browser (#298658) Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5. - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5) --- updated-dependencies: - dependency-name: minimatch dependency-version: 3.1.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/integration/browser/package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/integration/browser/package-lock.json b/test/integration/browser/package-lock.json index 538e7c4c3e979..90ce261a01be9 100644 --- a/test/integration/browser/package-lock.json +++ b/test/integration/browser/package-lock.json @@ -135,10 +135,11 @@ "dev": true }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, From d2a693d828af34df64964757cb71e7d342860519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Mon, 2 Mar 2026 09:54:03 +0100 Subject: [PATCH 13/15] fix missing sourcemaps (#298660) --- build/azure-pipelines/product-quality-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines/product-quality-checks.yml b/build/azure-pipelines/product-quality-checks.yml index 5692a55965a8f..983a0a4b25aea 100644 --- a/build/azure-pipelines/product-quality-checks.yml +++ b/build/azure-pipelines/product-quality-checks.yml @@ -132,7 +132,7 @@ jobs: continueOnError: true condition: and(succeeded(), eq(lower(variables['VSCODE_PUBLISH']), 'true')) - - script: npm exec -- npm-run-all2 -lp hygiene eslint valid-layers-check define-class-fields-check vscode-dts-compile-check tsec-compile-check test-build-scripts + - script: npm exec -- npm-run-all2 -lp core-ci hygiene eslint valid-layers-check define-class-fields-check vscode-dts-compile-check tsec-compile-check test-build-scripts env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Compile & Hygiene From c95cc7ae08401765bf2e89559afdd2364e3cf3c3 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Mon, 2 Mar 2026 01:07:48 -0800 Subject: [PATCH 14/15] Fix Guardian errors in sanity tests stage (#298662) --- build/azure-pipelines/common/sanity-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines/common/sanity-tests.yml b/build/azure-pipelines/common/sanity-tests.yml index 4e47f4672a002..ce6d95dd7e59d 100644 --- a/build/azure-pipelines/common/sanity-tests.yml +++ b/build/azure-pipelines/common/sanity-tests.yml @@ -48,7 +48,7 @@ jobs: - checkout: self fetchDepth: 1 fetchTags: false - sparseCheckoutDirectories: test/sanity .nvmrc + sparseCheckoutDirectories: build/azure-pipelines/config test/sanity .nvmrc displayName: Checkout test/sanity - ${{ if eq(parameters.os, 'windows') }}: From 43755b476288203130eaa84b553e107e0045c930 Mon Sep 17 00:00:00 2001 From: cathaysia Date: Mon, 2 Mar 2026 18:19:12 +0800 Subject: [PATCH 15/15] =?UTF-8?q?fix(json.schemaDownload.trustedDomains):?= =?UTF-8?q?=20avoid=20always=20update=20json.sch=E2=80=A6=20(#298423)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(json.schemaDownload.trustedDomains): avoid always update json.schemaDownload.trustedDomains Signed-off-by: loongtao.zhang --- .../client/src/jsonClient.ts | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index eb336e8f89bc5..06dfa8efe48db 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -690,6 +690,23 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP execute: () => Promise; } + const normalizeTrustedDomains = (domains: Record): Record => { + return Object.fromEntries(Object.entries(domains).sort(([a], [b]) => a.localeCompare(b))); + }; + + const updateTrustedDomains = async (updateDomain: string): Promise => { + const config = workspace.getConfiguration(); + const currentDomains = config.get>(SettingIds.trustedDomains, {}); + if (currentDomains[updateDomain] === true) { + return; + } + const nextDomains = normalizeTrustedDomains({ + ...currentDomains, + [updateDomain]: true + }); + await config.update(SettingIds.trustedDomains, nextDomains, true); + }; + const items: QuickPickItemWithAction[] = []; try { @@ -701,10 +718,7 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP label: l10n.t('Trust Domain: {0}', domain), description: l10n.t('Allow all schemas from this domain'), execute: async () => { - const config = workspace.getConfiguration(); - const currentDomains = config.get>(SettingIds.trustedDomains, {}); - currentDomains[domain] = true; - await config.update(SettingIds.trustedDomains, currentDomains, true); + await updateTrustedDomains(domain); await commands.executeCommand(CommandIds.workbenchActionOpenSettings, SettingIds.trustedDomains); } }); @@ -714,10 +728,7 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP label: l10n.t('Trust URI: {0}', schemaUri), description: l10n.t('Allow only this specific schema'), execute: async () => { - const config = workspace.getConfiguration(); - const currentDomains = config.get>(SettingIds.trustedDomains, {}); - currentDomains[schemaUri] = true; - await config.update(SettingIds.trustedDomains, currentDomains, true); + await updateTrustedDomains(schemaUri); await commands.executeCommand(CommandIds.workbenchActionOpenSettings, SettingIds.trustedDomains); } }); @@ -926,4 +937,3 @@ export function isSchemaResolveError(d: Diagnostic) { return typeof d.code === 'number' && d.code >= ErrorCodes.SchemaResolveError; } -