From aebed4269017b721c956ceca250b163d08be3dfd Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 22 May 2025 18:01:28 -0700 Subject: [PATCH 001/306] Enable basic math rendering in chat requests and responses --- package-lock.json | 26 ++++ package.json | 1 + src/vs/base/browser/dom.ts | 74 ++++++++++ src/vs/base/browser/markdownRenderer.ts | 28 ++-- .../chatMarkdownContentPart.ts | 26 +++- .../chatContentParts/markedKatexSupport.ts | 131 ++++++++++++++++++ .../media/chatMarkdownPart.css | 8 ++ .../chat/browser/chatMarkdownRenderer.ts | 1 + 8 files changed, 282 insertions(+), 13 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatMarkdownPart.css diff --git a/package-lock.json b/package-lock.json index 362802882ef..4e96cec8331 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", + "katex": "^0.16.22", "kerberos": "2.1.1", "minimist": "^1.2.6", "native-is-elevated": "0.7.0", @@ -10958,6 +10959,31 @@ "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, + "node_modules/katex": { + "version": "0.16.22", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", + "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/kerberos": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-2.1.1.tgz", diff --git a/package.json b/package.json index c8151ee376a..0191cbf1677 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", + "katex": "^0.16.22", "kerberos": "2.1.1", "minimist": "^1.2.6", "native-is-elevated": "0.7.0", diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 553c584a63d..925b9f2a5dd 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1771,6 +1771,80 @@ export const basicMarkupHtmlTags = Object.freeze([ 'wbr', ]); +export const trustedMathMlTags = Object.freeze([ + 'math', + 'menclose', + 'merror', + 'mfenced', + 'mfrac', + 'mglyph', + 'mi', + 'mlabeledtr', + 'mmultiscripts', + 'mn', + 'mo', + 'mover', + 'mpadded', + 'mphantom', + 'mroot', + 'mrow', + 'ms', + 'mspace', + 'msqrt', + 'mstyle', + 'msub', + 'msup', + 'msubsup', + 'mtable', + 'mtd', + 'mtext', + 'mtr', + 'munder', + 'munderover', + 'mprescripts', + + // svg tags + 'svg', + 'altglyph', + 'altglyphdef', + 'altglyphitem', + 'circle', + 'clippath', + 'defs', + 'desc', + 'ellipse', + 'filter', + 'font', + 'g', + 'glyph', + 'glyphref', + 'hkern', + 'line', + 'lineargradient', + 'marker', + 'mask', + 'metadata', + 'mpath', + 'path', + 'pattern', + 'polygon', + 'polyline', + 'radialgradient', + 'rect', + 'stop', + 'style', + 'switch', + 'symbol', + 'text', + 'textpath', + 'title', + 'tref', + 'tspan', + 'view', + 'vkern', +]); + + const defaultDomPurifyConfig = Object.freeze({ ALLOWED_TAGS: ['a', 'button', 'blockquote', 'code', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'input', 'label', 'li', 'p', 'pre', 'select', 'small', 'span', 'strong', 'textarea', 'ul', 'ol'], ALLOWED_ATTR: ['href', 'data-href', 'data-command', 'target', 'title', 'name', 'src', 'alt', 'class', 'id', 'role', 'tabindex', 'style', 'data-code', 'width', 'height', 'align', 'x-dispatch', 'required', 'checked', 'placeholder', 'type', 'start'], diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 2cb748afcdf..51fad18eafc 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -40,8 +40,9 @@ export interface MarkdownRenderOptions extends FormattedTextRenderOptions { } export interface ISanitizerOptions { - replaceWithPlaintext?: boolean; - allowedTags?: string[]; + readonly replaceWithPlaintext?: boolean; + readonly allowedTags?: readonly string[]; + readonly preserveClassAndStyleAttrs?: boolean; } const defaultMarkedRenderers = Object.freeze({ @@ -106,14 +107,14 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende const element = createElement(options); const markedInstance = new marked.Marked(...(markedOptions.markedExtensions ?? [])); - const { renderer, codeBlocks, syncCodeBlocks } = createMarkdownRenderer(markedInstance, options, markdown); + const { renderer, codeBlocks, syncCodeBlocks } = createMarkdownRenderer(markedInstance, options, markedOptions, markdown); const value = preprocessMarkdownString(markdown); let renderedMarkdown: string; if (options.fillInIncompleteTokens) { // The defaults are applied by parse but not lexer()/parser(), and they need to be present const opts: MarkedOptions = { - ...marked.defaults, + ...markedInstance.defaults, ...markedOptions, renderer }; @@ -244,8 +245,8 @@ function rewriteRenderedLinks(markdown: IMarkdownString, options: MarkdownRender } } -function createMarkdownRenderer(marked: marked.Marked, options: MarkdownRenderOptions, markdown: IMarkdownString): { renderer: marked.Renderer; codeBlocks: Promise<[string, HTMLElement]>[]; syncCodeBlocks: [string, HTMLElement][] } { - const renderer = new marked.Renderer(); +function createMarkdownRenderer(marked: marked.Marked, options: MarkdownRenderOptions, markedOptions: MarkedOptions, markdown: IMarkdownString): { renderer: marked.Renderer; codeBlocks: Promise<[string, HTMLElement]>[]; syncCodeBlocks: [string, HTMLElement][] } { + const renderer = new marked.Renderer(markedOptions); renderer.image = defaultMarkedRenderers.image; renderer.link = defaultMarkedRenderers.link; renderer.paragraph = defaultMarkedRenderers.paragraph; @@ -396,7 +397,7 @@ function resolveWithBaseUri(baseUri: URI, href: string): string { } interface IInternalSanitizerOptions extends ISanitizerOptions { - isTrusted?: boolean | MarkdownStringTrustedOptions; + readonly isTrusted?: boolean | MarkdownStringTrustedOptions; } const selfClosingTags = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']; @@ -409,6 +410,11 @@ function sanitizeRenderedMarkdown( const store = new DisposableStore(); store.add(addDompurifyHook('uponSanitizeAttribute', (element, e) => { if (e.attrName === 'style' || e.attrName === 'class') { + if (options.preserveClassAndStyleAttrs) { + e.keepAttr = true; + return; + } + if (element.tagName === 'SPAN') { if (e.attrName === 'style') { e.keepAttr = /^(color\:(#[0-9a-fA-F]+|var\(--vscode(-[a-zA-Z0-9]+)+\));)?(background-color\:(#[0-9a-fA-F]+|var\(--vscode(-[a-zA-Z0-9]+)+\));)?(border-radius:[0-9]+px;)?$/.test(e.attrValue); @@ -418,6 +424,7 @@ function sanitizeRenderedMarkdown( return; } } + e.keepAttr = false; return; } else if (element.tagName === 'INPUT' && element.attributes.getNamedItem('type')?.value === 'checkbox') { @@ -484,7 +491,8 @@ function sanitizeRenderedMarkdown( store.add(DOM.hookDomPurifyHrefAndSrcSanitizer(allowedSchemes)); try { - return dompurify.sanitize(renderedMarkdown, { ...config, RETURN_TRUSTED_TYPE: true }); + const a = dompurify.sanitize(renderedMarkdown, { ...config, RETURN_TRUSTED_TYPE: true }); + return a; } finally { store.dispose(); } @@ -541,8 +549,8 @@ function getSanitizerOptions(options: IInternalSanitizerOptions): { config: domp // Since we have our own sanitize function for marked, it's possible we missed some tag so let dompurify make sure. // HTML tags that can result from markdown are from reading https://spec.commonmark.org/0.29/ // HTML table tags that can result from markdown are from https://github.github.com/gfm/#tables-extension- - ALLOWED_TAGS: options.allowedTags ?? [...DOM.basicMarkupHtmlTags], - ALLOWED_ATTR: allowedMarkdownAttr, + ALLOWED_TAGS: options.allowedTags ? [...options.allowedTags] : [...DOM.basicMarkupHtmlTags], + ALLOWED_ATTR: [...allowedMarkdownAttr, ...(options.preserveClassAndStyleAttrs ? ['class'] : [])], ALLOW_UNKNOWN_PROTOCOLS: true, }, allowedSchemes diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index 295e2a0223a..c4c5fdec385 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -4,8 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../../base/browser/dom.js'; +import { MarkedOptions } from '../../../../../base/browser/markdownRenderer.js'; import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js'; +import { coalesce } from '../../../../../base/common/arrays.js'; import { findLast } from '../../../../../base/common/arraysFind.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../base/common/event.js'; @@ -46,6 +48,8 @@ import '../media/chatCodeBlockPill.css'; import { IDisposableReference, ResourcePool } from './chatCollections.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; import { ChatExtensionsContentPart } from './chatExtensionsContentPart.js'; +import { MarkedKatexSupport } from './markedKatexSupport.js'; +import './media/chatMarkdownPart.css'; const $ = dom.$; @@ -55,6 +59,7 @@ export interface IChatMarkdownContentPartOptions { export class ChatMarkdownContentPart extends Disposable implements IChatContentPart { private static idPool = 0; + public readonly codeblocksPartId = String(++ChatMarkdownContentPart.idPool); public readonly domNode: HTMLElement; private readonly allRefs: IDisposableReference[] = []; @@ -91,13 +96,27 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP let globalCodeBlockIndexStart = codeBlockStartIndex; let thisPartCodeBlockIndexStart = 0; + const markedExtensions = coalesce([ + MarkedKatexSupport.getExtension(context.container), + ]); + // Don't set to 'false' for responses, respect defaults - const markedOpts = isRequestVM(element) ? { + const markedOpts: MarkedOptions = isRequestVM(element) || true ? { gfm: true, breaks: true, - } : undefined; + markedExtensions, + } : { + markedExtensions, + }; const result = this._register(renderer.render(markdown.content, { + sanitizerOptions: { + allowedTags: [ + ...dom.basicMarkupHtmlTags, + ...dom.trustedMathMlTags, + ], + preserveClassAndStyleAttrs: true, + }, fillInIncompleteTokens, codeBlockRendererSync: (languageId, text, raw) => { const isCodeBlockComplete = !isResponseVM(context.element) || context.element.isComplete || !raw || codeblockHasClosingBackticks(raw); @@ -212,6 +231,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP this._register(markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(markdown, result.element)); orderedDisposablesList.reverse().forEach(d => this._register(d)); + result.element.classList.add('chat-markdown-part'); this.domNode = result.element; } @@ -306,7 +326,7 @@ function codeblockHasClosingBackticks(str: string): boolean { return !!str.match(/\n```+$/); } -class CollapsedCodeBlock extends Disposable { +export class CollapsedCodeBlock extends Disposable { public readonly element: HTMLElement; diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts new file mode 100644 index 00000000000..b8a5b00da1b --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { importAMDNodeModule, resolveAmdNodeModulePath } from '../../../../../amdX.js'; +import * as dom from '../../../../../base/browser/dom.js'; +import type * as marked from '../../../../../base/common/marked/marked.js'; + +export class MarkedKatexSupport { + + public static _katex?: typeof import('katex').default; + + static { + importAMDNodeModule('katex', 'dist/katex.js').then(katex => { + this._katex = katex; + }); + } + + public static getExtension(container: HTMLElement): marked.MarkedExtension | undefined { + if (!this._katex) { + return undefined; + } + + this.ensureKatexStyles(container); + return MarkedKatexExtension.extension(this._katex); + } + + public static ensureKatexStyles(container: HTMLElement) { + const doc = dom.getWindow(container).document; + if (!doc.querySelector('link.katex')) { + const katexStyle = document.createElement('link'); + katexStyle.classList.add('katex'); + katexStyle.rel = 'stylesheet'; + katexStyle.href = resolveAmdNodeModulePath('katex', 'dist/katex.min.css'); + doc.head.appendChild(katexStyle); + } + } +} + + +namespace MarkedKatexExtension { + + // From https://github.com/UziTech/marked-katex-extension/blob/main/src/index.js + export interface MarkedKatexOptions { + nonStandard?: boolean; + } + + // allow-any-unicode-next-line + const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1(?=[\s?!\.,:?!。,:]|$)/; + const inlineRuleNonStandard = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1/; // Non-standard, even if there are no spaces before and after $ or $$, try to parse + + const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/; + + export function extension(katex: typeof import('katex').default, options = {}): marked.MarkedExtension { + return { + extensions: [ + inlineKatex(options, createRenderer(katex, options, false)), + blockKatex(options, createRenderer(katex, options, true)), + ], + }; + } + + function createRenderer(katex: typeof import('katex').default, options: MarkedKatexOptions, newlineAfter: boolean): marked.RendererExtensionFunction { + return (token: marked.Tokens.Generic) => katex.renderToString(token.text, { ...options, displayMode: token.displayMode }) + (newlineAfter ? '\n' : ''); + } + + function inlineKatex(options: MarkedKatexOptions, renderer: marked.RendererExtensionFunction): marked.TokenizerAndRendererExtension { + const nonStandard = options && options.nonStandard; + const ruleReg = nonStandard ? inlineRuleNonStandard : inlineRule; + return { + name: 'inlineKatex', + level: 'inline', + start(src: string) { + let index; + let indexSrc = src; + + while (indexSrc) { + index = indexSrc.indexOf('$'); + if (index === -1) { + return; + } + const f = nonStandard ? index > -1 : index === 0 || indexSrc.charAt(index - 1) === ' '; + if (f) { + const possibleKatex = indexSrc.substring(index); + + if (possibleKatex.match(ruleReg)) { + return index; + } + } + + indexSrc = indexSrc.substring(index + 1).replace(/^\$+/, ''); + } + return; + }, + tokenizer(src: string, tokens: marked.Token[]) { + const match = src.match(ruleReg); + if (match) { + return { + type: 'inlineKatex', + raw: match[0], + text: match[2].trim(), + displayMode: match[1].length === 2, + }; + } + return; + }, + renderer, + }; + } + + function blockKatex(options: MarkedKatexOptions, renderer: marked.RendererExtensionFunction): marked.TokenizerAndRendererExtension { + return { + name: 'blockKatex', + level: 'block', + tokenizer(src: string, tokens: marked.Token[]) { + const match = src.match(blockRule); + if (match) { + return { + type: 'blockKatex', + raw: match[0], + text: match[2].trim(), + displayMode: match[1].length === 2, + }; + } + return; + }, + renderer, + }; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatMarkdownPart.css b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatMarkdownPart.css new file mode 100644 index 00000000000..a36ae69bc02 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatMarkdownPart.css @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-markdown-part .katex-display { + overflow-x: scroll; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts index 858bc89d5f5..051f535f9f6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts @@ -76,6 +76,7 @@ export class ChatMarkdownRenderer extends MarkdownRenderer { sanitizerOptions: { replaceWithPlaintext: true, allowedTags: allowedHtmlTags, + ...options?.sanitizerOptions, } }; From db077c894b3d242d9ad42340669eaaaf68d3bcd7 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 22 May 2025 18:10:21 -0700 Subject: [PATCH 002/306] Add setting --- .../workbench/contrib/chat/browser/chat.contribution.ts | 6 ++++++ .../browser/chatContentParts/chatMarkdownContentPart.ts | 9 ++++++--- .../chat/browser/chatContentParts/markedKatexSupport.ts | 5 +++++ src/vs/workbench/contrib/chat/common/constants.ts | 1 + 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 220b964e5a7..ed361cb6f24 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -293,6 +293,12 @@ configurationRegistry.registerConfiguration({ defaultValue: false } }, + [ChatConfiguration.EnableMath]: { + type: 'boolean', + description: nls.localize('chat.mathEnabled.description', "Enable math rendering in chat responses using Katex."), + default: false, + tags: ['preview'], + }, [mcpDiscoverySection]: { oneOf: [ { type: 'boolean' }, diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index c4c5fdec385..3be3d1651bc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -26,6 +26,7 @@ import { ITextModelService } from '../../../../../editor/common/services/resolve import { localize } from '../../../../../nls.js'; import { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { FileKind } from '../../../../../platform/files/common/files.js'; @@ -39,6 +40,7 @@ import { IChatProgressRenderableResponseContent } from '../../common/chatModel.j import { IChatMarkdownContent, IChatService, IChatUndoStop } from '../../common/chatService.js'; import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; import { CodeBlockEntry, CodeBlockModelCollection } from '../../common/codeBlockModelCollection.js'; +import { ChatConfiguration } from '../../common/constants.js'; import { IChatCodeBlockInfo } from '../chat.js'; import { IChatRendererDelegate } from '../chatListRenderer.js'; import { ChatMarkdownDecorationsRenderer } from '../chatMarkdownDecorationsRenderer.js'; @@ -80,6 +82,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP private readonly codeBlockModelCollection: CodeBlockModelCollection, private readonly rendererOptions: IChatMarkdownContentPartOptions, @IContextKeyService contextKeyService: IContextKeyService, + @IConfigurationService configurationService: IConfigurationService, @ITextModelService private readonly textModelService: ITextModelService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { @@ -96,9 +99,9 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP let globalCodeBlockIndexStart = codeBlockStartIndex; let thisPartCodeBlockIndexStart = 0; - const markedExtensions = coalesce([ - MarkedKatexSupport.getExtension(context.container), - ]); + const markedExtensions = configurationService.getValue(ChatConfiguration.EnableMath) + ? coalesce([MarkedKatexSupport.getExtension(context.container)]) + : []; // Don't set to 'false' for responses, respect defaults const markedOpts: MarkedOptions = isRequestVM(element) || true ? { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts index b8a5b00da1b..c6821e1ee59 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts @@ -12,6 +12,11 @@ export class MarkedKatexSupport { public static _katex?: typeof import('katex').default; static { + // TODO: figure out a better way to do this + // I ran into two issues: + // - We don't support to level imports of node_modules so you have to use `import(...)` + // - I also didn't want to make all the callers to markdown rendering async to properly await + // loading of the extension, especially because many of them are ctors. importAMDNodeModule('katex', 'dist/katex.js').then(katex => { this._katex = katex; }); diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 33201e8807a..b6c51f18eb3 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -8,6 +8,7 @@ export enum ChatConfiguration { AgentEnabled = 'chat.agent.enabled', Edits2Enabled = 'chat.edits2.enabled', ExtensionToolsEnabled = 'chat.extensionTools.enabled', + EnableMath = 'chat.math.enabled', } export enum ChatMode { From 4f983c5c34f06dcd05011161c9cac497a4d0cbb2 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 22 May 2025 18:14:55 -0700 Subject: [PATCH 003/306] revert small change --- src/vs/base/browser/markdownRenderer.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 51fad18eafc..50c0eac1609 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -491,8 +491,7 @@ function sanitizeRenderedMarkdown( store.add(DOM.hookDomPurifyHrefAndSrcSanitizer(allowedSchemes)); try { - const a = dompurify.sanitize(renderedMarkdown, { ...config, RETURN_TRUSTED_TYPE: true }); - return a; + return dompurify.sanitize(renderedMarkdown, { ...config, RETURN_TRUSTED_TYPE: true }); } finally { store.dispose(); } From 051c888be82f6b65b03d83ff48c2c0d4552bc42d Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 22 May 2025 22:27:52 -0700 Subject: [PATCH 004/306] Replace unicode chars --- .../chat/browser/chatContentParts/markedKatexSupport.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts index c6821e1ee59..2b9d885a7d6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts @@ -51,8 +51,7 @@ namespace MarkedKatexExtension { nonStandard?: boolean; } - // allow-any-unicode-next-line - const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1(?=[\s?!\.,:?!。,:]|$)/; + const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1(?=[\s?!\.,:'\uff1f\uff01\u3002\uff0c\uff1a']|$)/; const inlineRuleNonStandard = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1/; // Non-standard, even if there are no spaces before and after $ or $$, try to parse const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/; From 3fb700f32b9e80feff435714f59dc5ccc77a100c Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 23 May 2025 11:51:15 -0700 Subject: [PATCH 005/306] Add style sanitizer and fix bugs --- src/vs/base/browser/dom.ts | 2 + src/vs/base/browser/markdownRenderer.ts | 23 ++++-- .../chatMarkdownContentPart.ts | 76 ++++++++++++++++++- .../chatContentParts/markedKatexSupport.ts | 20 +++-- 4 files changed, 108 insertions(+), 13 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 925b9f2a5dd..3f0e98201e7 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1772,6 +1772,8 @@ export const basicMarkupHtmlTags = Object.freeze([ ]); export const trustedMathMlTags = Object.freeze([ + 'semantics', + 'annotation', 'math', 'menclose', 'merror', diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 50c0eac1609..9044a9995bf 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -42,7 +42,8 @@ export interface MarkdownRenderOptions extends FormattedTextRenderOptions { export interface ISanitizerOptions { readonly replaceWithPlaintext?: boolean; readonly allowedTags?: readonly string[]; - readonly preserveClassAndStyleAttrs?: boolean; + readonly customAttrSanitizer?: (attrName: string, attrValue: string) => boolean | string; + readonly allowedSchemes?: readonly string[]; } const defaultMarkedRenderers = Object.freeze({ @@ -409,12 +410,22 @@ function sanitizeRenderedMarkdown( const { config, allowedSchemes } = getSanitizerOptions(options); const store = new DisposableStore(); store.add(addDompurifyHook('uponSanitizeAttribute', (element, e) => { - if (e.attrName === 'style' || e.attrName === 'class') { - if (options.preserveClassAndStyleAttrs) { - e.keepAttr = true; - return; + if (options.customAttrSanitizer) { + const result = options.customAttrSanitizer(e.attrName, e.attrValue); + if (typeof result === 'string') { + if (result) { + e.attrValue = result; + e.keepAttr = true; + } else { + e.keepAttr = false; + } + } else { + e.keepAttr = result; } + return; + } + if (e.attrName === 'style' || e.attrName === 'class') { if (element.tagName === 'SPAN') { if (e.attrName === 'style') { e.keepAttr = /^(color\:(#[0-9a-fA-F]+|var\(--vscode(-[a-zA-Z0-9]+)+\));)?(background-color\:(#[0-9a-fA-F]+|var\(--vscode(-[a-zA-Z0-9]+)+\));)?(border-radius:[0-9]+px;)?$/.test(e.attrValue); @@ -549,7 +560,7 @@ function getSanitizerOptions(options: IInternalSanitizerOptions): { config: domp // HTML tags that can result from markdown are from reading https://spec.commonmark.org/0.29/ // HTML table tags that can result from markdown are from https://github.github.com/gfm/#tables-extension- ALLOWED_TAGS: options.allowedTags ? [...options.allowedTags] : [...DOM.basicMarkupHtmlTags], - ALLOWED_ATTR: [...allowedMarkdownAttr, ...(options.preserveClassAndStyleAttrs ? ['class'] : [])], + ALLOWED_ATTR: allowedMarkdownAttr, ALLOW_UNKNOWN_PROTOCOLS: true, }, allowedSchemes diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index 3be3d1651bc..ebfb7c802b6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -11,6 +11,7 @@ import { coalesce } from '../../../../../base/common/arrays.js'; import { findLast } from '../../../../../base/common/arraysFind.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../base/common/event.js'; +import { Lazy } from '../../../../../base/common/lazy.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { autorun, IObservable } from '../../../../../base/common/observable.js'; import { equalsIgnoreCase } from '../../../../../base/common/strings.js'; @@ -62,6 +63,67 @@ export interface IChatMarkdownContentPartOptions { export class ChatMarkdownContentPart extends Disposable implements IChatContentPart { private static idPool = 0; + private static tempSanitizerRule = new Lazy(() => { + // Create a CSSStyleDeclaration object via a style sheet rule + const styleSheet = new CSSStyleSheet(); + styleSheet.insertRule(`.temp{}`); + const rule = styleSheet.cssRules[0]; + if (!(rule instanceof CSSStyleRule)) { + throw new Error('Invalid CSS rule'); + } + return rule.style; + }); + + private static sanitizeStyles(styleString: string, allowedProperties: readonly string[]): string { + const style = this.tempSanitizerRule.value; + style.cssText = styleString; + + const sanitizedProps = []; + + for (let i = 0; i < style.length; i++) { + const prop = style[i]; + if (allowedProperties.includes(prop)) { + const value = style.getPropertyValue(prop); + // Allow through lists of numbers with units or bare words like 'block' + // Main goal is to block things like 'url()'. + if (/^(([\d\.\-]+\w*\s?)+|\w+)$/.test(value)) { + sanitizedProps.push(`${prop}: ${value}`); + } + } + } + + return sanitizedProps.join('; '); + } + + private static sanitizeKatexStyles(styleString: string): string { + const allowedProperties = [ + 'display', + 'position', + 'font-family', + 'font-style', + 'font-weight', + 'font-size', + 'height', + 'width', + 'margin', + 'padding', + 'top', + 'left', + 'right', + 'bottom', + 'vertical-align', + 'transform', + 'border', + 'color', + 'white-space', + 'text-align', + 'line-height', + 'float', + 'clear', + ]; + return this.sanitizeStyles(styleString, allowedProperties); + } + public readonly codeblocksPartId = String(++ChatMarkdownContentPart.idPool); public readonly domNode: HTMLElement; private readonly allRefs: IDisposableReference[] = []; @@ -100,7 +162,9 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP let thisPartCodeBlockIndexStart = 0; const markedExtensions = configurationService.getValue(ChatConfiguration.EnableMath) - ? coalesce([MarkedKatexSupport.getExtension(context.container)]) + ? coalesce([MarkedKatexSupport.getExtension(context.container, { + throwOnError: false + })]) : []; // Don't set to 'false' for responses, respect defaults @@ -118,7 +182,15 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP ...dom.basicMarkupHtmlTags, ...dom.trustedMathMlTags, ], - preserveClassAndStyleAttrs: true, + customAttrSanitizer: (attrName, attrValue) => { + if (attrName === 'class') { + return true; // TODO: allows all classes for now since we don't have a list of possible katex classes + } else if (attrName === 'style') { + return ChatMarkdownContentPart.sanitizeKatexStyles(attrValue); + } + + return false; + }, }, fillInIncompleteTokens, codeBlockRendererSync: (languageId, text, raw) => { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts index 2b9d885a7d6..b8e8c4f1c35 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts @@ -22,13 +22,13 @@ export class MarkedKatexSupport { }); } - public static getExtension(container: HTMLElement): marked.MarkedExtension | undefined { + public static getExtension(container: HTMLElement, options: MarkedKatexExtension.MarkedKatexOptions = {}): marked.MarkedExtension | undefined { if (!this._katex) { return undefined; } this.ensureKatexStyles(container); - return MarkedKatexExtension.extension(this._katex); + return MarkedKatexExtension.extension(this._katex, options); } public static ensureKatexStyles(container: HTMLElement) { @@ -45,9 +45,14 @@ export class MarkedKatexSupport { namespace MarkedKatexExtension { + type KatexOptions = import('katex').KatexOptions; // From https://github.com/UziTech/marked-katex-extension/blob/main/src/index.js - export interface MarkedKatexOptions { + export interface MarkedKatexOptions extends KatexOptions { + /** + * If true, the extension will try to parse $ and $$ even if there are no spaces before and after $ or $$. + * This is non-standard behavior and may not work with all markdown parsers. + */ nonStandard?: boolean; } @@ -56,7 +61,7 @@ namespace MarkedKatexExtension { const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/; - export function extension(katex: typeof import('katex').default, options = {}): marked.MarkedExtension { + export function extension(katex: typeof import('katex').default, options: MarkedKatexOptions = {}): marked.MarkedExtension { return { extensions: [ inlineKatex(options, createRenderer(katex, options, false)), @@ -66,7 +71,12 @@ namespace MarkedKatexExtension { } function createRenderer(katex: typeof import('katex').default, options: MarkedKatexOptions, newlineAfter: boolean): marked.RendererExtensionFunction { - return (token: marked.Tokens.Generic) => katex.renderToString(token.text, { ...options, displayMode: token.displayMode }) + (newlineAfter ? '\n' : ''); + return (token: marked.Tokens.Generic) => { + return katex.renderToString(token.text, { + ...options, + displayMode: token.displayMode, + }) + (newlineAfter ? '\n' : ''); + }; } function inlineKatex(options: MarkedKatexOptions, renderer: marked.RendererExtensionFunction): marked.TokenizerAndRendererExtension { From b4dd972f01f2cd20e0b006b1427cf1c7875326a2 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 23 May 2025 11:54:27 -0700 Subject: [PATCH 006/306] Use min.js --- .../contrib/chat/browser/chatContentParts/markedKatexSupport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts index b8e8c4f1c35..4594f999aea 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts @@ -17,7 +17,7 @@ export class MarkedKatexSupport { // - We don't support to level imports of node_modules so you have to use `import(...)` // - I also didn't want to make all the callers to markdown rendering async to properly await // loading of the extension, especially because many of them are ctors. - importAMDNodeModule('katex', 'dist/katex.js').then(katex => { + importAMDNodeModule('katex', 'dist/katex.min.js').then(katex => { this._katex = katex; }); } From c4c7118590be3a4ecdae7d9db8ec34434b9b1ad9 Mon Sep 17 00:00:00 2001 From: Gabriel Csapo Date: Thu, 5 Jun 2025 15:57:05 -0400 Subject: [PATCH 007/306] feat: adds (requestTime) logLevel to match tsserver options --- .../src/configuration/configuration.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/extensions/typescript-language-features/src/configuration/configuration.ts b/extensions/typescript-language-features/src/configuration/configuration.ts index 6a0a2e78beb..881e0cd1ad4 100644 --- a/extensions/typescript-language-features/src/configuration/configuration.ts +++ b/extensions/typescript-language-features/src/configuration/configuration.ts @@ -12,6 +12,7 @@ export enum TsServerLogLevel { Normal, Terse, Verbose, + RequestTime } export namespace TsServerLogLevel { @@ -23,6 +24,8 @@ export namespace TsServerLogLevel { return TsServerLogLevel.Terse; case 'verbose': return TsServerLogLevel.Verbose; + case 'requestTime': + return TsServerLogLevel.RequestTime; case 'off': default: return TsServerLogLevel.Off; @@ -37,6 +40,8 @@ export namespace TsServerLogLevel { return 'terse'; case TsServerLogLevel.Verbose: return 'verbose'; + case TsServerLogLevel.RequestTime: + return 'requestTime'; case TsServerLogLevel.Off: default: return 'off'; From dc82b8c45f24f53df8e5c4a16cff0dc4194daa78 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Jun 2025 14:49:10 +0000 Subject: [PATCH 008/306] Initial plan From f98477ef03eb96424fa0bd1ffe2233ca42948d21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Jun 2025 14:58:48 +0000 Subject: [PATCH 009/306] Allow completion providers for undefined shell types Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com> --- .../browser/terminalCompletionService.ts | 8 +++--- .../suggest/browser/terminalSuggestAddon.ts | 26 ++++++++++--------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index ba7186834a7..f9e9f0fce55 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -72,7 +72,7 @@ export interface ITerminalCompletionService { _serviceBrand: undefined; readonly providers: IterableIterator; registerTerminalCompletionProvider(extensionIdentifier: string, id: string, provider: ITerminalCompletionProvider, ...triggerCharacters: string[]): IDisposable; - provideCompletions(promptValue: string, cursorPosition: number, allowFallbackCompletions: boolean, shellType: TerminalShellType, capabilities: ITerminalCapabilityStore, token: CancellationToken, triggerCharacter?: boolean, skipExtensionCompletions?: boolean, explicitlyInvoked?: boolean): Promise; + provideCompletions(promptValue: string, cursorPosition: number, allowFallbackCompletions: boolean, shellType: TerminalShellType | undefined, capabilities: ITerminalCapabilityStore, token: CancellationToken, triggerCharacter?: boolean, skipExtensionCompletions?: boolean, explicitlyInvoked?: boolean): Promise; } export class TerminalCompletionService extends Disposable implements ITerminalCompletionService { @@ -122,7 +122,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo }); } - async provideCompletions(promptValue: string, cursorPosition: number, allowFallbackCompletions: boolean, shellType: TerminalShellType, capabilities: ITerminalCapabilityStore, token: CancellationToken, triggerCharacter?: boolean, skipExtensionCompletions?: boolean, explicitlyInvoked?: boolean): Promise { + async provideCompletions(promptValue: string, cursorPosition: number, allowFallbackCompletions: boolean, shellType: TerminalShellType | undefined, capabilities: ITerminalCapabilityStore, token: CancellationToken, triggerCharacter?: boolean, skipExtensionCompletions?: boolean, explicitlyInvoked?: boolean): Promise { if (!this._providers || !this._providers.values || cursorPosition < 0) { return undefined; } @@ -164,9 +164,9 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return this._collectCompletions(providers, shellType, promptValue, cursorPosition, allowFallbackCompletions, capabilities, token, explicitlyInvoked); } - private async _collectCompletions(providers: ITerminalCompletionProvider[], shellType: TerminalShellType, promptValue: string, cursorPosition: number, allowFallbackCompletions: boolean, capabilities: ITerminalCapabilityStore, token: CancellationToken, explicitlyInvoked?: boolean): Promise { + private async _collectCompletions(providers: ITerminalCompletionProvider[], shellType: TerminalShellType | undefined, promptValue: string, cursorPosition: number, allowFallbackCompletions: boolean, capabilities: ITerminalCapabilityStore, token: CancellationToken, explicitlyInvoked?: boolean): Promise { const completionPromises = providers.map(async provider => { - if (provider.shellTypes && !provider.shellTypes.includes(shellType)) { + if (provider.shellTypes && shellType && !provider.shellTypes.includes(shellType)) { return undefined; } const timeoutMs = explicitlyInvoked ? 30000 : 5000; diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 27c4d3bedb1..948c6ecf4ef 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -254,13 +254,11 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest return; } - // Require a shell type for completions. This will wait a short period after launching to - // wait for the shell type to initialize. This prevents user requests sometimes getting lost - // if requested shortly after the terminal is created. + // Wait for the shell type to initialize. This will wait a short period after launching to + // allow the shell type to be set if possible. This prevents user requests sometimes getting lost + // if requested shortly after the terminal is created. Completion providers can still work + // with undefined shell types (e.g., for pseudoterminal-based terminals). await this._shellTypeInit; - if (!this.shellType) { - return; - } let doNotRequestExtensionCompletions = false; // Ensure that a key has been pressed since the last accepted completion in order to prevent @@ -719,7 +717,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest // Track the time when completions are shown for the first time if (this._completionRequestTimestamp !== undefined) { const completionLatency = Date.now() - this._completionRequestTimestamp; - if (this._suggestTelemetry && this.shellType) { + if (this._suggestTelemetry) { const firstShown = this.getFirstShown(this.shellType); this.updateShown(); this._suggestTelemetry.logCompletionLatency(this._sessionId, completionLatency, firstShown); @@ -901,17 +899,18 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest this._suggestWidget?.hide(); } - getFirstShown(shellType: TerminalShellType): { window: boolean; shell: boolean } { + getFirstShown(shellType: TerminalShellType | undefined): { window: boolean; shell: boolean } { if (!firstShownTracker) { firstShownTracker = { window: true, - shell: new Set([shellType]) + shell: shellType ? new Set([shellType]) : new Set() }; return { window: true, shell: true }; } const isFirstForWindow = firstShownTracker.window; - const isFirstForShell = !firstShownTracker.shell.has(shellType); + // For undefined shellType, always consider it as first for shell to ensure telemetry is reported + const isFirstForShell = shellType ? !firstShownTracker.shell.has(shellType) : true; if (isFirstForWindow || isFirstForShell) { this.updateShown(); @@ -924,12 +923,15 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } updateShown(): void { - if (!this.shellType || !firstShownTracker) { + if (!firstShownTracker) { return; } firstShownTracker.window = false; - firstShownTracker.shell.add(this.shellType); + // Only add to shell set if shellType is defined + if (this.shellType) { + firstShownTracker.shell.add(this.shellType); + } } } From 23f24ddaf0f394344b869c3aea6277b064c8eb2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:01:41 +0000 Subject: [PATCH 010/306] Add tests for completion providers with undefined shell types Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com> --- .../test/browser/terminalSuggestAddon.test.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.test.ts index 046f0f15800..41c644577e7 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.test.ts @@ -33,3 +33,39 @@ suite('Terminal Suggest Addon - Inline Completion, Shell Type Support', () => { strictEqual(isInlineCompletionSupported(undefined), false); }); }); + +suite('Terminal Suggest Addon - Provider Filtering with Undefined Shell Type', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + /** + * Test function that simulates the provider filtering logic from TerminalCompletionService._collectCompletions + */ + function shouldProviderBeFiltered(provider: { shellTypes?: string[] }, shellType: string | undefined): boolean { + // This replicates the logic from terminalCompletionService.ts line 169 + if (provider.shellTypes && shellType && !provider.shellTypes.includes(shellType)) { + return true; // Provider should be filtered out + } + return false; // Provider should be included + } + + test('providers with no shellTypes restriction should work with undefined shellType', () => { + const provider = {}; // No shellTypes specified + strictEqual(shouldProviderBeFiltered(provider, undefined), false); + }); + + test('providers with shellTypes restriction should work with undefined shellType', () => { + const provider = { shellTypes: ['bash', 'zsh'] }; + strictEqual(shouldProviderBeFiltered(provider, undefined), false); + }); + + test('providers with shellTypes restriction should work with matching shellType', () => { + const provider = { shellTypes: ['bash', 'zsh'] }; + strictEqual(shouldProviderBeFiltered(provider, 'bash'), false); + strictEqual(shouldProviderBeFiltered(provider, 'zsh'), false); + }); + + test('providers with shellTypes restriction should be filtered out for non-matching shellType', () => { + const provider = { shellTypes: ['bash'] }; + strictEqual(shouldProviderBeFiltered(provider, 'powershell'), true); + }); +}); From 561654327b8a82166c9910f20cb4aa2a627e624b Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Mon, 30 Jun 2025 21:07:46 -0700 Subject: [PATCH 011/306] Propose chat prompt after MCP server install --- .../mcp/browser/mcpWorkbenchService.ts | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts index 5a050170b70..1db996b59da 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts @@ -34,6 +34,9 @@ import { IRemoteAgentService } from '../../../services/remote/common/remoteAgent import { mcpConfigurationSection } from '../common/mcpConfiguration.js'; import { HasInstalledMcpServersContext, IMcpConfigPath, IMcpWorkbenchService, IWorkbenchMcpServer, McpCollectionSortOrder, McpServersGalleryEnabledContext } from '../common/mcpTypes.js'; import { McpServerEditorInput } from './mcpServerEditorInput.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { CHAT_OPEN_ACTION_ID, IChatViewOpenOptions } from '../../chat/browser/actions/chatActions.js'; +import { ChatModeKind } from '../../chat/common/constants.js'; class McpWorkbenchServer implements IWorkbenchMcpServer { @@ -151,6 +154,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ @IProductService private readonly productService: IProductService, @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @ICommandService private readonly commandService: ICommandService, @IURLService urlService: IURLService, ) { super(); @@ -241,15 +245,30 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ async install(server: IWorkbenchMcpServer): Promise { if (server.installable) { await this.mcpManagementService.install(server.installable); - return; - } - - if (server.gallery) { + } else if (server.gallery) { await this.mcpManagementService.installFromGallery(server.gallery, { packageType: server.gallery.packageTypes[0] }); - return; + } else { + throw new Error('No installable server found'); } - throw new Error('No installable server found'); + // After successful installation, check if the server has a readme and prompt + await this.queryLocal(); // Refresh local servers to get the updated state + const installedServer = this._local.find(s => s.name === server.name); + + if (installedServer?.local?.readmeUrl) { + try { + // Open chat with prompt about the installed server + const options: IChatViewOpenOptions = { + query: `Suggest interesting developer workflows I could run with MCP tools from ${installedServer.local.readmeUrl.toString()}`, + isPartialQuery: true, + mode: ChatModeKind.Agent + }; + await this.commandService.executeCommand(CHAT_OPEN_ACTION_ID, options); + } catch (error) { + // If we can't open the chat, just skip + console.debug('Could not open chat for MCP server:', error); + } + } } async uninstall(server: IWorkbenchMcpServer): Promise { From 8b0de91413290e62f940bcf167e0cd8ab38d3890 Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Mon, 30 Jun 2025 21:30:46 -0700 Subject: [PATCH 012/306] Fall back to use tools for explainer prompt --- .../contrib/mcp/browser/mcpWorkbenchService.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts index 1db996b59da..509190bf189 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts @@ -255,11 +255,21 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ await this.queryLocal(); // Refresh local servers to get the updated state const installedServer = this._local.find(s => s.name === server.name); - if (installedServer?.local?.readmeUrl) { + if (installedServer) { try { // Open chat with prompt about the installed server + let query: string; + if (installedServer.local?.readmeUrl) { + // If readme exists, reference it + query = `Suggest interesting developer workflows I could run with MCP tools from ${installedServer.local.readmeUrl.toString()}`; + } else { + // Fallback: use the server name + const serverName = installedServer.label || installedServer.name; + query = `Suggest interesting developer workflows I could run with the ${serverName} MCP tools`; + } + const options: IChatViewOpenOptions = { - query: `Suggest interesting developer workflows I could run with MCP tools from ${installedServer.local.readmeUrl.toString()}`, + query, isPartialQuery: true, mode: ChatModeKind.Agent }; From 7374799aeea83647499e56b7fd7a379fb9d39cc6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 1 Jul 2025 12:17:35 +0000 Subject: [PATCH 013/306] Initial plan From 0859d1c26e410dc0719d3491d6369ca17ea29d1c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 1 Jul 2025 12:25:05 +0000 Subject: [PATCH 014/306] Add shell type to terminal shell integration tooltip Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com> --- src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts b/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts index 9874708eb42..192c9878d0a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts @@ -92,6 +92,9 @@ export function refreshShellIntegrationInfoStatus(instance: ITerminalInstance) { ); const detailedAdditions: string[] = []; + if (instance.shellType) { + detailedAdditions.push(`Shell type: \`${instance.shellType}\``); + } const seenSequences = Array.from(instance.xterm.shellIntegration.seenSequences); if (seenSequences.length > 0) { detailedAdditions.push(`Seen sequences: ${seenSequences.map(e => `\`${e}\``).join(', ')}`); From 12bb8d4f0387a8a1f495c4081015ba79f5cde392 Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Tue, 1 Jul 2025 07:53:20 -0700 Subject: [PATCH 015/306] Address feedback --- .../contrib/mcp/browser/mcpServerActions.ts | 32 ++++++++++++++++++ .../mcp/browser/mcpWorkbenchService.ts | 33 ------------------- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts index c924e97ba0f..818647e36bf 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts @@ -21,6 +21,8 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { McpCommandIds } from '../common/mcpCommandIds.js'; import { IAccountQuery, IAuthenticationQueryService } from '../../../services/authentication/common/authenticationQuery.js'; import { IAuthenticationService } from '../../../services/authentication/common/authentication.js'; +import { CHAT_OPEN_ACTION_ID, IChatViewOpenOptions } from '../../chat/browser/actions/chatActions.js'; +import { ChatModeKind } from '../../chat/common/constants.js'; export abstract class McpServerAction extends Action implements IMcpServerContainer { @@ -101,6 +103,7 @@ export class InstallAction extends McpServerAction { constructor( @IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService, + @ICommandService private readonly commandService: ICommandService, ) { super('extensions.install', localize('install', "Install"), InstallAction.CLASS, false); this.update(); @@ -125,6 +128,35 @@ export class InstallAction extends McpServerAction { return; } await this.mcpWorkbenchService.install(this.mcpServer); + + // After successful installation, check if the server has a readme and prompt + await this.mcpWorkbenchService.queryLocal(); // Refresh local servers to get the updated state + const installedServer = this.mcpWorkbenchService.local.find(s => s.name === this.mcpServer!.name); + + if (installedServer) { + try { + // Open chat with prompt about the installed server + let query: string; + if (installedServer.local?.readmeUrl) { + // If readme exists, reference it + query = `Suggest interesting developer workflows I could run with MCP tools from ${installedServer.local.readmeUrl.toString()}`; + } else { + // Fallback: use the server name + const serverName = installedServer.label || installedServer.name; + query = `Suggest interesting developer workflows I could run with the ${serverName} MCP tools`; + } + + const options: IChatViewOpenOptions = { + query, + isPartialQuery: true, + mode: ChatModeKind.Agent + }; + await this.commandService.executeCommand(CHAT_OPEN_ACTION_ID, options); + } catch (error) { + // If we can't open the chat, just skip + console.debug('Could not open chat for MCP server:', error); + } + } } } diff --git a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts index 509190bf189..a1b111ee12a 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts @@ -34,9 +34,6 @@ import { IRemoteAgentService } from '../../../services/remote/common/remoteAgent import { mcpConfigurationSection } from '../common/mcpConfiguration.js'; import { HasInstalledMcpServersContext, IMcpConfigPath, IMcpWorkbenchService, IWorkbenchMcpServer, McpCollectionSortOrder, McpServersGalleryEnabledContext } from '../common/mcpTypes.js'; import { McpServerEditorInput } from './mcpServerEditorInput.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { CHAT_OPEN_ACTION_ID, IChatViewOpenOptions } from '../../chat/browser/actions/chatActions.js'; -import { ChatModeKind } from '../../chat/common/constants.js'; class McpWorkbenchServer implements IWorkbenchMcpServer { @@ -154,7 +151,6 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ @IProductService private readonly productService: IProductService, @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @ICommandService private readonly commandService: ICommandService, @IURLService urlService: IURLService, ) { super(); @@ -250,35 +246,6 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ } else { throw new Error('No installable server found'); } - - // After successful installation, check if the server has a readme and prompt - await this.queryLocal(); // Refresh local servers to get the updated state - const installedServer = this._local.find(s => s.name === server.name); - - if (installedServer) { - try { - // Open chat with prompt about the installed server - let query: string; - if (installedServer.local?.readmeUrl) { - // If readme exists, reference it - query = `Suggest interesting developer workflows I could run with MCP tools from ${installedServer.local.readmeUrl.toString()}`; - } else { - // Fallback: use the server name - const serverName = installedServer.label || installedServer.name; - query = `Suggest interesting developer workflows I could run with the ${serverName} MCP tools`; - } - - const options: IChatViewOpenOptions = { - query, - isPartialQuery: true, - mode: ChatModeKind.Agent - }; - await this.commandService.executeCommand(CHAT_OPEN_ACTION_ID, options); - } catch (error) { - // If we can't open the chat, just skip - console.debug('Could not open chat for MCP server:', error); - } - } } async uninstall(server: IWorkbenchMcpServer): Promise { From 667923708ac8a9e15f2012b19e068bf936a1b95e Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Tue, 1 Jul 2025 08:33:48 -0700 Subject: [PATCH 016/306] Revert leftover changes --- .../contrib/mcp/browser/mcpWorkbenchService.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts index a1b111ee12a..5a050170b70 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts @@ -241,11 +241,15 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ async install(server: IWorkbenchMcpServer): Promise { if (server.installable) { await this.mcpManagementService.install(server.installable); - } else if (server.gallery) { - await this.mcpManagementService.installFromGallery(server.gallery, { packageType: server.gallery.packageTypes[0] }); - } else { - throw new Error('No installable server found'); + return; } + + if (server.gallery) { + await this.mcpManagementService.installFromGallery(server.gallery, { packageType: server.gallery.packageTypes[0] }); + return; + } + + throw new Error('No installable server found'); } async uninstall(server: IWorkbenchMcpServer): Promise { From 0bc10a4e46959d80b90b28fa743c3621902f0b5a Mon Sep 17 00:00:00 2001 From: Dirk Baeumer Date: Tue, 1 Jul 2025 18:17:27 +0200 Subject: [PATCH 017/306] Fixes 251268: Errors from task with same owner as extension don't propagate to the EH --- src/vs/monaco.d.ts | 2 ++ .../platform/markers/common/markerService.ts | 3 +- src/vs/platform/markers/common/markers.ts | 2 ++ .../api/browser/mainThreadDiagnostics.ts | 34 +++++++++++++++---- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index d87109d2f84..dcf8e1e3359 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1471,6 +1471,7 @@ declare namespace monaco.editor { modelVersionId?: number; relatedInformation?: IRelatedInformation[]; tags?: MarkerTag[]; + origin?: string | undefined; } /** @@ -1491,6 +1492,7 @@ declare namespace monaco.editor { modelVersionId?: number; relatedInformation?: IRelatedInformation[]; tags?: MarkerTag[]; + origin?: string | undefined; } /** diff --git a/src/vs/platform/markers/common/markerService.ts b/src/vs/platform/markers/common/markerService.ts index 07e7c1ee34e..21fda6c76c4 100644 --- a/src/vs/platform/markers/common/markerService.ts +++ b/src/vs/platform/markers/common/markerService.ts @@ -232,7 +232,7 @@ export class MarkerService implements IMarkerService { message, source, startLineNumber, startColumn, endLineNumber, endColumn, relatedInformation, - tags, + tags, origin } = data; if (!message) { @@ -258,6 +258,7 @@ export class MarkerService implements IMarkerService { endColumn, relatedInformation, tags, + origin }; } diff --git a/src/vs/platform/markers/common/markers.ts b/src/vs/platform/markers/common/markers.ts index cc686747a83..5e5408eb57e 100644 --- a/src/vs/platform/markers/common/markers.ts +++ b/src/vs/platform/markers/common/markers.ts @@ -118,6 +118,7 @@ export interface IMarkerData { modelVersionId?: number; relatedInformation?: IRelatedInformation[]; tags?: MarkerTag[]; + origin?: string | undefined; } export interface IResourceMarker { @@ -139,6 +140,7 @@ export interface IMarker { modelVersionId?: number; relatedInformation?: IRelatedInformation[]; tags?: MarkerTag[]; + origin?: string | undefined; } export interface MarkerStatistics { diff --git a/src/vs/workbench/api/browser/mainThreadDiagnostics.ts b/src/vs/workbench/api/browser/mainThreadDiagnostics.ts index a484e898e01..6fd3280a2eb 100644 --- a/src/vs/workbench/api/browser/mainThreadDiagnostics.ts +++ b/src/vs/workbench/api/browser/mainThreadDiagnostics.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IMarkerService, IMarkerData } from '../../../platform/markers/common/markers.js'; +import { IMarkerService, IMarkerData, type IMarker } from '../../../platform/markers/common/markers.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { MainThreadDiagnosticsShape, MainContext, ExtHostDiagnosticsShape, ExtHostContext } from '../common/extHost.protocol.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; @@ -18,6 +18,9 @@ export class MainThreadDiagnostics implements MainThreadDiagnosticsShape { private readonly _proxy: ExtHostDiagnosticsShape; private readonly _markerListener: IDisposable; + private static ExtHostCounter: number = 1; + private readonly extHostId: string; + constructor( extHostContext: IExtHostContext, @IMarkerService private readonly _markerService: IMarkerService, @@ -26,12 +29,28 @@ export class MainThreadDiagnostics implements MainThreadDiagnosticsShape { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDiagnostics); this._markerListener = this._markerService.onMarkerChanged(this._forwardMarkers, this); + this.extHostId = `extHost${MainThreadDiagnostics.ExtHostCounter++}`; } dispose(): void { this._markerListener.dispose(); - this._activeOwners.forEach(owner => this._markerService.changeAll(owner, [])); - this._activeOwners.clear(); + for (const owner of this._activeOwners) { + const markersData: Map = new Map(); + for (const marker of this._markerService.read({ owner })) { + const resource = marker.resource.toString(); + let data = markersData.get(resource); + if (data === undefined) { + data = { resource: marker.resource, local: [] }; + markersData.set(resource, data); + } + if (marker.origin !== this.extHostId) { + data.local.push(marker); + } + } + for (const { resource, local } of markersData.values()) { + this._markerService.changeOne(owner, resource, local); + } + } } private _forwardMarkers(resources: readonly URI[]): void { @@ -41,9 +60,9 @@ export class MainThreadDiagnostics implements MainThreadDiagnosticsShape { if (allMarkerData.length === 0) { data.push([resource, []]); } else { - const forgeinMarkerData = allMarkerData.filter(marker => !this._activeOwners.has(marker.owner)); - if (forgeinMarkerData.length > 0) { - data.push([resource, forgeinMarkerData]); + const foreignMarkerData = allMarkerData.filter(marker => marker?.origin !== this.extHostId); + if (foreignMarkerData.length > 0) { + data.push([resource, foreignMarkerData]); } } } @@ -65,6 +84,9 @@ export class MainThreadDiagnostics implements MainThreadDiagnosticsShape { if (marker.code && typeof marker.code !== 'string') { marker.code.target = URI.revive(marker.code.target); } + if (marker.origin === undefined) { + marker.origin = this.extHostId; + } } } this._markerService.changeOne(owner, this._uriIdentService.asCanonicalUri(URI.revive(uri)), markers); From 3f48f0cedc185e8a7034b8ce841a635c8733becc Mon Sep 17 00:00:00 2001 From: Dirk Baeumer Date: Tue, 1 Jul 2025 18:39:21 +0200 Subject: [PATCH 018/306] Clear active owner and use resource map --- .../api/browser/mainThreadDiagnostics.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadDiagnostics.ts b/src/vs/workbench/api/browser/mainThreadDiagnostics.ts index 6fd3280a2eb..d4d46de4755 100644 --- a/src/vs/workbench/api/browser/mainThreadDiagnostics.ts +++ b/src/vs/workbench/api/browser/mainThreadDiagnostics.ts @@ -9,6 +9,7 @@ import { MainThreadDiagnosticsShape, MainContext, ExtHostDiagnosticsShape, ExtHo import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { IDisposable } from '../../../base/common/lifecycle.js'; import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js'; +import { ResourceMap } from '../../../base/common/map.js'; @extHostNamedCustomer(MainContext.MainThreadDiagnostics) export class MainThreadDiagnostics implements MainThreadDiagnosticsShape { @@ -35,22 +36,22 @@ export class MainThreadDiagnostics implements MainThreadDiagnosticsShape { dispose(): void { this._markerListener.dispose(); for (const owner of this._activeOwners) { - const markersData: Map = new Map(); + const markersData: ResourceMap = new ResourceMap(); for (const marker of this._markerService.read({ owner })) { - const resource = marker.resource.toString(); - let data = markersData.get(resource); + let data = markersData.get(marker.resource); if (data === undefined) { - data = { resource: marker.resource, local: [] }; - markersData.set(resource, data); + data = []; + markersData.set(marker.resource, data); } if (marker.origin !== this.extHostId) { - data.local.push(marker); + data.push(marker); } } - for (const { resource, local } of markersData.values()) { + for (const [resource, local] of markersData.entries()) { this._markerService.changeOne(owner, resource, local); } } + this._activeOwners.clear(); } private _forwardMarkers(resources: readonly URI[]): void { From 1ba742aa12a7de3a7d5b39ca8767b4430ac029c3 Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Tue, 1 Jul 2025 16:05:02 -0700 Subject: [PATCH 019/306] Customization file snippets refresh --- .../chat/browser/promptSyntax/newPromptFileActions.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts index eac5bbf7c32..e3ce0b0b8d1 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts @@ -146,21 +146,22 @@ function getDefaultContentSnippet(promptType: PromptsType): string { `---`, `mode: \${1|ask,edit,agent|}`, `---`, - `\${2:Expected output and any relevant constraints for this task.}`, + `\${2:Define the task to achieve, including specific requirements, constraints, and success criteria.}`, ].join('\n'); case PromptsType.instructions: return [ `---`, `applyTo: '\${1|**,**/*.ts|}'`, `---`, - `\${2:Coding standards, domain knowledge, and preferences that AI should follow.}`, + `\${2:Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.}`, ].join('\n'); case PromptsType.mode: return [ `---`, `description: '\${1:Description of the custom chat mode.}'`, - `tools: [ '\${2:tool1}', '\${3:tool2}' ]`, + `tools: []`, `---`, + `\${2:Define the purpose of this chat mode and how AI should behave: response style, available tools, focus areas, and any mode-specific instructions or constraints.}`, ].join('\n'); default: throw new Error(`Unknown prompt type: ${promptType}`); From eb01d2b25d091586021becfc3e6ca0a4be9880cc Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Tue, 1 Jul 2025 18:27:08 -0700 Subject: [PATCH 020/306] Refactor Getting Started page logic and improve editor group handling (#253533) * Refactor Getting Started page logic and improve editor group handling * Fix walkthrough focus --- .../browser/gettingStarted.contribution.ts | 67 ++----------------- .../browser/gettingStarted.ts | 5 +- .../browser/gettingStartedService.ts | 7 ++ 3 files changed, 17 insertions(+), 62 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts index 6aa706f7bf5..ab7fcb47f2b 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts @@ -8,7 +8,7 @@ import { GettingStartedInputSerializer, GettingStartedPage, inWelcomeContext } f import { Registry } from '../../../../platform/registry/common/platform.js'; import { EditorExtensions, IEditorFactoryRegistry } from '../../../common/editor.js'; import { MenuId, registerAction2, Action2 } from '../../../../platform/actions/common/actions.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; @@ -20,7 +20,6 @@ import { GettingStartedEditorOptions, GettingStartedInput } from './gettingStart import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; import { workbenchConfigurationNodeBase } from '../../../common/configuration.js'; -import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js'; import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; @@ -28,7 +27,6 @@ import { isLinux, isMacintosh, isWindows, OperatingSystem as OS } from '../../.. import { IExtensionManagementServerService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { StartupPageEditorResolverContribution, StartupPageRunnerContribution } from './startupPage.js'; -import { ExtensionsInput } from '../../extensions/common/extensionsInput.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; @@ -59,8 +57,6 @@ registerAction2(class extends Action2 { walkthroughID: string | { category: string; step: string } | undefined, optionsOrToSide: { toSide?: boolean; inactive?: boolean } | boolean | undefined ) { - const editorGroupsService = accessor.get(IEditorGroupsService); - const instantiationService = accessor.get(IInstantiationService); const editorService = accessor.get(IEditorService); const commandService = accessor.get(ICommandService); @@ -76,44 +72,6 @@ registerAction2(class extends Action2 { selectedStep = undefined; } - // We're trying to open the welcome page from the Help menu - if (!selectedCategory && !selectedStep) { - editorService.openEditor({ - resource: GettingStartedInput.RESOURCE, - options: { preserveFocus: toSide ?? false, inactive, forceReload: true } - }, toSide ? SIDE_GROUP : undefined); - return; - } - - // Try first to select the walkthrough on an active welcome page with no selected walkthrough - for (const group of editorGroupsService.groups) { - if (group.activeEditor instanceof GettingStartedInput) { - const activeEditor = group.activeEditor as GettingStartedInput; - activeEditor.showWelcome = false; - if (activeEditor.selectedCategory && activeEditor.selectedStep) { - // currently in a walkthrough. - return; - } - (group.activeEditorPane as GettingStartedPage).makeCategoryVisibleWhenAvailable(selectedCategory, selectedStep); - return; - } - } - - // Otherwise, try to find a welcome input somewhere with no selected walkthrough, and open it to this one. - const result = editorService.findEditors({ typeId: GettingStartedInput.ID, editorId: undefined, resource: GettingStartedInput.RESOURCE }); - for (const { editor, groupId } of result) { - if (editor instanceof GettingStartedInput) { - const group = editorGroupsService.getGroup(groupId); - if (!editor.selectedCategory && group) { - editor.selectedCategory = selectedCategory; - editor.selectedStep = selectedStep; - editor.showWelcome = false; - group.openEditor(editor, { revealIfOpened: true, inactive }); - return; - } - } - } - const activeEditor = editorService.activeEditor; // If the walkthrough is already open just reveal the step if (selectedStep && activeEditor instanceof GettingStartedInput && activeEditor.selectedCategory === selectedCategory) { @@ -122,24 +80,13 @@ registerAction2(class extends Action2 { return; } - // If it's the extension install page then lets replace it with the getting started page - if (activeEditor instanceof ExtensionsInput) { - const activeGroup = editorGroupsService.activeGroup; - activeGroup.replaceEditors([{ - editor: activeEditor, - replacement: instantiationService.createInstance(GettingStartedInput, { selectedCategory: selectedCategory, selectedStep: selectedStep, showWelcome: false }) - }]); - } else { - // else open respecting toSide - const options: GettingStartedEditorOptions = { selectedCategory: selectedCategory, selectedStep: selectedStep, showWelcome: false, preserveFocus: toSide ?? false, inactive }; - editorService.openEditor({ - resource: GettingStartedInput.RESOURCE, - options - }, toSide ? SIDE_GROUP : undefined).then((editor) => { - (editor as GettingStartedPage)?.makeCategoryVisibleWhenAvailable(selectedCategory, selectedStep); - }); + // Otherwise open the walkthrough editor with the selected category and step + const options: GettingStartedEditorOptions = { selectedCategory: selectedCategory, selectedStep: selectedStep, showWelcome: false, preserveFocus: toSide ?? false, inactive }; + editorService.openEditor({ + resource: GettingStartedInput.RESOURCE, + options + }, toSide ? SIDE_GROUP : undefined); - } } else { editorService.openEditor({ resource: GettingStartedInput.RESOURCE, diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index cc5c3bd5727..0302e33400c 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -961,7 +961,7 @@ export class GettingStartedPage extends EditorPane { const fistContentBehaviour = daysSinceFirstSession < 1 ? 'openToFirstCategory' : 'index'; const startupExpValue = startupExpContext.getValue(this.contextService); - if (fistContentBehaviour === 'openToFirstCategory' && ((startupExpValue === '' || startupExpValue === StartupExperimentGroup.Control))) { + if (fistContentBehaviour === 'openToFirstCategory' && ((!startupExpValue || startupExpValue === '' || startupExpValue === StartupExperimentGroup.Control))) { startupExpContext.bindTo(this.contextService).reset(); const first = this.gettingStartedCategories.filter(c => !c.when || this.contextService.contextMatchesRules(c.when))[0]; if (first) { @@ -1265,7 +1265,7 @@ export class GettingStartedPage extends EditorPane { private focusSideEditorGroup() { const fullSize = this.groupsService.getPart(this.group).contentDimension; - if (!fullSize || fullSize.width <= 700) { return; } + if (!fullSize || fullSize.width <= 700 || this.container.classList.contains('width-constrained') || this.container.classList.contains('width-semi-constrained')) { return; } if (this.groupsService.count === 1) { const sideGroup = this.groupsService.addGroup(this.groupsService.groups[0], GroupDirection.RIGHT); this.groupsService.activateGroup(sideGroup); @@ -1669,6 +1669,7 @@ export class GettingStartedPage extends EditorPane { // Add next button this.nextButton = $('button.button-link.navigation.next', { + 'aria-label': localize('nextStep', "Next"), }, localize('next', "Next"), $('span.codicon.codicon-arrow-right')); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts index cc37ab45cad..183288a5a10 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts @@ -37,6 +37,8 @@ import { IProductService } from '../../../../platform/product/common/productServ import { asWebviewUri } from '../../webview/common/webview.js'; import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; import { extensionDefaultIcon } from '../../../services/extensionManagement/common/extensionsIcons.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { GettingStartedInput } from './gettingStartedInput.js'; export const HasMultipleNewFileEntries = new RawContextKey('hasMultipleNewFileEntries', false); @@ -161,6 +163,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ @IWorkbenchAssignmentService private readonly tasExperimentService: IWorkbenchAssignmentService, @IProductService private readonly productService: IProductService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, + @IEditorService private readonly editorService: IEditorService ) { super(); @@ -458,6 +461,10 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ id: string; }; this.telemetryService.publicLog2('gettingStarted.didAutoOpenWalkthrough', { id: sectionToOpen }); + const activeEditor = this.editorService.activeEditor; + if (activeEditor instanceof GettingStartedInput) { + this.commandService.executeCommand('workbench.action.keepEditor'); + } this.commandService.executeCommand('workbench.action.openWalkthrough', sectionToOpen, { inactive: this.layoutService.hasFocus(Parts.EDITOR_PART) // do not steal the active editor away }); From 2c4a3983b1370813458bf5a795c5275f5000a730 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Tue, 1 Jul 2025 18:45:25 -0700 Subject: [PATCH 021/306] do not edit on selection (#253519) * some minor fixes for edits * cleanup --- .../contrib/chat/browser/chatListRenderer.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 753223f595c..8b62fdd82a5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -489,8 +489,11 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer('chat.editRequests') === 'hover'); - templateData.elementDisposables.add(dom.addDisposableListener(templateData.rowContainer, dom.EventType.CLICK, () => { - if (this.viewModel?.editing && element.id !== this.viewModel.editing.id && element === this.templateDataByRequestId.get(element.id)?.currentElement) { + templateData.elementDisposables.add(dom.addDisposableListener(templateData.rowContainer, dom.EventType.CLICK, (e) => { + const current = templateData.currentElement; + if (current && this.viewModel?.editing && current.id !== this.viewModel.editing.id) { + e.stopPropagation(); + e.preventDefault(); this._onDidFocusOutside.fire(); } })); @@ -634,6 +637,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { if (this.viewModel?.editing?.id !== element.id && !this.viewModel?.requestInProgress) { + const selection = dom.getWindow(templateData.rowContainer).getSelection(); + if (selection && !selection.isCollapsed && selection.toString().length > 0) { + return; + } + e.preventDefault(); + e.stopPropagation(); this._onDidClickRequest.fire(templateData); } })); From 6e855004c713d9c2610091ab7eb68b677830087f Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Tue, 1 Jul 2025 21:59:22 -0700 Subject: [PATCH 022/306] Adjust chat input mode handling and update max-width for exp welcome view input (#253550) --- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 8 +++++++- .../contrib/chat/browser/media/chatViewWelcome.css | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 483b1f05a51..dd82570e524 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -715,9 +715,15 @@ export class ChatWidget extends Disposable implements IChatWidget { // reset the input in welcome view if it was rendered in experimental mode if (this.container.classList.contains('experimental-welcome-view')) { this.container.classList.remove('experimental-welcome-view'); + // Preserve the current mode before recreating the input + const currentMode = this.input?.currentModeKind; const renderFollowups = this.viewOptions.renderFollowups ?? false; const renderStyle = this.viewOptions.renderStyle; this.createInput(this.container, { renderFollowups, renderStyle }); + // Restore the mode after recreating the input + if (currentMode && this.input) { + this.input.setChatMode(currentMode, false); + } } if (this.viewOptions.renderStyle === 'compact' || this.viewOptions.renderStyle === 'minimal') { @@ -1727,7 +1733,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } if (this.container.classList.contains('experimental-welcome-view')) { - this.inputPart.layout(layoutHeight, Math.min(width, 700)); + this.inputPart.layout(layoutHeight, Math.min(width, 650)); } else { this.inputPart.layout(layoutHeight, width); diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css index d9d69aa9615..23b7ce84566 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css @@ -22,7 +22,7 @@ } .interactive-session .experimental-welcome-view & > .chat-welcome-view-input-part { - max-width: 700px; + max-width: 650px; } /* Container for ChatViewPane welcome view */ From f9546778105fc67258b705fc9132d7ded83e4b1d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 2 Jul 2025 07:35:58 +0200 Subject: [PATCH 023/306] Window border: Default handling of an invalid color (fix #253194) (#253555) --- src/vs/platform/windows/electron-main/windows.ts | 2 +- src/vs/workbench/electron-browser/desktop.contribution.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index d26a157c8e3..f1849bb1cc1 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -161,7 +161,7 @@ export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowSt }; if (isWindows) { - const borderSetting = windowSettings?.border ?? 'default'; + const borderSetting = windowSettings?.border || 'default'; if (borderSetting !== 'default') { if (borderSetting === 'off') { options.accentColor = false; diff --git a/src/vs/workbench/electron-browser/desktop.contribution.ts b/src/vs/workbench/electron-browser/desktop.contribution.ts index 987a02a2c60..3d94f59a785 100644 --- a/src/vs/workbench/electron-browser/desktop.contribution.ts +++ b/src/vs/workbench/electron-browser/desktop.contribution.ts @@ -314,7 +314,7 @@ import { registerWorkbenchContribution2, WorkbenchPhase } from '../common/contri 'type': 'string', 'scope': ConfigurationScope.APPLICATION, 'default': 'default', - 'markdownDescription': localize('window.border', "Controls the border color of the window. Set to `off` to disable or to a specific color in Hex, RGB, RGBA, HSL, HSLA format. This requires Windows to have the 'Show accent color on title bars and window borders' enabled and is ignored when {0} is set to {1}.", '`#window.titleBarStyle#`', '`native`'), + 'markdownDescription': localize('window.border', "Controls the border color of the window. Set to `default` to respect Windows settings, `off` to disable or to a specific color in Hex, RGB, RGBA, HSL, HSLA format. This requires Windows to have the 'Show accent color on title bars and window borders' enabled and is ignored when {0} is set to {1}.", '`#window.titleBarStyle#`', '`native`'), 'included': isWindows } } From 7b8ec4b364eca2492eba12723bbd43b112003c7b Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Tue, 1 Jul 2025 22:44:15 -0700 Subject: [PATCH 024/306] fixes on chat editing state (#253556) --- .../contrib/chat/browser/chatEditing/chatEditingActions.ts | 4 ++-- src/vs/workbench/contrib/chat/browser/chatListRenderer.ts | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index daa14ed166e..ec609baba4c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -326,7 +326,7 @@ registerAction2(class RemoveAction extends Action2 { id: MenuId.ChatMessageTitle, group: 'navigation', order: 2, - when: ContextKeyExpr.and(ChatContextKeys.isRequest, ChatContextKeys.currentlyEditing.negate(), ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'input').negate()) + when: ContextKeyExpr.and(ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'input').negate()) } ] }); @@ -435,7 +435,7 @@ registerAction2(class EditAction extends Action2 { id: MenuId.ChatMessageTitle, group: 'navigation', order: 2, - when: ContextKeyExpr.and(ChatContextKeys.isRequest, ChatContextKeys.currentlyEditing.negate(), ContextKeyExpr.or(ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'hover'), ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'input'))) + when: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'hover'), ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'input'))) } ] }); diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 8b62fdd82a5..a11fd5c56cf 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -430,7 +430,11 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer Date: Wed, 2 Jul 2025 07:52:27 +0200 Subject: [PATCH 025/306] `code chat` includes fake files as attachements if it doesnt find the file in the path. (fix #253482) (#253559) --- .../workbench/contrib/chat/browser/actions/chatActions.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index b81bac0ea46..cd4abdc66be 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -29,6 +29,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IsLinuxContext, IsWindowsContext } from '../../../../../platform/contextkey/common/contextkeys.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { INotificationService } from '../../../../../platform/notification/common/notification.js'; @@ -141,6 +142,7 @@ abstract class OpenChatGlobalAction extends Action2 { const instaService = accessor.get(IInstantiationService); const commandService = accessor.get(ICommandService); const chatModeService = accessor.get(IChatModeService); + const fileService = accessor.get(IFileService); let chatWidget = widgetService.lastFocusedWidget; // When this was invoked to switch to a mode via keybinding, and some chat widget is focused, use that one. @@ -175,7 +177,9 @@ abstract class OpenChatGlobalAction extends Action2 { } if (opts?.attachFiles) { for (const file of opts.attachFiles) { - chatWidget.attachmentModel.addFile(file); + if (await fileService.exists(file)) { + chatWidget.attachmentModel.addFile(file); + } } } if (opts?.query) { From 25cc496bf7b61def7ce7587b92fe0f19a9015621 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 2 Jul 2025 08:58:52 +0200 Subject: [PATCH 026/306] Code chat help doesn't tell me what modes I can use (fix #253380) (#253568) --- extensions/terminal-suggest/src/completions/code.ts | 2 +- src/vs/platform/environment/node/argv.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/terminal-suggest/src/completions/code.ts b/extensions/terminal-suggest/src/completions/code.ts index 7cc0cb639a8..58e8726b0e8 100644 --- a/extensions/terminal-suggest/src/completions/code.ts +++ b/extensions/terminal-suggest/src/completions/code.ts @@ -809,7 +809,7 @@ export const codeTunnelSubcommands: Fig.Subcommand[] = [ options: [ { name: ['-m', '--mode'], - description: 'The mode to use for the chat session. Defaults to \'agent\'', + description: 'The mode to use for the chat session. Available options: \'ask\', \'edit\', \'agent\', or the identifier of a custom mode. Defaults to \'agent\'', args: { name: 'mode', suggestions: ['agent', 'ask', 'edit'], diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 66896be5119..417624ed476 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -53,7 +53,7 @@ export const OPTIONS: OptionDescriptions> = { description: 'Pass in a prompt to run in a chat session in the current working directory.', options: { '_': { type: 'string[]', description: localize('prompt', "The prompt to use as chat.") }, - 'mode': { type: 'string', cat: 'o', alias: 'm', args: 'mode', description: localize('chatMode', "The mode to use for the chat session. Defaults to 'agent'.") }, + 'mode': { type: 'string', cat: 'o', alias: 'm', args: 'mode', description: localize('chatMode', "The mode to use for the chat session. Available options: 'ask', 'edit', 'agent', or the identifier of a custom mode. Defaults to 'agent'.") }, 'add-file': { type: 'string[]', cat: 'o', alias: 'a', args: 'path', description: localize('addFile', "Add files as context to the chat session.") }, 'help': { type: 'boolean', cat: 'o', alias: 'h', description: localize('help', "Print usage.") } } From d63338e7a9f595607b2c2156a147533f092c02db Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 2 Jul 2025 09:39:42 +0200 Subject: [PATCH 027/306] revert prompting after installing mcp (#253574) --- .../contrib/mcp/browser/mcpServerActions.ts | 32 ----------------- .../contrib/mcp/browser/mcpServerEditor.ts | 2 +- .../mcp/browser/mcpWorkbenchService.ts | 35 +++++++++++-------- .../workbench/contrib/mcp/common/mcpTypes.ts | 4 +-- 4 files changed, 23 insertions(+), 50 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts index 818647e36bf..c924e97ba0f 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts @@ -21,8 +21,6 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { McpCommandIds } from '../common/mcpCommandIds.js'; import { IAccountQuery, IAuthenticationQueryService } from '../../../services/authentication/common/authenticationQuery.js'; import { IAuthenticationService } from '../../../services/authentication/common/authentication.js'; -import { CHAT_OPEN_ACTION_ID, IChatViewOpenOptions } from '../../chat/browser/actions/chatActions.js'; -import { ChatModeKind } from '../../chat/common/constants.js'; export abstract class McpServerAction extends Action implements IMcpServerContainer { @@ -103,7 +101,6 @@ export class InstallAction extends McpServerAction { constructor( @IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService, - @ICommandService private readonly commandService: ICommandService, ) { super('extensions.install', localize('install', "Install"), InstallAction.CLASS, false); this.update(); @@ -128,35 +125,6 @@ export class InstallAction extends McpServerAction { return; } await this.mcpWorkbenchService.install(this.mcpServer); - - // After successful installation, check if the server has a readme and prompt - await this.mcpWorkbenchService.queryLocal(); // Refresh local servers to get the updated state - const installedServer = this.mcpWorkbenchService.local.find(s => s.name === this.mcpServer!.name); - - if (installedServer) { - try { - // Open chat with prompt about the installed server - let query: string; - if (installedServer.local?.readmeUrl) { - // If readme exists, reference it - query = `Suggest interesting developer workflows I could run with MCP tools from ${installedServer.local.readmeUrl.toString()}`; - } else { - // Fallback: use the server name - const serverName = installedServer.label || installedServer.name; - query = `Suggest interesting developer workflows I could run with the ${serverName} MCP tools`; - } - - const options: IChatViewOpenOptions = { - query, - isPartialQuery: true, - mode: ChatModeKind.Agent - }; - await this.commandService.executeCommand(CHAT_OPEN_ACTION_ID, options); - } catch (error) { - // If we can't open the chat, just skip - console.debug('Could not open chat for MCP server:', error); - } - } } } diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts index a9eb0474c15..e4879394dcd 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts @@ -322,7 +322,7 @@ export class McpServerEditor extends EditorPane { this.currentIdentifier = extension.id; } - if (extension.hasReadme()) { + if (extension.readmeUrl) { template.navbar.push(McpServerEditorTab.Readme, localize('details', "Details"), localize('detailstooltip', "Extension details, rendered from the extension's 'README.md' file")); } diff --git a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts index 5a050170b70..557e6d19ac3 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts @@ -98,8 +98,8 @@ class McpWorkbenchServer implements IWorkbenchMcpServer { return this.local?.config ?? this.installable?.config; } - hasReadme(): boolean { - return !!(this.local?.readmeUrl || this.gallery?.readmeUrl); + get readmeUrl(): URI | undefined { + return this.local?.readmeUrl ?? (this.gallery?.readmeUrl ? URI.parse(this.gallery.readmeUrl) : undefined); } async getReadme(token: CancellationToken): Promise { @@ -181,17 +181,22 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ if (!result.local) { continue; } - let server = this._local.find(server => server.local?.name === result.name); - if (server) { - server.local = result.local; - } else { - server = this.instantiationService.createInstance(McpWorkbenchServer, result.local, result.source, undefined); - this._local.push(server); - } - this._onChange.fire(server); + this.onDidInstallMcpServer(result.local); } } + private onDidInstallMcpServer(local: IWorkbenchLocalMcpServer): IWorkbenchMcpServer { + let server = this._local.find(server => server.local?.name === local.name); + if (server) { + server.local = local; + } else { + server = this.instantiationService.createInstance(McpWorkbenchServer, local, undefined, undefined); + this._local.push(server); + } + this._onChange.fire(server); + return server; + } + private onDidUpdateMcpServers(e: readonly InstallMcpServerResult[]) { for (const result of e) { if (!result.local) { @@ -238,15 +243,15 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ return this._local; } - async install(server: IWorkbenchMcpServer): Promise { + async install(server: IWorkbenchMcpServer): Promise { if (server.installable) { - await this.mcpManagementService.install(server.installable); - return; + const local = await this.mcpManagementService.install(server.installable); + return this.onDidInstallMcpServer(local); } if (server.gallery) { - await this.mcpManagementService.installFromGallery(server.gallery, { packageType: server.gallery.packageTypes[0] }); - return; + const local = await this.mcpManagementService.installFromGallery(server.gallery, { packageType: server.gallery.packageTypes[0] }); + return this.onDidInstallMcpServer(local); } throw new Error('No installable server found'); diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 9dcfd3504ba..00417a348c2 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -590,7 +590,7 @@ export interface IWorkbenchMcpServer { readonly url?: string; readonly repository?: string; readonly config?: IMcpServerConfiguration | undefined; - hasReadme(): boolean; + readonly readmeUrl?: URI; getReadme(token: CancellationToken): Promise; getManifest(token: CancellationToken): Promise; } @@ -602,7 +602,7 @@ export interface IMcpWorkbenchService { readonly local: readonly IWorkbenchMcpServer[]; queryLocal(): Promise; queryGallery(options?: IQueryOptions, token?: CancellationToken): Promise; - install(server: IWorkbenchMcpServer, installOptions?: IWorkbencMcpServerInstallOptions): Promise; + install(server: IWorkbenchMcpServer, installOptions?: IWorkbencMcpServerInstallOptions): Promise; uninstall(mcpServer: IWorkbenchMcpServer): Promise; getMcpConfigPath(arg: IWorkbenchLocalMcpServer): IMcpConfigPath | undefined; getMcpConfigPath(arg: URI): Promise; From 10a97742582741a61841940b4a23cfed68a680bf Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 2 Jul 2025 10:30:25 +0200 Subject: [PATCH 028/306] fix #253279 (#253581) --- .../platform/mcp/common/mcpGalleryService.ts | 8 +-- src/vs/platform/mcp/common/mcpManagement.ts | 6 +- .../platform/mcp/common/mcpManagementIpc.ts | 7 ++ .../mcp/common/mcpManagementService.ts | 71 ++++++++++++------- .../mcp/browser/mcpWorkbenchService.ts | 58 ++++++++++++--- .../common/mcpWorkbenchManagementService.ts | 26 ++++++- 6 files changed, 136 insertions(+), 40 deletions(-) diff --git a/src/vs/platform/mcp/common/mcpGalleryService.ts b/src/vs/platform/mcp/common/mcpGalleryService.ts index 00388af6715..37b4dc17070 100644 --- a/src/vs/platform/mcp/common/mcpGalleryService.ts +++ b/src/vs/platform/mcp/common/mcpGalleryService.ts @@ -87,15 +87,15 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService return galleryServers; } - async getMcpServer(name: string): Promise { + async getMcpServers(names: string[]): Promise { const mcpUrl = this.getMcpGalleryUrl() ?? this.productService.extensionsGallery?.mcpUrl; if (!mcpUrl) { - return undefined; + return []; } const { servers } = await this.fetchGallery(mcpUrl, CancellationToken.None); - const server = servers.find(item => item.name === name); - return server ? this.toGalleryMcpServer(server) : undefined; + const filteredServers = servers.filter(item => names.includes(item.name)); + return filteredServers.map(item => this.toGalleryMcpServer(item)); } async getManifest(gallery: IGalleryMcpServer, token: CancellationToken): Promise { diff --git a/src/vs/platform/mcp/common/mcpManagement.ts b/src/vs/platform/mcp/common/mcpManagement.ts index 46688f2802a..5817b455e1f 100644 --- a/src/vs/platform/mcp/common/mcpManagement.ts +++ b/src/vs/platform/mcp/common/mcpManagement.ts @@ -10,6 +10,8 @@ import { SortBy, SortOrder } from '../../extensionManagement/common/extensionMan import { createDecorator } from '../../instantiation/common/instantiation.js'; import { IMcpServerConfiguration, IMcpServerVariable } from './mcpPlatformTypes.js'; +export type InstallSource = 'gallery' | 'local'; + export interface ILocalMcpServer { readonly name: string; readonly config: IMcpServerConfiguration; @@ -30,6 +32,7 @@ export interface ILocalMcpServer { }; readonly codicon?: string; readonly manifest?: IMcpServerManifest; + readonly source: InstallSource; } export interface IMcpServerInput { @@ -133,7 +136,7 @@ export interface IMcpGalleryService { readonly _serviceBrand: undefined; isEnabled(): boolean; query(options?: IQueryOptions, token?: CancellationToken): Promise; - getMcpServer(server: string): Promise; + getMcpServers(servers: string[]): Promise; getManifest(extension: IGalleryMcpServer, token: CancellationToken): Promise; getReadme(extension: IGalleryMcpServer, token: CancellationToken): Promise; } @@ -189,6 +192,7 @@ export interface IMcpManagementService { getInstalled(mcpResource?: URI): Promise; install(server: IInstallableMcpServer, options?: InstallOptions): Promise; installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise; + updateMetadata(local: ILocalMcpServer, server: IGalleryMcpServer, profileLocation?: URI): Promise; uninstall(server: ILocalMcpServer, options?: UninstallOptions): Promise; } diff --git a/src/vs/platform/mcp/common/mcpManagementIpc.ts b/src/vs/platform/mcp/common/mcpManagementIpc.ts index d1fe377df58..0ed514d443e 100644 --- a/src/vs/platform/mcp/common/mcpManagementIpc.ts +++ b/src/vs/platform/mcp/common/mcpManagementIpc.ts @@ -106,6 +106,9 @@ export class McpManagementChannel implements IServerChannel { case 'uninstall': { return this.service.uninstall(transformIncomingServer(args[0], uriTransformer), transformIncomingOptions(args[1], uriTransformer)); } + case 'updateMetadata': { + return this.service.updateMetadata(transformIncomingServer(args[0], uriTransformer), args[1], transformIncomingURI(args[2], uriTransformer)); + } } throw new Error('Invalid call'); @@ -156,4 +159,8 @@ export class McpManagementChannelClient extends Disposable implements IMcpManage return Promise.resolve(this.channel.call('getInstalled', [mcpResource])) .then(servers => servers.map(server => transformIncomingServer(server, null))); } + + updateMetadata(local: ILocalMcpServer, gallery: IGalleryMcpServer, mcpResource?: URI): Promise { + return Promise.resolve(this.channel.call('updateMetadata', [local, gallery, mcpResource])).then(local => transformIncomingServer(local, null)); + } } diff --git a/src/vs/platform/mcp/common/mcpManagementService.ts b/src/vs/platform/mcp/common/mcpManagementService.ts index 9fe50af63dc..34239c0fd25 100644 --- a/src/vs/platform/mcp/common/mcpManagementService.ts +++ b/src/vs/platform/mcp/common/mcpManagementService.ts @@ -40,6 +40,7 @@ export interface ILocalMcpServerInfo { manifest?: IMcpServerManifest; readmeUrl?: URI; location?: URI; + licenseUrl?: string; } export abstract class AbstractMcpResourceManagementService extends Disposable implements IMcpManagementService { @@ -184,7 +185,8 @@ export abstract class AbstractMcpResourceManagementService extends Disposable im readmeUrl: mcpServerInfo.readmeUrl, icon: mcpServerInfo.icon, codicon: mcpServerInfo.codicon, - manifest: mcpServerInfo.manifest + manifest: mcpServerInfo.manifest, + source: config.gallery ? 'gallery' : 'local' }; } @@ -382,6 +384,7 @@ export abstract class AbstractMcpResourceManagementService extends Disposable im } abstract installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise; + abstract updateMetadata(local: ILocalMcpServer, server: IGalleryMcpServer, profileLocation: URI): Promise; protected abstract getLocalServerInfo(name: string, mcpServerConfig: IMcpServerConfiguration): Promise; } @@ -408,30 +411,8 @@ export class McpUserResourceManagementService extends AbstractMcpResourceManagem this._onInstallMcpServer.fire({ name: server.name, mcpResource: this.mcpResource }); try { - const manifest = await this.mcpGalleryService.getManifest(server, CancellationToken.None); - const location = this.getLocation(server.name, server.version); - const manifestPath = this.uriIdentityService.extUri.joinPath(location, 'manifest.json'); - await this.fileService.writeFile(manifestPath, VSBuffer.fromString(JSON.stringify({ - id: server.id, - name: server.name, - displayName: server.displayName, - description: server.description, - version: server.version, - publisher: server.publisher, - publisherDisplayName: server.publisherDisplayName, - repository: server.repositoryUrl, - licenseUrl: server.licenseUrl, - icon: server.icon, - codicon: server.codicon, - ...manifest, - }))); - - if (server.readmeUrl) { - const readme = await this.mcpGalleryService.getReadme(server, CancellationToken.None); - await this.fileService.writeFile(this.uriIdentityService.extUri.joinPath(location, 'README.md'), VSBuffer.fromString(readme)); - } + const manifest = await this.updateMetadataFromGallery(server); const { config, inputs } = this.toScannedMcpServerAndInputs(manifest, options?.packageType); - const installable: IInstallableMcpServer = { name: server.name, config: { @@ -456,6 +437,44 @@ export class McpUserResourceManagementService extends AbstractMcpResourceManagem } } + async updateMetadata(local: ILocalMcpServer, gallery: IGalleryMcpServer): Promise { + await this.updateMetadataFromGallery(gallery); + await this.updateLocal(); + const updatedLocal = (await this.getInstalled()).find(s => s.name === local.name); + if (!updatedLocal) { + throw new Error(`Failed to find MCP server: ${local.name}`); + } + return updatedLocal; + } + + private async updateMetadataFromGallery(gallery: IGalleryMcpServer): Promise { + const manifest = await this.mcpGalleryService.getManifest(gallery, CancellationToken.None); + const location = this.getLocation(gallery.name, gallery.version); + const manifestPath = this.uriIdentityService.extUri.joinPath(location, 'manifest.json'); + const local: ILocalMcpServerInfo = { + id: gallery.id, + name: gallery.name, + displayName: gallery.displayName, + description: gallery.description, + version: gallery.version, + publisher: gallery.publisher, + publisherDisplayName: gallery.publisherDisplayName, + repositoryUrl: gallery.repositoryUrl, + licenseUrl: gallery.licenseUrl, + icon: gallery.icon, + codicon: gallery.codicon, + manifest, + }; + await this.fileService.writeFile(manifestPath, VSBuffer.fromString(JSON.stringify(local))); + + if (gallery.readmeUrl) { + const readme = await this.mcpGalleryService.getReadme(gallery, CancellationToken.None); + await this.fileService.writeFile(this.uriIdentityService.extUri.joinPath(location, 'README.md'), VSBuffer.fromString(readme)); + } + + return manifest; + } + protected async getLocalServerInfo(name: string, mcpServerConfig: IMcpServerConfiguration): Promise { let storedMcpServerInfo: ILocalMcpServerInfo | undefined; let location: URI | undefined; @@ -549,6 +568,10 @@ export class McpManagementService extends Disposable implements IMcpManagementSe return this.getMcpResourceManagementService(mcpResourceUri).installFromGallery(server, options); } + async updateMetadata(local: ILocalMcpServer, gallery: IGalleryMcpServer, mcpResource?: URI): Promise { + return this.getMcpResourceManagementService(mcpResource || this.userDataProfilesService.defaultProfile.mcpResource).updateMetadata(local, gallery); + } + override dispose(): void { this.mcpResourceManagementServices.forEach(service => service.dispose()); this.mcpResourceManagementServices.clear(); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts index 557e6d19ac3..5971005ca64 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts @@ -17,7 +17,7 @@ import { IEditorOptions } from '../../../../platform/editor/common/editor.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; -import { DidUninstallMcpServerEvent, IGalleryMcpServer, IMcpGalleryService, InstallMcpServerResult, IQueryOptions, IInstallableMcpServer, IMcpServerManifest } from '../../../../platform/mcp/common/mcpManagement.js'; +import { DidUninstallMcpServerEvent, IGalleryMcpServer, IMcpGalleryService, InstallMcpServerResult, IQueryOptions, IInstallableMcpServer, IMcpServerManifest, ILocalMcpServer } from '../../../../platform/mcp/common/mcpManagement.js'; import { IMcpServerConfiguration, IMcpServerVariable, IMcpStdioServerConfiguration, McpServerType } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { StorageScope } from '../../../../platform/storage/common/storage.js'; @@ -157,10 +157,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ this._register(this.mcpManagementService.onDidInstallMcpServersInCurrentProfile(e => this.onDidInstallMcpServers(e))); this._register(this.mcpManagementService.onDidUpdateMcpServersInCurrentProfile(e => this.onDidUpdateMcpServers(e))); this._register(this.mcpManagementService.onDidUninstallMcpServerInCurrentProfile(e => this.onDidUninstallMcpServer(e))); - this.queryLocal().then(async () => { - await this.queryGallery(); - this._onChange.fire(undefined); - }); + this.queryLocal().then(() => this.syncInstalledMcpServers()); urlService.registerHandler(this); } @@ -177,20 +174,24 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ } private onDidInstallMcpServers(e: readonly InstallMcpServerResult[]) { + const servers: IWorkbenchMcpServer[] = []; for (const result of e) { if (!result.local) { continue; } - this.onDidInstallMcpServer(result.local); + servers.push(this.onDidInstallMcpServer(result.local, result.source)); + } + if (servers.some(server => server.local?.source === 'gallery' && !server.gallery)) { + this.syncInstalledMcpServers(); } } - private onDidInstallMcpServer(local: IWorkbenchLocalMcpServer): IWorkbenchMcpServer { + private onDidInstallMcpServer(local: IWorkbenchLocalMcpServer, gallery?: IGalleryMcpServer): IWorkbenchMcpServer { let server = this._local.find(server => server.local?.name === local.name); if (server) { server.local = local; } else { - server = this.instantiationService.createInstance(McpWorkbenchServer, local, undefined, undefined); + server = this.instantiationService.createInstance(McpWorkbenchServer, local, gallery, undefined); this._local.push(server); } this._onChange.fire(server); @@ -225,6 +226,45 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ return undefined; } + private async syncInstalledMcpServers(): Promise { + const installedGalleryServers: ILocalMcpServer[] = []; + for (const installed of this.local) { + if (installed.local?.source !== 'gallery') { + continue; + } + installedGalleryServers.push(installed.local); + } + if (installedGalleryServers.length) { + const galleryServers = await this.mcpGalleryService.getMcpServers(installedGalleryServers.map(server => server.name)); + if (galleryServers.length) { + this.syncInstalledMcpServersWithGallery(galleryServers); + } + } + } + + private async syncInstalledMcpServersWithGallery(gallery: IGalleryMcpServer[]): Promise { + const galleryMap = new Map(gallery.map(server => [server.name, server])); + for (const mcpServer of this.local) { + if (!mcpServer.gallery) { + if (!mcpServer.local) { + continue; + } + if (mcpServer.gallery) { + continue; + } + const galleryServer = galleryMap.get(mcpServer.name); + if (!galleryServer) { + continue; + } + mcpServer.gallery = galleryServer; + if (!mcpServer.id) { + mcpServer.local = await this.mcpManagementService.updateMetadata(mcpServer.local, galleryServer); + } + this._onChange.fire(mcpServer); + } + } + } + async queryGallery(options?: IQueryOptions, token?: CancellationToken): Promise { if (!this.mcpGalleryService.isEnabled()) { return []; @@ -378,7 +418,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ const { name, inputs, gallery, ...config } = parsed; if (gallery || !config || Object.keys(config).length === 0) { - const galleryServer = await this.mcpGalleryService.getMcpServer(name); + const [galleryServer] = await this.mcpGalleryService.getMcpServers([name]); if (!galleryServer) { throw new Error(`MCP server '${name}' not found in gallery`); } diff --git a/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts b/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts index 4169ec6db83..b6e6d7bc93f 100644 --- a/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts +++ b/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts @@ -298,6 +298,21 @@ class WorkbenchMcpManagementService extends Disposable implements IWorkbenchMcpM return this.mcpManagementService.installFromGallery(server, options); } + updateMetadata(local: IWorkbenchLocalMcpServer, server: IGalleryMcpServer, profileLocation: URI): Promise { + if (local.scope === LocalMcpServerScope.Workspace) { + return this.workspaceMcpManagementService.updateMetadata(local, server, profileLocation); + } + + if (local.scope === LocalMcpServerScope.RemoteUser) { + if (!this.remoteMcpManagementService) { + throw new Error(`Illegal target: ${local.scope}`); + } + return this.remoteMcpManagementService.updateMetadata(local, server, profileLocation); + } + + return this.mcpManagementService.updateMetadata(local, server, profileLocation); + } + async uninstall(server: IWorkbenchLocalMcpServer): Promise { if (server.scope === LocalMcpServerScope.Workspace) { return this.workspaceMcpManagementService.uninstall(server); @@ -346,10 +361,13 @@ class WorkspaceMcpResourceManagementService extends AbstractMcpResourceManagemen throw new Error('Not supported'); } + override updateMetadata(): Promise { + throw new Error('Not supported'); + } + protected override async getLocalServerInfo(): Promise { return undefined; } - } class WorkspaceMcpManagementService extends Disposable implements IMcpManagementService { @@ -522,7 +540,11 @@ class WorkspaceMcpManagementService extends Disposable implements IMcpManagement return mcpManagementServiceItem.service.uninstall(server, options); } - async installFromGallery(): Promise { + installFromGallery(): Promise { + throw new Error('Not supported'); + } + + updateMetadata(): Promise { throw new Error('Not supported'); } From 34854488d4c5bd1a7a370f734d7885e9c7f6d6ab Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 2 Jul 2025 10:31:55 +0200 Subject: [PATCH 029/306] fix #252517 (#253584) --- src/vs/workbench/contrib/extensions/common/extensionQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/extensions/common/extensionQuery.ts b/src/vs/workbench/contrib/extensions/common/extensionQuery.ts index cd74a11bd4d..fab81196b99 100644 --- a/src/vs/workbench/contrib/extensions/common/extensionQuery.ts +++ b/src/vs/workbench/contrib/extensions/common/extensionQuery.ts @@ -20,7 +20,7 @@ export class Query { commands.push('featured'); } - commands.push(...['popular', 'recommended', 'recentlyPublished', 'workspaceUnsupported', 'deprecated', 'sort']); + commands.push(...['mcp', 'popular', 'recommended', 'recentlyPublished', 'workspaceUnsupported', 'deprecated', 'sort']); const isCategoriesEnabled = galleryManifest?.capabilities.extensionQuery?.filtering?.some(c => c.name === FilterType.Category); if (isCategoriesEnabled) { commands.push('category'); From 2ed2eb30c7379343dd2e45e6a9dd9fe476ac247e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 2 Jul 2025 10:44:47 +0200 Subject: [PATCH 030/306] skip flaky tests (#253586) --- test/smoke/src/areas/terminal/terminal.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/smoke/src/areas/terminal/terminal.test.ts b/test/smoke/src/areas/terminal/terminal.test.ts index dc28a7c1190..57e4ee9b5fc 100644 --- a/test/smoke/src/areas/terminal/terminal.test.ts +++ b/test/smoke/src/areas/terminal/terminal.test.ts @@ -45,7 +45,7 @@ export function setup(logger: Logger) { setupTerminalProfileTests({ skipSuite: process.platform === 'linux' }); setupTerminalTabsTests({ skipSuite: process.platform === 'linux' }); setupTerminalShellIntegrationTests({ skipSuite: process.platform === 'linux' }); - setupTerminalStickyScrollTests({ skipSuite: process.platform === 'linux' }); + setupTerminalStickyScrollTests({ skipSuite: true }); // https://github.com/microsoft/vscode/pull/141974 // Windows is skipped here as well as it was never enabled from the start setupTerminalSplitCwdTests({ skipSuite: process.platform === 'linux' || process.platform === 'win32' }); From e73a19b8a666bc64bb5a3ede6bc845a96f0f7b12 Mon Sep 17 00:00:00 2001 From: Anthony Stewart <150152+a-stewart@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:52:32 +0200 Subject: [PATCH 031/306] Fix typing in asyncDataTree.test.ts (#209394) * Fix typing in asyncDataTree.test.ts * Removing typing entirely now that `querySelector` returns `HTMLElement | null` --- .../test/browser/ui/tree/asyncDataTree.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts b/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts index 985c97e9d1a..4eb5e2b7e28 100644 --- a/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts @@ -307,7 +307,7 @@ suite('AsyncDataTree', function () { assert(!aNode.collapsed); assert.equal(aNode.children.length, 1); assert.equal(aNode.children[0].element.id, 'b'); - const bChild = container.querySelector('.monaco-list-row:nth-child(2)') as HTMLElement | undefined; + const bChild = container.querySelector('.monaco-list-row:nth-child(2)'); assert.equal(bChild?.textContent, 'b'); tree.collapse(a); assert(aNode.collapsed); @@ -319,8 +319,8 @@ suite('AsyncDataTree', function () { assert.equal(aNodeUpdated1.children.length, 0); let didCheckNoChildren = false; const event = tree.onDidChangeCollapseState(e => { - const child = container.querySelector('.monaco-list-row:nth-child(2)') as HTMLElement | undefined; - assert.equal(child, undefined); + const child = container.querySelector('.monaco-list-row:nth-child(2)'); + assert.equal(child, null); didCheckNoChildren = true; }); await tree.expand(aUpdated1); @@ -331,7 +331,7 @@ suite('AsyncDataTree', function () { assert(!aNodeUpdated2.collapsed); assert.equal(aNodeUpdated2.children.length, 1); assert.equal(aNodeUpdated2.children[0].element.id, 'c'); - const child = container.querySelector('.monaco-list-row:nth-child(2)') as HTMLElement | undefined; + const child = container.querySelector('.monaco-list-row:nth-child(2)'); assert.equal(child?.textContent, 'c'); }); @@ -364,7 +364,7 @@ suite('AsyncDataTree', function () { assert(!aNode.collapsed); assert.equal(aNode.children.length, 1); assert.equal(aNode.children[0].element.id, 'b'); - const bChild = container.querySelector('.monaco-list-row:nth-child(2)') as HTMLElement | undefined; + const bChild = container.querySelector('.monaco-list-row:nth-child(2)'); assert.equal(bChild?.textContent, 'b'); tree.collapse(a); assert(aNode.collapsed); @@ -375,7 +375,7 @@ suite('AsyncDataTree', function () { assert.equal(aNodeUpdated1.children.length, 1); let didCheckSameChildren = false; const event = tree.onDidChangeCollapseState(e => { - const child = container.querySelector('.monaco-list-row:nth-child(2)') as HTMLElement | undefined; + const child = container.querySelector('.monaco-list-row:nth-child(2)'); assert.equal(child?.textContent, 'b'); didCheckSameChildren = true; }); @@ -387,7 +387,7 @@ suite('AsyncDataTree', function () { assert(!aNodeUpdated2.collapsed); assert.equal(aNodeUpdated2.children.length, 1); assert.equal(aNodeUpdated2.children[0].element.id, 'b'); - const child = container.querySelector('.monaco-list-row:nth-child(2)') as HTMLElement | undefined; + const child = container.querySelector('.monaco-list-row:nth-child(2)'); assert.equal(child?.textContent, 'b'); }); From ca8f724454e48084529b0fa1f2fed0f76f1211bd Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 2 Jul 2025 11:31:13 +0200 Subject: [PATCH 032/306] open mcp server editor and report telemetry (#253601) * open mcp server editor and report telemetry * fix alert --- .../contrib/mcp/browser/mcpServerActions.ts | 20 +++++++++++++++++++ .../contrib/mcp/browser/mcpServerEditor.ts | 2 +- .../contrib/mcp/browser/mcpServersView.ts | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts index c924e97ba0f..e5c7ea62c1a 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts @@ -21,6 +21,8 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { McpCommandIds } from '../common/mcpCommandIds.js'; import { IAccountQuery, IAuthenticationQueryService } from '../../../services/authentication/common/authenticationQuery.js'; import { IAuthenticationService } from '../../../services/authentication/common/authentication.js'; +import { alert } from '../../../../base/browser/ui/aria/aria.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; export abstract class McpServerAction extends Action implements IMcpServerContainer { @@ -100,7 +102,9 @@ export class InstallAction extends McpServerAction { private static readonly HIDE = `${this.CLASS} hide`; constructor( + private readonly editor: boolean, @IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService, + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super('extensions.install', localize('install', "Install"), InstallAction.CLASS, false); this.update(); @@ -124,6 +128,22 @@ export class InstallAction extends McpServerAction { if (!this.mcpServer) { return; } + + if (!this.editor) { + this.mcpWorkbenchService.open(this.mcpServer); + alert(localize('mcpServerInstallation', "Installing MCP Server {0} started. An editor is now open with more details on this MCP Server", this.mcpServer.label)); + } + + type McpServerInstallClassification = { + owner: 'sandy081'; + comment: 'Used to understand if the action to install the MCP server is used.'; + name?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The gallery name of the MCP server being installed' }; + }; + type McpServerInstall = { + name?: string; + }; + this.telemetryService.publicLog2('mcp:action:install', { name: this.mcpServer.gallery?.name }); + await this.mcpWorkbenchService.install(this.mcpServer); } } diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts index e4879394dcd..b91e8522bc0 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts @@ -220,7 +220,7 @@ export class McpServerEditor extends EditorPane { const description = append(details, $('.description')); const actions = [ - this.instantiationService.createInstance(InstallAction), + this.instantiationService.createInstance(InstallAction, true), this.instantiationService.createInstance(UninstallAction), this.instantiationService.createInstance(ManageMcpServerAction, true), ]; diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts index 23d90aec64d..949d6dda331 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts @@ -264,7 +264,7 @@ class McpServerRenderer implements IListRenderer error && this.notificationService.error(error)); const actions = [ - this.instantiationService.createInstance(InstallAction), + this.instantiationService.createInstance(InstallAction, false), this.instantiationService.createInstance(ManageMcpServerAction, false), ]; From 2e9d6feebcd5fe12dea7211116b9b7b3cb86b100 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 2 Jul 2025 11:31:39 +0200 Subject: [PATCH 033/306] SCM - update hide strategy on the SCM input action bar (#253596) --- src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 56a5397e985..3c51be33044 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -93,7 +93,7 @@ import { EditorOption, EditorOptions, IEditorOptions } from '../../../../editor/ import { IAsyncDataTreeViewState, ITreeCompressionDelegate } from '../../../../base/browser/ui/tree/asyncDataTree.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { EditOperation } from '../../../../editor/common/core/editOperation.js'; -import { IMenuWorkbenchToolBarOptions, WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; +import { HiddenItemStrategy, IMenuWorkbenchToolBarOptions, WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { DropdownWithPrimaryActionViewItem } from '../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js'; import { clamp, rot } from '../../../../base/common/numbers.js'; @@ -1449,7 +1449,7 @@ class SCMInputWidgetToolbar extends WorkbenchToolBar { @IStorageService private readonly storageService: IStorageService, @ITelemetryService telemetryService: ITelemetryService, ) { - super(container, { resetMenu: MenuId.SCMInputBox, ...options }, menuService, contextKeyService, contextMenuService, keybindingService, commandService, telemetryService); + super(container, options, menuService, contextKeyService, contextMenuService, keybindingService, commandService, telemetryService); this._dropdownAction = new Action( 'scmInputMoreActions', @@ -1947,6 +1947,7 @@ class SCMInputWidget { return createActionViewItem(instantiationService, action, options); }, + hiddenItemStrategy: HiddenItemStrategy.NoHide, menuOptions: { shouldForwardArgs: true } From 394e0dfe10da6ca6fe5b505d1d30d7fa52f5e30b Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 2 Jul 2025 11:41:40 +0200 Subject: [PATCH 034/306] SCM - fix timeline and graph hover when views are in the panel (#253605) --- .../contrib/scm/browser/scmHistoryViewPane.ts | 4 +++- .../contrib/timeline/browser/timelinePane.ts | 15 +++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index ebb20fe2bc7..95118b980b1 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -759,7 +759,9 @@ class HistoryItemHoverDelegate extends WorkbenchHoverDelegate { @IHoverService hoverService: IHoverService, ) { - super('element', { instantHover: true }, () => this.getHoverOptions(), configurationService, hoverService); + super(_viewContainerLocation === ViewContainerLocation.Panel ? 'mouse' : 'element', { + instantHover: _viewContainerLocation !== ViewContainerLocation.Panel + }, () => this.getHoverOptions(), configurationService, hoverService); } private getHoverOptions(): Partial { diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 8081ab47d8d..0fc7adb151f 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -35,7 +35,7 @@ import { SideBySideEditor, EditorResourceAccessor } from '../../../common/editor import { ICommandService, CommandsRegistry } from '../../../../platform/commands/common/commands.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; -import { IViewDescriptorService } from '../../../common/views.js'; +import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; import { IProgressService } from '../../../../platform/progress/common/progress.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { ActionBar, IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js'; @@ -926,7 +926,7 @@ export class TimelinePane extends ViewPane { // this.treeElement.classList.add('show-file-icons'); container.appendChild(this.$tree); - this.treeRenderer = this.instantiationService.createInstance(TimelineTreeRenderer, this.commands); + this.treeRenderer = this.instantiationService.createInstance(TimelineTreeRenderer, this.commands, this.viewDescriptorService.getViewLocationById(this.id)); this._register(this.treeRenderer.onDidScrollToEnd(item => { if (this.pageOnScroll) { this.loadMore(item); @@ -1165,11 +1165,18 @@ class TimelineTreeRenderer implements ITreeRenderer Date: Wed, 2 Jul 2025 12:18:53 +0200 Subject: [PATCH 035/306] `Welcome Page + Standard Chat Panel` should not have 50 / 50 split (fix microsoft/vscode-internalbacklog#5553) (#253611) * `Welcome Page + Standard Chat Panel` should not have 50 / 50 split (fix microsoft/vscode-internalbacklog#5553) * round --- src/vs/workbench/browser/layout.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 78db93aa0b5..ec7757a570e 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -2956,7 +2956,7 @@ class LayoutStateModel extends Disposable { ) { const mainContainerDimension = configuration.mainContainerDimension; this.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN, false); - this.setInitializationValue(LayoutStateKeys.AUXILIARYBAR_SIZE, mainContainerDimension.width / 2); + this.setInitializationValue(LayoutStateKeys.AUXILIARYBAR_SIZE, Math.ceil(mainContainerDimension.width / (1.618 * 1.618 /* golden ratio */))); } } From 9f988324a4f3df7ab5d1b7c66884e1cc82c58c98 Mon Sep 17 00:00:00 2001 From: Alexandru Dima Date: Wed, 2 Jul 2025 12:27:19 +0200 Subject: [PATCH 036/306] Undo the changes in `inlineCompletionsView.ts` introduced by #252308 (#253614) --- .../inlineCompletions/browser/view/inlineCompletionsView.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts index fe32ff410c2..adc0faf9d00 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts @@ -82,17 +82,11 @@ export class InlineCompletionsView extends Disposable { this._register(createStyleSheetFromObservable(derived(reader => { const fontFamily = this._fontFamily.read(reader); - let fontSize: string = this._editor.getOption(EditorOption.fontSize) + 'px'; - const cursorSelection = this._editorObs.cursorSelection.read(reader); - if (cursorSelection) { - fontSize = this._editor.getFontSizeAtPosition(cursorSelection.getEndPosition()) ?? fontSize; - } return ` .monaco-editor .ghost-text-decoration, .monaco-editor .ghost-text-decoration-preview, .monaco-editor .ghost-text { font-family: ${fontFamily}; - font-size: ${fontSize}; }`; }))); From 94ea5b31f469d82d46bfdd36cb62c1073dc3e05f Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 2 Jul 2025 12:31:00 +0200 Subject: [PATCH 037/306] Change snooze title and description (#253616) change the snooze title and description --- .../workbench/contrib/chat/browser/chatStatus.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus.ts b/src/vs/workbench/contrib/chat/browser/chatStatus.ts index 62d45271e89..caa45e15333 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus.ts @@ -412,7 +412,7 @@ class ChatStatusDashboard extends Disposable { // Settings { const chatSentiment = this.chatEntitlementService.sentiment; - addSeparator(localize('completionsAndNES', "Completions / NES"), chatSentiment.installed && !chatSentiment.disabled && !chatSentiment.untrusted ? toAction({ + addSeparator(localize('completionsAndNES', "Completions"), chatSentiment.installed && !chatSentiment.disabled && !chatSentiment.untrusted ? toAction({ id: 'workbench.action.openChatSettings', label: localize('settingsLabel', "Settings"), tooltip: localize('settingsTooltip', "Open Settings"), @@ -599,11 +599,11 @@ class ChatStatusDashboard extends Disposable { // --- Code completions { const globalSetting = append(settings, $('div.setting')); - this.createCodeCompletionsSetting(globalSetting, localize('settings.codeCompletions', "Code completions (all files)"), '*', disposables); + this.createCodeCompletionsSetting(globalSetting, localize('settings.codeCompletions.allFiles', "All files"), '*', disposables); if (modeId) { const languageSetting = append(settings, $('div.setting')); - this.createCodeCompletionsSetting(languageSetting, localize('settings.codeCompletionsLanguage', "Code completions ({0})", this.languageService.getLanguageName(modeId) ?? modeId), modeId, disposables); + this.createCodeCompletionsSetting(languageSetting, localize('settings.codeCompletions.language', "{0}", this.languageService.getLanguageName(modeId) ?? modeId), modeId, disposables); } } @@ -743,9 +743,9 @@ class ChatStatusDashboard extends Disposable { const timeLeftMs = this.inlineCompletionsService.snoozeTimeLeft; if (!isEnabled || timeLeftMs <= 0) { - timerDisplay.textContent = localize('settings.mute5minutes', "Mute for 5 mins"); + timerDisplay.textContent = localize('completions.snooze5minutesTitle', "Hide completions for 5 mins"); button.label = label; - button.setTitle(localize('settings.snooze5minutes', "Snooze completions and NES for 5 mins")); + button.setTitle(localize('completions.snooze5minutes', "Hide completions and NES for 5 mins")); return true; } @@ -753,9 +753,9 @@ class ChatStatusDashboard extends Disposable { const minutes = Math.floor(timeLeftSeconds / 60); const seconds = timeLeftSeconds % 60; - timerDisplay.textContent = `${minutes}:${seconds < 10 ? '0' : ''}${seconds} remaining`; - button.label = localize('settings.plus5mins', "+5 mins"); - button.setTitle(localize('settings.snoozeAdditional5minutes', "Snooze additional 5 mins")); + timerDisplay.textContent = `${minutes}:${seconds < 10 ? '0' : ''}${seconds} ${localize('completions.remainingTime', "remaining")}`; + button.label = localize('completions.plus5mins', "+5 mins"); + button.setTitle(localize('completions.snoozeAdditional5minutes', "Hide additional 5 mins")); toolbar.push([cancelAction], { icon: true, label: false }); return false; From 7b36e11dc9d4117d68d7f82a0852e1737e3e196f Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 2 Jul 2025 12:39:15 +0200 Subject: [PATCH 038/306] fix #253228 (#253617) --- .../contrib/mcp/browser/mcpServersView.ts | 77 ++++++++++++++++--- .../mcp/browser/mcpWorkbenchService.ts | 75 ++++++++++++++---- .../workbench/contrib/mcp/common/mcpTypes.ts | 8 ++ 3 files changed, 135 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts index 949d6dda331..cfbe4aae27d 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts @@ -7,7 +7,7 @@ import './media/mcpServersView.css'; import * as dom from '../../../../base/browser/dom.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IListContextMenuEvent, IListRenderer } from '../../../../base/browser/ui/list/list.js'; -import { Event } from '../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; import { combinedDisposable, Disposable, DisposableStore, dispose, IDisposable, isDisposable } from '../../../../base/common/lifecycle.js'; import { DelayedPagedModel, IPagedModel, PagedModel } from '../../../../base/common/paging.js'; import { localize, localize2 } from '../../../../nls.js'; @@ -24,7 +24,7 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { getLocationBasedViewColors, ViewPane } from '../../../browser/parts/views/viewPane.js'; import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; import { IViewDescriptorService, IViewsRegistry, Extensions as ViewExtensions } from '../../../common/views.js'; -import { HasInstalledMcpServersContext, IMcpWorkbenchService, InstalledMcpServersViewId, IWorkbenchMcpServer, McpServerContainers, mcpServerIcon } from '../common/mcpTypes.js'; +import { HasInstalledMcpServersContext, IMcpWorkbenchService, InstalledMcpServersViewId, IWorkbenchMcpServer, McpServerContainers, mcpServerIcon, McpServerInstallState } from '../common/mcpTypes.js'; import { DropDownAction, InstallAction, ManageMcpServerAction } from './mcpServerActions.js'; import { PublisherWidget, InstallCountWidget, RatingsWidget, McpServerIconWidget } from './mcpServerWidgets.js'; import { ActionRunner, IAction, Separator } from '../../../../base/common/actions.js'; @@ -47,8 +47,10 @@ export interface McpServerListViewOptions { } interface IQueryResult { - showWelcomeContent: boolean; model: IPagedModel; + disposables: DisposableStore; + showWelcomeContent?: boolean; + onDidChangeModel?: Event>; } export class McpServersListView extends ViewPane { @@ -157,18 +159,26 @@ export class McpServersListView extends ViewPane { async show(query: string): Promise> { if (this.input) { + this.input.disposables.dispose(); this.input = undefined; } - query = query.trim(); - const servers = query ? await this.mcpWorkbenchService.queryGallery({ text: query.replace('@mcp', '') }) : await this.mcpWorkbenchService.queryLocal(); - const showWelcomeContent = !this.mcpGalleryService.isEnabled() && servers.length === 0 && !!this.mpcViewOptions.showWelcomeOnEmpty; - - const model = new PagedModel(servers); - this.input = { model, showWelcomeContent }; + this.input = await this.query(query.trim()); + this.input.showWelcomeContent = !this.mcpGalleryService.isEnabled() && this.input.model.length === 0 && !!this.mpcViewOptions.showWelcomeOnEmpty; this.renderInput(); - return model; + if (this.input.onDidChangeModel) { + this.input.disposables.add(this.input.onDidChangeModel(model => { + if (!this.input) { + return; + } + this.input.model = model; + this.input.showWelcomeContent = !this.mcpGalleryService.isEnabled() && this.input.model.length === 0 && !!this.mpcViewOptions.showWelcomeOnEmpty; + this.renderInput(); + })); + } + + return this.input.model; } private renderInput() { @@ -178,7 +188,7 @@ export class McpServersListView extends ViewPane { if (this.list) { this.list.model = new DelayedPagedModel(this.input.model); } - this.showWelcomeContent(this.input.showWelcomeContent); + this.showWelcomeContent(!!this.input.showWelcomeContent); } private showWelcomeContent(show: boolean): void { @@ -212,6 +222,51 @@ export class McpServersListView extends ViewPane { description.appendChild(markdownResult.element); } + private async query(query: string): Promise { + const disposables = new DisposableStore(); + if (query) { + const servers = await this.mcpWorkbenchService.queryGallery({ text: query.replace('@mcp', '') }); + return { model: new PagedModel(servers), disposables }; + } + + const onDidChangeModel = disposables.add(new Emitter>()); + let servers = await this.mcpWorkbenchService.queryLocal(); + disposables.add(Event.debounce(Event.filter(this.mcpWorkbenchService.onChange, e => e?.installState === McpServerInstallState.Installed), () => undefined)(async () => { + const mergedMcpServers = this.mergeAddedMcpServers(servers, [...this.mcpWorkbenchService.local]); + if (mergedMcpServers) { + servers = mergedMcpServers; + onDidChangeModel.fire(new PagedModel(servers)); + } + })); + return { model: new PagedModel(servers), onDidChangeModel: onDidChangeModel.event, disposables }; + } + + private mergeAddedMcpServers(mcpServers: IWorkbenchMcpServer[], newMcpServers: IWorkbenchMcpServer[]): IWorkbenchMcpServer[] | undefined { + const oldMcpServers = [...mcpServers]; + const findPreviousMcpServerIndex = (from: number): number => { + let index = -1; + const previousMcpServerInNew = newMcpServers[from]; + if (previousMcpServerInNew) { + index = oldMcpServers.findIndex(e => e.name === previousMcpServerInNew.name); + if (index === -1) { + return findPreviousMcpServerIndex(from - 1); + } + } + return index; + }; + + let hasChanged: boolean = false; + for (let index = 0; index < newMcpServers.length; index++) { + const mcpServer = newMcpServers[index]; + if (mcpServers.every(r => r.name !== mcpServer.name)) { + hasChanged = true; + mcpServers.splice(findPreviousMcpServerIndex(index - 1) + 1, 0, mcpServer); + } + } + + return hasChanged ? mcpServers : undefined; + } + } interface IMcpServerTemplateData { diff --git a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts index 5971005ca64..6de7eb1378c 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Emitter } from '../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; import { basename } from '../../../../base/common/resources.js'; @@ -32,12 +32,17 @@ import { IWorkbenchEnvironmentService } from '../../../services/environment/comm import { IWorkbenchLocalMcpServer, IWorkbenchMcpManagementService, LocalMcpServerScope } from '../../../services/mcp/common/mcpWorkbenchManagementService.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; import { mcpConfigurationSection } from '../common/mcpConfiguration.js'; -import { HasInstalledMcpServersContext, IMcpConfigPath, IMcpWorkbenchService, IWorkbenchMcpServer, McpCollectionSortOrder, McpServersGalleryEnabledContext } from '../common/mcpTypes.js'; +import { HasInstalledMcpServersContext, IMcpConfigPath, IMcpWorkbenchService, IWorkbenchMcpServer, McpCollectionSortOrder, McpServerInstallState, McpServersGalleryEnabledContext } from '../common/mcpTypes.js'; import { McpServerEditorInput } from './mcpServerEditorInput.js'; +interface IMcpServerStateProvider { + (mcpWorkbenchServer: McpWorkbenchServer): T; +} + class McpWorkbenchServer implements IWorkbenchMcpServer { constructor( + private installStateProvider: IMcpServerStateProvider, public local: IWorkbenchLocalMcpServer | undefined, public gallery: IGalleryMcpServer | undefined, public readonly installable: IInstallableMcpServer | undefined, @@ -66,6 +71,10 @@ class McpWorkbenchServer implements IWorkbenchMcpServer { return this.gallery?.icon ?? this.local?.icon; } + get installState(): McpServerInstallState { + return this.installStateProvider(this); + } + get codicon(): string | undefined { return this.gallery?.codicon ?? this.local?.codicon; } @@ -133,8 +142,11 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ _serviceBrand: undefined; + private installing: McpWorkbenchServer[] = []; + private uninstalling: McpWorkbenchServer[] = []; + private _local: McpWorkbenchServer[] = []; - get local(): readonly McpWorkbenchServer[] { return this._local; } + get local(): readonly McpWorkbenchServer[] { return [...this._local]; } private readonly _onChange = this._register(new Emitter()); readonly onChange = this._onChange.event; @@ -191,7 +203,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ if (server) { server.local = local; } else { - server = this.instantiationService.createInstance(McpWorkbenchServer, local, gallery, undefined); + server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), local, gallery, undefined); this._local.push(server); } this._onChange.fire(server); @@ -209,7 +221,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ this._local[serverIndex].local = result.local; server = this._local[serverIndex]; } else { - server = this.instantiationService.createInstance(McpWorkbenchServer, result.local, result.source, undefined); + server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), result.local, result.source, undefined); this._local.push(server); } this._onChange.fire(server); @@ -270,28 +282,32 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ return []; } const result = await this.mcpGalleryService.query(options, token); - return result.map(gallery => this.fromGallery(gallery) ?? this.instantiationService.createInstance(McpWorkbenchServer, undefined, gallery, undefined)); + return result.map(gallery => this.fromGallery(gallery) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, gallery, undefined)); } async queryLocal(): Promise { const installed = await this.mcpManagementService.getInstalled(); this._local = installed.map(i => { - const local = this._local.find(server => server.name === i.name) ?? this.instantiationService.createInstance(McpWorkbenchServer, undefined, undefined, undefined); + const local = this._local.find(server => server.name === i.name) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, undefined, undefined); local.local = i; return local; }); - return this._local; + return [...this.local]; } async install(server: IWorkbenchMcpServer): Promise { + if (!(server instanceof McpWorkbenchServer)) { + throw new Error('Invalid server instance'); + } + if (server.installable) { - const local = await this.mcpManagementService.install(server.installable); - return this.onDidInstallMcpServer(local); + const installable = server.installable; + return this.doInstall(server, () => this.mcpManagementService.install(installable)); } if (server.gallery) { - const local = await this.mcpManagementService.installFromGallery(server.gallery, { packageType: server.gallery.packageTypes[0] }); - return this.onDidInstallMcpServer(local); + const gallery = server.gallery; + return this.doInstall(server, () => this.mcpManagementService.installFromGallery(gallery, { packageType: gallery.packageTypes[0] })); } throw new Error('No installable server found'); @@ -304,6 +320,26 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ await this.mcpManagementService.uninstall(server.local); } + private async doInstall(server: McpWorkbenchServer, installTask: () => Promise): Promise { + this.installing.push(server); + this._onChange.fire(server); + await installTask().finally(() => this.installing = this.installing.filter(s => s !== server)); + return this.waitAndGetInstalledMcpServer(server); + } + + private async waitAndGetInstalledMcpServer(server: McpWorkbenchServer): Promise { + let installed = this.local.find(local => local.name === server.name); + if (!installed) { + await Event.toPromise(Event.filter(this.onChange, e => !!e && this.local.some(local => local.name === server.name))); + } + installed = this.local.find(local => local.name === server.name); + if (!installed) { + // This should not happen + throw new Error('Extension should have been installed'); + } + return installed; + } + getMcpConfigPath(localMcpServer: IWorkbenchLocalMcpServer): IMcpConfigPath | undefined; getMcpConfigPath(mcpResource: URI): Promise; getMcpConfigPath(arg: URI | IWorkbenchLocalMcpServer): Promise | IMcpConfigPath | undefined { @@ -422,12 +458,12 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ if (!galleryServer) { throw new Error(`MCP server '${name}' not found in gallery`); } - this.open(this.instantiationService.createInstance(McpWorkbenchServer, undefined, galleryServer, undefined)); + this.open(this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, galleryServer, undefined)); } else { if (config.type === undefined) { (>config).type = (parsed).command ? McpServerType.LOCAL : McpServerType.REMOTE; } - this.open(this.instantiationService.createInstance(McpWorkbenchServer, undefined, undefined, { name, config, inputs })); + this.open(this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, undefined, { name, config, inputs })); } } catch (e) { // ignore @@ -439,6 +475,17 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ await this.editorService.openEditor(this.instantiationService.createInstance(McpServerEditorInput, extension), options, ACTIVE_GROUP); } + private getInstallState(extension: McpWorkbenchServer): McpServerInstallState { + if (this.installing.some(i => i.name === extension.name)) { + return McpServerInstallState.Installing; + } + if (this.uninstalling.some(e => e.name === extension.name)) { + return McpServerInstallState.Uninstalling; + } + const local = this.local.find(e => e === extension); + return local ? McpServerInstallState.Installed : McpServerInstallState.Uninstalled; + } + } export class MCPContextsInitialisation extends Disposable implements IWorkbenchContribution { diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 00417a348c2..c0d3a382b62 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -569,10 +569,18 @@ export interface IMcpServerContainer extends IDisposable { update(): void; } +export const enum McpServerInstallState { + Installing, + Installed, + Uninstalling, + Uninstalled +} + export interface IWorkbenchMcpServer { readonly gallery: IGalleryMcpServer | undefined; readonly local: IWorkbenchLocalMcpServer | undefined; readonly installable: IInstallableMcpServer | undefined; + readonly installState: McpServerInstallState; readonly id: string; readonly name: string; readonly label: string; From 74683d4d8f1a4a7279c5732be0182a55563245f9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 2 Jul 2025 12:48:53 +0200 Subject: [PATCH 039/306] `Copilot setup failed` message: Could have more information regarding why it failed (fix #253242) (#253618) --- .../contrib/chat/browser/chatSetup.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index fff7e526613..dd98ad46471 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -42,7 +42,7 @@ import { IProgressService, ProgressLocation } from '../../../../platform/progres import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; -import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js'; +import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; import { IActivityService, ProgressBadge } from '../../../services/activity/common/activity.js'; @@ -191,6 +191,7 @@ class SetupAgent extends Disposable implements IChatAgentImplementation { } private static readonly SETUP_NEEDED_MESSAGE = new MarkdownString(localize('settingUpCopilotNeeded', "You need to set up Copilot and be signed in to use Chat.")); + private static readonly TRUST_NEEDED_MESSAGE = new MarkdownString(localize('trustNeeded', "You need to trust this workspace to use Chat.")); private readonly _onUnresolvableError = this._register(new Emitter()); readonly onUnresolvableError = this._onUnresolvableError.event; @@ -205,7 +206,8 @@ class SetupAgent extends Disposable implements IChatAgentImplementation { @ILogService private readonly logService: ILogService, @IConfigurationService private readonly configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService ) { super(); } @@ -439,7 +441,7 @@ class SetupAgent extends Disposable implements IChatAgentImplementation { else { progress({ kind: 'markdownContent', - content: SetupAgent.SETUP_NEEDED_MESSAGE, + content: this.workspaceTrustManagementService.isWorkspaceTrusted() ? SetupAgent.SETUP_NEEDED_MESSAGE : SetupAgent.TRUST_NEEDED_MESSAGE }); } @@ -590,7 +592,7 @@ class ChatSetup { let instance = ChatSetup.instance; if (!instance) { instance = ChatSetup.instance = instantiationService.invokeFunction(accessor => { - return new ChatSetup(context, controller, instantiationService, accessor.get(ITelemetryService), accessor.get(IWorkbenchLayoutService), accessor.get(IKeybindingService), accessor.get(IChatEntitlementService) as ChatEntitlementService, accessor.get(ILogService), accessor.get(IConfigurationService), accessor.get(IViewsService), accessor.get(IOpenerService)); + return new ChatSetup(context, controller, instantiationService, accessor.get(ITelemetryService), accessor.get(IWorkbenchLayoutService), accessor.get(IKeybindingService), accessor.get(IChatEntitlementService) as ChatEntitlementService, accessor.get(ILogService), accessor.get(IConfigurationService), accessor.get(IViewsService), accessor.get(IOpenerService), accessor.get(IWorkspaceTrustRequestService)); }); } @@ -613,6 +615,7 @@ class ChatSetup { @IConfigurationService private readonly configurationService: IConfigurationService, @IViewsService private readonly viewsService: IViewsService, @IOpenerService private readonly openerService: IOpenerService, + @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService ) { } skipDialog(): void { @@ -639,6 +642,16 @@ class ChatSetup { const dialogSkipped = this.skipDialogOnce; this.skipDialogOnce = false; + const trusted = await this.workspaceTrustRequestService.requestWorkspaceTrust({ + message: localize('copilotWorkspaceTrust', "Copilot is currently only supported in trusted workspaces.") + }); + if (!trusted) { + this.context.update({ later: true }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotTrusted', installDuration: 0, signUpErrorCode: undefined }); + + return { dialogSkipped, success: undefined /* canceled */ }; + } + let setupStrategy: ChatSetupStrategy; if (dialogSkipped || isProUser(this.chatEntitlementService.entitlement) || this.chatEntitlementService.entitlement === ChatEntitlement.Free) { setupStrategy = ChatSetupStrategy.DefaultSetup; // existing pro/free users setup without a dialog @@ -1186,7 +1199,6 @@ class ChatSetupController extends Disposable { @IProgressService private readonly progressService: IProgressService, @IActivityService private readonly activityService: IActivityService, @ICommandService private readonly commandService: ICommandService, - @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, @IDialogService private readonly dialogService: IDialogService, @IConfigurationService private readonly configurationService: IConfigurationService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @@ -1252,15 +1264,7 @@ class ChatSetupController extends Disposable { entitlement = result.entitlement; } - const trusted = await this.workspaceTrustRequestService.requestWorkspaceTrust({ - message: localize('copilotWorkspaceTrust', "Copilot is currently only supported in trusted workspaces.") - }); - if (!trusted) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotTrusted', installDuration: watch.elapsed(), signUpErrorCode: undefined }); - return false; - } - - // Install + // Await Install this.setStep(ChatSetupStep.Installing); success = await this.install(session, entitlement ?? this.context.state.entitlement, providerId, watch, installation); } finally { From af59930bf7ddd09e95cf178e61ad2e7badbf2722 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 2 Jul 2025 12:59:00 +0200 Subject: [PATCH 040/306] Use a command link to open a PR (#253623) Fixes #253315 --- .../services/extensions/browser/extensionUrlHandler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts index b5123044a0a..c1c1e01c961 100644 --- a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts +++ b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts @@ -26,6 +26,7 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { isCancellationError } from '../../../../base/common/errors.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { equalsIgnoreCase } from '../../../../base/common/strings.js'; const FIVE_MINUTES = 5 * 60 * 1000; const THIRTY_SECONDS = 30 * 1000; @@ -181,7 +182,7 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { } const trusted = options?.trusted - || this.productService.trustedExtensionProtocolHandlers?.includes(extensionId) + || this.productService.trustedExtensionProtocolHandlers?.some(value => equalsIgnoreCase(value, extensionId)) || this.didUserTrustExtension(ExtensionIdentifier.toKey(extensionId)); if (!trusted) { From c20ffd022dbe81f45032ae7940b899ad74bb9171 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 2 Jul 2025 13:01:06 +0200 Subject: [PATCH 041/306] fix #253222 (#253624) --- src/vs/workbench/contrib/mcp/browser/mcpServersView.ts | 3 ++- .../contrib/mcp/browser/mcpWorkbenchService.ts | 10 ++++++++++ src/vs/workbench/contrib/mcp/common/mcpTypes.ts | 1 + .../mcp/common/mcpWorkbenchManagementService.ts | 10 ++++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts index cfbe4aae27d..2e743bcfd5a 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts @@ -231,13 +231,14 @@ export class McpServersListView extends ViewPane { const onDidChangeModel = disposables.add(new Emitter>()); let servers = await this.mcpWorkbenchService.queryLocal(); - disposables.add(Event.debounce(Event.filter(this.mcpWorkbenchService.onChange, e => e?.installState === McpServerInstallState.Installed), () => undefined)(async () => { + disposables.add(Event.debounce(Event.filter(this.mcpWorkbenchService.onChange, e => e?.installState === McpServerInstallState.Installed), () => undefined)(() => { const mergedMcpServers = this.mergeAddedMcpServers(servers, [...this.mcpWorkbenchService.local]); if (mergedMcpServers) { servers = mergedMcpServers; onDidChangeModel.fire(new PagedModel(servers)); } })); + disposables.add(this.mcpWorkbenchService.onReset(() => onDidChangeModel.fire(new PagedModel([...this.mcpWorkbenchService.local])))); return { model: new PagedModel(servers), onDidChangeModel: onDidChangeModel.event, disposables }; } diff --git a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts index 6de7eb1378c..ddf3a31ae1d 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts @@ -151,6 +151,9 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ private readonly _onChange = this._register(new Emitter()); readonly onChange = this._onChange.event; + private readonly _onReset = this._register(new Emitter()); + readonly onReset = this._onReset.event; + constructor( @IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService, @IWorkbenchMcpManagementService private readonly mcpManagementService: IWorkbenchMcpManagementService, @@ -169,10 +172,17 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ this._register(this.mcpManagementService.onDidInstallMcpServersInCurrentProfile(e => this.onDidInstallMcpServers(e))); this._register(this.mcpManagementService.onDidUpdateMcpServersInCurrentProfile(e => this.onDidUpdateMcpServers(e))); this._register(this.mcpManagementService.onDidUninstallMcpServerInCurrentProfile(e => this.onDidUninstallMcpServer(e))); + this._register(this.mcpManagementService.onDidChangeProfile(e => this.onDidChangeProfile())); this.queryLocal().then(() => this.syncInstalledMcpServers()); urlService.registerHandler(this); } + private async onDidChangeProfile() { + await this.queryLocal(); + this._onChange.fire(undefined); + this._onReset.fire(); + } + private onDidUninstallMcpServer(e: DidUninstallMcpServerEvent) { if (e.error) { return; diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index c0d3a382b62..a882e1c9c45 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -607,6 +607,7 @@ export const IMcpWorkbenchService = createDecorator('IMcpW export interface IMcpWorkbenchService { readonly _serviceBrand: undefined; readonly onChange: Event; + readonly onReset: Event; readonly local: readonly IWorkbenchMcpServer[]; queryLocal(): Promise; queryGallery(options?: IQueryOptions, token?: CancellationToken): Promise; diff --git a/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts b/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts index b6e6d7bc93f..ef7ef2ad444 100644 --- a/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts +++ b/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts @@ -52,6 +52,7 @@ export interface IWorkbenchMcpManagementService extends IMcpManagementService { readonly onDidUpdateMcpServersInCurrentProfile: Event; readonly onUninstallMcpServerInCurrentProfile: Event; readonly onDidUninstallMcpServerInCurrentProfile: Event; + readonly onDidChangeProfile: Event; getInstalled(): Promise; install(server: IInstallableMcpServer, options?: IWorkbencMcpServerInstallOptions): Promise; @@ -93,6 +94,9 @@ class WorkbenchMcpManagementService extends Disposable implements IWorkbenchMcpM private readonly _onDidUninstallMcpServerInCurrentProfile = this._register(new Emitter()); readonly onDidUninstallMcpServerInCurrentProfile = this._onDidUninstallMcpServerInCurrentProfile.event; + private readonly _onDidChangeProfile = this._register(new Emitter()); + readonly onDidChangeProfile = this._onDidChangeProfile.event; + private readonly workspaceMcpManagementService: IMcpManagementService; private readonly remoteMcpManagementService: IMcpManagementService | undefined; @@ -194,6 +198,12 @@ class WorkbenchMcpManagementService extends Disposable implements IWorkbenchMcpM } })); } + + this._register(userDataProfileService.onDidChangeCurrentProfile(e => { + if (!this.uriIdentityService.extUri.isEqual(e.previous.mcpResource, e.profile.mcpResource)) { + this._onDidChangeProfile.fire(); + } + })); } private handleInstallMcpServerResultsFromEvent(e: readonly InstallMcpServerResult[], emitter: Emitter, currentProfileEmitter: Emitter): void { From e3f4ec9f18a2ec173f5f10625fb07bbe34cff210 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 2 Jul 2025 13:15:45 +0200 Subject: [PATCH 042/306] fix #253230 (#253627) --- .../workbench/contrib/mcp/browser/mcpServerEditorInput.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerEditorInput.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerEditorInput.ts index 18a9416a0bc..f82a38ff41e 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServerEditorInput.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerEditorInput.ts @@ -14,7 +14,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; import { IWorkbenchMcpServer } from '../common/mcpTypes.js'; -const ExtensionEditorIcon = registerIcon('extensions-editor-label-icon', Codicon.extensions, localize('extensionsEditorLabelIcon', 'Icon of the extensions editor label.')); +const MCPServerEditorIcon = registerIcon('mcp-server-editor-icon', Codicon.mcp, localize('mcpServerEditorLabelIcon', 'Icon of the MCP Server editor.')); export class McpServerEditorInput extends EditorInput { @@ -42,11 +42,11 @@ export class McpServerEditorInput extends EditorInput { get mcpServer(): IWorkbenchMcpServer { return this._mcpServer; } override getName(): string { - return localize('extensionsInputName', "Extension: {0}", this._mcpServer.label); + return localize('extensionsInputName', "MCP Server: {0}", this._mcpServer.label); } override getIcon(): ThemeIcon | undefined { - return ExtensionEditorIcon; + return MCPServerEditorIcon; } override matches(other: EditorInput | IUntypedEditorInput): boolean { From 221f260282f5030d7271ea2278847a12b1d65c1d Mon Sep 17 00:00:00 2001 From: Holger Jeromin Date: Wed, 2 Jul 2025 13:31:10 +0200 Subject: [PATCH 043/306] vscode api: Raise compatibility to webview content The webview (or its `acquireVsCodeApi` injector) adds the vscode api but also deletes the properties `window.parent`, `window.top` and `window.frameElement`. This voilates the HTML Spec and makes problems with some code like `if (window.parent === window)` to detect if we are running in an iframe. Yes, this affects Hyrum's Law and https://xkcd.com/1172/ but detecting `acquireVsCodeApi` object itself feels to be cleaner anyway. --- src/vs/workbench/contrib/webview/browser/pre/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/webview/browser/pre/index.html b/src/vs/workbench/contrib/webview/browser/pre/index.html index 76c86d49dd3..b30d1d3c694 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index.html @@ -221,9 +221,9 @@ }); }; })(); - delete window.parent; - delete window.top; - delete window.frameElement; + window.parent = window; + window.top = window; + window.frameElement = null; `; } From 9cc20ca7d492a8f7d749ffc61986c6abdccc5a1e Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 2 Jul 2025 13:59:36 +0200 Subject: [PATCH 044/306] Adding a description for the setting `allowVariableLineHeights` (#253621) adding a description for the setting `allowVariableLineHeights` --- src/vs/editor/common/config/editorOptions.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 78efae7449f..291dc666573 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -5732,7 +5732,10 @@ export const EditorOptions = { tags: ['accessibility'] })), allowVariableLineHeights: register(new EditorBooleanOption( - EditorOption.allowVariableLineHeights, 'allowVariableLineHeights', true + EditorOption.allowVariableLineHeights, 'allowVariableLineHeights', true, + { + description: nls.localize('allowVariableLineHeights', "Controls whether to allow using variable line heights in the editor.") + } )), allowVariableFonts: register(new EditorBooleanOption( EditorOption.allowVariableFonts, 'allowVariableFonts', true, From 9add99d1ccc09737951dc4412d3c0ee0887dff54 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 2 Jul 2025 14:02:52 +0200 Subject: [PATCH 045/306] fix #253329 (#253634) --- src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts index b91e8522bc0..287eebf6793 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts @@ -558,7 +558,7 @@ export class McpServerEditor extends EditorPane { private async openConfiguration(mcpServer: IWorkbenchMcpServer, template: IExtensionEditorTemplate, token: CancellationToken): Promise { const configContainer = append(template.content, $('.configuration')); - const content = $('div', { class: 'configuration-content', tabindex: '0' }); + const content = $('div', { class: 'configuration-content' }); this.renderConfigurationDetails(content, mcpServer); @@ -573,7 +573,7 @@ export class McpServerEditor extends EditorPane { private async openManifest(mcpServer: IWorkbenchMcpServer, template: IExtensionEditorTemplate, token: CancellationToken): Promise { const manifestContainer = append(template.content, $('.manifest')); - const content = $('div', { class: 'manifest-content', tabindex: '0' }); + const content = $('div', { class: 'manifest-content' }); try { const manifest = await this.loadContents(() => this.mcpServerManifest!.get(), content); From 70fa27585228f7a1cd9d4010a574e6fc4f3af9c4 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 2 Jul 2025 14:03:15 +0200 Subject: [PATCH 046/306] fix #253240 (#253631) --- .../services/mcp/common/mcpWorkbenchManagementService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts b/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts index ef7ef2ad444..cc447afc203 100644 --- a/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts +++ b/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts @@ -347,9 +347,9 @@ class WorkbenchMcpManagementService extends Disposable implements IWorkbenchMcpM if (profile) { profile = await this.remoteUserDataProfilesService.getRemoteProfile(profile); } else { - profile = (await this.remoteUserDataProfilesService.getRemoteProfiles()).find(p => this.uriIdentityService.extUri.isEqual(p.extensionsResource, mcpResource)); + profile = (await this.remoteUserDataProfilesService.getRemoteProfiles()).find(p => this.uriIdentityService.extUri.isEqual(p.mcpResource, mcpResource)); } - return profile?.extensionsResource; + return profile?.mcpResource; } } From 2a4faa2c6da3d41ce3a281c0720b1f49d55d623f Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 2 Jul 2025 14:03:31 +0200 Subject: [PATCH 047/306] Setting variable line height and font setting options to false for simple editors (#253626) setting options to false for simple editor --- .../contrib/codeEditor/browser/simpleEditorOptions.ts | 3 +++ src/vs/workbench/contrib/debug/browser/breakpointWidget.ts | 1 - src/vs/workbench/contrib/debug/browser/repl.ts | 1 - src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 1 - 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts b/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts index d366a6c17b2..07af7312d21 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts @@ -49,6 +49,9 @@ export function getSimpleEditorOptions(configurationService: IConfigurationServi cursorBlinking: configurationService.getValue<'blink' | 'smooth' | 'phase' | 'expand' | 'solid'>('editor.cursorBlinking'), editContext: configurationService.getValue('editor.editContext'), defaultColorDecorators: 'never', + allowVariableLineHeights: false, + allowVariableFonts: false, + allowVariableFontsInAccessibilityMode: false, }; } diff --git a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index 84524c76602..07b92710f72 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -421,7 +421,6 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi options.lineHeight = editorConfig.lineHeight; options.fontLigatures = editorConfig.fontLigatures; options.ariaLabel = this.placeholder; - options.allowVariableLineHeights = false; return options; } diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index ce162293526..930812de0f9 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -737,7 +737,6 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { const config = this.configurationService.getValue('debug'); options.acceptSuggestionOnEnter = config.console.acceptSuggestionOnEnter === 'on' ? 'on' : 'off'; options.ariaLabel = this.getAriaLabel(); - options.allowVariableLineHeights = false; this.replInput = this.scopedInstantiationService.createInstance(CodeEditorWidget, this.replInputContainer, options, getSimpleCodeEditorWidgetOptions()); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 3c51be33044..6f6c7441af7 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1561,7 +1561,6 @@ class SCMInputWidgetEditorOptions { return { ...getSimpleEditorOptions(this.configurationService), ...this.getEditorOptions(), - allowVariableLineHeights: false, dragAndDrop: true, dropIntoEditor: { enabled: true }, formatOnType: true, From d63c9dd28e598238732b7a5dab4b9ad07feb1691 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 2 Jul 2025 14:24:54 +0200 Subject: [PATCH 048/306] fix #253287 (#253638) --- .../contrib/mcp/browser/mcp.contribution.ts | 3 ++- .../contrib/mcp/browser/mcpCommands.ts | 23 +++++++++++++++++++ .../contrib/mcp/common/mcpCommandIds.ts | 1 + 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts index 4b9b193ab5c..fa946fff60e 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts @@ -30,7 +30,7 @@ import { McpSamplingService } from '../common/mcpSamplingService.js'; import { McpService } from '../common/mcpService.js'; import { IMcpElicitationService, IMcpSamplingService, IMcpService, IMcpWorkbenchService } from '../common/mcpTypes.js'; import { McpAddContextContribution } from './mcpAddContextContribution.js'; -import { AddConfigurationAction, EditStoredInput, ListMcpServerCommand, McpBrowseCommand, McpBrowseResourcesCommand, McpConfigureSamplingModels, MCPServerActionRendering, McpServerOptionsCommand, McpStartPromptingServerCommand, OpenRemoteUserMcpResourceCommand, OpenUserMcpResourceCommand, OpenWorkspaceFolderMcpResourceCommand, OpenWorkspaceMcpResourceCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, RestartServer, ShowConfiguration, ShowInstalledMcpServersCommand, ShowOutput, StartServer, StopServer } from './mcpCommands.js'; +import { AddConfigurationAction, BrowseMcpServersPageCommand, EditStoredInput, ListMcpServerCommand, McpBrowseCommand, McpBrowseResourcesCommand, McpConfigureSamplingModels, MCPServerActionRendering, McpServerOptionsCommand, McpStartPromptingServerCommand, OpenRemoteUserMcpResourceCommand, OpenUserMcpResourceCommand, OpenWorkspaceFolderMcpResourceCommand, OpenWorkspaceMcpResourceCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, RestartServer, ShowConfiguration, ShowInstalledMcpServersCommand, ShowOutput, StartServer, StopServer } from './mcpCommands.js'; import { McpDiscovery } from './mcpDiscovery.js'; import { McpElicitationService } from './mcpElicitationService.js'; import { McpLanguageFeatures } from './mcpLanguageFeatures.js'; @@ -71,6 +71,7 @@ registerAction2(ShowOutput); registerAction2(RestartServer); registerAction2(ShowConfiguration); registerAction2(McpBrowseCommand); +registerAction2(BrowseMcpServersPageCommand); registerAction2(OpenUserMcpResourceCommand); registerAction2(OpenRemoteUserMcpResourceCommand); registerAction2(OpenWorkspaceMcpResourceCommand); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index aa54c5deb60..a6ad8e425ea 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -52,6 +52,8 @@ import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from '../../../browser/actions/works import { MCP_CONFIGURATION_KEY, WORKSPACE_STANDALONE_CONFIGURATIONS } from '../../../services/configuration/common/configuration.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; // acroynms do not get localized const category: ILocalizedString = { @@ -698,6 +700,27 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { }, }); +export class BrowseMcpServersPageCommand extends Action2 { + constructor() { + super({ + id: McpCommandIds.BrowsePage, + title: localize2('mcp.command.open', "Browse MCP Servers"), + icon: Codicon.globe, + menu: [{ + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', InstalledMcpServersViewId), + group: 'navigation', + }], + }); + } + + async run(accessor: ServicesAccessor) { + const productService = accessor.get(IProductService); + const openerService = accessor.get(IOpenerService); + return openerService.open(productService.quality === 'insider' ? 'https://code.visualstudio.com/insider/mcp' : 'https://code.visualstudio.com/mcp'); + } +} + export class ShowInstalledMcpServersCommand extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts b/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts index cf6a27064b0..9c8ae9608f0 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts @@ -9,6 +9,7 @@ export const enum McpCommandIds { AddConfiguration = 'workbench.mcp.addConfiguration', Browse = 'workbench.mcp.browseServers', + BrowsePage = 'workbench.mcp.browseServersPage', ShowInstalled = 'workbench.mcp.showInstalledServers', OpenUserMcp = 'workbench.mcp.openUserMcpJson', OpenRemoteUserMcp = 'workbench.mcp.openRemoteUserMcpJson', From 9f7ea0588dc74f3bdaea542bfdeb105aa7c4bc83 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:45:45 +0200 Subject: [PATCH 049/306] Immediately snooze on pressing snooze (#253641) immidiately snooze when pressing snooze --- .../services/inlineCompletionsService.ts | 35 +++++++++++-------- .../controller/inlineCompletionsController.ts | 5 +-- .../browser/model/inlineCompletionsModel.ts | 10 +++++- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/vs/editor/browser/services/inlineCompletionsService.ts b/src/vs/editor/browser/services/inlineCompletionsService.ts index 0573984ea22..f060fbddfb2 100644 --- a/src/vs/editor/browser/services/inlineCompletionsService.ts +++ b/src/vs/editor/browser/services/inlineCompletionsService.ts @@ -83,26 +83,31 @@ export class InlineCompletionsService extends Disposable implements IInlineCompl } setSnoozeDuration(durationMs: number): void { + if (durationMs < 0) { + throw new BugIndicatingError(`Invalid snooze duration: ${durationMs}. Duration must be non-negative.`); + } + if (durationMs === 0) { + this.cancelSnooze(); + return; + } + const wasSnoozing = this.isSnoozing(); this._snoozeTimeEnd = Date.now() + durationMs; - const isSnoozing = this.isSnoozing(); - if (wasSnoozing !== isSnoozing) { - this._onDidChangeIsSnoozing.fire(isSnoozing); + if (!wasSnoozing) { + this._onDidChangeIsSnoozing.fire(true); } - if (isSnoozing) { - this._timer.cancelAndSet( - () => { - if (!this.isSnoozing()) { - this._onDidChangeIsSnoozing.fire(false); - } else { - throw new BugIndicatingError('Snooze timer did not fire as expected'); - } - }, - this.snoozeTimeLeft + 1, - ); - } + this._timer.cancelAndSet( + () => { + if (!this.isSnoozing()) { + this._onDidChangeIsSnoozing.fire(false); + } else { + throw new BugIndicatingError('Snooze timer did not fire as expected'); + } + }, + this.snoozeTimeLeft + 1, + ); } isSnoozing(): boolean { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index 0ae14aef6d5..7aab79fce9c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -37,7 +37,6 @@ import { ObservableContextKeyService } from '../utils.js'; import { InlineCompletionsView } from '../view/inlineCompletionsView.js'; import { inlineSuggestCommitId } from './commandIds.js'; import { InlineCompletionContextKeys } from './inlineCompletionContextKeys.js'; -import { IInlineCompletionsService } from '../../../../browser/services/inlineCompletionsService.js'; export class InlineCompletionsController extends Disposable { private static readonly _instances = new Set(); @@ -96,7 +95,6 @@ export class InlineCompletionsController extends Disposable { @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, - @IInlineCompletionsService private readonly _inlineCompletionsService: IInlineCompletionsService, ) { super(); this._editorObs = observableCodeEditor(this.editor); @@ -112,8 +110,7 @@ export class InlineCompletionsController extends Disposable { this._contextKeyService.onDidChangeContext, () => this._contextKeyService.getContext(this.editor.getDomNode()).getValue('editorDictation.inProgress') === true ); - const isSnoozing = observableFromEvent(this, this._inlineCompletionsService.onDidChangeIsSnoozing, () => this._inlineCompletionsService.isSnoozing()); - this._enabled = derived(this, reader => this._enabledInConfig.read(reader) && !isSnoozing.read(reader) && (!this._isScreenReaderEnabled.read(reader) || !this._editorDictationInProgress.read(reader))); + this._enabled = derived(this, reader => this._enabledInConfig.read(reader) && (!this._isScreenReaderEnabled.read(reader) || !this._editorDictationInProgress.read(reader))); this._debounceValue = this._debounceService.for( this._languageFeaturesService.inlineCompletionsProvider, 'InlineCompletionsDebounce', diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index 964b23884c8..ad4f401d00a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -46,6 +46,7 @@ import { SuggestItemInfo } from './suggestWidgetAdapter.js'; import { TextModelEditReason, EditReasons } from '../../../../common/textModelEditReason.js'; import { ICodeEditorService } from '../../../../browser/services/codeEditorService.js'; import { InlineCompletionViewData, InlineCompletionViewKind } from '../view/inlineEdits/inlineEditsViewInterface.js'; +import { IInlineCompletionsService } from '../../../../browser/services/inlineCompletionsService.js'; export class InlineCompletionsModel extends Disposable { private readonly _source; @@ -90,6 +91,7 @@ export class InlineCompletionsModel extends Disposable { @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, + @IInlineCompletionsService inlineCompletionsService: IInlineCompletionsService ) { super(); this.primaryPosition = derived(this, reader => this._positions.read(reader)[0] ?? new Position(1, 1)); @@ -111,6 +113,11 @@ export class InlineCompletionsModel extends Disposable { this._inlineEditsEnabled = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !!v.edits.enabled); this._inlineEditsShowCollapsedEnabled = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.showCollapsed); this._triggerCommandOnProviderChange = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.experimental.triggerCommandOnProviderChange); + this._register(inlineCompletionsService.onDidChangeIsSnoozing((isSnoozing) => { + if (isSnoozing) { + this.stop(); + } + })); this._lastShownInlineCompletionInfo = undefined; this._lastAcceptedInlineCompletionInfo = undefined; @@ -183,7 +190,8 @@ export class InlineCompletionsModel extends Disposable { this._onlyRequestInlineEditsSignal.read(reader); this._forceUpdateExplicitlySignal.read(reader); this._fetchSpecificProviderSignal.read(reader); - const shouldUpdate = (this._enabled.read(reader) && this._selectedSuggestItem.read(reader)) || this._isActive.read(reader); + const shouldUpdate = ((this._enabled.read(reader) && this._selectedSuggestItem.read(reader)) || this._isActive.read(reader)) + && (!inlineCompletionsService.isSnoozing() || changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit); if (!shouldUpdate) { this._source.cancelUpdate(); return undefined; From 12fb67223fdc6212eb9d299b7c08edf50047b256 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 2 Jul 2025 14:57:05 +0200 Subject: [PATCH 050/306] fix #253366 (#253639) --- .../contrib/mcp/browser/mcpServerActions.ts | 35 +++---------------- .../contrib/mcp/browser/mcpServerEditor.ts | 16 +++++++-- .../workbench/contrib/mcp/common/mcpTypes.ts | 13 ++++++- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts index e5c7ea62c1a..879b5f72215 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts @@ -12,10 +12,7 @@ import { IContextMenuService } from '../../../../platform/contextview/browser/co import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { manageExtensionIcon } from '../../extensions/browser/extensionsIcons.js'; import { getDomNodePagePosition } from '../../../../base/browser/dom.js'; -import { IMcpSamplingService, IMcpServer, IMcpServerContainer, IMcpService, IMcpWorkbenchService, IWorkbenchMcpServer, McpCapability, McpConnectionState } from '../common/mcpTypes.js'; -import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; -import { URI } from '../../../../base/common/uri.js'; -import { Location } from '../../../../editor/common/languages.js'; +import { IMcpSamplingService, IMcpServer, IMcpServerContainer, IMcpService, IMcpWorkbenchService, IWorkbenchMcpServer, McpCapability, McpConnectionState, McpServerEditorTab } from '../common/mcpTypes.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { McpCommandIds } from '../common/mcpCommandIds.js'; @@ -540,9 +537,7 @@ export class ShowServerConfigurationAction extends McpServerAction { private static readonly HIDE = `${this.CLASS} hide`; constructor( - @IMcpService private readonly mcpService: IMcpService, - @IMcpRegistry private readonly mcpRegistry: IMcpRegistry, - @IEditorService private readonly editorService: IEditorService, + @IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService ) { super('extensions.config', localize('config', "Show Configuration"), ShowServerConfigurationAction.CLASS, false); this.update(); @@ -551,8 +546,7 @@ export class ShowServerConfigurationAction extends McpServerAction { update(): void { this.enabled = false; this.class = ShowServerConfigurationAction.HIDE; - const configurationTarget = this.getConfigurationTarget(); - if (!configurationTarget) { + if (!this.mcpServer?.local) { return; } this.class = ShowServerConfigurationAction.CLASS; @@ -561,31 +555,12 @@ export class ShowServerConfigurationAction extends McpServerAction { } override async run(): Promise { - const configurationTarget = this.getConfigurationTarget(); - if (!configurationTarget) { + if (!this.mcpServer?.local) { return; } - this.editorService.openEditor({ - resource: URI.isUri(configurationTarget) ? configurationTarget : configurationTarget!.uri, - options: { selection: URI.isUri(configurationTarget) ? undefined : configurationTarget!.range } - }); + this.mcpWorkbenchService.open(this.mcpServer, { tab: McpServerEditorTab.Configuration }); } - private getConfigurationTarget(): Location | URI | undefined { - if (!this.mcpServer) { - return; - } - if (!this.mcpServer.local) { - return; - } - const server = this.mcpService.servers.get().find(s => s.definition.label === this.mcpServer?.name); - if (!server) { - return; - } - const collection = this.mcpRegistry.collections.get().find(c => c.id === server.collection.id); - const serverDefinition = collection?.serverDefinitions.get().find(s => s.id === server.definition.id); - return serverDefinition?.presentation?.origin || collection?.presentation?.origin; - } } export class ConfigureModelAccessAction extends McpServerAction { diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts index 287eebf6793..bd88574cc4b 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts @@ -37,11 +37,10 @@ import { IWebview, IWebviewService } from '../../webview/browser/webview.js'; import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { IWorkbenchMcpServer, McpServerContainers } from '../common/mcpTypes.js'; +import { IMcpServerEditorOptions, IWorkbenchMcpServer, McpServerContainers } from '../common/mcpTypes.js'; import { InstallCountWidget, McpServerIconWidget, McpServerWidget, onClick, PublisherWidget, RatingsWidget } from './mcpServerWidgets.js'; import { DropDownAction, InstallAction, ManageMcpServerAction, UninstallAction } from './mcpServerActions.js'; import { McpServerEditorInput } from './mcpServerEditorInput.js'; -import { IEditorOptions } from '../../../../platform/editor/common/editor.js'; import { ILocalMcpServer, IMcpServerManifest, IMcpServerPackage, PackageType } from '../../../../platform/mcp/common/mcpManagement.js'; import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { McpServerType } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; @@ -286,7 +285,7 @@ export class McpServerEditor extends EditorPane { }; } - override async setInput(input: McpServerEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: McpServerEditorInput, options: IMcpServerEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); if (this.template) { await this.render(input.mcpServer, this.template, !!options?.preserveFocus); @@ -313,6 +312,13 @@ export class McpServerEditor extends EditorPane { this.renderNavbar(mcpServer, template, preserveFocus); } + override setOptions(options: IMcpServerEditorOptions | undefined): void { + super.setOptions(options); + if (options?.tab) { + this.template?.navbar.switch(options.tab); + } + } + private renderNavbar(extension: IWorkbenchMcpServer, template: IExtensionEditorTemplate, preserveFocus: boolean): void { template.content.innerText = ''; template.navbar.clear(); @@ -334,6 +340,10 @@ export class McpServerEditor extends EditorPane { template.navbar.push(McpServerEditorTab.Manifest, localize('manifest', "Manifest"), localize('manifesttooltip', "Server manifest details")); } + if ((this.options)?.tab) { + template.navbar.switch((this.options).tab!); + } + if (template.navbar.currentId) { this.onNavbarChange(extension, { id: template.navbar.currentId, focus: !preserveFocus }, template); } diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index a882e1c9c45..1d9f3f1af2c 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -569,6 +569,11 @@ export interface IMcpServerContainer extends IDisposable { update(): void; } +export interface IMcpServerEditorOptions extends IEditorOptions { + tab?: McpServerEditorTab; + sideByside?: boolean; +} + export const enum McpServerInstallState { Installing, Installed, @@ -576,6 +581,12 @@ export const enum McpServerInstallState { Uninstalled } +export const enum McpServerEditorTab { + Readme = 'readme', + Manifest = 'manifest', + Configuration = 'configuration', +} + export interface IWorkbenchMcpServer { readonly gallery: IGalleryMcpServer | undefined; readonly local: IWorkbenchLocalMcpServer | undefined; @@ -615,7 +626,7 @@ export interface IMcpWorkbenchService { uninstall(mcpServer: IWorkbenchMcpServer): Promise; getMcpConfigPath(arg: IWorkbenchLocalMcpServer): IMcpConfigPath | undefined; getMcpConfigPath(arg: URI): Promise; - open(extension: IWorkbenchMcpServer | string, options?: IEditorOptions): Promise; + open(extension: IWorkbenchMcpServer | string, options?: IMcpServerEditorOptions): Promise; } export class McpServerContainers extends Disposable { From 1adf9f7677f666bcf60ced9604fa61774993b833 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 2 Jul 2025 17:34:19 +0200 Subject: [PATCH 051/306] prompt files: fix for hovers --- .../languageProviders/promptHeaderHovers.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderHovers.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderHovers.ts index 85aac5ad625..e9d12ef8916 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderHovers.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderHovers.ts @@ -124,7 +124,7 @@ export class PromptHeaderHoverProvider extends Disposable implements HoverProvid if (toolRange?.containsPosition(position)) { const tool = this.languageModelToolsService.getToolByName(toolName); if (tool) { - return this.createHover(tool.displayName, toolRange); + return this.createHover(tool.modelDescription, toolRange); } const toolSet = this.languageModelToolsService.getToolSetByName(toolName); if (toolSet) { @@ -143,22 +143,25 @@ export class PromptHeaderHoverProvider extends Disposable implements HoverProvid lines.push(toolSet.description); } for (const tool of toolSet.getTools()) { - lines.push(`- ${tool.toolReferenceName ?? tool.displayName} (${tool.displayName})`); + lines.push(`- ${tool.toolReferenceName ?? tool.displayName}`); } return this.createHover(lines.join('\n'), range); } private getModelHover(node: PromptModelMetadata, range: Range, baseMessage: string): Hover | undefined { - if (node.value) { - + const modelName = node.value; + if (modelName) { for (const id of this.languageModelsService.getLanguageModelIds()) { const meta = this.languageModelsService.lookupLanguageModel(id); - if (meta) { + if (meta && meta.name === modelName) { const lines: string[] = []; lines.push(baseMessage + '\n'); - lines.push(localize('modelName', '{0}', meta.description ?? meta.name)); + lines.push(localize('modelName', '- Name: {0}', meta.name)); lines.push(localize('modelFamily', '- Family: {0}', meta.family)); lines.push(localize('modelVendor', '- Vendor: {0}', meta.vendor)); + if (meta.description) { + lines.push('', '', meta.description); + } return this.createHover(lines.join('\n'), range); } } From d65071909487b9159e08726c57adf5eb008d7d3a Mon Sep 17 00:00:00 2001 From: JJJJJJ-git Date: Wed, 2 Jul 2025 09:28:30 -0700 Subject: [PATCH 052/306] Fixing ChatService undo bug (#253478) During an undo operation, we should ensure that the request has completed its deletion process. --- src/vs/workbench/contrib/chat/common/chatServiceImpl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 7e937103259..3690cdf64b3 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -614,7 +614,7 @@ export class ChatService extends Disposable implements IChatService { if (request.shouldBeRemovedOnSend.afterUndoStop) { request.response?.finalizeUndoState(); } else { - this.removeRequest(sessionId, request.id); + await this.removeRequest(sessionId, request.id); } } } From dfe124cea49046c8ad5d7902b3f05a60d27bc70b Mon Sep 17 00:00:00 2001 From: lemurra_microsoft Date: Wed, 2 Jul 2025 17:30:46 +0100 Subject: [PATCH 053/306] Add 'copilot-snooze' icon to codicons library --- .../browser/ui/codicons/codicon/codicon.ttf | Bin 88532 -> 88852 bytes src/vs/base/common/codiconsLibrary.ts | 1 + 2 files changed, 1 insertion(+) diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 8d8e3a824b0b9642b6977782a4d08de25c1350f0..29a5a9895e04c81052383ce1009744711dea5266 100644 GIT binary patch delta 7747 zcmZwM2Xs|cz6S8`yGia%LPCIqL?DDTlF$MMLIQ*U0U{;RK?uEv9wZ<-z@-bwC>lh? z##pc+;|SKVAR;P*ii(Jc4kEU5u80ofFb+@OxBb7h-g;}juzu&e=ay6UK4xRB?ygS(I9MGb}tuN&y`k=lmJ5QEPx;1ho{e2lM3+x*cH9S~6 z?e`B6Jpoa_ziO|H2V$lnI=Yj7)&!08PQcsbZS~&t-uC1CA(0XFr{lWKJEc#f*nqP+ z9fvuRPqQ~~VjJFyVLZc?ypbER3nd(k?KrE$t>9DGfHZ!J$FYNxS;~&=#5Zsn&*ELa z%zJSc_i_gx=7YS0yZIy^d-!<$>iAX-YA^?LF%Ju|sQ&TzZsDnzhUu7rC0L5< za06ChHP&D))?qz1;zn%3W^BPt_#AMhid;K%%g zr}zcW@=KoMSNx718#0NF*#z^k04vY~E5oSe9{d4eT#wnvM*(wjH~xfA_?|ALE3lX$ zcH?6Hnf-B;s~KibZf7M&V-`=MKU?D-9A`5Ya}3(CAIngRZm8m4_#xln-*^-II4CH1!$I~ccA9i3Kr(zQ0Ifq+u3%2rh zuE*PW6L0ZzeuhdeVJ)WM9cHj4K43YLaSXTNDoo@MT!RVt0i!S$dl|Rd9w_kR=VE^dn;=UZ&J=NW7tM7<{GsI@a7o|Q_eR$qg-IPQn}FZM&%-- z_5)t6!LIsaNofNIYi)^9s{*gi;H+|~aSfxl%%DPfol)xpZ@IyS`c93~!k=pG4@PYg zycGsJlla;FsOO;{6j>^@BYA3xlM(r8AwMH!)ymdxx9K7`g??rD{nWvN9lGbYK7|>zuMDP_#;N`F}z2OT4s2U8MV>yTzQIGYj{r>EZSGwWIWYy z!#ir!gu{Ess4<7U8-6L{`=5%nzaT-}QL7KWnvVAMy#yI|B?!u!#v--M@! z6-PZNynh(=rSL8q^{VjxY1F^!4*FROQCAD^Uq+oSyh}#iFT8&nb;R&~G3t`x{l}vtO zUQ5GFWrkt4vXx(jAm=sL~yj zaG0`-;c#WK;RvNW*siL7UZWT8XoX{yrH12_-3+Uh?kt4kmF_Hr6O?6!6O}y;Cn?Jf zrzm?FPE+N>c5XRG)q}w^mqwhMdRo}1%5xnxyt^A^OTi_^OdgjgbS3e^n?qQ zuJnXUm9F%J%antS9$fXp6{T>w(iNrfdgTzq6-rl>;zm2ZD@x&7r7KF|I^{^i4a!l5 zo0OvswR8=am7s{1}XO*iA zzf`&l+@0h(w=@o2Mkv$A2f8w{E%T!<->;CmAl>Drv)pu@`%xZ5dNbEvy_h+4HDr$Zs2z8 zE`HI#5xy%h(GU{;lLp6?dkvc@-GK{>l}{NBHR~5=q$c*$>ORA=`t2DV!++7*^M)TP zUod<}`J&<9lz%lEox*>~;5FsThKrQ$4ivhZ0>@ZCuW8Y|sN2qx>F@6J>- zq=x^lfr{9F&uEwp|CrH$8~*!7LvQ%*wiXS-;k(ik4aecTDiQoxpPAWX^jBJQ_psoA z@>8RMI{ec{Lw5L|8Pq91HyXyncSj%^(8E7taBcllnQ6^jOY)_GYf0QO2`lQ)Wafm^ zwC0LdkfQv~@VL@5yi(~KO$@+~7)=qt|K4b_0RDNSX#@B_7)>C+zhE?#0RKm$Nd@>S zg|J)TSFO0>5mMpuS!=3C2hS<5 zFg#s9EGI48M{6w%J1E_m2=kOJjb>sH$uO9tbY~@uS7sW{QDzy<*dUT^+#^vW$H2Xx zYk0e|wQATcG_`|>+o5QZ2N8EdqUjz)3Ji32M%oyDu54>G8H7kXgGyz4!zD_0LZYc6 zL<$Y2C_5Uwqi!P7Nh?BkhPsv=%_$*LWblEqv(Y>gB3+DToDeBCntMW|tI;eJA|*!i zQHYcpOjLF=nxjIbyQ8AkbQK~!j3%xSDKq%tw$?e}y;|vQ7^iggC7RVjq{3jlvaiuh z7b5)(zE}1)IIncYBRHsZM<$vPLu7!_+!!JQjb_OZ8KgOEx4>^&8EhD+-`_f?SGCrL z8n~JmW;C0I$Z(^1HAF@j&9LD|8oUD3$L2i~9&^Rl%^qkzw?(kU^DR!N7o|@~uSq}E zvascyEiYwMXKc&ZpK&zfbVelOQmcek&0CeXda2ckRu?kMGxuhm%8X>?Wz}Zw%6c*D zY}T*YDcP0THQ8IU4`iRr8Jn{yXK&8s+~&ExbEo94&wVBLRPN>0ZCg)hy{Yxtye)Zq z@{Z>tzaW1~IRE*A?gjG;HWs{6@N=6{ZFaQzrR~_Zx3xXjuBzSkcBk4mZ@;SjuJ-S? zKhyqlhk_1!IvnZnbzz`zSmDiu2MaHBoYwJpr>ahOb~@JSmn(0$a#vAa(TknCc7ChN zurBv?Ia|D}_}St}*X*uSN&+P-N_LfYFMX->Vz-KJJGwpI?LzmI?!CLO?jGLVBf3YI z9y@w`P=>O~vfX7T$}aY-=(()t&hpsuN##4skMz2t*RkG#-Zj0q_I|4Okv>IzHuia| z&(9SNEAlG(Rm`v0R`G1b$%>!*rt~f9JE(7M-<$jH?t8Sa*DuhodB0Kpw)H#PzkC0~ zmBGrkm0K$xsyuMjn5!0Eb@NpR2ILN?7_fc7f$)Iy1G@~Y9(cpR+Xn6#cydtmptgga z8gyoG_29a}N2&rI9sTs^OJh2X88D`H%(iQ)uGw?V znXz-nzBR7xxC!I-j5}E!F0Wo#y}ddzK6ZTS_+t~gPN<%6a^k3oJ150Y+B50gfUL=X?fEIO}lT}FVnkBubaMe`l0FPXOzx}%p5dx#mt>E zPt2M&>*(yt*?VddYSz}gSaWgCkU0nE#?GyqdwJf$d1vPRIzO_Ye8IH~o?jSPIACE- zc;U{4pD$XtXxpMAwehuuwbixT7UwOlUc7(tnQQZ|{d`I6l1)nv)fLu_soS1E8bjbBZFa6ZvA+HKh3j8je`-V3 zhHV=@*w}t!)y6FwpS`isjfFSNp>s*{u63Py?WJ+pL*PiT>ms66XPjZ5xV0M09eok>o zYUiR7{UVs1*QK0kscD_NmUQi0oL&Fa$n^0=C0gmla+du5+x-8tr8f)mdWPEM_naRy zw5(TXSXtT9U~FtcY;15`K>ryLpO9Ob+n@oNkdl}XY8(^aCLyU)EcG8a3^vJV7*$^| zIjb!3Zu`mm1<8#HlKaFb4@qh=ct>n7U2nTZR?L)OOj1%za9AKFx>1w(hWRNmu>t)B z)065ijBD1SapPbxIwm$47Zr?2OKKL%`rqH~%N_UjxNtzt6_}>!mIC)gR8o}MG&L=? z6^l|+f;l0TLMTONUYwVo*O4lLuASW(mvrfxUz8EuG$j;FqWg}%njH+~G)+nEL060_ zrL>fWfp!cQ23(=$7qYlZdGyIZbX;mMHYO@MI!XtX8W$aoiH=PTg+c+n6AXn?W246> zG)->QAQ%%01Y%;`R|BC2QLUo`8BtN8xB#O=aWNS&aiOUA_=Ln@gQVo9_eKS>5~B5A zD#2(5q7pNs^p}|!rLXD@MkiziqE`I=#e}59nAlJ-CN@66*kBv&GLY0P$XK^E+iqf7 zd_qQJ9Y9ip;J(cLkM#&g|6V*H4PSyt)MccuKUr5$)12b8oTl#L>8z`$MAwj_Y3JgO z72TRNX%bh_?WIyxR$QObWQLMU`)sXxU&{ z#+*ei~dI*S&5+3-`a`yrrd^1X-C>v*8{l~W}>f4w){iQfJc7Dw!#7&#=6SN_MB zh#H5;Ki1kQVpytn4uH#xfiUJNq4K6YXCHx!KAc>z~4<6t|7BHI~_y+!o=kYEN^I>e` z(|mwCxr1Bz1o!euK8jQPGxs0@QHa)c)k1C5K|JcBUPE14Q#3;gnqxXob83n8?O#!scwjG`3`GwqaYgXD8+`kNNDvt}J0c_Gc*v za1aNxj5l&9hjBD-;uwzQIF9E8-ppG#iBma^)3KQ|Ig7V)Hs^2=7c;bkw{a<#vzm8s z71wY*@8sRw%zJn*@8g5q#y{~P{snvZ2)FZb?&5Af#r-_Mm-z~h@F-vBn>@z1c${zZ zUB1Ute4l6dA03S%>kg#~W}fW@9;uup)$c{418B3QJLemT1Lv zJcR8y$CJ9p8ljQ_7P6B2*a!b$h*d0R4f}E=rtu^6Wd{C?57>yEISOsrn?)!@7Yybz zJk57_g8#q~&gFdO;x>NCuXu$A`2r8|MZUzp^Ld1Bz(jV(U-=mp;|qL`@9-@~U^s>$ z5ts2Qj$$enav}C(J{sZ-K4dSXq6HhG0Y6|zM&k$kh@bHv{11M?uLL6*g`e&l3}O*v1pbTPk&KU-!lo!j6Ewz+A-s+^cst6tlACaeS$vhRVE{MaApeOR_GCL| zvK%)v$Qir~n{gNK=V~0sF}%f3c^>_^fb(z*-eEH)<9&8R0^Y;D=#L2;f}1cN*Dw-e z@HAuj7{+l5!uSTSF%=KvIud^)F&d*V7Hzl;_u*wd{(&0C|6EfqJ{YR42Ny{V+)|(W zfLCtxpu?MFs7An>Y*?Y3V)U@Xn`-c;a+=|ArQ1*Vta7?x59JKQjmnuiu{TpYwNlP9 z+)}f-Zc^xTZOt)Up`2^DPC3u0O@UWw)Vjc%Z&09IVASHkTWHkwz*}T+QMuTtJ%YEy zphS5a5ONDOQt*}Az)!CR#bQJV;FwNdK`&s~V%U8O4l;bEmK8ByB_Z@s}bXqPqZqz@)yI|B)>Hctcu&C35cgd*x zg7<|{M+Wb*QI`hqOQX&W-W8*64&GNr9Ui=^MqMAguZ=oEc%D&r2+uVVzKTRWBfPLt z9|`XpquvtUHKTqL-oK1`PGwN00eQ(si!u!Ffr-k>UQJ)L%-$uQ!o}iz! z5Ou`x{$tc7!@F+OIm7!Oqi!1B&qf_Ky#E??-SB=f>crvwYTWCW_nT424)1rPE?!-@ z38}Y-PosVxeuU8*0Q^X!cLDfOMsEf1qmAAV;0KJ}6yV1gy)(d%HF|r1A2fQ8fM3f% z)#%q&{c8ceTfnbl^p*iX-spV;eqEzC5BT+r-a+8sVBD|b*EdiV`3Z*ZVj38_lO`JK ze11d2x~hLa$4t5U8HVkZnTDN|S%x`EcQ9d|vXx=J(j8aWMd^+!?5cFf6_)7v_uFYh z*iY$BBJ8isHY`)AQuZ{Qs_YeV3mtTtQpMru zvCY+;mB_>>?9B8;$=}J%ZTB{XTe8St5 zLkyQHT~P{`D_!vktCgRh96F(pA-L)jyAF z#nqMYb>&>cH|m zo27?bl|F13P`VpcSg3r&(Czc6;Xb81qG+54|1pDqD0djT5`Wy#UGq-EV&xNtHOgJ? z>C=LJweh6Ua1s7)gK5e=Mng#W?uHk*{aqo6hL!O58VxYvKW*@V(p{{$Srz|phMkqq z7!5?z;XkVlL0jc>hP{>Ui4qpoJe1NQ0x(jyCs?c4q zdsc<+g59$!bQkO{T=;y=zUE1jCTiB_`>c!EF3&M#81C*+}kXvXP2H_-wgGxQ~j;3Z1ZfuaFY+~3`+0?L| zGTAUw+03w9>3&_o&C2G6L1n7p4E5mQ7TOTa`XHQU+>28<-N1dGVR*mNokTP{gs}Tf zMe{@mw=|kDLb#QI9^PIj1j8*nBngK( *AVx6!N^!hMY9#}Mvo@Qt#c!E4I? zhN;R@qj@xh-CZM^QA2p3ro!CM9SvTtNzQsc zl+);1%} z>(APBY_qM+g|=C3E83oD8*bO6UAuOr?H0AW-o9b`oc3kyA8!9ic6@em_P!3O9kzA2 z+%dUhzmCT`hC40D3FNHGZIJtLuGcxc^U=JByu!S>d3*EA@{i|-3)U6J7WORMTzIMQ zr!K`^%DZgp@=llUyM~5#t?qiMD5Yp{(S1eV7pE2XEZ$svwD?N5k=>r^cBOm6?o)dt z_bBUepvSo$zw~U_v#RImUJZJc^cvS|L9Z>n4)r=;5>b*-Qc_Z0QdP3Gi=;6Yo$#}OGBlb zOAnNuDGd(@49FQUalo2^aRa*!JTUO&po~Ei2JISjYH;nreFm=?d}v7Qkgh{!4cR&5 zXjz~vyX-*OwHs&L_}tK~Lr)IFu!>r$@w(7&&71$kdUGMjjh^epKyI z!$)l#wPVzYQQqiXHwA9$dea+Y(#EVAb8u{6Y|hwiV?)QsUK`hM+~jeU{_t()85n)zg2Pkv(I^jH5I2XRerebynrA`L~X| zb;0cN+0V_sGN;{~C36nXIX^c(Gb9bEMD;_i#97avL+?B_xDyp_uJyUh0>QvQ*P|r|x=ty;3_0Z}g)!rS2cf7J{+^UVM z&aaMN-Fo2a?W-@X$zD^o=E&Npwf)v^UwdTjZ|mBuTeEJ*`ik{uH!R)o?VX+PtOWM| z*t&0r{f~`1J~kAor%la|<_n56)jb<1$S=t6K`rt+XGf*QN5!NtDMdRpNQo$7PWOns zmg!k76B`8DGA*kcbGtLQASX$?7v$%5?9?*rkAHy}{j1BQhJXCiS2=E_f%`Pg?a+C5 zbZj6hA|fU_6sT1%A+b?nLfu*cvQF)|xX74bbV_tECW;Zkn8?VKh^WlSS}_q(xqF$fX+`_|$vnneK6M`j$Kh*D1609FA>2k5KB$zO0Yv!Oqz2Xvry$Tby>T`Xe zR{~c>O;kkz{avns{vemGzxQB5fsU7#n^=&So|u%I7n7)sytYit)k@2P7@ejdHz_7P zF}I{cX2PC?V28ql%nl!ACIlZ1t_kYDPIOV9#AS8}COoHIv>r?j1}6rC$ywNc>btx$ XdM^Ze@=W9l=X*0L>V=3>)=l_dST!xZ diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index 2c819c26f12..0421cac571d 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -608,4 +608,5 @@ export const codiconsLibrary = { chatSparkle: register('chat-sparkle', 0xec4f), searchSparkle: register('search-sparkle', 0xec50), editSparkle: register('edit-sparkle', 0xec51), + copilotSnooze: register('copilot-snooze', 0xec52), } as const; From 655eda39fe3e1ceeeda7eaa0b5f99a9b4aacb51c Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 2 Jul 2025 11:01:58 -0700 Subject: [PATCH 054/306] fix some edits papercuts --- src/vs/workbench/contrib/chat/browser/chatListRenderer.ts | 4 ++-- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index a11fd5c56cf..e6e7d2f3ac7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -640,7 +640,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { const ev = new StandardKeyboardEvent(e); if (ev.equals(KeyCode.Space) || ev.equals(KeyCode.Enter)) { - if (this.viewModel?.editing?.id !== element.id && !this.viewModel?.requestInProgress) { + if (this.viewModel?.editing?.id !== element.id) { ev.preventDefault(); ev.stopPropagation(); this._onDidClickRequest.fire(templateData); @@ -1231,7 +1231,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer('chat.editRequests') === 'inline' && !this.disableEdits) { markdownPart.domNode.classList.add('clickable'); markdownPart.addDisposable(dom.addDisposableListener(markdownPart.domNode, dom.EventType.CLICK, (e: MouseEvent) => { - if (this.viewModel?.editing?.id !== element.id && !this.viewModel?.requestInProgress) { + if (this.viewModel?.editing?.id !== element.id) { const selection = dom.getWindow(templateData.rowContainer).getSelection(); if (selection && !selection.isCollapsed && selection.toString().length > 0) { return; diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index dd82570e524..82198e04104 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1089,6 +1089,12 @@ export class ChatWidget extends Disposable implements IChatWidget { ChatContextKeys.currentlyEditing.bindTo(editedRequest.contextKeyService).set(false); } + this.inputPart.setChatMode(this.inlineInputPart.currentModeKind); + const currentModelName = this.inlineInputPart.selectedLanguageModel?.metadata.name; + if (currentModelName) { + this.inputPart.switchModelByName(currentModelName); + } + const isInput = this.configurationService.getValue('chat.editRequests') === 'input'; if (!isInput) { From 00fa27eb04ccacc24fe3eaf36371d1b326c1760c Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 2 Jul 2025 17:00:03 -0400 Subject: [PATCH 055/306] use `TimeoutTimer` to properly cancel timeout for suggest widget discoverability update (#253497) fix #253460 --- .../suggest/browser/terminalSuggestAddon.ts | 1 + .../browser/terminalSuggestShownTracker.ts | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index ac0444c8f78..1f5365fb007 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -907,6 +907,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } hideSuggestWidget(cancelAnyRequest: boolean): void { + this._discoverability?.resetTimer(); if (cancelAnyRequest) { this._cancellationTokenSource?.cancel(); this._cancellationTokenSource = undefined; diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestShownTracker.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestShownTracker.ts index 6fb1932dd14..ab45b94fb48 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestShownTracker.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestShownTracker.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { TimeoutTimer } from '../../../../../base/common/async.js'; import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { TerminalShellType } from '../../../../../platform/terminal/common/terminal.js'; @@ -23,7 +24,7 @@ interface ITerminalSuggestShownTracker extends IDisposable { export class TerminalSuggestShownTracker extends Disposable implements ITerminalSuggestShownTracker { private _done: boolean; private _count: number; - private _timeout: Timeout | undefined; + private _timeout: TimeoutTimer | undefined; private _start: number | undefined; private _firstShownTracker: { shell: Set; window: boolean } | undefined = undefined; @@ -51,6 +52,14 @@ export class TerminalSuggestShownTracker extends Disposable implements ITerminal this._firstShownTracker = undefined; } + resetTimer(): void { + if (this._timeout) { + this._timeout.cancel(); + this._timeout = undefined; + } + this._start = undefined; + } + update(widgetElt: HTMLElement | undefined): void { if (this._done) { return; @@ -63,10 +72,11 @@ export class TerminalSuggestShownTracker extends Disposable implements ITerminal if (this._count >= TERMINAL_SUGGEST_DISCOVERABILITY_MAX_COUNT) { this._setDone(widgetElt); } else if (!this._start) { + this.resetTimer(); this._start = Date.now(); - this._timeout = setTimeout(() => { + this._timeout = this._register(new TimeoutTimer(() => { this._setDone(widgetElt); - }, TERMINAL_SUGGEST_DISCOVERABILITY_MIN_MS); + }, TERMINAL_SUGGEST_DISCOVERABILITY_MIN_MS)); } } @@ -77,7 +87,7 @@ export class TerminalSuggestShownTracker extends Disposable implements ITerminal widgetElt.classList.remove('increased-discoverability'); } if (this._timeout) { - clearTimeout(this._timeout); + this._timeout.cancel(); this._timeout = undefined; } this._start = undefined; From 0f58db74803c27f10770568b4bd8fec037bc2cdc Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 3 Jul 2025 07:49:43 +0200 Subject: [PATCH 056/306] add installing label action (#253712) Co-authored-by: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> --- .../contrib/mcp/browser/mcpServerActions.ts | 23 ++++++++++++++++++- .../contrib/mcp/browser/mcpServerEditor.ts | 3 ++- .../contrib/mcp/browser/mcpServersView.ts | 3 ++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts index 879b5f72215..928d25c8372 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts @@ -12,7 +12,7 @@ import { IContextMenuService } from '../../../../platform/contextview/browser/co import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { manageExtensionIcon } from '../../extensions/browser/extensionsIcons.js'; import { getDomNodePagePosition } from '../../../../base/browser/dom.js'; -import { IMcpSamplingService, IMcpServer, IMcpServerContainer, IMcpService, IMcpWorkbenchService, IWorkbenchMcpServer, McpCapability, McpConnectionState, McpServerEditorTab } from '../common/mcpTypes.js'; +import { IMcpSamplingService, IMcpServer, IMcpServerContainer, IMcpService, IMcpWorkbenchService, IWorkbenchMcpServer, McpCapability, McpConnectionState, McpServerEditorTab, McpServerInstallState } from '../common/mcpTypes.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { McpCommandIds } from '../common/mcpCommandIds.js'; @@ -116,6 +116,9 @@ export class InstallAction extends McpServerAction { if (!this.mcpServer?.gallery && !this.mcpServer?.installable) { return; } + if (this.mcpServer.installState !== McpServerInstallState.Uninstalled) { + return; + } this.class = InstallAction.CLASS; this.enabled = true; this.label = localize('install', "Install"); @@ -145,6 +148,20 @@ export class InstallAction extends McpServerAction { } } +export class InstallingLabelAction extends McpServerAction { + + private static readonly LABEL = localize('installing', "Installing"); + private static readonly CLASS = `${McpServerAction.LABEL_ACTION_CLASS} install installing`; + + constructor() { + super('extension.installing', InstallingLabelAction.LABEL, InstallingLabelAction.CLASS, false); + } + + update(): void { + this.class = `${InstallingLabelAction.CLASS}${this.mcpServer && this.mcpServer.installState === McpServerInstallState.Installing ? '' : ' hide'}`; + } +} + export class UninstallAction extends McpServerAction { static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent uninstall`; @@ -166,6 +183,10 @@ export class UninstallAction extends McpServerAction { if (!this.mcpServer.local) { return; } + if (this.mcpServer.installState !== McpServerInstallState.Installed) { + this.enabled = false; + return; + } this.class = UninstallAction.CLASS; this.enabled = true; this.label = localize('uninstall', "Uninstall"); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts index bd88574cc4b..be635988841 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts @@ -39,7 +39,7 @@ import { IExtensionService } from '../../../services/extensions/common/extension import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IMcpServerEditorOptions, IWorkbenchMcpServer, McpServerContainers } from '../common/mcpTypes.js'; import { InstallCountWidget, McpServerIconWidget, McpServerWidget, onClick, PublisherWidget, RatingsWidget } from './mcpServerWidgets.js'; -import { DropDownAction, InstallAction, ManageMcpServerAction, UninstallAction } from './mcpServerActions.js'; +import { DropDownAction, InstallAction, InstallingLabelAction, ManageMcpServerAction, UninstallAction } from './mcpServerActions.js'; import { McpServerEditorInput } from './mcpServerEditorInput.js'; import { ILocalMcpServer, IMcpServerManifest, IMcpServerPackage, PackageType } from '../../../../platform/mcp/common/mcpManagement.js'; import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; @@ -220,6 +220,7 @@ export class McpServerEditor extends EditorPane { const actions = [ this.instantiationService.createInstance(InstallAction, true), + this.instantiationService.createInstance(InstallingLabelAction), this.instantiationService.createInstance(UninstallAction), this.instantiationService.createInstance(ManageMcpServerAction, true), ]; diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts index 2e743bcfd5a..f54a3bd4327 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts @@ -25,7 +25,7 @@ import { getLocationBasedViewColors, ViewPane } from '../../../browser/parts/vie import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; import { IViewDescriptorService, IViewsRegistry, Extensions as ViewExtensions } from '../../../common/views.js'; import { HasInstalledMcpServersContext, IMcpWorkbenchService, InstalledMcpServersViewId, IWorkbenchMcpServer, McpServerContainers, mcpServerIcon, McpServerInstallState } from '../common/mcpTypes.js'; -import { DropDownAction, InstallAction, ManageMcpServerAction } from './mcpServerActions.js'; +import { DropDownAction, InstallAction, InstallingLabelAction, ManageMcpServerAction } from './mcpServerActions.js'; import { PublisherWidget, InstallCountWidget, RatingsWidget, McpServerIconWidget } from './mcpServerWidgets.js'; import { ActionRunner, IAction, Separator } from '../../../../base/common/actions.js'; import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; @@ -321,6 +321,7 @@ class McpServerRenderer implements IListRenderer Date: Thu, 3 Jul 2025 09:07:16 +0200 Subject: [PATCH 057/306] chat - add used provider to telemetry --- .../contrib/chat/browser/chatSetup.ts | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index dd98ad46471..2f0c121ef7d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -81,6 +81,7 @@ const defaultChat = { manageOveragesUrl: product.defaultChatAgent?.manageOverageUrl ?? '', upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '', signUpUrl: product.defaultChatAgent?.signUpUrl ?? '', + providerId: product.defaultChatAgent?.providerId ?? '', providerName: product.defaultChatAgent?.providerName ?? '', enterpriseProviderId: product.defaultChatAgent?.enterpriseProviderId ?? '', enterpriseProviderName: product.defaultChatAgent?.enterpriseProviderName ?? '', @@ -647,7 +648,7 @@ class ChatSetup { }); if (!trusted) { this.context.update({ later: true }); - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotTrusted', installDuration: 0, signUpErrorCode: undefined }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotTrusted', installDuration: 0, signUpErrorCode: undefined, provider: undefined }); return { dialogSkipped, success: undefined /* canceled */ }; } @@ -689,7 +690,7 @@ class ChatSetup { return this.doRun(options); // open dialog again case ChatSetupStrategy.Canceled: this.context.update({ later: true }); - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedMaybeLater', installDuration: 0, signUpErrorCode: undefined }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedMaybeLater', installDuration: 0, signUpErrorCode: undefined, provider: undefined }); break; } } catch (error) { @@ -1167,11 +1168,13 @@ type InstallChatClassification = { installResult: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the extension was installed successfully, cancelled or failed to install.' }; installDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The duration it took to install the extension.' }; signUpErrorCode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The error code in case of an error signing up.' }; + provider: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider used for the chat installation.' }; }; type InstallChatEvent = { installResult: 'installed' | 'alreadyInstalled' | 'cancelled' | 'failedInstall' | 'failedNotSignedIn' | 'failedSignUp' | 'failedNotTrusted' | 'failedNoSession' | 'failedMaybeLater'; installDuration: number; signUpErrorCode: number | undefined; + provider: string | undefined; }; enum ChatSetupStep { @@ -1222,7 +1225,7 @@ class ChatSetupController extends Disposable { this._onDidChange.fire(); } - async setup(options?: { forceSignIn?: boolean; useAlternateProvider?: boolean }): Promise { + async setup(options?: { forceSignIn?: boolean; useAlternateProvider?: boolean; useEnterpriseProvider?: boolean }): Promise { const watch = new StopWatch(false); const title = localize('setupChatProgress', "Getting Copilot ready..."); const badge = this.activityService.showViewContainerActivity(CHAT_SIDEBAR_PANEL_ID, { @@ -1240,7 +1243,7 @@ class ChatSetupController extends Disposable { } } - private async doSetup(options: { forceSignIn?: boolean; useAlternateProvider?: boolean }, watch: StopWatch): Promise { + private async doSetup(options: { forceSignIn?: boolean; useAlternateProvider?: boolean; useEnterpriseProvider?: boolean }, watch: StopWatch): Promise { this.context.suspend(); // reduces flicker let success: ChatSetupResultValue = false; @@ -1256,7 +1259,8 @@ class ChatSetupController extends Disposable { this.setStep(ChatSetupStep.SigningIn); const result = await this.signIn({ useAlternateProvider: options.useAlternateProvider }); if (!result.session) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', installDuration: watch.elapsed(), signUpErrorCode: undefined }); + const provider = options.useAlternateProvider ? defaultChat.alternativeProviderId : options.useEnterpriseProvider ? defaultChat.enterpriseProviderId : defaultChat.providerName; + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider }); return undefined; // treat as cancelled because signing in already triggers an error dialog } @@ -1266,7 +1270,7 @@ class ChatSetupController extends Disposable { // Await Install this.setStep(ChatSetupStep.Installing); - success = await this.install(session, entitlement ?? this.context.state.entitlement, providerId, watch, installation); + success = await this.install(session, entitlement ?? this.context.state.entitlement, providerId, watch, installation, options); } finally { this.setStep(ChatSetupStep.Initial); this.context.resume(); @@ -1300,10 +1304,12 @@ class ChatSetupController extends Disposable { return { session, entitlement: entitlements?.entitlement }; } - private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, providerId: string, watch: StopWatch, installation: Promise): Promise { + private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, providerId: string, watch: StopWatch, installation: Promise, options: { useAlternateProvider?: boolean; useEnterpriseProvider?: boolean }): Promise { const wasRunning = this.context.state.installed && !this.context.state.disabled; let signUpResult: boolean | { errorCode: number } | undefined = undefined; + const provider = options.useAlternateProvider ? defaultChat.alternativeProviderId : options.useEnterpriseProvider ? defaultChat.enterpriseProviderId : defaultChat.providerName; + try { if ( @@ -1319,7 +1325,7 @@ class ChatSetupController extends Disposable { } if (!session) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNoSession', installDuration: watch.elapsed(), signUpErrorCode: undefined }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNoSession', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider }); return false; // unexpected } } @@ -1327,19 +1333,19 @@ class ChatSetupController extends Disposable { signUpResult = await this.requests.signUpFree(session); if (typeof signUpResult !== 'boolean' /* error */) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedSignUp', installDuration: watch.elapsed(), signUpErrorCode: signUpResult.errorCode }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedSignUp', installDuration: watch.elapsed(), signUpErrorCode: signUpResult.errorCode, provider }); } } await this.doInstallWithRetry(installation); } catch (error) { this.logService.error(`[chat setup] install: error ${error}`); - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: isCancellationError(error) ? 'cancelled' : 'failedInstall', installDuration: watch.elapsed(), signUpErrorCode: undefined }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: isCancellationError(error) ? 'cancelled' : 'failedInstall', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider }); return false; } if (typeof signUpResult === 'boolean') { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: wasRunning && !signUpResult ? 'alreadyInstalled' : 'installed', installDuration: watch.elapsed(), signUpErrorCode: undefined }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: wasRunning && !signUpResult ? 'alreadyInstalled' : 'installed', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider }); } if (wasRunning && signUpResult === true) { From 908f0803baa5db645190d05f66cb8d8067962e8d Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 3 Jul 2025 11:59:42 +0200 Subject: [PATCH 058/306] If a mode has no tools specified, it should include all tools (#253843) --- .../contrib/chat/browser/chatSelectedTools.ts | 2 +- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 2 +- .../chat/browser/languageModelToolsService.ts | 12 +++++++++--- .../promptSyntax/promptToolsCodeLensProvider.ts | 2 +- .../contrib/chat/common/languageModelToolsService.ts | 2 +- .../test/common/mockLanguageModelToolsService.ts | 2 +- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts b/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts index ef3f732ed15..6bf867e7e38 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts @@ -124,7 +124,7 @@ export class ChatSelectedTools extends Disposable { let currentMap = this._sessionStates.get(currentMode.id); let defaultEnablement = false; if (!currentMap && currentMode.kind === ChatModeKind.Agent && currentMode.customTools) { - currentMap = this._toolsService.toToolAndToolSetEnablementMap(new Set(currentMode.customTools.read(r))); + currentMap = this._toolsService.toToolAndToolSetEnablementMap(currentMode.customTools.read(r)); } if (!currentMap) { currentMap = this._selectedTools.read(r); diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index dd82570e524..d6a95c8cb8d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1909,7 +1909,7 @@ export class ChatWidget extends Disposable implements IChatWidget { // if not tools to enable are present, we are done if (tools !== undefined && this.input.currentModeKind === ChatModeKind.Agent) { - const enablementMap = this.toolsService.toToolAndToolSetEnablementMap(new Set(tools)); + const enablementMap = this.toolsService.toToolAndToolSetEnablementMap(tools); this.input.selectedToolsModel.set(enablementMap, true); } diff --git a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index 965f71dd31d..a13152e5427 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -484,13 +484,19 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return result; } - toToolAndToolSetEnablementMap(toolOrToolSetNames: Set): Map { + /** + * Create a map that contains all tools and toolsets with their enablement state. + * @param toolOrToolSetNames A list of tool or toolset names to check for enablement. If undefined, all tools and toolsets are enabled. + * @returns A map of tool or toolset instances to their enablement state. + */ + toToolAndToolSetEnablementMap(enabledToolOrToolSetNames: readonly string[] | undefined): Map { + const toolOrToolSetNames = enabledToolOrToolSetNames ? new Set(enabledToolOrToolSetNames) : undefined; const result = new Map(); for (const tool of this._tools.values()) { - result.set(tool.data, tool.data.toolReferenceName !== undefined && toolOrToolSetNames.has(tool.data.toolReferenceName)); + result.set(tool.data, tool.data.toolReferenceName !== undefined && (toolOrToolSetNames === undefined || toolOrToolSetNames.has(tool.data.toolReferenceName))); } for (const toolSet of this._toolSets) { - result.set(toolSet, toolOrToolSetNames.has(toolSet.referenceName)); + result.set(toolSet, (toolOrToolSetNames === undefined || toolOrToolSetNames.has(toolSet.referenceName))); } return result; } diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts index 018237936be..3d904edb187 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts @@ -78,7 +78,7 @@ class PromptToolsCodeLensProvider extends Disposable implements CodeLensProvider private async updateTools(model: ITextModel, tools: PromptToolsMetadata) { - const selectedToolsNow = tools.value ? this.languageModelToolsService.toToolAndToolSetEnablementMap(new Set(tools.value)) : new Map(); + const selectedToolsNow = tools.value ? this.languageModelToolsService.toToolAndToolSetEnablementMap(tools.value) : new Map(); const newSelectedAfter = await this.instantiationService.invokeFunction(showToolsPicker, localize('placeholder', "Select tools"), undefined, selectedToolsNow); if (!newSelectedAfter) { return; diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index be257861899..f1df32784ce 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -275,7 +275,7 @@ export interface ILanguageModelToolsService { resetToolAutoConfirmation(): void; cancelToolCallsForRequest(requestId: string): void; toToolEnablementMap(toolOrToolSetNames: Set): Record; - toToolAndToolSetEnablementMap(toolOrToolSetNames: Set): IToolAndToolSetEnablementMap; + toToolAndToolSetEnablementMap(toolOrToolSetNames: readonly string[] | undefined): IToolAndToolSetEnablementMap; readonly toolSets: IObservable>; getToolSet(id: string): ToolSet | undefined; diff --git a/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts b/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts index 511ff781e42..69bdd684bfd 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts @@ -76,7 +76,7 @@ export class MockLanguageModelToolsService implements ILanguageModelToolsService throw new Error('Method not implemented.'); } - toToolAndToolSetEnablementMap(toolOrToolSetNames: Set): Map { + toToolAndToolSetEnablementMap(toolOrToolSetNames: readonly string[] | undefined): Map { throw new Error('Method not implemented.'); } } From 4950db1f74217e09ceecb05fa51e826b69627251 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 3 Jul 2025 11:59:45 +0200 Subject: [PATCH 059/306] layout - never show secondary sidebar by default if empty (fix #253855) (#253828) --- src/vs/workbench/browser/layout.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index ec7757a570e..352b7df5b1a 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -633,10 +633,13 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private initLayoutState(lifecycleService: ILifecycleService, fileService: IFileService, coreExperimentationService: ICoreExperimentationService): void { this._mainContainerDimension = getClientArea(this.parent, DEFAULT_WINDOW_DIMENSIONS); // running with fallback to ensure no error is thrown (https://github.com/microsoft/vscode/issues/240242) - this.stateModel = new LayoutStateModel(this.storageService, this.configurationService, this.contextService, coreExperimentationService, this.environmentService); + this.stateModel = new LayoutStateModel(this.storageService, this.configurationService, this.contextService, coreExperimentationService); this.stateModel.load({ mainContainerDimension: this._mainContainerDimension, - resetLayout: Boolean(this.layoutOptions?.resetLayout) + resetLayout: Boolean(this.layoutOptions?.resetLayout), + isAuxiliaryBarEmpty: this.viewDescriptorService + .getViewContainersByLocation(ViewContainerLocation.AuxiliaryBar) + .find(viewContainer => this.hasViews(viewContainer.id))?.id !== undefined }); this._register(this.stateModel.onDidChangeState(change => { @@ -2789,6 +2792,7 @@ enum LegacyWorkbenchLayoutSettings { interface ILayoutStateLoadConfiguration { readonly mainContainerDimension: IDimension; readonly resetLayout: boolean; + readonly isAuxiliaryBarEmpty: boolean; } class LayoutStateModel extends Disposable { @@ -2804,8 +2808,7 @@ class LayoutStateModel extends Disposable { private readonly storageService: IStorageService, private readonly configurationService: IConfigurationService, private readonly contextService: IWorkspaceContextService, - private readonly coreExperimentationService: ICoreExperimentationService, - private readonly environmentService: IBrowserWorkbenchEnvironmentService + private readonly coreExperimentationService: ICoreExperimentationService ) { super(); @@ -2868,9 +2871,8 @@ class LayoutStateModel extends Disposable { LayoutStateKeys.SIDEBAR_HIDDEN.defaultValue = workbenchState === WorkbenchState.EMPTY; LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, mainContainerDimension.width / 4); LayoutStateKeys.AUXILIARYBAR_HIDDEN.defaultValue = (() => { - const configuration = this.configurationService.inspect(WorkbenchLayoutSettings.AUXILIARYBAR_DEFAULT_VISIBILITY); - if (configuration.defaultValue !== 'hidden' && isWeb && !this.environmentService.remoteAuthority) { - return true; // TODO@bpasero revisit this when Chat is available in serverless web + if (configuration.isAuxiliaryBarEmpty) { + return true; // require a view in the auxiliary bar to show it by default } switch (this.configurationService.getValue(WorkbenchLayoutSettings.AUXILIARYBAR_DEFAULT_VISIBILITY)) { From 791e7ef3dbd801366087e896d79681dcc0b49a7a Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 3 Jul 2025 03:02:26 -0700 Subject: [PATCH 060/306] Fix cancelled tool calls missing from history (#253780) * Fix cancelled tool calls missing from history When they are cancelled by sending a followup message Fix microsoft/vscode-copilot#18495 * Add comment --- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index d6a95c8cb8d..e6620e58803 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -185,6 +185,7 @@ export class ChatWidget extends Disposable implements IChatWidget { private isRequestPaused: IContextKey; private canRequestBePaused: IContextKey; private agentInInput: IContextKey; + private currentRequest: Promise | undefined; private _visible = false; @@ -1626,6 +1627,11 @@ export class ChatWidget extends Disposable implements IChatWidget { } this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionId); + if (this.currentRequest) { + // We have to wait the current request to be properly cancelled so that it has a chance to update the model with its result metadata. + // This is awkward, it's basically a limitation of the chat provider-based agent. + await Promise.race([this.currentRequest, timeout(1000)]); + } this.input.validateAgentMode(); @@ -1654,7 +1660,7 @@ export class ChatWidget extends Disposable implements IChatWidget { if (result) { this.input.acceptInput(isUserQuery); this._onDidSubmitAgent.fire({ agent: result.agent, slashCommand: result.slashCommand }); - result.responseCompletePromise.then(() => { + this.currentRequest = result.responseCompletePromise.then(() => { const responses = this.viewModel?.getItems().filter(isResponseVM); const lastResponse = responses?.[responses.length - 1]; this.chatAccessibilityService.acceptResponse(lastResponse, requestId, options?.isVoiceInput); @@ -1665,6 +1671,8 @@ export class ChatWidget extends Disposable implements IChatWidget { this.input.setValue(question, false); } } + + this.currentRequest = undefined; }); if (this.viewModel?.editing) { From f075c4377992e7b8eba5eda8152aad39e03fa9d6 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 3 Jul 2025 03:03:33 -0700 Subject: [PATCH 061/306] Use custom mode properly for all requests (#253781) It was not used in the setup, retry, or error retry cases --- .../workbench/contrib/chat/browser/actions/chatTitleActions.ts | 3 +-- src/vs/workbench/contrib/chat/browser/chat.ts | 3 ++- .../browser/chatContentParts/chatConfirmationContentPart.ts | 2 +- .../chat/browser/chatContentParts/chatErrorConfirmationPart.ts | 3 +-- src/vs/workbench/contrib/chat/browser/chatSetup.ts | 2 +- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 3 +-- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index 6c0e276c612..23e25bdad55 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -262,9 +262,8 @@ export function registerChatTitleActions() { chatService.resendRequest(request!, { userSelectedModelId: languageModelId, - userSelectedTools: widget?.getUserSelectedTools(), attempt: (request?.attempt ?? -1) + 1, - mode: widget?.input.currentModeKind, + ...widget?.getModeRequestOptions(), }); } }); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 25e0792faeb..ccdd8ffcc65 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -17,6 +17,7 @@ import { IChatAgentCommand, IChatAgentData } from '../common/chatAgents.js'; import { IChatResponseModel } from '../common/chatModel.js'; import { IParsedChatRequest } from '../common/chatParserTypes.js'; import { CHAT_PROVIDER_ID } from '../common/chatParticipantContribTypes.js'; +import { IChatSendRequestOptions } from '../common/chatService.js'; import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel } from '../common/chatViewModel.js'; import { ChatAgentLocation, ChatModeKind } from '../common/constants.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; @@ -208,7 +209,7 @@ export interface IChatWidget { focusLastMessage(): void; focusInput(): void; hasInputFocus(): boolean; - getUserSelectedTools(): Record | undefined; + getModeRequestOptions(): Partial; getCodeBlockInfoForEditor(uri: URI): IChatCodeBlockInfo | undefined; getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[]; getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[]; diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts index 249d3343bcc..3a215c401c6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts @@ -56,7 +56,7 @@ export class ChatConfirmationContentPart extends Disposable implements IChatCont const widget = chatWidgetService.getWidgetBySessionId(element.sessionId); options.userSelectedModelId = widget?.input.currentLanguageModel; options.mode = widget?.input.currentModeKind; - options.userSelectedTools = widget?.getUserSelectedTools(); + Object.assign(options, widget?.getModeRequestOptions()); if (await this.chatService.sendRequest(element.sessionId, prompt, options)) { confirmation.isUsed = true; diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatErrorConfirmationPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatErrorConfirmationPart.ts index 654d2256b53..1d4f9d18389 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatErrorConfirmationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatErrorConfirmationPart.ts @@ -61,8 +61,7 @@ export class ChatErrorConfirmationContentPart extends Disposable implements ICha options.confirmation = buttonData.label; const widget = chatWidgetService.getWidgetBySessionId(element.sessionId); options.userSelectedModelId = widget?.input.currentLanguageModel; - options.userSelectedTools = widget?.getUserSelectedTools(); - options.mode = widget?.input.currentModeKind; + Object.assign(options, widget?.getModeRequestOptions()); if (await chatService.sendRequest(element.sessionId, prompt, options)) { this._onDidChangeHeight.fire(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index dd98ad46471..9cdd2f7ddf8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -327,9 +327,9 @@ class SetupAgent extends Disposable implements IChatAgentImplementation { } await chatService.resendRequest(requestModel, { + ...widget?.getModeRequestOptions(), mode, userSelectedModelId: languageModel, - userSelectedTools: widget?.getUserSelectedTools() }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index e6620e58803..8faac4fb00f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1646,14 +1646,13 @@ export class ChatWidget extends Disposable implements IChatWidget { } const result = await this.chatService.sendRequest(this.viewModel.sessionId, requestInputs.input, { - mode: this.input.currentModeKind, userSelectedModelId: this.input.currentLanguageModel, location: this.location, locationData: this._location.resolveData?.(), parserContext: { selectedAgent: this._lastSelectedAgent, mode: this.input.currentModeKind }, attachedContext: requestInputs.attachedContext.asArray(), noCommandDetection: options?.noCommandDetection, - userSelectedTools: this.getUserSelectedTools(), + ...this.getModeRequestOptions(), modeInstructions: this.input.currentModeObs.get().body?.get() }); From c66b907bd73e601e120fdba47066f73a2ea240be Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 3 Jul 2025 03:08:13 -0700 Subject: [PATCH 062/306] Make Agent mode the first entry (#253783) Fix #253620 --- src/vs/workbench/contrib/chat/common/chatModes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/chatModes.ts b/src/vs/workbench/contrib/chat/common/chatModes.ts index 6cabc697dcd..f805a65d42e 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -185,7 +185,7 @@ export class ChatModeService extends Disposable implements IChatModeService { ]; if (this.chatAgentService.hasToolsAgent) { - builtinModes.push(ChatMode.Agent); + builtinModes.unshift(ChatMode.Agent); } builtinModes.push(ChatMode.Edit); return builtinModes; From a30b6fb92dffdef2066f1b08bb4d55247c8d6605 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 3 Jul 2025 03:17:33 -0700 Subject: [PATCH 063/306] Don't patch [text] as an incomplete markdown link (#253786) Fix microsoft/vscode-copilot#19103 --- src/vs/base/browser/markdownRenderer.ts | 2 +- src/vs/base/test/browser/markdownRenderer.test.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 06aec51b9bb..04e4ea0ef9e 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -727,7 +727,7 @@ function completeSingleLinePattern(token: marked.Tokens.Text | marked.Tokens.Par } // Contains the start of link text, and no following tokens contain the link target - else if (lastLine.match(/(^|\s)\[\w*/)) { + else if (lastLine.match(/(^|\s)\[\w*[^\]]*$/)) { return completeLinkText(token); } } diff --git a/src/vs/base/test/browser/markdownRenderer.test.ts b/src/vs/base/test/browser/markdownRenderer.test.ts index cd2a7ceda50..cacc1bb404b 100644 --- a/src/vs/base/test/browser/markdownRenderer.test.ts +++ b/src/vs/base/test/browser/markdownRenderer.test.ts @@ -958,6 +958,14 @@ suite('MarkdownRenderer', () => { assert.deepStrictEqual(newTokens, tokens); }); + test('square braces in text', () => { + const incomplete = 'hello [what] is going on'; + const tokens = marked.marked.lexer(incomplete); + const newTokens = fillInIncompleteTokens(tokens); + + assert.deepStrictEqual(newTokens, tokens); + }); + test('complete link', () => { const incomplete = 'text [link](http://microsoft.com)'; const tokens = marked.marked.lexer(incomplete); From 42491d8408b999bf74a5d8fd788cd9451d1e9968 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Date: Thu, 3 Jul 2025 12:22:52 +0200 Subject: [PATCH 064/306] Remove bad contribution for now (#253810) Fixes https://github.com/microsoft/vscode/issues/253690 --- .../browser/authentication.contribution.ts | 101 +++++++++--------- 1 file changed, 50 insertions(+), 51 deletions(-) diff --git a/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts b/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts index b150e3daeaa..693975bc45a 100644 --- a/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts +++ b/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts @@ -7,7 +7,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { localize } from '../../../../nls.js'; import { registerAction2 } from '../../../../platform/actions/common/actions.js'; import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; -import { IExtensionManifest, IExtensionDescription } from '../../../../platform/extensions/common/extensions.js'; +import { IExtensionManifest } from '../../../../platform/extensions/common/extensions.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; @@ -20,7 +20,6 @@ import { IAuthenticationUsageService } from '../../../services/authentication/br import { ManageAccountPreferencesForMcpServerAction } from './actions/manageAccountPreferencesForMcpServerAction.js'; import { ManageTrustedMcpServersForAccountAction } from './actions/manageTrustedMcpServersForAccountAction.js'; import { RemoveDynamicAuthenticationProvidersAction } from './actions/manageDynamicAuthenticationProvidersAction.js'; -import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IAuthenticationQueryService } from '../../../services/authentication/common/authenticationQuery.js'; import { IMcpRegistry } from '../../mcp/common/mcpRegistryTypes.js'; import { autorun } from '../../../../base/common/observable.js'; @@ -116,61 +115,61 @@ class AuthenticationUsageContribution implements IWorkbenchContribution { } } -class AuthenticationExtensionsContribution extends Disposable implements IWorkbenchContribution { - static ID = 'workbench.contrib.authenticationExtensions'; +// class AuthenticationExtensionsContribution extends Disposable implements IWorkbenchContribution { +// static ID = 'workbench.contrib.authenticationExtensions'; - constructor( - @IExtensionService private readonly _extensionService: IExtensionService, - @IAuthenticationQueryService private readonly _authenticationQueryService: IAuthenticationQueryService, - @IAuthenticationService private readonly _authenticationService: IAuthenticationService - ) { - super(); - void this.run(); - this._register(this._extensionService.onDidChangeExtensions(this._onDidChangeExtensions, this)); - this._register( - Event.any( - this._authenticationService.onDidChangeDeclaredProviders, - this._authenticationService.onDidRegisterAuthenticationProvider - )(() => this._cleanupRemovedExtensions()) - ); - } +// constructor( +// @IExtensionService private readonly _extensionService: IExtensionService, +// @IAuthenticationQueryService private readonly _authenticationQueryService: IAuthenticationQueryService, +// @IAuthenticationService private readonly _authenticationService: IAuthenticationService +// ) { +// super(); +// void this.run(); +// this._register(this._extensionService.onDidChangeExtensions(this._onDidChangeExtensions, this)); +// this._register( +// Event.any( +// this._authenticationService.onDidChangeDeclaredProviders, +// this._authenticationService.onDidRegisterAuthenticationProvider +// )(() => this._cleanupRemovedExtensions()) +// ); +// } - async run(): Promise { - await this._extensionService.whenInstalledExtensionsRegistered(); - this._cleanupRemovedExtensions(); - } +// async run(): Promise { +// await this._extensionService.whenInstalledExtensionsRegistered(); +// this._cleanupRemovedExtensions(); +// } - private _onDidChangeExtensions(delta: { readonly added: readonly IExtensionDescription[]; readonly removed: readonly IExtensionDescription[] }): void { - if (delta.removed.length > 0) { - this._cleanupRemovedExtensions(delta.removed); - } - } +// private _onDidChangeExtensions(delta: { readonly added: readonly IExtensionDescription[]; readonly removed: readonly IExtensionDescription[] }): void { +// if (delta.removed.length > 0) { +// this._cleanupRemovedExtensions(delta.removed); +// } +// } - private _cleanupRemovedExtensions(removedExtensions?: readonly IExtensionDescription[]): void { - const extensionIdsToRemove = removedExtensions - ? new Set(removedExtensions.map(e => e.identifier.value)) - : new Set(this._extensionService.extensions.map(e => e.identifier.value)); +// private _cleanupRemovedExtensions(removedExtensions?: readonly IExtensionDescription[]): void { +// const extensionIdsToRemove = removedExtensions +// ? new Set(removedExtensions.map(e => e.identifier.value)) +// : new Set(this._extensionService.extensions.map(e => e.identifier.value)); - // If we are cleaning up specific removed extensions, we only remove those. - const isTargetedCleanup = !!removedExtensions; +// // If we are cleaning up specific removed extensions, we only remove those. +// const isTargetedCleanup = !!removedExtensions; - const providerIds = this._authenticationQueryService.getProviderIds(); - for (const providerId of providerIds) { - this._authenticationQueryService.provider(providerId).forEachAccount(account => { - account.extensions().forEach(extension => { - const shouldRemove = isTargetedCleanup - ? extensionIdsToRemove.has(extension.extensionId) - : !extensionIdsToRemove.has(extension.extensionId); +// const providerIds = this._authenticationQueryService.getProviderIds(); +// for (const providerId of providerIds) { +// this._authenticationQueryService.provider(providerId).forEachAccount(account => { +// account.extensions().forEach(extension => { +// const shouldRemove = isTargetedCleanup +// ? extensionIdsToRemove.has(extension.extensionId) +// : !extensionIdsToRemove.has(extension.extensionId); - if (shouldRemove) { - extension.removeUsage(); - extension.setAccessAllowed(false); - } - }); - }); - } - } -} +// if (shouldRemove) { +// extension.removeUsage(); +// extension.setAccessAllowed(false); +// } +// }); +// }); +// } +// } +// } class AuthenticationMcpContribution extends Disposable implements IWorkbenchContribution { static ID = 'workbench.contrib.authenticationMcp'; @@ -216,5 +215,5 @@ class AuthenticationMcpContribution extends Disposable implements IWorkbenchCont registerWorkbenchContribution2(AuthenticationContribution.ID, AuthenticationContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(AuthenticationUsageContribution.ID, AuthenticationUsageContribution, WorkbenchPhase.Eventually); -registerWorkbenchContribution2(AuthenticationExtensionsContribution.ID, AuthenticationExtensionsContribution, WorkbenchPhase.Eventually); +// registerWorkbenchContribution2(AuthenticationExtensionsContribution.ID, AuthenticationExtensionsContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(AuthenticationMcpContribution.ID, AuthenticationMcpContribution, WorkbenchPhase.Eventually); From 9f88ccaac49476e00dbcbc6107543cfb2b4d504c Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 3 Jul 2025 10:24:20 +0000 Subject: [PATCH 065/306] Engineering - disable pipeline (#253650) --- build/azure-pipelines/product-build-macos.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/build/azure-pipelines/product-build-macos.yml b/build/azure-pipelines/product-build-macos.yml index 7be9e7cfa00..cc8985c07ca 100644 --- a/build/azure-pipelines/product-build-macos.yml +++ b/build/azure-pipelines/product-build-macos.yml @@ -1,9 +1,6 @@ pr: none -trigger: - batch: true - branches: - include: ["main"] +trigger: none parameters: - name: VSCODE_QUALITY From 648bffe15e63c50c1e96617515e8a0c975298872 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Thu, 3 Jul 2025 03:30:06 -0700 Subject: [PATCH 066/306] clear chat on remote agent success (#253767) --- .../contrib/chat/browser/actions/chatExecuteActions.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 726c4d43304..73b6a898546 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -643,6 +643,12 @@ export class CreateRemoteAgentJobAction extends Action2 { chatModel.acceptResponseProgress(addedRequest, { content, kind: 'markdownContent' }); chatModel.setResponse(addedRequest, {}); chatModel.completeResponse(addedRequest); + + // Clear chat (start a new chat) + if (resultMarkdown) { + widget.clear(); + } + } finally { remoteJobCreatingKey.set(false); } From 7ce94dd09ab8cc96884d18abdbdd9825ff38e7a7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 2 Jul 2025 14:43:12 +0200 Subject: [PATCH 067/306] Code chat - add option to maximize the chat panel (fix #253306) --- .../terminal-suggest/src/completions/code.ts | 4 ++++ src/vs/platform/environment/common/argv.ts | 1 + src/vs/platform/environment/node/argv.ts | 3 ++- .../chat/electron-browser/chat.contribution.ts | 18 +++++++++++++++++- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/extensions/terminal-suggest/src/completions/code.ts b/extensions/terminal-suggest/src/completions/code.ts index 58e8726b0e8..2c85b83f1c1 100644 --- a/extensions/terminal-suggest/src/completions/code.ts +++ b/extensions/terminal-suggest/src/completions/code.ts @@ -824,6 +824,10 @@ export const codeTunnelSubcommands: Fig.Subcommand[] = [ template: 'filepaths', }, }, + { + name: ['--maximize'], + description: 'Maximize the chat session view.', + }, { name: ['-h', '--help'], description: 'Print usage', diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 6c74026f3d5..8f83c924b85 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -28,6 +28,7 @@ export interface NativeParsedArgs { _: string[]; 'add-file'?: string[]; mode?: string; + maximize?: boolean; help?: boolean; }; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 417624ed476..3053c420925 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -55,7 +55,8 @@ export const OPTIONS: OptionDescriptions> = { '_': { type: 'string[]', description: localize('prompt', "The prompt to use as chat.") }, 'mode': { type: 'string', cat: 'o', alias: 'm', args: 'mode', description: localize('chatMode', "The mode to use for the chat session. Available options: 'ask', 'edit', 'agent', or the identifier of a custom mode. Defaults to 'agent'.") }, 'add-file': { type: 'string[]', cat: 'o', alias: 'a', args: 'path', description: localize('addFile', "Add files as context to the chat session.") }, - 'help': { type: 'boolean', cat: 'o', alias: 'h', description: localize('help', "Print usage.") } + 'maximize': { type: 'boolean', cat: 'o', description: localize('chatMaximize', "Maximize the chat session view.") }, + 'help': { type: 'boolean', alias: 'h', description: localize('help', "Print usage.") } } }, 'serve-web': { diff --git a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts index 05f9f8c87ca..e9434bfd546 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts @@ -23,6 +23,10 @@ import { resolve } from '../../../../base/common/path.js'; import { showChatView } from '../browser/chat.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { ChatContextKeys } from '../common/chatContextKeys.js'; +import { ViewContainerLocation } from '../../../common/views.js'; class NativeBuiltinToolsContribution extends Disposable implements IWorkbenchContribution { @@ -49,7 +53,9 @@ class ChatCommandLineHandler extends Disposable { @ICommandService private readonly commandService: ICommandService, @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, @IViewsService private readonly viewsService: IViewsService, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, + @IContextKeyService private readonly contextKeyService: IContextKeyService ) { super(); @@ -84,6 +90,16 @@ class ChatCommandLineHandler extends Disposable { }; const chatWidget = await showChatView(this.viewsService); + + if (args.maximize) { + const location = this.contextKeyService.getContextKeyValue(ChatContextKeys.panelLocation.key); + if (location === ViewContainerLocation.AuxiliaryBar) { + this.layoutService.setAuxiliaryBarMaximized(true); + } else if (location === ViewContainerLocation.Panel && !this.layoutService.isPanelMaximized()) { + this.layoutService.toggleMaximizedPanel(); + } + } + await chatWidget?.waitForReady(); await this.commandService.executeCommand(ACTION_ID_NEW_CHAT); await this.commandService.executeCommand(CHAT_OPEN_ACTION_ID, opts); From c59715207b04d1ca3d164fc81bd1b60c5747f7d4 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 2 Jul 2025 14:53:37 +0200 Subject: [PATCH 068/306] Provide option to open `code chat` in empty workspace or existing workspace (fix #253383) --- .../terminal-suggest/src/completions/code.ts | 8 ++++++++ src/vs/code/electron-main/main.ts | 15 ++++++++++++--- src/vs/platform/environment/common/argv.ts | 2 ++ src/vs/platform/environment/node/argv.ts | 2 ++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/extensions/terminal-suggest/src/completions/code.ts b/extensions/terminal-suggest/src/completions/code.ts index 2c85b83f1c1..99e1371b181 100644 --- a/extensions/terminal-suggest/src/completions/code.ts +++ b/extensions/terminal-suggest/src/completions/code.ts @@ -828,6 +828,14 @@ export const codeTunnelSubcommands: Fig.Subcommand[] = [ name: ['--maximize'], description: 'Maximize the chat session view.', }, + { + name: ['-r', '--reuse-window'], + description: 'Force to use the last active window for the chat session', + }, + { + name: ['-n', '--new-window'], + description: 'Force to open an empty window for the chat session', + }, { name: ['-h', '--help'], description: 'Print usage', diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index dc6983c8b0d..90721608230 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -507,9 +507,18 @@ class CodeMain { } if (args.chat) { - // If we are started with chat subcommand, the current working - // directory is always the path to open - args._ = [cwd()]; + if (args.chat['new-window']) { + // Apply `--new-window` flag to the main arguments + args['new-window'] = true; + } else if (args.chat['reuse-window']) { + // Apply `--reuse-window` flag to the main arguments + args['reuse-window'] = true; + } else { + // Unless we are started with specific instructions about + // new windows or reusing existing ones, always take the + // current working directory as workspace to open. + args._ = [cwd()]; + } } return args; diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 8f83c924b85..185ce5ad1b1 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -29,6 +29,8 @@ export interface NativeParsedArgs { 'add-file'?: string[]; mode?: string; maximize?: boolean; + 'reuse-window'?: boolean; + 'new-window'?: boolean; help?: boolean; }; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 3053c420925..b5958ee0ec3 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -56,6 +56,8 @@ export const OPTIONS: OptionDescriptions> = { 'mode': { type: 'string', cat: 'o', alias: 'm', args: 'mode', description: localize('chatMode', "The mode to use for the chat session. Available options: 'ask', 'edit', 'agent', or the identifier of a custom mode. Defaults to 'agent'.") }, 'add-file': { type: 'string[]', cat: 'o', alias: 'a', args: 'path', description: localize('addFile', "Add files as context to the chat session.") }, 'maximize': { type: 'boolean', cat: 'o', description: localize('chatMaximize', "Maximize the chat session view.") }, + 'reuse-window': { type: 'boolean', cat: 'o', alias: 'r', description: localize('reuseWindowForChat', "Force to use the last active window for the chat session.") }, + 'new-window': { type: 'boolean', cat: 'o', alias: 'n', description: localize('newWindowForChat', "Force to open an empty window for the chat session.") }, 'help': { type: 'boolean', alias: 'h', description: localize('help', "Print usage.") } } }, From 7f22d076265b0101b009bc7d3017a1f87c6e0656 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 2 Jul 2025 17:59:21 +0200 Subject: [PATCH 069/306] fix tests --- extensions/terminal-suggest/src/test/completions/code.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/terminal-suggest/src/test/completions/code.test.ts b/extensions/terminal-suggest/src/test/completions/code.test.ts index 323024e06f4..9022c0c5a3e 100644 --- a/extensions/terminal-suggest/src/test/completions/code.test.ts +++ b/extensions/terminal-suggest/src/test/completions/code.test.ts @@ -72,7 +72,7 @@ export function createCodeTestSpecs(executable: string): ITestSpec[] { const categoryOptions = ['azure', 'data science', 'debuggers', 'extension packs', 'education', 'formatters', 'keymaps', 'language packs', 'linters', 'machine learning', 'notebooks', 'programming languages', 'scm providers', 'snippets', 'testing', 'themes', 'visualization', 'other']; const logOptions = ['critical', 'error', 'warn', 'info', 'debug', 'trace', 'off']; const syncOptions = ['on', 'off']; - const chatOptions = ['--add-file ', '--help', '--mode ', '-a ', '-h', '-m ']; + const chatOptions = ['--add-file ', '--help', '--maximize', '--mode ', '--new-window', '--reuse-window', '-a ', '-h', '-m ', '-n', '-r']; const typingTests: ITestSpec[] = []; for (let i = 1; i < executable.length; i++) { @@ -281,7 +281,7 @@ export function createCodeTunnelTestSpecs(executable: string): ITestSpec[] { { input: `${executable} tunnel unregister |`, expectedCompletions: [...commonFlags] }, { input: `${executable} tunnel service |`, expectedCompletions: [...commonFlags, 'help', 'install', 'log', 'uninstall'] }, { input: `${executable} tunnel help |`, expectedCompletions: helpSubcommands }, - { input: `${executable} chat |`, expectedCompletions: ['--mode ', '--add-file ', '--help', '-m ', '-a ', '-h'] }, + { input: `${executable} chat |`, expectedCompletions: ['--mode ', '--add-file ', '--help', '--maximize', '--new-window', '--reuse-window', '-m ', '-a ', '-h', '-n', '-r'] }, { input: `${executable} chat --mode |`, expectedCompletions: ['agent', 'ask', 'edit'] }, { input: `${executable} chat --add-file |`, expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } }, { input: `${executable} serve-web |`, expectedCompletions: serveWebSubcommandsAndFlags }, From c10a833054b2f4cf11cc25e1468cf31b98045bec Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 3 Jul 2025 12:37:27 +0200 Subject: [PATCH 070/306] Agent confirmation badge doesn't clear if window is closed (fix #253418) (#253669) --- .../windows/electron-main/windowImpl.ts | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 31d0dc59843..3030b1c3198 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -8,7 +8,7 @@ import { DeferredPromise, RunOnceScheduler, timeout, Delayer } from '../../../ba import { CancellationToken } from '../../../base/common/cancellation.js'; import { toErrorMessage } from '../../../base/common/errorMessage.js'; import { Emitter, Event } from '../../../base/common/event.js'; -import { Disposable } from '../../../base/common/lifecycle.js'; +import { Disposable, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js'; import { FileAccess, Schemas } from '../../../base/common/network.js'; import { getMarks, mark } from '../../../base/common/performance.js'; import { isBigSurOrNewer, isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js'; @@ -84,6 +84,29 @@ const enum ReadyState { READY } +class DockBadgeManager { + + static readonly INSTANCE = new DockBadgeManager(); + + private readonly windows = new Set(); + + acquireBadge(window: IBaseWindow): IDisposable { + this.windows.add(window.id); + + electron.app.setBadgeCount(isLinux ? 1 /* only numbers supported */ : undefined /* generic dot */); + + return { + dispose: () => { + this.windows.delete(window.id); + + if (this.windows.size === 0) { + electron.app.setBadgeCount(0); + } + } + }; + } +} + export abstract class BaseWindow extends Disposable implements IBaseWindow { //#region Events @@ -325,18 +348,18 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { case FocusMode.Notify: if (isMacintosh) { - this.setFocusNotificationBadge(undefined /* generic dot */); + this.showFocusNotificationBadge(); // On macOS we have direct API to bounce the dock icon electron.app.dock?.bounce('informational'); } else if (isWindows) { - this.setFocusNotificationBadge(undefined /* generic dot */); + this.showFocusNotificationBadge(); // On Windows, calling focus() will bounce the taskbar icon // https://github.com/electron/electron/issues/2867 this.win?.focus(); } else if (isLinux) { - this.setFocusNotificationBadge(1 /* only number supported */); + this.showFocusNotificationBadge(); // On Linux, there seems to be no way to bounce the taskbar icon // as calling focus() will actually steal focus away. @@ -352,18 +375,16 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { } } - private hasFocusNotificationBadge = false; + private readonly focusNotificationBadgeDisposable = this._register(new MutableDisposable()); - private setFocusNotificationBadge(count?: number): void { - electron.app.setBadgeCount(count); - this.hasFocusNotificationBadge = true; + private showFocusNotificationBadge(): void { + if (!this.focusNotificationBadgeDisposable.value) { + this.focusNotificationBadgeDisposable.value = DockBadgeManager.INSTANCE.acquireBadge(this); + } } private clearFocusNotificationBadge(): void { - if (this.hasFocusNotificationBadge) { - electron.app.setBadgeCount(0); - this.hasFocusNotificationBadge = false; - } + this.focusNotificationBadgeDisposable.clear(); } private doFocusWindow() { From fe90d48cd91384d13373a992e6a79ec106298804 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Thu, 3 Jul 2025 12:47:24 +0200 Subject: [PATCH 071/306] Using view position instead of model position in editor getLineHeightForPosition (#253670) using view position instead of model position in editor getLineHeightForPosition --- src/vs/editor/browser/editorBrowser.ts | 2 +- .../editor/browser/widget/codeEditor/codeEditorWidget.ts | 7 +++++-- src/vs/monaco.d.ts | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 3de37886d17..c9284cd6662 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -1098,7 +1098,7 @@ export interface ICodeEditor extends editorCommon.IEditor { getTopForPosition(lineNumber: number, column: number): number; /** - * Get the line height for the line number. + * Get the line height for a model position. */ getLineHeightForPosition(position: IPosition): number; diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index efcbb6a7e50..34333bc3f85 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -607,8 +607,11 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return -1; } const viewModel = this._modelData.viewModel; - if (viewModel.coordinatesConverter.modelPositionIsVisible(Position.lift(position))) { - return viewModel.viewLayout.getLineHeightForLineNumber(position.lineNumber); + const coordinatesConverter = viewModel.coordinatesConverter; + const pos = Position.lift(position); + if (coordinatesConverter.modelPositionIsVisible(pos)) { + const viewPosition = coordinatesConverter.convertModelPositionToViewPosition(pos); + return viewModel.viewLayout.getLineHeightForLineNumber(viewPosition.lineNumber); } return 0; } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index dcf8e1e3359..80dec01eee6 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -6217,7 +6217,7 @@ declare namespace monaco.editor { */ getTopForPosition(lineNumber: number, column: number): number; /** - * Get the line height for the line number. + * Get the line height for a model position. */ getLineHeightForPosition(position: IPosition): number; /** From a1034cb0afb761adbf409264dc8e0b3a4d817b33 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 3 Jul 2025 12:49:17 +0200 Subject: [PATCH 072/306] Make it easier to understand that `workbench.secondarySideBar.defaultVisibility` shows chat (in most cases) (fix #253706) (#253820) --- .../chat/browser/actions/chatActions.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index cd4abdc66be..b532a026787 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -1113,3 +1113,30 @@ MenuRegistry.appendMenuItem(MenuId.TerminalInstanceContext, { title, when: menuContext }); + +// --- Chat Default Visibility + +registerAction2(class ToggleDefaultVisibilityAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.toggleDefaultVisibility', + title: localize2('chat.toggleDefaultVisibility.label', "Show View by Default"), + precondition: ChatContextKeys.panelLocation.isEqualTo(ViewContainerLocation.AuxiliaryBar), + toggled: ContextKeyExpr.equals('config.workbench.secondarySideBar.defaultVisibility', 'hidden').negate(), + f1: false, + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', ChatViewId), + order: 0, + group: '5_configure' + }, + }); + } + + async run(accessor: ServicesAccessor) { + const configurationService = accessor.get(IConfigurationService); + + const currentValue = configurationService.getValue<'hidden' | 'visibleInWorkspace' | 'visible'>('workbench.secondarySideBar.defaultVisibility'); + configurationService.updateValue('workbench.secondarySideBar.defaultVisibility', currentValue !== 'hidden' ? 'hidden' : 'visible'); + } +}); From 6307b2e05830e59099f10a3ab650a9ef56e9b9e9 Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Thu, 3 Jul 2025 04:23:25 -0700 Subject: [PATCH 073/306] Fix MCP configure menu (#253794) --- .../contrib/chat/browser/actions/chatActions.ts | 11 ----------- src/vs/workbench/contrib/mcp/browser/mcpCommands.ts | 9 ++++++++- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index b532a026787..864bdf1f9c5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -47,7 +47,6 @@ import { IHostService } from '../../../../services/host/browser/host.js'; import { IWorkbenchLayoutService, Parts } from '../../../../services/layout/browser/layoutService.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { EXTENSIONS_CATEGORY, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js'; -import { McpCommandIds } from '../../../mcp/common/mcpCommandIds.js'; import { IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { IChatEditingSession, ModifiedFileEntryState } from '../../common/chatEditingService.js'; @@ -879,16 +878,6 @@ Update \`.github/copilot-instructions.md\` for the user, then ask for feedback o icon: Codicon.settings, order: 6 }); - - MenuRegistry.appendMenuItem(CHAT_CONFIG_MENU_ID, { - command: { - id: McpCommandIds.ShowInstalled, - title: localize2('mcp.servers', "MCP Servers") - }, - when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)), - order: 14, - group: '0_level' - }); } export function stringifyItem(item: IChatRequestViewModel | IChatResponseViewModel, includeName = true): string { diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index a6ad8e425ea..8d54d9fa28b 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -33,7 +33,7 @@ import { IAccountQuery, IAuthenticationQueryService } from '../../../services/au import { IAuthenticationService } from '../../../services/authentication/common/authentication.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; -import { IChatWidgetService } from '../../chat/browser/chat.js'; +import { ChatViewId, IChatWidgetService } from '../../chat/browser/chat.js'; import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; import { ChatModeKind } from '../../chat/common/constants.js'; import { ILanguageModelsService } from '../../chat/common/languageModels.js'; @@ -54,6 +54,7 @@ import { IFileService } from '../../../../platform/files/common/files.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import { CHAT_CONFIG_MENU_ID } from '../../chat/browser/actions/chatActions.js'; // acroynms do not get localized const category: ILocalizedString = { @@ -729,6 +730,12 @@ export class ShowInstalledMcpServersCommand extends Action2 { category, precondition: HasInstalledMcpServersContext, f1: true, + menu: { + id: CHAT_CONFIG_MENU_ID, + when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)), + order: 14, + group: '0_level' + } }); } From b8612ce6ae3ee465c4955461923595d203e1c58d Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 2 Jul 2025 21:18:19 +0200 Subject: [PATCH 074/306] Moves ARC telemetry tracking to core --- .../base/common/observableInternal/index.ts | 2 +- .../browser/services/editorWorkerService.ts | 13 + src/vs/editor/common/core/edits/edit.ts | 20 +- src/vs/editor/common/core/edits/stringEdit.ts | 374 +++++++++++---- .../common/core/text/positionToOffset.ts | 5 + src/vs/editor/common/diff/rangeMapping.ts | 11 + .../editor/common/services/editorWebWorker.ts | 21 + src/vs/editor/common/services/editorWorker.ts | 3 + .../services/testEditorWorkerService.ts | 5 + src/vs/platform/telemetry/common/telemetry.ts | 4 + .../editTelemetry/browser/arcTracker.ts | 62 +++ .../browser/documentWithAnnotatedEdits.ts | 424 ++++++++++++++++ .../browser/editSourceTrackingFeature.ts | 241 ++++++++++ .../browser/editSourceTrackingImpl.ts | 451 ++++++++++++++++++ .../browser/editTelemetry.contribution.ts | 41 ++ .../browser/editTelemetryService.ts | 39 ++ .../editTelemetry/browser/editTracker.ts | 96 ++++ .../browser/observableWorkspace.ts | 94 ++++ .../contrib/editTelemetry/browser/settings.ts | 8 + .../browser/vscodeObservableWorkspace.ts | 91 ++++ src/vs/workbench/workbench.common.main.ts | 2 + 21 files changed, 1922 insertions(+), 85 deletions(-) create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/arcTracker.ts create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/documentWithAnnotatedEdits.ts create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingFeature.ts create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/editTelemetryService.ts create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/editTracker.ts create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/observableWorkspace.ts create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/settings.ts create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/vscodeObservableWorkspace.ts diff --git a/src/vs/base/common/observableInternal/index.ts b/src/vs/base/common/observableInternal/index.ts index df8df29a80c..07967b03f25 100644 --- a/src/vs/base/common/observableInternal/index.ts +++ b/src/vs/base/common/observableInternal/index.ts @@ -27,7 +27,7 @@ export { observableFromEventOpts } from './observables/observableFromEvent.js'; export { observableSignalFromEvent } from './observables/observableSignalFromEvent.js'; export { asyncTransaction, globalTransaction, subtransaction, transaction, TransactionImpl } from './transaction.js'; export { observableFromValueWithChangeEvent, ValueWithChangeEventFromObservable } from './utils/valueWithChangeEvent.js'; -export { runOnChange, runOnChangeWithCancellationToken, runOnChangeWithStore } from './utils/runOnChange.js'; +export { runOnChange, runOnChangeWithCancellationToken, runOnChangeWithStore, RemoveUndefined } from './utils/runOnChange.js'; export { derivedConstOnceDefined, latestChangedValue } from './experimental/utils.js'; export { observableFromEvent } from './observables/observableFromEvent.js'; export { observableValue } from './observables/observableValue.js'; diff --git a/src/vs/editor/browser/services/editorWorkerService.ts b/src/vs/editor/browser/services/editorWorkerService.ts index 77cc168f558..7c0d74d79c6 100644 --- a/src/vs/editor/browser/services/editorWorkerService.ts +++ b/src/vs/editor/browser/services/editorWorkerService.ts @@ -33,6 +33,8 @@ import { mainWindow } from '../../../base/browser/window.js'; import { WindowIntervalTimer } from '../../../base/browser/dom.js'; import { WorkerTextModelSyncClient } from '../../common/services/textModelSync/textModelSync.impl.js'; import { EditorWorkerHost } from '../../common/services/editorWorkerHost.js'; +import { StringEdit } from '../../common/core/edits/stringEdit.js'; +import { OffsetRange } from '../../common/core/ranges/offsetRange.js'; /** * Stop the worker if it was not needed for 5 min. @@ -180,6 +182,17 @@ export abstract class EditorWorkerService extends Disposable implements IEditorW } } + public async computeStringEditFromDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): Promise { + try { + const worker = await this._workerWithResources([]); + const edit = await worker.$computeStringDiff(original, modified, options, algorithm); + return StringEdit.fromJson(edit); + } catch (e) { + onUnexpectedError(e); + return StringEdit.replace(OffsetRange.ofLength(original.length), modified); // approximation + } + } + public canNavigateValueSet(resource: URI): boolean { return (canSyncModel(this._modelService, resource)); } diff --git a/src/vs/editor/common/core/edits/edit.ts b/src/vs/editor/common/core/edits/edit.ts index 75a8cb5d263..80cbdb4aa10 100644 --- a/src/vs/editor/common/core/edits/edit.ts +++ b/src/vs/editor/common/core/edits/edit.ts @@ -48,7 +48,7 @@ export abstract class BaseEdit, TEdit extends BaseE * Normalizes the edit by removing empty replacements and joining touching replacements (if the replacements allow joining). * Two edits have an equal normalized edit if and only if they have the same effect on any input. * - * ![](./docs/BaseEdit_normalize.dio.png) + * ![](https://raw.githubusercontent.com/microsoft/vscode/refs/heads/main/src/vs/editor/common/core/edits/docs/BaseEdit_normalize.drawio.png) * * Invariant: * ``` @@ -90,7 +90,7 @@ export abstract class BaseEdit, TEdit extends BaseE /** * Combines two edits into one with the same effect. * - * ![](./docs/BaseEdit_compose.dio.png) + * ![](https://raw.githubusercontent.com/microsoft/vscode/refs/heads/main/src/vs/editor/common/core/edits/docs/BaseEdit_compose.drawio.png) * * Invariant: * ``` @@ -183,6 +183,22 @@ export abstract class BaseEdit, TEdit extends BaseE return this._createNew(result).normalize(); } + public decomposeSplit(shouldBeInE1: (repl: T) => boolean): { e1: TEdit; e2: TEdit } { + const e1: T[] = []; + const e2: T[] = []; + + let e2delta = 0; + for (const edit of this.replacements) { + if (shouldBeInE1(edit)) { + e1.push(edit); + e2delta += edit.getNewLength() - edit.replaceRange.length; + } else { + e2.push(edit.slice(edit.replaceRange.delta(e2delta), new OffsetRange(0, edit.getNewLength()))); + } + } + return { e1: this._createNew(e1), e2: this._createNew(e2) }; + } + /** * Returns the range of each replacement in the applied value. */ diff --git a/src/vs/editor/common/core/edits/stringEdit.ts b/src/vs/editor/common/core/edits/stringEdit.ts index f7763163166..ba29bdfd6d8 100644 --- a/src/vs/editor/common/core/edits/stringEdit.ts +++ b/src/vs/editor/common/core/edits/stringEdit.ts @@ -5,58 +5,26 @@ import { commonPrefixLength, commonSuffixLength } from '../../../../base/common/strings.js'; import { OffsetRange } from '../ranges/offsetRange.js'; +import { StringText } from '../text/abstractText.js'; import { BaseEdit, BaseReplacement } from './edit.js'; -/** - * Represents a set of replacements to a string. - * All these replacements are applied at once. -*/ -export class StringEdit extends BaseEdit { - public static readonly empty = new StringEdit([]); - public static create(replacements: readonly StringReplacement[]): StringEdit { - return new StringEdit(replacements); +export abstract class BaseStringEdit = BaseStringReplacement, TEdit extends BaseStringEdit = BaseStringEdit> extends BaseEdit { + get TReplacement(): T { + throw new Error('TReplacement is not defined for BaseStringEdit'); } - public static single(replacement: StringReplacement): StringEdit { - return new StringEdit([replacement]); - } - - public static replace(range: OffsetRange, replacement: string): StringEdit { - return new StringEdit([new StringReplacement(range, replacement)]); - } - - public static insert(offset: number, replacement: string): StringEdit { - return new StringEdit([new StringReplacement(OffsetRange.emptyAt(offset), replacement)]); - } - - public static delete(range: OffsetRange): StringEdit { - return new StringEdit([new StringReplacement(range, '')]); - } - - public static fromJson(data: ISerializedStringEdit): StringEdit { - return new StringEdit(data.map(StringReplacement.fromJson)); - } - - public static compose(edits: readonly StringEdit[]): StringEdit { + public static composeOrUndefined(edits: readonly T[]): T | undefined { if (edits.length === 0) { - return StringEdit.empty; + return undefined; } let result = edits[0]; for (let i = 1; i < edits.length; i++) { - result = result.compose(edits[i]); + result = result.compose(edits[i]) as any; } return result; } - constructor(replacements: readonly StringReplacement[]) { - super(replacements); - } - - protected override _createNew(replacements: readonly StringReplacement[]): StringEdit { - return new StringEdit(replacements); - } - public apply(base: string): string { const resultText: string[] = []; let pos = 0; @@ -162,39 +130,41 @@ export class StringEdit extends BaseEdit { public normalizeEOL(eol: '\r\n' | '\n'): StringEdit { return new StringEdit(this.replacements.map(edit => edit.normalizeEOL(eol))); } + + /** + * If `e1.apply(source) === e2.apply(source)`, then `e1.normalizeOnSource(source).equals(e2.normalizeOnSource(source))`. + */ + public normalizeOnSource(source: string): StringEdit { + const result = this.apply(source); + + const edit = StringReplacement.replace(OffsetRange.ofLength(source.length), result); + const e = edit.removeCommonSuffixAndPrefix(source); + if (e.isEmpty) { + return StringEdit.empty; + } + return e.toEdit(); + } + + removeCommonSuffixAndPrefix(source: string): TEdit { + return this._createNew(this.replacements.map(e => e.removeCommonSuffixAndPrefix(source))).normalize(); + } + + applyOnText(docContents: StringText): StringText { + return new StringText(this.apply(docContents.value)); + } + + public mapData>(f: (replacement: T) => TData): AnnotatedStringEdit { + return new AnnotatedStringEdit( + this.replacements.map(e => new AnnotatedStringReplacement( + e.replaceRange, + e.newText, + f(e) + )) + ); + } } -/** - * Warning: Be careful when changing this type, as it is used for serialization! -*/ -export type ISerializedStringEdit = ISerializedStringReplacement[]; - -/** - * Warning: Be careful when changing this type, as it is used for serialization! -*/ -export interface ISerializedStringReplacement { - txt: string; - pos: number; - len: number; -} - -export class StringReplacement extends BaseReplacement { - public static insert(offset: number, text: string): StringReplacement { - return new StringReplacement(OffsetRange.emptyAt(offset), text); - } - - public static replace(range: OffsetRange, text: string): StringReplacement { - return new StringReplacement(range, text); - } - - public static delete(range: OffsetRange): StringReplacement { - return new StringReplacement(range, ''); - } - - public static fromJson(data: ISerializedStringReplacement): StringReplacement { - return new StringReplacement(OffsetRange.ofStartAndLength(data.pos, data.len), data.txt); - } - +export abstract class BaseStringReplacement = BaseStringReplacement> extends BaseReplacement { constructor( range: OffsetRange, public readonly newText: string, @@ -202,20 +172,8 @@ export class StringReplacement extends BaseReplacement { super(range); } - override equals(other: StringReplacement): boolean { - return this.replaceRange.equals(other.replaceRange) && this.newText === other.newText; - } - getNewLength(): number { return this.newText.length; } - tryJoinTouching(other: StringReplacement): StringReplacement | undefined { - return new StringReplacement(this.replaceRange.joinRightTouching(other.replaceRange), this.newText + other.newText); - } - - slice(range: OffsetRange, rangeInReplacement: OffsetRange): StringReplacement { - return new StringReplacement(range, rangeInReplacement.substring(this.newText)); - } - override toString(): string { return `${this.replaceRange} -> "${this.newText}"`; } @@ -254,6 +212,155 @@ export class StringReplacement extends BaseReplacement { const newText = this.newText.replace(/\r\n|\n/g, eol); return new StringReplacement(this.replaceRange, newText); } + + public removeCommonSuffixAndPrefix(source: string): T { + return this.removeCommonSuffix(source).removeCommonPrefix(source); + } + + public removeCommonPrefix(source: string): T { + const oldText = this.replaceRange.substring(source); + + const prefixLen = commonPrefixLength(oldText, this.newText); + if (prefixLen === 0) { + return this as unknown as T; + } + + return this.slice(this.replaceRange.deltaStart(prefixLen), new OffsetRange(prefixLen, this.newText.length)); + } + + public removeCommonSuffix(source: string): T { + const oldText = this.replaceRange.substring(source); + + const suffixLen = commonSuffixLength(oldText, this.newText); + if (suffixLen === 0) { + return this as unknown as T; + } + return this.slice(this.replaceRange.deltaEnd(-suffixLen), new OffsetRange(0, this.newText.length - suffixLen)); + } + + public toEdit(): StringEdit { + return new StringEdit([this]); + } +} + + +/** + * Represents a set of replacements to a string. + * All these replacements are applied at once. +*/ +export class StringEdit extends BaseStringEdit { + public static readonly empty = new StringEdit([]); + + public static create(replacements: readonly StringReplacement[]): StringEdit { + return new StringEdit(replacements); + } + + public static single(replacement: StringReplacement): StringEdit { + return new StringEdit([replacement]); + } + + public static replace(range: OffsetRange, replacement: string): StringEdit { + return new StringEdit([new StringReplacement(range, replacement)]); + } + + public static insert(offset: number, replacement: string): StringEdit { + return new StringEdit([new StringReplacement(OffsetRange.emptyAt(offset), replacement)]); + } + + public static delete(range: OffsetRange): StringEdit { + return new StringEdit([new StringReplacement(range, '')]); + } + + public static fromJson(data: ISerializedStringEdit): StringEdit { + return new StringEdit(data.map(StringReplacement.fromJson)); + } + + public static compose(edits: readonly StringEdit[]): StringEdit { + if (edits.length === 0) { + return StringEdit.empty; + } + let result = edits[0]; + for (let i = 1; i < edits.length; i++) { + result = result.compose(edits[i]); + } + return result; + } + + /** + * The replacements are applied in order! + * Equals `StringEdit.compose(replacements.map(r => r.toEdit()))`, but is much more performant. + */ + public static composeSequentialReplacements(replacements: readonly StringReplacement[]): StringEdit { + let edit = StringEdit.empty; + let curEditReplacements: StringReplacement[] = []; // These are reverse sorted + + for (const r of replacements) { + const last = curEditReplacements.at(-1); + if (!last || r.replaceRange.isBefore(last.replaceRange)) { + // Detect subsequences of reverse sorted replacements + curEditReplacements.push(r); + } else { + // Once the subsequence is broken, compose the current replacements and look for a new subsequence. + edit = edit.compose(StringEdit.create(curEditReplacements.reverse())); + curEditReplacements = [r]; + } + } + + edit = edit.compose(StringEdit.create(curEditReplacements.reverse())); + return edit; + } + + constructor(replacements: readonly StringReplacement[]) { + super(replacements); + } + + protected override _createNew(replacements: readonly StringReplacement[]): StringEdit { + return new StringEdit(replacements); + } +} + +/** + * Warning: Be careful when changing this type, as it is used for serialization! +*/ +export type ISerializedStringEdit = ISerializedStringReplacement[]; + +/** + * Warning: Be careful when changing this type, as it is used for serialization! +*/ +export interface ISerializedStringReplacement { + txt: string; + pos: number; + len: number; +} + +export class StringReplacement extends BaseStringReplacement { + public static insert(offset: number, text: string): StringReplacement { + return new StringReplacement(OffsetRange.emptyAt(offset), text); + } + + public static replace(range: OffsetRange, text: string): StringReplacement { + return new StringReplacement(range, text); + } + + public static delete(range: OffsetRange): StringReplacement { + return new StringReplacement(range, ''); + } + + public static fromJson(data: ISerializedStringReplacement): StringReplacement { + return new StringReplacement(OffsetRange.ofStartAndLength(data.pos, data.len), data.txt); + } + + override equals(other: StringReplacement): boolean { + return this.replaceRange.equals(other.replaceRange) && this.newText === other.newText; + } + + override tryJoinTouching(other: StringReplacement): StringReplacement | undefined { + return new StringReplacement(this.replaceRange.joinRightTouching(other.replaceRange), this.newText + other.newText); + } + + override slice(range: OffsetRange, rangeInReplacement: OffsetRange): StringReplacement { + return new StringReplacement(range, rangeInReplacement.substring(this.newText)); + } } export function applyEditsToRanges(sortedRanges: OffsetRange[], edit: StringEdit): OffsetRange[] { @@ -322,3 +429,106 @@ export function applyEditsToRanges(sortedRanges: OffsetRange[], edit: StringEdit return result; } + +/** + * Represents data associated to a single edit, which survives certain edit operations. +*/ +export interface IEditData { + join(other: T): T | undefined; +} + +export class VoidEditData implements IEditData { + join(other: VoidEditData): VoidEditData | undefined { + return this; + } +} + +/** + * Represents a set of replacements to a string. + * All these replacements are applied at once. +*/ +export class AnnotatedStringEdit> extends BaseStringEdit, AnnotatedStringEdit> { + public static readonly empty = new AnnotatedStringEdit([]); + + public static create>(replacements: readonly AnnotatedStringReplacement[]): AnnotatedStringEdit { + return new AnnotatedStringEdit(replacements); + } + + public static single>(replacement: AnnotatedStringReplacement): AnnotatedStringEdit { + return new AnnotatedStringEdit([replacement]); + } + + public static replace>(range: OffsetRange, replacement: string, data: T): AnnotatedStringEdit { + return new AnnotatedStringEdit([new AnnotatedStringReplacement(range, replacement, data)]); + } + + public static insert>(offset: number, replacement: string, data: T): AnnotatedStringEdit { + return new AnnotatedStringEdit([new AnnotatedStringReplacement(OffsetRange.emptyAt(offset), replacement, data)]); + } + + public static delete>(range: OffsetRange, data: T): AnnotatedStringEdit { + return new AnnotatedStringEdit([new AnnotatedStringReplacement(range, '', data)]); + } + + public static compose>(edits: readonly AnnotatedStringEdit[]): AnnotatedStringEdit { + if (edits.length === 0) { + return AnnotatedStringEdit.empty; + } + let result = edits[0]; + for (let i = 1; i < edits.length; i++) { + result = result.compose(edits[i]); + } + return result; + } + + constructor(replacements: readonly AnnotatedStringReplacement[]) { + super(replacements); + } + + protected override _createNew(replacements: readonly AnnotatedStringReplacement[]): AnnotatedStringEdit { + return new AnnotatedStringEdit(replacements); + } + + toStringEdit(): StringEdit { + return new StringEdit(this.replacements.map(e => new StringReplacement(e.replaceRange, e.newText))); + } +} + +export class AnnotatedStringReplacement> extends BaseStringReplacement> { + public static insert>(offset: number, text: string, data: T): AnnotatedStringReplacement { + return new AnnotatedStringReplacement(OffsetRange.emptyAt(offset), text, data); + } + + public static replace>(range: OffsetRange, text: string, data: T): AnnotatedStringReplacement { + return new AnnotatedStringReplacement(range, text, data); + } + + public static delete>(range: OffsetRange, data: T): AnnotatedStringReplacement { + return new AnnotatedStringReplacement(range, '', data); + } + + constructor( + range: OffsetRange, + newText: string, + public readonly data: T + ) { + super(range, newText); + } + + override equals(other: AnnotatedStringReplacement): boolean { + return this.replaceRange.equals(other.replaceRange) && this.newText === other.newText && this.data === other.data; + } + + tryJoinTouching(other: AnnotatedStringReplacement): AnnotatedStringReplacement | undefined { + const joined = this.data.join(other.data); + if (joined === undefined) { + return undefined; + } + return new AnnotatedStringReplacement(this.replaceRange.joinRightTouching(other.replaceRange), this.newText + other.newText, joined); + } + + slice(range: OffsetRange, rangeInReplacement?: OffsetRange): AnnotatedStringReplacement { + return new AnnotatedStringReplacement(range, rangeInReplacement ? rangeInReplacement.substring(this.newText) : this.newText, this.data); + } +} + diff --git a/src/vs/editor/common/core/text/positionToOffset.ts b/src/vs/editor/common/core/text/positionToOffset.ts index 97d6ee3bf7c..07447565fd6 100644 --- a/src/vs/editor/common/core/text/positionToOffset.ts +++ b/src/vs/editor/common/core/text/positionToOffset.ts @@ -17,3 +17,8 @@ _setPositionOffsetTransformerDependencies({ TextEdit: TextEdit, TextLength: TextLength, }); + +// TODO@hediet this is dept and needs to go. See https://github.com/microsoft/vscode/issues/251126. +export function ensureDependenciesAreSet(): void { + // Noop +} diff --git a/src/vs/editor/common/diff/rangeMapping.ts b/src/vs/editor/common/diff/rangeMapping.ts index c5a66c6a8d9..487ec42833b 100644 --- a/src/vs/editor/common/diff/rangeMapping.ts +++ b/src/vs/editor/common/diff/rangeMapping.ts @@ -194,6 +194,17 @@ function isValidLineNumber(lineNumber: number, lines: string[]): boolean { * Also contains inner range mappings. */ export class DetailedLineRangeMapping extends LineRangeMapping { + public static toTextEdit(mapping: readonly DetailedLineRangeMapping[], modified: AbstractText): TextEdit { + const replacements: TextReplacement[] = []; + for (const m of mapping) { + for (const r of m.innerChanges ?? []) { + const replacement = r.toTextEdit(modified); + replacements.push(replacement); + } + } + return new TextEdit(replacements); + } + public static fromRangeMappings(rangeMappings: RangeMapping[]): DetailedLineRangeMapping { const originalRange = LineRange.join(rangeMappings.map(r => LineRange.fromRangeInclusive(r.originalRange))); const modifiedRange = LineRange.join(rangeMappings.map(r => LineRange.fromRangeInclusive(r.modifiedRange))); diff --git a/src/vs/editor/common/services/editorWebWorker.ts b/src/vs/editor/common/services/editorWebWorker.ts index c5e8ea184f8..ca63478efbd 100644 --- a/src/vs/editor/common/services/editorWebWorker.ts +++ b/src/vs/editor/common/services/editorWebWorker.ts @@ -28,6 +28,9 @@ import { computeDefaultDocumentColors } from '../languages/defaultDocumentColors import { FindSectionHeaderOptions, SectionHeader, findSectionHeaders } from './findSectionHeaders.js'; import { IRawModelData, IWorkerTextModelSyncChannelServer } from './textModelSync/textModelSync.protocol.js'; import { ICommonModel, WorkerTextModelSyncServer } from './textModelSync/textModelSync.impl.js'; +import { ISerializedStringEdit } from '../core/edits/stringEdit.js'; +import { StringText } from '../core/text/abstractText.js'; +import { ensureDependenciesAreSet } from '../core/text/positionToOffset.js'; export interface IMirrorModel extends IMirrorTextModel { readonly uri: URI; @@ -201,6 +204,24 @@ export class EditorWorker implements IDisposable, IWorkerTextModelSyncChannelSer return diffComputer.computeDiff().changes; } + public $computeStringDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): ISerializedStringEdit { + const diffAlgorithm: ILinesDiffComputer = algorithm === 'advanced' ? linesDiffComputers.getDefault() : linesDiffComputers.getLegacy(); + + ensureDependenciesAreSet(); + + const originalText = new StringText(original); + const originalLines = originalText.getLines(); + const modifiedText = new StringText(modified); + const modifiedLines = modifiedText.getLines(); + + const result = diffAlgorithm.computeDiff(originalLines, modifiedLines, { ignoreTrimWhitespace: false, maxComputationTimeMs: options.maxComputationTimeMs, computeMoves: false, extendToSubwords: false }); + + const textEdit = DetailedLineRangeMapping.toTextEdit(result.changes, modifiedText); + const strEdit = originalText.getTransformer().getStringEdit(textEdit); + + return strEdit.toJson(); + } + // ---- END diff -------------------------------------------------------------------------- diff --git a/src/vs/editor/common/services/editorWorker.ts b/src/vs/editor/common/services/editorWorker.ts index fc0f44fa458..6b0720d60ff 100644 --- a/src/vs/editor/common/services/editorWorker.ts +++ b/src/vs/editor/common/services/editorWorker.ts @@ -12,6 +12,7 @@ import { UnicodeHighlighterOptions } from './unicodeTextModelHighlighter.js'; import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; import type { EditorWorker } from './editorWebWorker.js'; import { SectionHeader, FindSectionHeaderOptions } from './findSectionHeaders.js'; +import { StringEdit } from '../core/edits/stringEdit.js'; export const IEditorWorkerService = createDecorator('editorWorkerService'); @@ -32,6 +33,8 @@ export interface IEditorWorkerService { computeMoreMinimalEdits(resource: URI, edits: TextEdit[] | null | undefined, pretty?: boolean): Promise; computeHumanReadableDiff(resource: URI, edits: TextEdit[] | null | undefined): Promise; + computeStringEditFromDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): Promise; + canComputeWordRanges(resource: URI): boolean; computeWordRanges(resource: URI, range: IRange): Promise<{ [word: string]: IRange[] } | null>; diff --git a/src/vs/editor/test/common/services/testEditorWorkerService.ts b/src/vs/editor/test/common/services/testEditorWorkerService.ts index 44a9d5fffc3..a03cd461405 100644 --- a/src/vs/editor/test/common/services/testEditorWorkerService.ts +++ b/src/vs/editor/test/common/services/testEditorWorkerService.ts @@ -10,6 +10,7 @@ import { TextEdit, IInplaceReplaceSupportResult, IColorInformation } from '../.. import { IDocumentDiff, IDocumentDiffProviderOptions } from '../../../common/diff/documentDiffProvider.js'; import { IChange } from '../../../common/diff/legacyLinesDiffComputer.js'; import { SectionHeader } from '../../../common/services/findSectionHeaders.js'; +import { StringEdit } from '../../../common/core/edits/stringEdit.js'; export class TestEditorWorkerService implements IEditorWorkerService { @@ -28,4 +29,8 @@ export class TestEditorWorkerService implements IEditorWorkerService { async navigateValueSet(resource: URI, range: IRange, up: boolean): Promise { return null; } async findSectionHeaders(uri: URI): Promise { return []; } async computeDefaultDocumentColors(uri: URI): Promise { return null; } + + computeStringEditFromDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/src/vs/platform/telemetry/common/telemetry.ts b/src/vs/platform/telemetry/common/telemetry.ts index 6bfcb9159cd..5f82501a81e 100644 --- a/src/vs/platform/telemetry/common/telemetry.ts +++ b/src/vs/platform/telemetry/common/telemetry.ts @@ -53,6 +53,10 @@ export interface ITelemetryService { setExperimentProperty(name: string, value: string): void; } +export function telemetryLevelEnabled(service: ITelemetryService, level: TelemetryLevel): boolean { + return service.telemetryLevel >= level; +} + export interface ITelemetryEndpoint { id: string; aiKey: string; diff --git a/src/vs/workbench/contrib/editTelemetry/browser/arcTracker.ts b/src/vs/workbench/contrib/editTelemetry/browser/arcTracker.ts new file mode 100644 index 00000000000..2b1dcb7bc0a --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/arcTracker.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { sumBy } from '../../../../base/common/arrays.js'; +import { AnnotatedStringEdit, BaseStringEdit, IEditData } from '../../../../editor/common/core/edits/stringEdit.js'; + +/** + * The ARC (accepted and retained characters) counts how many characters inserted by the initial suggestion (trackedEdit) + * stay unmodified after a certain amount of time after acceptance. +*/ +export class ArcTracker { + private _updatedTrackedEdit: AnnotatedStringEdit; + + constructor( + public readonly originalText: string, + private readonly _trackedEdit: BaseStringEdit, + ) { + const eNormalized = _trackedEdit.removeCommonSuffixPrefix(originalText); + this._updatedTrackedEdit = eNormalized.mapData(() => new IsTrackedEditData(true)); + } + + handleEdits(edit: BaseStringEdit): void { + const e = edit.mapData(_d => new IsTrackedEditData(false)); + const composedEdit = this._updatedTrackedEdit.compose(e); + const onlyTrackedEdit = composedEdit.decomposeSplit(e => !e.data.isTrackedEdit).e2; + this._updatedTrackedEdit = onlyTrackedEdit; + } + + getAcceptedRestrainedCharactersCount(): number { + const s = sumBy(this._updatedTrackedEdit.replacements, e => e.getNewLength()); + return s; + } + + getOriginalCharacterCount(): number { + return sumBy(this._trackedEdit.replacements, e => e.getNewLength()); + } + + getDebugState(): unknown { + return { + edits: this._updatedTrackedEdit.replacements.map(e => ({ + range: e.replaceRange.toString(), + newText: e.newText, + isTrackedEdit: e.data.isTrackedEdit, + })) + }; + } +} + +export class IsTrackedEditData implements IEditData { + constructor( + public readonly isTrackedEdit: boolean + ) { } + + join(data: IsTrackedEditData): IsTrackedEditData | undefined { + if (this.isTrackedEdit !== data.isTrackedEdit) { + return undefined; + } + return this; + } +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/documentWithAnnotatedEdits.ts b/src/vs/workbench/contrib/editTelemetry/browser/documentWithAnnotatedEdits.ts new file mode 100644 index 00000000000..08ddfb6a33c --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/documentWithAnnotatedEdits.ts @@ -0,0 +1,424 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AsyncIterableObject, raceTimeout } from '../../../../base/common/async.js'; +import { CachedFunction } from '../../../../base/common/cache.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; +import { IObservableWithChange, ISettableObservable, observableValue, RemoveUndefined, runOnChange } from '../../../../base/common/observable.js'; +import { AnnotatedStringEdit, IEditData } from '../../../../editor/common/core/edits/stringEdit.js'; +import { StringText } from '../../../../editor/common/core/text/abstractText.js'; +import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; +import { TextModelEditReason } from '../../../../editor/common/textModelEditReason.js'; +import { IObservableDocument } from './observableWorkspace.js'; + +export interface IDocumentWithAnnotatedEdits = EditSourceData> { + readonly value: IObservableWithChange }>; + waitForQueue(): Promise; +} + +/** + * Creates a document that is a delayed copy of the original document, + * but with edits annotated with the source of the edit. +*/ +export class DocumentWithAnnotatedEdits extends Disposable implements IDocumentWithAnnotatedEdits { + public readonly value: IObservableWithChange }>; + + constructor(private readonly _originalDoc: IObservableDocument) { + super(); + + const v = this.value = observableValue(this, _originalDoc.value.get()); + + this._register(runOnChange(this._originalDoc.value, (val, _prevVal, edits) => { + const eComposed = AnnotatedStringEdit.compose(edits.map(e => { + const editSourceData = new EditReasonData(e.reason); + return e.mapData(() => editSourceData); + })); + + v.set(val, undefined, { edit: eComposed }); + })); + } + + public waitForQueue(): Promise { + return Promise.resolve(); + } +} + +/** + * Only joins touching edits if the source and the metadata is the same. +*/ +export class EditReasonData implements IEditData { + public readonly source; + public readonly key; + + constructor( + public readonly editReason: TextModelEditReason + ) { + this.key = this.editReason.toKey(1); + this.source = EditSourceBase.create(this.editReason); + } + + join(data: EditReasonData): EditReasonData | undefined { + if (this.editReason !== data.editReason) { + return undefined; + } + return this; + } + + toEditSourceData(): EditSourceData { + return new EditSourceData(this.key, this.source); + } +} + +export class EditSourceData implements IEditData { + constructor( + public readonly key: string, + public readonly source: EditSource, + ) { } + + join(data: EditSourceData): EditSourceData | undefined { + if (this.key !== data.key) { + return undefined; + } + if (this.source !== data.source) { + return undefined; + } + return this; + } +} + +export abstract class EditSourceBase { + private static _cache = new CachedFunction({ getCacheKey: v => v.toString() }, (arg: EditSource) => arg); + + public static create(reason: TextModelEditReason): EditSource { + const data = reason.metadata; + switch (data.source) { + case 'reloadFromDisk': + return this._cache.get(new ExternalEditSource()); + case 'inlineCompletionPartialAccept': + case 'inlineCompletionAccept': { + const type = 'type' in data ? data.type : undefined; + if ('$nes' in data && data.$nes) { + return this._cache.get(new InlineSuggestEditSource('nes', data.$extensionId ?? '', type)); + } + return this._cache.get(new InlineSuggestEditSource('completion', data.$extensionId ?? '', type)); + } + case 'snippet': + return this._cache.get(new IdeEditSource('suggest')); + case 'unknown': + if (!data.name) { + return this._cache.get(new UnknownEditSource()); + } + switch (data.name) { + case 'formatEditsCommand': + return this._cache.get(new IdeEditSource('format')); + } + return this._cache.get(new UnknownEditSource()); + + case 'Chat.applyEdits': + return this._cache.get(new ChatEditSource('sidebar')); + case 'inlineChat.applyEdits': + return this._cache.get(new ChatEditSource('inline')); + case 'cursor': + return this._cache.get(new UserEditSource()); + default: + return this._cache.get(new UnknownEditSource()); + } + } + + public abstract getColor(): string; +} + +export type EditSource = InlineSuggestEditSource | ChatEditSource | IdeEditSource | UserEditSource | UnknownEditSource | ExternalEditSource; + +export class InlineSuggestEditSource extends EditSourceBase { + public readonly category = 'ai'; + public readonly feature = 'inlineSuggest'; + constructor( + public readonly kind: 'completion' | 'nes', + public readonly extensionId: string, + public readonly type: 'word' | 'line' | undefined, + ) { super(); } + + override toString() { return `${this.category}/${this.feature}/${this.kind}/${this.extensionId}/${this.type}`; } + + public getColor(): string { return '#00ff0033'; } +} + +class ChatEditSource extends EditSourceBase { + public readonly category = 'ai'; + public readonly feature = 'chat'; + constructor( + public readonly kind: 'sidebar' | 'inline', + ) { super(); } + + override toString() { return `${this.category}/${this.feature}/${this.kind}`; } + + public getColor(): string { return '#00ff0066'; } +} + +class IdeEditSource extends EditSourceBase { + public readonly category = 'ide'; + constructor( + public readonly feature: 'suggest' | 'format' | string, + ) { super(); } + + override toString() { return `${this.category}/${this.feature}`; } + + public getColor(): string { return this.feature === 'format' ? '#0000ff33' : '#80808033'; } +} + +class UserEditSource extends EditSourceBase { + public readonly category = 'user'; + constructor() { super(); } + + override toString() { return this.category; } + + public getColor(): string { return '#d3d3d333'; } +} + +/** Caused by external tools that trigger a reload from disk */ +class ExternalEditSource extends EditSourceBase { + public readonly category = 'external'; + constructor() { super(); } + + override toString() { return this.category; } + + public getColor(): string { return '#009ab254'; } +} + +class UnknownEditSource extends EditSourceBase { + public readonly category = 'unknown'; + constructor() { super(); } + + override toString() { return this.category; } + + public getColor(): string { return '#ff000033'; } +} + +export class CombineStreamedChanges> extends Disposable implements IDocumentWithAnnotatedEdits { + private readonly _value: ISettableObservable }>; + readonly value: IObservableWithChange }>; + private readonly _runStore = this._register(new DisposableStore()); + private _runQueue: Promise = Promise.resolve(); + + constructor( + private readonly _originalDoc: IDocumentWithAnnotatedEdits, + @IEditorWorkerService private readonly _diffService: IEditorWorkerService, + ) { + super(); + + this.value = this._value = observableValue(this, _originalDoc.value.get()); + this._restart(); + + this._diffService.computeStringEditFromDiff('foo', 'last.value.value', { maxComputationTimeMs: 500 }, 'advanced'); + } + + async _restart(): Promise { + this._runStore.clear(); + const iterator = iterateChangesFromObservable(this._originalDoc.value, this._runStore)[Symbol.asyncIterator](); + const p = this._runQueue; + this._runQueue = this._runQueue.then(() => this._run(iterator)); + await p; + } + + private async _run(iterator: AsyncIterator<{ value: StringText; prevValue: StringText; change: { edit: AnnotatedStringEdit }[] }, any, any>) { + const reader = new AsyncReader(iterator); + while (true) { + let peeked = await reader.peek(); + if (peeked === AsyncReaderEndOfStream) { + return; + } else if (isChatEdit(peeked)) { + const first = peeked; + + let last = first; + let chatEdit = AnnotatedStringEdit.empty as AnnotatedStringEdit; + + do { + reader.readSyncOrThrow(); + last = peeked; + chatEdit = chatEdit.compose(AnnotatedStringEdit.compose(peeked.change.map(c => c.edit))); + if (!await reader.waitForBufferTimeout(1000)) { + break; + } + peeked = reader.peekSyncOrThrow(); + } while (peeked !== AsyncReaderEndOfStream && isChatEdit(peeked)); + + if (!chatEdit.isEmpty()) { + const data = chatEdit.replacements[0].data; + const diffEdit = await this._diffService.computeStringEditFromDiff(first.prevValue.value, last.value.value, { maxComputationTimeMs: 500 }, 'advanced'); + const edit = diffEdit.mapData(_e => data); + this._value.set(last.value, undefined, { edit }); + } + } else { + reader.readSyncOrThrow(); + const e = AnnotatedStringEdit.compose(peeked.change.map(c => c.edit)); + this._value.set(peeked.value, undefined, { edit: e }); + } + } + } + + async waitForQueue(): Promise { + await this._originalDoc.waitForQueue(); + await this._restart(); + } +} + +function isChatEdit(next: { value: StringText; change: { edit: AnnotatedStringEdit }[] }) { + return next.change.every(c => c.edit.replacements.every(e => { + if (e.data.source.category === 'ai' && e.data.source.feature === 'chat') { + return true; + } + return false; + })); +} + +function iterateChangesFromObservable(obs: IObservableWithChange, store: DisposableStore): AsyncIterable<{ value: T; prevValue: T; change: RemoveUndefined[] }> { + return new AsyncIterableObject<{ value: T; prevValue: T; change: RemoveUndefined[] }>((e) => { + store.add(runOnChange(obs, (value, prevValue, change) => { + e.emitOne({ value, prevValue, change: change }); + })); + + return new Promise((res) => { + store.add(toDisposable(() => { + res(undefined); + })); + }); + }); +} + +export class MinimizeEditsProcessor> extends Disposable implements IDocumentWithAnnotatedEdits { + readonly value: IObservableWithChange }>; + + constructor( + private readonly _originalDoc: IDocumentWithAnnotatedEdits, + ) { + super(); + + const v = this.value = observableValue(this, _originalDoc.value.get()); + + let prevValue: string = this._originalDoc.value.get().value; + this._register(runOnChange(this._originalDoc.value, (val, _prevVal, edits) => { + const eComposed = AnnotatedStringEdit.compose(edits.map(e => e.edit)); + + const e = eComposed.removeCommonSuffixAndPrefix(prevValue); + prevValue = val.value; + + v.set(val, undefined, { edit: e }); + })); + } + + async waitForQueue(): Promise { + await this._originalDoc.waitForQueue(); + } +} + +export const AsyncReaderEndOfStream = Symbol('AsyncReaderEndOfStream'); + +export class AsyncReader { + private _buffer: T[] = []; + private _atEnd = false; + + public get endOfStream(): boolean { return this._buffer.length === 0 && this._atEnd; } + + constructor( + private readonly _source: AsyncIterator + ) { + } + + private async _extendBuffer(): Promise { + if (this._atEnd) { + return; + } + const { value, done } = await this._source.next(); + if (done) { + this._atEnd = true; + } else { + this._buffer.push(value); + } + } + + public async peek(): Promise { + if (this._buffer.length === 0 && !this._atEnd) { + await this._extendBuffer(); + } + if (this._buffer.length === 0) { + return AsyncReaderEndOfStream; + } + return this._buffer[0]; + } + + public peekSyncOrThrow(): T | typeof AsyncReaderEndOfStream { + if (this._buffer.length === 0) { + if (this._atEnd) { + return AsyncReaderEndOfStream; + } + throw new Error('No more elements'); + } + + return this._buffer[0]; + } + + public readSyncOrThrow(): T | typeof AsyncReaderEndOfStream { + if (this._buffer.length === 0) { + if (this._atEnd) { + return AsyncReaderEndOfStream; + } + throw new Error('No more elements'); + } + + return this._buffer.shift()!; + } + + public async peekNextTimeout(timeoutMs: number): Promise { + if (this._buffer.length === 0 && !this._atEnd) { + await raceTimeout(this._extendBuffer(), timeoutMs); + } + if (this._atEnd) { + return AsyncReaderEndOfStream; + } + if (this._buffer.length === 0) { + return undefined; + } + return this._buffer[0]; + } + + public async waitForBufferTimeout(timeoutMs: number): Promise { + if (this._buffer.length > 0 || this._atEnd) { + return true; + } + const result = await raceTimeout(this._extendBuffer().then(() => true), timeoutMs); + return result !== undefined; + } + + public async read(): Promise { + if (this._buffer.length === 0 && !this._atEnd) { + await this._extendBuffer(); + } + if (this._buffer.length === 0) { + return AsyncReaderEndOfStream; + } + return this._buffer.shift()!; + } + + public async readWhile(predicate: (value: T) => boolean, callback: (element: T) => unknown): Promise { + do { + const piece = await this.peek(); + if (piece === AsyncReaderEndOfStream) { + break; + } + if (!predicate(piece)) { + break; + } + await this.read(); // consume + await callback(piece); + } while (true); + } + + public async consumeToEnd(): Promise { + while (!this.endOfStream) { + await this.read(); + } + } +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingFeature.ts b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingFeature.ts new file mode 100644 index 00000000000..7d437dd8d5f --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingFeature.ts @@ -0,0 +1,241 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { CachedFunction } from '../../../../base/common/cache.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun, mapObservableArrayCached, derived, IObservable, ISettableObservable, observableValue, derivedWithSetter, observableSignalFromEvent, observableFromEvent } from '../../../../base/common/observable.js'; +import { isDefined } from '../../../../base/common/types.js'; +import { URI } from '../../../../base/common/uri.js'; +import { DynamicCssRules } from '../../../../editor/browser/editorDom.js'; +import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; +import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { IModelDeltaDecoration } from '../../../../editor/common/model.js'; +import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { EditorResourceAccessor } from '../../../common/editor.js'; +import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js'; +import { EditSource } from './documentWithAnnotatedEdits.js'; +import { EditSourceTrackingImpl } from './editSourceTrackingImpl.js'; +import { EDIT_TELEMETRY_SHOW_DECORATIONS, EDIT_TELEMETRY_SHOW_STATUS_BAR } from './settings.js'; +import { VSCodeWorkspace } from './vscodeObservableWorkspace.js'; + +export class EditTrackingFeature extends Disposable { + + private readonly _editSourceTrackingShowDecorations; + private readonly _editSourceTrackingShowStatusBar; + private readonly _showStateInMarkdownDoc = 'editTelemetry.showDebugDetails'; + private readonly _toggleDecorations = 'editTelemetry.toggleDebugDecorations'; + + constructor( + private readonly _workspace: VSCodeWorkspace, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IStatusbarService private readonly _statusbarService: IStatusbarService, + @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, + @IEditorService private readonly _editorService: IEditorService, + ) { + super(); + + this._editSourceTrackingShowDecorations = makeSettable(observableConfigValue(EDIT_TELEMETRY_SHOW_DECORATIONS, false, this._configurationService)); + this._editSourceTrackingShowStatusBar = observableConfigValue(EDIT_TELEMETRY_SHOW_STATUS_BAR, false, this._configurationService); + + const onDidAddGroupSignal = observableSignalFromEvent(this, this._editorGroupsService.onDidAddGroup); + const onDidRemoveGroupSignal = observableSignalFromEvent(this, this._editorGroupsService.onDidRemoveGroup); + const groups = derived(this, reader => { + onDidAddGroupSignal.read(reader); + onDidRemoveGroupSignal.read(reader); + return this._editorGroupsService.groups; + }); + const visibleUris: IObservable> = mapObservableArrayCached(this, groups, g => { + const editors = observableFromEvent(this, g.onDidModelChange, () => g.editors); + return editors.map(e => e.map(editor => EditorResourceAccessor.getCanonicalUri(editor))); + }).map((editors, reader) => { + const map = new Map(); + for (const urisObs of editors) { + for (const uri of urisObs.read(reader)) { + if (isDefined(uri)) { + map.set(uri.toString(), uri); + } + } + } + return map; + }); + + const impl = this._register(this._instantiationService.createInstance(EditSourceTrackingImpl, this._workspace, (doc, reader) => { + const map = visibleUris.read(reader); + return map.get(doc.uri.toString()) !== undefined; + })); + + this._register(autorun((reader) => { + if (!this._editSourceTrackingShowDecorations.read(reader)) { + return; + } + + const visibleEditors = observableFromEvent(this, this._editorService.onDidVisibleEditorsChange, () => this._editorService.visibleTextEditorControls); + + mapObservableArrayCached(this, visibleEditors, (editor, store) => { + if (editor instanceof CodeEditorWidget) { + const obsEditor = observableCodeEditor(editor); + + const cssStyles = new DynamicCssRules(editor); + const decorations = new CachedFunction((source: EditSource) => { + const r = store.add(cssStyles.createClassNameRef({ + backgroundColor: source.getColor(), + })); + return r.className; + }); + + store.add(obsEditor.setDecorations(derived(reader => { + const uri = obsEditor.model.read(reader)?.uri; + if (!uri) { return []; } + const doc = this._workspace.getDocument(uri); + if (!doc) { return []; } + const docsState = impl.docsState.read(reader).get(doc); + if (!docsState) { return []; } + + const ranges = (docsState.longtermTracker.read(reader)?.getTrackedRanges(reader)) ?? []; + + return ranges.map(r => ({ + range: doc.value.get().getTransformer().getRange(r.range), + options: { + description: 'editSourceTracking', + inlineClassName: decorations.get(r.source), + } + })); + }))); + } + }).recomputeInitiallyAndOnChange(reader.store); + })); + + this._register(autorun(reader => { + if (!this._editSourceTrackingShowStatusBar.read(reader)) { + return; + } + + const statusBarItem = reader.store.add(this._statusbarService.addEntry( + { + name: '', + text: '', + command: this._showStateInMarkdownDoc, + tooltip: 'Edit Source Tracking', + ariaLabel: '', + }, + 'editTelemetry', + StatusbarAlignment.RIGHT, + 100 + )); + + const sumChangedCharacters = derived(reader => { + const docs = impl.docsState.read(reader); + let sum = 0; + for (const state of docs.values()) { + const t = state.longtermTracker.read(reader); + if (!t) { continue; } + const d = state.getTelemetryData(t.getTrackedRanges(reader)); + sum += d.totalModifiedCharactersInFinalState; + } + return sum; + }); + + const tooltipMarkdownString = derived(reader => { + const docs = impl.docsState.read(reader); + const docsDataInTooltip: string[] = []; + const editSources: EditSource[] = []; + for (const [doc, state] of docs) { + const tracker = state.longtermTracker.read(reader); + if (!tracker) { + continue; + } + const trackedRanges = tracker.getTrackedRanges(reader); + const data = state.getTelemetryData(trackedRanges); + if (data.totalModifiedCharactersInFinalState === 0) { + continue; // Don't include unmodified documents in tooltip + } + + editSources.push(...trackedRanges.map(r => r.source)); + + // Filter out unmodified properties as these are not interesting to see in the hover + const filteredData = Object.fromEntries( + Object.entries(data).filter(([_, value]) => !(typeof value === 'number') || value !== 0) + ); + + docsDataInTooltip.push([ + `### ${doc.uri.fsPath}`, + '```json', + JSON.stringify(filteredData, undefined, '\t'), + '```', + '\n' + ].join('\n')); + } + + let tooltipContent: string; + if (docsDataInTooltip.length === 0) { + tooltipContent = 'No modified documents'; + } else if (docsDataInTooltip.length <= 3) { + tooltipContent = docsDataInTooltip.join('\n\n'); + } else { + const lastThree = docsDataInTooltip.slice(-3); + tooltipContent = '...\n\n' + lastThree.join('\n\n'); + } + + const agenda = this._createEditSourceAgenda(editSources); + + const tooltipWithCommand = new MarkdownString(tooltipContent + '\n\n[View Details](command:' + this._showStateInMarkdownDoc + ')'); + tooltipWithCommand.appendMarkdown('\n\n' + agenda + '\n\nToggle decorations: [Click here](command:' + this._toggleDecorations + ')'); + tooltipWithCommand.isTrusted = { enabledCommands: [this._toggleDecorations] }; + tooltipWithCommand.supportHtml = true; + + return tooltipWithCommand; + }); + + reader.store.add(autorun(reader => { + statusBarItem.update({ + name: 'editTelemetry', + text: `$(edit) ${sumChangedCharacters.read(reader)} chars inserted`, + ariaLabel: `Edit Source Tracking: ${sumChangedCharacters.read(reader)} modified characters`, + tooltip: tooltipMarkdownString.read(reader), + command: this._showStateInMarkdownDoc, + }); + })); + + reader.store.add(CommandsRegistry.registerCommand(this._toggleDecorations, () => { + this._editSourceTrackingShowDecorations.set(!this._editSourceTrackingShowDecorations.get(), undefined); + })); + })); + } + + private _createEditSourceAgenda(editSources: EditSource[]): string { + // Collect all edit sources from the tracked documents + const editSourcesSeen = new Set(); + const editSourceInfo = []; + for (const editSource of editSources) { + if (!editSourcesSeen.has(editSource.toString())) { + editSourcesSeen.add(editSource.toString()); + editSourceInfo.push({ name: editSource.toString(), color: editSource.getColor() }); + } + } + + const agendaItems = editSourceInfo.map(info => + `${info.name}` + ); + + return agendaItems.join(' '); + } +} + +export function makeSettable(obs: IObservable): ISettableObservable { + const overrideObs = observableValue('overrideObs', undefined); + return derivedWithSetter(overrideObs, (reader) => { + return overrideObs.read(reader) ?? obs.read(reader); + }, (value, tx) => { + overrideObs.set(value, tx); + }); +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts new file mode 100644 index 00000000000..edadf7750b5 --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts @@ -0,0 +1,451 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { reverseOrder, compareBy, numberComparator, sumBy } from '../../../../base/common/arrays.js'; +import { IntervalTimer, TimeoutTimer } from '../../../../base/common/async.js'; +import { toDisposable, DisposableStore, Disposable } from '../../../../base/common/lifecycle.js'; +import { mapObservableArrayCached, derived, IReader, IObservable, observableSignal, runOnChange, IObservableWithChange, observableValue, transaction, derivedObservableWithCache } from '../../../../base/common/observable.js'; +import { isDefined } from '../../../../base/common/types.js'; +import { URI } from '../../../../base/common/uri.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { AnnotatedStringEdit, BaseStringEdit } from '../../../../editor/common/core/edits/stringEdit.js'; +import { StringText } from '../../../../editor/common/core/text/abstractText.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { ISCMRepository, ISCMService } from '../../scm/common/scm.js'; +import { ArcTracker } from './arcTracker.js'; +import { CombineStreamedChanges, DocumentWithAnnotatedEdits, EditReasonData, EditSource, EditSourceData, IDocumentWithAnnotatedEdits, MinimizeEditsProcessor } from './documentWithAnnotatedEdits.js'; +import { DocumentEditSourceTracker, TrackedEdit } from './editTracker.js'; +import { ObservableWorkspace, IObservableDocument } from './observableWorkspace.js'; + +export class EditSourceTrackingImpl extends Disposable { + public readonly docsState; + + constructor( + private readonly _workspace: ObservableWorkspace, + private readonly _docIsVisible: (doc: IObservableDocument, reader: IReader) => boolean, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + + const scmBridge = this._instantiationService.createInstance(ScmBridge); + + this.docsState = mapObservableArrayCached(this, this._workspace.documents, (doc, store) => { + const docIsVisible = derived(reader => this._docIsVisible(doc, reader)); + const wasEverVisible = derivedObservableWithCache(this, (reader, lastVal) => lastVal || docIsVisible.read(reader)); + return wasEverVisible.map(v => v ? [doc, store.add(this._instantiationService.createInstance(TrackedDocumentInfo, doc, docIsVisible, scmBridge))] as const : undefined); + }).recomputeInitiallyAndOnChange(this._store).map((entries, reader) => new Map(entries.map(e => e.read(reader)).filter(isDefined))); + } +} + +class ScmBridge { + constructor( + @ISCMService private readonly _scmService: ISCMService + ) { } + + public async getRepo(uri: URI): Promise { + const repo = this._scmService.getRepository(uri); + if (!repo) { + return undefined; + } + return new ScmRepoBridge(repo); + } +} + +class ScmRepoBridge { + public readonly headBranchNameObs: IObservable = derived(reader => this._repo.provider.historyProvider.read(reader)?.historyItemRef.read(reader)?.name); + public readonly headCommitHashObs: IObservable = derived(reader => this._repo.provider.historyProvider.read(reader)?.historyItemRef.read(reader)?.revision); + + constructor( + private readonly _repo: ISCMRepository, + ) { + } + + async isIgnored(uri: URI): Promise { + return false; + } +} + +class TrackedDocumentInfo extends Disposable { + public readonly longtermTracker: IObservable | undefined>; + public readonly windowedTracker: IObservable | undefined>; + + private readonly _repo: Promise; + + constructor( + private readonly _doc: IObservableDocument, + docIsVisible: IObservable, + private readonly _scm: ScmBridge, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ITelemetryService private readonly _telemetryService: ITelemetryService + ) { + super(); + + // Use the listener service and special events from core to annotate where an edit came from (is async) + let processedDoc: IDocumentWithAnnotatedEdits = this._store.add(new DocumentWithAnnotatedEdits(_doc)); + // Combine streaming edits into one and make edit smaller + processedDoc = this._store.add(this._instantiationService.createInstance((CombineStreamedChanges), processedDoc)); + // Remove common suffix and prefix from edits + processedDoc = this._store.add(new MinimizeEditsProcessor(processedDoc)); + + const docWithJustReason = createDocWithJustReason(processedDoc, this._store); + + const longtermResetSignal = observableSignal('resetSignal'); + + this.longtermTracker = derived((reader) => { + longtermResetSignal.read(reader); + + const t = reader.store.add(new DocumentEditSourceTracker(docWithJustReason, undefined)); + reader.store.add(toDisposable(() => { + // send long term document telemetry + if (!t.isEmpty()) { + this.sendTelemetry('longterm', t.getTrackedRanges()); + } + t.dispose(); + })); + return t; + }).recomputeInitiallyAndOnChange(this._store); + + this._store.add(new IntervalTimer()).cancelAndSet(() => { + // Reset after 10 hours + longtermResetSignal.trigger(undefined); + }, 10 * 60 * 60 * 1000); + + (async () => { + const repo = await this._scm.getRepo(_doc.uri); + if (this._store.isDisposed) { + return; + } + // Reset on branch change or commit + if (repo) { + this._store.add(runOnChange(repo.headCommitHashObs, () => { + longtermResetSignal.trigger(undefined); + })); + this._store.add(runOnChange(repo.headBranchNameObs, () => { + longtermResetSignal.trigger(undefined); + })); + } + + this._store.add(this._instantiationService.createInstance(ArcTelemetrySender, processedDoc, repo)); + })(); + + const resetSignal = observableSignal('resetSignal'); + + this.windowedTracker = derived((reader) => { + if (!docIsVisible.read(reader)) { + return undefined; + } + resetSignal.read(reader); + + reader.store.add(new TimeoutTimer(() => { + // Reset after 5 minutes + resetSignal.trigger(undefined); + }, 5 * 60 * 1000)); + + const t = reader.store.add(new DocumentEditSourceTracker(docWithJustReason, undefined)); + reader.store.add(toDisposable(async () => { + // send long term document telemetry + this.sendTelemetry('5minWindow', t.getTrackedRanges()); + t.dispose(); + })); + + return t; + }).recomputeInitiallyAndOnChange(this._store); + + this._repo = this._scm.getRepo(_doc.uri); + } + + async sendTelemetry(mode: 'longterm' | '5minWindow', ranges: readonly TrackedEdit[]) { + if (ranges.length === 0) { + return; + } + + const data = this.getTelemetryData(ranges); + const isTrackedByGit = await data.isTrackedByGit; + + const statsUuid = generateUuid(); + + this._telemetryService.publicLog2<{ + mode: string; + languageId: string; + statsUuid: string; + nesModifiedCount: number; + inlineCompletionsCopilotModifiedCount: number; + inlineCompletionsNESModifiedCount: number; + otherAIModifiedCount: number; + unknownModifiedCount: number; + userModifiedCount: number; + ideModifiedCount: number; + totalModifiedCharacters: number; + externalModifiedCount: number; + isTrackedByGit: number; + }, { + owner: 'hediet'; + comment: 'Reports distribution of AI vs user edited characters.'; + + mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'longterm or 5minWindow' }; + languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' }; + statsUuid: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The unique identifier for the telemetry event.' }; + + nesModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of nes modified characters'; isMeasurement: true }; + inlineCompletionsCopilotModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of inline completions copilot modified characters'; isMeasurement: true }; + inlineCompletionsNESModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of inline completions nes modified characters'; isMeasurement: true }; + otherAIModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of other AI modified characters'; isMeasurement: true }; + unknownModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of unknown modified characters'; isMeasurement: true }; + userModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of user modified characters'; isMeasurement: true }; + ideModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of IDE modified characters'; isMeasurement: true }; + totalModifiedCharacters: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Total modified characters'; isMeasurement: true }; + externalModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of external modified characters'; isMeasurement: true }; + isTrackedByGit: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Indicates if the document is tracked by git.' }; + }>('editTelemetry.editSources.stats', { + mode, + languageId: this._doc.languageId.get(), + statsUuid: statsUuid, + nesModifiedCount: data.nesModifiedCount, + inlineCompletionsCopilotModifiedCount: data.inlineCompletionsCopilotModifiedCount, + inlineCompletionsNESModifiedCount: data.inlineCompletionsNESModifiedCount, + otherAIModifiedCount: data.otherAIModifiedCount, + unknownModifiedCount: data.unknownModifiedCount, + userModifiedCount: data.userModifiedCount, + ideModifiedCount: data.ideModifiedCount, + totalModifiedCharacters: data.totalModifiedCharactersInFinalState, + externalModifiedCount: data.externalModifiedCount, + isTrackedByGit: isTrackedByGit ? 1 : 0, + }); + + + const sums = sumByCategory(ranges, r => r.range.length, r => r.sourceKey); + const entries = Object.entries(sums).filter(([key, value]) => value !== undefined); + entries.sort(reverseOrder(compareBy(([key, value]) => value!, numberComparator))); + entries.length = mode === 'longterm' ? 30 : 10; + + for (const [key, value] of Object.entries(sums)) { + if (value === undefined) { + continue; + } + this._telemetryService.publicLog2<{ + mode: string; + reasonKey: string; + languageId: string; + statsUuid: string; + modifiedCount: number; + totalModifiedCount: number; + }, { + owner: 'hediet'; + comment: 'Reports distribution of various edit kinds.'; + + reasonKey: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The reason for the edit.' }; + mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'longterm or 5minWindow' }; + languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' }; + statsUuid: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The unique identifier for the telemetry event.' }; + + modifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of nes modified characters'; isMeasurement: true }; + totalModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Total number of characters'; isMeasurement: true }; + }>('editTelemetry.editSources.details', { + mode, + reasonKey: key, + languageId: this._doc.languageId.get(), + statsUuid: statsUuid, + modifiedCount: value, + totalModifiedCount: data.totalModifiedCharactersInFinalState, + }); + } + } + + getTelemetryData(ranges: readonly TrackedEdit[]) { + const getEditCategory = (source: EditSource) => { + if (source.category === 'ai' && source.kind === 'nes') { return 'nes'; } + if (source.category === 'ai' && source.kind === 'completion' && source.extensionId === 'github.copilot') { return 'inlineCompletionsCopilot'; } + if (source.category === 'ai' && source.kind === 'completion' && source.extensionId === 'github.copilot-chat') { return 'inlineCompletionsNES'; } + if (source.category === 'ai' && source.kind === 'completion') { return 'inlineCompletionsOther'; } + if (source.category === 'ai') { return 'otherAI'; } + if (source.category === 'user') { return 'user'; } + if (source.category === 'ide') { return 'ide'; } + if (source.category === 'external') { return 'external'; } + if (source.category === 'unknown') { return 'unknown'; } + + return 'unknown'; + }; + + const sums = sumByCategory(ranges, r => r.range.length, r => getEditCategory(r.source)); + const totalModifiedCharactersInFinalState = sumBy(ranges, r => r.range.length); + + return { + nesModifiedCount: sums.nes ?? 0, + inlineCompletionsCopilotModifiedCount: sums.inlineCompletionsCopilot ?? 0, + inlineCompletionsNESModifiedCount: sums.inlineCompletionsNES ?? 0, + otherAIModifiedCount: sums.otherAI ?? 0, + userModifiedCount: sums.user ?? 0, + ideModifiedCount: sums.ide ?? 0, + unknownModifiedCount: sums.unknown ?? 0, + externalModifiedCount: sums.external ?? 0, + totalModifiedCharactersInFinalState, + languageId: this._doc.languageId.get(), + isTrackedByGit: this._repo.then(async (repo) => !!repo && !await repo.isIgnored(this._doc.uri)), + }; + } +} + + +function mapObservableDelta(obs: IObservableWithChange, mapFn: (value: TDelta) => TDeltaNew, store: DisposableStore): IObservableWithChange { + const obsResult = observableValue('mapped', obs.get()); + store.add(runOnChange(obs, (value, _prevValue, changes) => { + transaction(tx => { + for (const c of changes) { + obsResult.set(value, tx, mapFn(c)); + } + }); + })); + return obsResult; +} + +/** + * Removing the metadata allows touching edits from the same source to merged, even if they were caused by different actions (e.g. two user edits). + */ +function createDocWithJustReason(docWithAnnotatedEdits: IDocumentWithAnnotatedEdits, store: DisposableStore): IDocumentWithAnnotatedEdits { + const docWithJustReason: IDocumentWithAnnotatedEdits = { + value: mapObservableDelta(docWithAnnotatedEdits.value, edit => ({ edit: edit.edit.mapData(d => d.data.toEditSourceData()) }), store), + waitForQueue: () => docWithAnnotatedEdits.waitForQueue(), + }; + return docWithJustReason; +} + +class ArcTelemetrySender extends Disposable { + constructor( + docWithAnnotatedEdits: IDocumentWithAnnotatedEdits, + scmRepoBridge: ScmRepoBridge | undefined, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + + this._register(runOnChange(docWithAnnotatedEdits.value, (_val, _prev, changes) => { + const edit = AnnotatedStringEdit.compose(changes.map(c => c.edit)); + if (edit.replacements.length !== 1) { + return; + } + const singleEdit = edit.replacements[0]; + const data = singleEdit.data.editReason.metadata; + if (data?.source !== 'inlineCompletionAccept') { + return; + } + + const docWithJustReason = createDocWithJustReason(docWithAnnotatedEdits, this._store); + const reporter = this._instantiationService.createInstance(ArcTelemetryReporter, docWithJustReason, scmRepoBridge, singleEdit.toEdit(), res => { + + res.telemetryService.publicLog2<{ + extensionId: string; + opportunityId: string; + didBranchChange: number; + timeDelayMs: number; + arc: number; + originalCharCount: number; + }, { + owner: 'hediet'; + comment: 'Reports the accepted and retained character count for an inline completion/edit.'; + + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id (copilot or copilot-chat); which provided this inline completion.' }; + opportunityId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Unique identifier for an opportunity to show an inline completion or NES.' }; + + didBranchChange: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Indicates if the branch changed in the meantime. If the branch changed (value is 1); this event should probably be ignored.' }; + timeDelayMs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The time delay between the user accepting the edit and measuring the survival rate.' }; + arc: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The accepted and restrained character count.' }; + originalCharCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The original character count before any edits.' }; + }>('editTelemetry.reportInlineEditArc', { + extensionId: data.$extensionId ?? '', + opportunityId: data.$$requestUuid ?? 'unknown', + didBranchChange: res.didBranchChange ? 1 : 0, + timeDelayMs: res.timeDelayMs, + arc: res.arc, + originalCharCount: res.originalCharCount, + }); + }); + + this._register(toDisposable(() => { + reporter.cancel(); + })); + })); + } +} + +export interface EditTelemetryData { + telemetryService: ITelemetryService; + timeDelayMs: number; + didBranchChange: boolean; + arc: number; + originalCharCount: number; +} + +export class ArcTelemetryReporter { + private readonly _store = new DisposableStore(); + private readonly _arcTracker; + private readonly _initialBranchName: string | undefined; + + constructor( + private readonly _document: { value: IObservableWithChange }, + // _markedEdits -> document.value + private readonly _gitRepo: ScmRepoBridge | undefined, + private readonly _trackedEdit: BaseStringEdit, + private readonly _sendTelemetryEvent: (res: EditTelemetryData) => void, + + @ITelemetryService private readonly _telemetryService: ITelemetryService + ) { + this._arcTracker = new ArcTracker(this._document.value.get().value, this._trackedEdit); + + this._store.add(runOnChange(this._document.value, (_val, _prevVal, changes) => { + const edit = BaseStringEdit.composeOrUndefined(changes.map(c => c.edit)); + if (edit) { + this._arcTracker.handleEdits(edit); + } + })); + + this._initialBranchName = this._gitRepo?.headBranchNameObs.get(); + + // This aligns with github inline completions + this._reportAfter(30 * 1000); + this._reportAfter(120 * 1000); + this._reportAfter(300 * 1000); + this._reportAfter(600 * 1000); + // track up to 15min to allow for slower edit responses from legacy SD endpoint + this._reportAfter(900 * 1000, () => { + this._store.dispose(); + }); + } + + private _reportAfter(timeoutMs: number, cb?: () => void) { + const timer = new TimeoutTimer(() => { + this._report(timeoutMs); + timer.dispose(); + if (cb) { + cb(); + } + }, timeoutMs); + this._store.add(timer); + } + + private _report(timeMs: number): void { + const currentBranch = this._gitRepo?.headBranchNameObs.get(); + const didBranchChange = currentBranch !== this._initialBranchName; + + this._sendTelemetryEvent({ + telemetryService: this._telemetryService, + timeDelayMs: timeMs, + didBranchChange, + arc: this._arcTracker.getAcceptedRestrainedCharactersCount(), + originalCharCount: this._arcTracker.getOriginalCharacterCount(), + }); + } + + public cancel(): void { + this._store.dispose(); + } +} + +function sumByCategory(items: readonly T[], getValue: (item: T) => number, getCategory: (item: T) => TCategory): Record { + return items.reduce((acc, item) => { + const category = getCategory(item); + acc[category] = (acc[category] || 0) + getValue(item); + return acc; + }, {} as any as Record); +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts new file mode 100644 index 00000000000..4e9310cb8e8 --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { EditTelemetryService } from './editTelemetryService.js'; +import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { localize } from '../../../../nls.js'; +import { EDIT_TELEMETRY_SETTING_ID, EDIT_TELEMETRY_SHOW_DECORATIONS, EDIT_TELEMETRY_SHOW_STATUS_BAR } from './settings.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; + +registerWorkbenchContribution2('EditTelemetryService', EditTelemetryService, WorkbenchPhase.AfterRestored); + +const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); +configurationRegistry.registerConfiguration({ + id: 'task', + order: 100, + title: localize('editTelemetry', "Edit Telemetry"), + type: 'object', + properties: { + [EDIT_TELEMETRY_SETTING_ID]: { + markdownDescription: localize('telemetry.editStats.enabled', "Controls whether to enable telemetry for edit statistics (only sends statistics if general telemetry is enabled)."), + type: 'boolean', + default: true, + tags: ['experimental'], + }, + [EDIT_TELEMETRY_SHOW_STATUS_BAR]: { + markdownDescription: localize('telemetry.editStats.showStatusBar', "Controls whether to show the status bar for edit telemetry."), + type: 'boolean', + default: false, + tags: ['experimental'], + }, + [EDIT_TELEMETRY_SHOW_DECORATIONS]: { + markdownDescription: localize('telemetry.editStats.showDecorations', "Controls whether to show decorations for edit telemetry."), + type: 'boolean', + default: false, + tags: ['experimental'], + }, + } +}); diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetryService.ts b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetryService.ts new file mode 100644 index 00000000000..e77b9a3cb78 --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetryService.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../base/common/observable.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { ITelemetryService, TelemetryLevel, telemetryLevelEnabled } from '../../../../platform/telemetry/common/telemetry.js'; +import { EditTrackingFeature } from './editSourceTrackingFeature.js'; +import { EDIT_TELEMETRY_SETTING_ID } from './settings.js'; +import { VSCodeWorkspace } from './vscodeObservableWorkspace.js'; + +export class EditTelemetryService extends Disposable { + private readonly _editSourceTrackingEnabled; + + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + ) { + super(); + + this._editSourceTrackingEnabled = observableConfigValue(EDIT_TELEMETRY_SETTING_ID, true, this._configurationService); + + this._register(autorun(r => { + const enabled = this._editSourceTrackingEnabled.read(r); + if (!enabled || !telemetryLevelEnabled(this._telemetryService, TelemetryLevel.USAGE)) { + return; + } + + const workspace = this._instantiationService.createInstance(VSCodeWorkspace); + + r.store.add(this._instantiationService.createInstance(EditTrackingFeature, workspace)); + })); + } +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editTracker.ts b/src/vs/workbench/contrib/editTelemetry/browser/editTracker.ts new file mode 100644 index 00000000000..6311aee4ee4 --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/editTracker.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { observableSignal, runOnChange, IReader } from '../../../../base/common/observable.js'; +import { AnnotatedStringEdit } from '../../../../editor/common/core/edits/stringEdit.js'; +import { OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js'; +import { IDocumentWithAnnotatedEdits, EditSourceData, EditSource } from './documentWithAnnotatedEdits.js'; + +/** + * Tracks a single document. +*/ +export class DocumentEditSourceTracker extends Disposable { + private _edits: AnnotatedStringEdit = AnnotatedStringEdit.empty; + private _pendingExternalEdits: AnnotatedStringEdit = AnnotatedStringEdit.empty; + + private readonly _update = observableSignal(this); + + constructor( + private readonly _doc: IDocumentWithAnnotatedEdits, + public readonly data: T, + ) { + super(); + + this._register(runOnChange(this._doc.value, (_val, _prevVal, edits) => { + const eComposed = AnnotatedStringEdit.compose(edits.map(e => e.edit)); + if (eComposed.replacements.every(e => e.data.source.category === 'external')) { + if (this._edits.isEmpty()) { + // Ignore initial external edits + } else { + // queue pending external edits + this._pendingExternalEdits = this._pendingExternalEdits.compose(eComposed); + } + } else { + if (!this._pendingExternalEdits.isEmpty()) { + this._edits = this._edits.compose(this._pendingExternalEdits); + this._pendingExternalEdits = AnnotatedStringEdit.empty; + } + this._edits = this._edits.compose(eComposed); + } + + this._update.trigger(undefined); + })); + } + + async waitForQueue(): Promise { + await this._doc.waitForQueue(); + } + + getTrackedRanges(reader?: IReader): TrackedEdit[] { + this._update.read(reader); + const ranges = this._edits.getNewRanges(); + return ranges.map((r, idx) => { + const e = this._edits.replacements[idx]; + const reason = e.data.source; + const te = new TrackedEdit(e.replaceRange, r, reason, e.data.key); + return te; + }); + } + + isEmpty(): boolean { + return this._edits.isEmpty(); + } + + public reset(): void { + this._edits = AnnotatedStringEdit.empty; + } + + public _getDebugVisualization() { + const ranges = this.getTrackedRanges(); + const txt = this._doc.value.get().value; + + return { + ...{ $fileExtension: 'text.w' }, + 'value': txt, + 'decorations': ranges.map(r => { + return { + range: [r.range.start, r.range.endExclusive], + color: r.source.getColor(), + }; + }) + }; + } +} + +export class TrackedEdit { + constructor( + public readonly originalRange: OffsetRange, + public readonly range: OffsetRange, + public readonly source: EditSource, + public readonly sourceKey: string, + ) { } +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/observableWorkspace.ts b/src/vs/workbench/contrib/editTelemetry/browser/observableWorkspace.ts new file mode 100644 index 00000000000..537b9e00ccf --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/observableWorkspace.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IObservableWithChange, derivedHandleChanges, derivedWithStore, observableValue, autorunWithStore, runOnChange, IObservable } from '../../../../base/common/observable.js'; +import { URI } from '../../../../base/common/uri.js'; +import { StringEdit } from '../../../../editor/common/core/edits/stringEdit.js'; +import { StringText } from '../../../../editor/common/core/text/abstractText.js'; +import { TextModelEditReason } from '../../../../editor/common/textModelEditReason.js'; + +export abstract class ObservableWorkspace { + abstract get documents(): IObservableWithChange; + + + getFirstOpenDocument(): IObservableDocument | undefined { + return this.documents.get()[0]; + } + + getDocument(documentId: URI): IObservableDocument | undefined { + return this.documents.get().find(d => d.uri.toString() === documentId.toString()); + } + + private _version = 0; + + /** + * Is fired when any open document changes. + */ + public readonly onDidOpenDocumentChange = derivedHandleChanges({ + owner: this, + changeTracker: { + createChangeSummary: () => ({ didChange: false }), + handleChange: (ctx, changeSummary) => { + if (!ctx.didChange(this.documents)) { + changeSummary.didChange = true; // A document changed + } + return true; + } + } + }, (reader, changeSummary) => { + const docs = this.documents.read(reader); + for (const d of docs) { + d.value.read(reader); // add dependency + } + if (changeSummary.didChange) { + this._version++; // to force a change + } + return this._version; + + // TODO@hediet make this work: + /* + const docs = this.openDocuments.read(reader); + for (const d of docs) { + if (reader.readChangesSinceLastRun(d.value).length > 0) { + reader.reportChange(d); + } + } + return undefined; + */ + }); + + public readonly lastActiveDocument = derivedWithStore((_reader, store) => { + const obs = observableValue('lastActiveDocument', undefined as IObservableDocument | undefined); + store.add(autorunWithStore((reader, store) => { + const docs = this.documents.read(reader); + for (const d of docs) { + store.add(runOnChange(d.value, () => { + obs.set(d, undefined); + })); + } + })); + return obs; + }).flatten(); +} + +export interface IObservableDocument { + readonly uri: URI; + readonly value: IObservableWithChange; + + /** + * Increases whenever the value changes. Is also used to reference document states from the past. + */ + readonly version: IObservable; + readonly languageId: IObservable; +} + +export class StringEditWithReason extends StringEdit { + constructor( + replacements: StringEdit['replacements'], + public readonly reason: TextModelEditReason, + ) { + super(replacements); + } +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/settings.ts b/src/vs/workbench/contrib/editTelemetry/browser/settings.ts new file mode 100644 index 00000000000..e337e5e734f --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/settings.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const EDIT_TELEMETRY_SETTING_ID = 'telemetry.editStats.enabled'; +export const EDIT_TELEMETRY_SHOW_DECORATIONS = 'telemetry.editStats.showDecorations'; +export const EDIT_TELEMETRY_SHOW_STATUS_BAR = 'telemetry.editStats.showStatusBar'; diff --git a/src/vs/workbench/contrib/editTelemetry/browser/vscodeObservableWorkspace.ts b/src/vs/workbench/contrib/editTelemetry/browser/vscodeObservableWorkspace.ts new file mode 100644 index 00000000000..ea0afcac958 --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/vscodeObservableWorkspace.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { onUnexpectedError } from '../../../../base/common/errors.js'; +import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { derived, IObservable, IObservableWithChange, mapObservableArrayCached, observableSignalFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; +import { isDefined } from '../../../../base/common/types.js'; +import { URI } from '../../../../base/common/uri.js'; +import { StringText } from '../../../../editor/common/core/text/abstractText.js'; +import { ITextModel } from '../../../../editor/common/model.js'; +import { offsetEditFromContentChanges } from '../../../../editor/common/model/textModelStringEdit.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { IObservableDocument, ObservableWorkspace, StringEditWithReason } from './observableWorkspace.js'; + +export class VSCodeWorkspace extends ObservableWorkspace implements IDisposable { + private readonly _documents; + public get documents() { return this._documents; } + + private readonly _store = new DisposableStore(); + + constructor( + @IModelService private readonly _textModelService: IModelService, + ) { + super(); + + const onModelAdded = observableSignalFromEvent(this, this._textModelService.onModelAdded); + const onModelRemoved = observableSignalFromEvent(this, this._textModelService.onModelRemoved); + + const models = derived(this, reader => { + onModelAdded.read(reader); + onModelRemoved.read(reader); + const models = this._textModelService.getModels(); + return models; + }); + + const documents = mapObservableArrayCached(this, models, (m, store) => { + if (m.isTooLargeForSyncing()) { + return undefined; + } + return store.add(new VSCodeDocument(m)); + }).recomputeInitiallyAndOnChange(this._store).map(d => d.filter(isDefined)); + + this._documents = documents; + } + + dispose(): void { + this._store.dispose(); + } +} + +export class VSCodeDocument extends Disposable implements IObservableDocument { + get uri(): URI { return this.textModel.uri; } + private readonly _value; + private readonly _version; + private readonly _languageId; + get value(): IObservableWithChange { return this._value; } + get version(): IObservable { return this._version; } + get languageId(): IObservable { return this._languageId; } + + constructor( + public readonly textModel: ITextModel, + ) { + super(); + + this._value = observableValue(this, new StringText(this.textModel.getValue())); + this._version = observableValue(this, this.textModel.getVersionId()); + this._languageId = observableValue(this, this.textModel.getLanguageId()); + + this._register(this.textModel.onDidChangeContent((e) => { + transaction(tx => { + const edit = offsetEditFromContentChanges(e.changes); + if (e.detailedReasons.length !== 1) { + onUnexpectedError(new Error(`Unexpected number of detailed reasons: ${e.detailedReasons.length}`)); + } + + const change = new StringEditWithReason(edit.replacements, e.detailedReasons[0]); + + this._value.set(new StringText(this.textModel.getValue()), tx, change); + this._version.set(this.textModel.getVersionId(), tx); + }); + })); + + this._register(this.textModel.onDidChangeLanguage(e => { + transaction(tx => { + this._languageId.set(this.textModel.getLanguageId(), tx); + }); + })); + } +} diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 27d5c5a4b13..750087f2e72 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -417,6 +417,8 @@ import './contrib/inlineCompletions/browser/inlineCompletions.contribution.js'; // Drop or paste into import './contrib/dropOrPasteInto/browser/dropOrPasteInto.contribution.js'; +// Edit Telemetry +import './contrib/editTelemetry/browser/editTelemetry.contribution.js'; //#endregion From e82ec194fef22a8d228e71a948f761b0b3705eef Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 3 Jul 2025 12:33:46 +0200 Subject: [PATCH 075/306] Fixes CI --- src/vs/base/common/observableInternal/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/common/observableInternal/index.ts b/src/vs/base/common/observableInternal/index.ts index 07967b03f25..635d25d4ec2 100644 --- a/src/vs/base/common/observableInternal/index.ts +++ b/src/vs/base/common/observableInternal/index.ts @@ -27,7 +27,7 @@ export { observableFromEventOpts } from './observables/observableFromEvent.js'; export { observableSignalFromEvent } from './observables/observableSignalFromEvent.js'; export { asyncTransaction, globalTransaction, subtransaction, transaction, TransactionImpl } from './transaction.js'; export { observableFromValueWithChangeEvent, ValueWithChangeEventFromObservable } from './utils/valueWithChangeEvent.js'; -export { runOnChange, runOnChangeWithCancellationToken, runOnChangeWithStore, RemoveUndefined } from './utils/runOnChange.js'; +export { runOnChange, runOnChangeWithCancellationToken, runOnChangeWithStore, type RemoveUndefined } from './utils/runOnChange.js'; export { derivedConstOnceDefined, latestChangedValue } from './experimental/utils.js'; export { observableFromEvent } from './observables/observableFromEvent.js'; export { observableValue } from './observables/observableValue.js'; From 072c5317789c00bcb3b00ee8d9337f3659d99a92 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 3 Jul 2025 16:03:52 +0200 Subject: [PATCH 076/306] update distro --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5501dd89e1e..dea32398691 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.102.0", - "distro": "969a2e84edcb47f53fbc4f8aa419dc7c062c71cf", + "distro": "f79d65a2e4f2caf1099ed08b494763e63710c2bc", "author": { "name": "Microsoft Corporation" }, From c422a55de78704302c3f2706923accdb78332039 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 3 Jul 2025 16:24:58 +0200 Subject: [PATCH 077/306] Moves ARC telemetry tracking to core (#253918) From 9bfe176dd2d7bb31e631a26e71169532c9b06144 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 3 Jul 2025 17:55:21 +0200 Subject: [PATCH 078/306] resolving links does not work on remote (#253677) * resolving links does not work on remote * remove old comment * fix compile error * mock IWorkbenchEnvironmentService --- .../promptPathAutocompletion.ts | 10 +- .../promptSyntax/parsers/basePromptParser.ts | 93 +++++-------------- .../promptSyntax/parsers/filePromptParser.ts | 6 +- .../promptSyntax/parsers/promptParser.ts | 6 +- .../parsers/textModelPromptParser.ts | 6 +- .../service/promptsServiceImpl.ts | 6 +- .../parsers/textModelPromptParser.test.ts | 5 +- .../promptSyntax/promptFileReference.test.ts | 2 +- .../service/promptsService.test.ts | 2 + 9 files changed, 43 insertions(+), 93 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptPathAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptPathAutocompletion.ts index 8470df895c9..f61e625523f 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptPathAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptPathAutocompletion.ts @@ -17,7 +17,7 @@ import { IPromptsService } from '../service/promptsService.js'; import { URI } from '../../../../../../base/common/uri.js'; import { isOneOf } from '../../../../../../base/common/types.js'; -import { extUri } from '../../../../../../base/common/resources.js'; +import { dirname, extUri } from '../../../../../../base/common/resources.js'; import { ITextModel } from '../../../../../../editor/common/model.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { CancellationError } from '../../../../../../base/common/errors.js'; @@ -152,13 +152,7 @@ export class PromptPathAutocompletion extends Disposable implements CompletionIt return undefined; } - const { parentFolder } = parser; - - // if didn't find a folder URI to start the suggestions from, - // don't provide any suggestions - if (parentFolder === null) { - return undefined; - } + const parentFolder = dirname(parser.uri); // in the case of the '.' trigger character, we must check if this is the first // dot in the link path, otherwise the dot could be a part of a folder name diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index e900e1ce2b8..a5bb792f5ba 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -23,7 +23,7 @@ import type { IPromptContentsProvider } from '../contentProviders/types.js'; import type { TPromptReference, ITopError } from './types.js'; import { type IDisposable } from '../../../../../../base/common/lifecycle.js'; import { assert, assertNever } from '../../../../../../base/common/assert.js'; -import { basename, dirname } from '../../../../../../base/common/resources.js'; +import { basename, dirname, joinPath } from '../../../../../../base/common/resources.js'; import { BaseToken } from '../codecs/base/baseToken.js'; import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; import { type IRange, Range } from '../../../../../../editor/common/core/range.js'; @@ -31,7 +31,6 @@ import { PromptHeader, type TPromptMetadata } from './promptHeader/promptHeader. import { ObservableDisposable } from '../utils/observableDisposable.js'; import { INSTRUCTIONS_LANGUAGE_ID, MODE_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../promptTypes.js'; import { LinesDecoder } from '../codecs/base/linesCodec/linesDecoder.js'; -import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { MarkdownLink } from '../codecs/base/markdownCodec/tokens/markdownLink.js'; import { MarkdownToken } from '../codecs/base/markdownCodec/tokens/markdownToken.js'; @@ -39,17 +38,13 @@ import { FrontMatterHeader } from '../codecs/base/markdownExtensionsCodec/tokens import { OpenFailed, NotPromptFile, RecursiveReference, FolderReference, ResolveError } from '../../promptFileReferenceErrors.js'; import { type IPromptContentsProviderOptions } from '../contentProviders/promptContentsProviderBase.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js'; +import { Schemas } from '../../../../../../base/common/network.js'; /** * Options of the {@link BasePromptParser} class. */ export interface IBasePromptParserOptions { - /** - * List of reference paths have been already seen before - * getting to the current prompt. Used to prevent infinite - * recursion in prompt file references. - */ - readonly seenReferences: readonly string[]; } export type IPromptParserOptions = IBasePromptParserOptions & IPromptContentsProviderOptions; @@ -66,8 +61,7 @@ export type TErrorCondition = OpenFailed | RecursiveReference | FolderReference */ export class BasePromptParser extends ObservableDisposable { /** - * Options passed to the constructor, extended with - * value defaults from {@link DEFAULT_OPTIONS}. + * Options passed to the constructor. */ protected readonly options: IBasePromptParserOptions; @@ -244,40 +238,17 @@ export class BasePromptParser private readonly promptContentsProvider: TContentsProvider, options: IBasePromptParserOptions, @IInstantiationService protected readonly instantiationService: IInstantiationService, - @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + @IWorkbenchEnvironmentService private readonly envService: IWorkbenchEnvironmentService, @ILogService protected readonly logService: ILogService, ) { super(); this.options = options; - const seenReferences = [...this.options.seenReferences]; - - // to prevent infinite file recursion, we keep track of all references in - // the current branch of the file reference tree and check if the current - // file reference has been already seen before - if (seenReferences.includes(this.uri.path)) { - seenReferences.push(this.uri.path); - - this._errorCondition = new RecursiveReference( - this.uri, - seenReferences, - ); - this._onUpdate.fire(); - this.firstParseResult.end(); - - return this; - } - - // we don't care if reading the file fails below, hence can add the path - // of the current reference to the `seenReferences` set immediately, - - // even if the file doesn't exist, we would never end up in the recursion - seenReferences.push(this.uri.path); - this._register( this.promptContentsProvider.onContentChanged((streamOrError) => { // process the received message - this.onContentsChanged(streamOrError, seenReferences); + this.onContentsChanged(streamOrError); // indicate that we've received at least one `onContentChanged` event this.firstParseResult.end(); @@ -306,8 +277,7 @@ export class BasePromptParser * references recursion. */ private onContentsChanged( - streamOrError: VSBufferReadableStream | ResolveError, - seenReferences: string[], + streamOrError: VSBufferReadableStream | ResolveError ): void { // dispose and cleanup the previously received stream // object or an error condition, if any received yet @@ -363,7 +333,7 @@ export class BasePromptParser // try to convert a prompt variable with data token into a file reference if (token instanceof PromptVariableWithData) { try { - this.handleLinkToken(FileReference.from(token), [...seenReferences]); + this.handleLinkToken(FileReference.from(token)); } catch (error) { // the `FileReference.from` call might throw if the `PromptVariableWithData` token // can not be converted into a valid `#file` reference, hence we ignore the error @@ -373,7 +343,7 @@ export class BasePromptParser // note! the `isURL` is a simple check and needs to be improved to truly // handle only file references, ignoring broken URLs or references if (token instanceof MarkdownLink && !token.isURL) { - this.handleLinkToken(token, [...seenReferences]); + this.handleLinkToken(token); } }); @@ -417,16 +387,20 @@ export class BasePromptParser /** * Handle a new reference token inside prompt contents. */ - private handleLinkToken( - token: FileReference | MarkdownLink, - seenReferences: string[], - ): this { - const { parentFolder } = this; - - const referenceUri = ((parentFolder !== null) && (path.isAbsolute(token.path) === false)) - ? URI.joinPath(parentFolder, token.path) - : URI.file(token.path); + private handleLinkToken(token: FileReference | MarkdownLink): this { + let referenceUri: URI; + if (path.isAbsolute(token.path)) { + referenceUri = URI.file(token.path); + if (this.envService.remoteAuthority) { + referenceUri = referenceUri.with({ + scheme: Schemas.vscodeRemote, + authority: this.envService.remoteAuthority, + }); + } + } else { + referenceUri = joinPath(dirname(this.uri), token.path); + } this._references.push(new PromptReference(referenceUri, token)); this._onUpdate.fire(); @@ -504,29 +478,6 @@ export class BasePromptParser return this.promptContentsProvider.uri; } - /** - * Get the parent folder URI of the prompt. - * For instance, if prompt URI points to a file on a disk, this - * function will return the folder URI that contains that file, - * but if the URI points to an `untitled` document, will try to - * use a different folder URI based on the workspace state. - */ - public get parentFolder(): URI | null { - if (this.uri.scheme === 'file') { - return dirname(this.uri); - } - - const { folders } = this.workspaceService.getWorkspace(); - - // single-root workspace, use root folder URI - if (folders.length === 1) { - return folders[0].uri; - } - - // if a multi-root workspace, or no workspace at all - return null; - } - /** * Get a list of immediate child references of the prompt. */ diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts index a79ef3b6ec2..c73efe10884 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts @@ -7,8 +7,8 @@ import { URI } from '../../../../../../base/common/uri.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { BasePromptParser, IPromptParserOptions } from './basePromptParser.js'; import { FilePromptContentProvider } from '../contentProviders/filePromptContentsProvider.js'; -import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js'; /** * Class capable of parsing prompt syntax out of a provided file, @@ -19,11 +19,11 @@ export class FilePromptParser extends BasePromptParser { @ILogService logService: ILogService, @IModelService modelService: IModelService, @IInstantiationService instaService: IInstantiationService, - @IWorkspaceContextService workspaceService: IWorkspaceContextService, + @IWorkbenchEnvironmentService envService: IWorkbenchEnvironmentService, ) { const contentsProvider = getContentsProvider(uri, options, modelService, instaService); @@ -54,7 +54,7 @@ export class PromptParser extends BasePromptParser { contentsProvider, options, instaService, - workspaceService, + envService, logService, ); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts index 17f4716d628..bb2b7060abb 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts @@ -7,8 +7,8 @@ import { ITextModel } from '../../../../../../editor/common/model.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { BasePromptParser, IPromptParserOptions } from './basePromptParser.js'; import { TextModelContentsProvider } from '../contentProviders/textModelContentsProvider.js'; -import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js'; /** * Class capable of parsing prompt syntax out of a provided text model, @@ -19,7 +19,7 @@ export class TextModelPromptParser extends BasePromptParser { instantiationService = disposables.add(new TestInstantiationService()); instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(IFileService, disposables.add(instantiationService.createInstance(FileService))); + instantiationService.stub(IWorkbenchEnvironmentService, {}); }); /** diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts index ab4b302e8d8..d810ed8299b 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts @@ -104,7 +104,7 @@ class TestPromptFileReference extends Disposable { this.instantiationService.createInstance( FilePromptParser, this.rootFileUri, - { seenReferences: [], allowNonPromptFiles: true, languageId: undefined }, + { allowNonPromptFiles: true, languageId: undefined }, ), ).start(); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index b7d38ee3c51..eff89d6cca3 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -34,6 +34,7 @@ import { ILabelService } from '../../../../../../../platform/label/common/label. import { ComputeAutomaticInstructions } from '../../../../common/promptSyntax/computeAutomaticInstructions.js'; import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; import { ResourceSet } from '../../../../../../../base/common/map.js'; +import { IWorkbenchEnvironmentService } from '../../../../../../services/environment/common/environmentService.js'; /** * Helper class to assert the properties of a link. @@ -113,6 +114,7 @@ suite('PromptsService', () => { instaService.stub(ILogService, new NullLogService()); instaService.stub(IWorkspacesService, {}); instaService.stub(IConfigurationService, new TestConfigurationService()); + instaService.stub(IWorkbenchEnvironmentService, {}); const fileService = disposables.add(instaService.createInstance(FileService)); instaService.stub(IFileService, fileService); From cf11f45c017275f45a66315dcf73dbfb66c77c8e Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 3 Jul 2025 17:55:37 +0200 Subject: [PATCH 079/306] prompt url handler fixes (#253708) --- .../browser/promptSyntax/promptUrlHandler.ts | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptUrlHandler.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptUrlHandler.ts index 9c40e9f90a0..436171fb529 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptUrlHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptUrlHandler.ts @@ -24,6 +24,8 @@ import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.j import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { Schemas } from '../../../../../base/common/network.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { IHostService } from '../../../../services/host/browser/host.js'; +import { mainWindow } from '../../../../../base/browser/window.js'; // example URL: code-oss:chat-prompt/install?url=https://gist.githubusercontent.com/aeschli/43fe78babd5635f062aef0195a476aad/raw/dfd71f60058a4dd25f584b55de3e20f5fd580e63/filterEvenNumbers.prompt.md @@ -43,6 +45,7 @@ export class PromptUrlHandler extends Disposable implements IWorkbenchContributi @ILogService private readonly logService: ILogService, @IDialogService private readonly dialogService: IDialogService, @IStorageService private readonly storageService: IStorageService, + @IHostService private readonly hostService: IHostService, ) { super(); this._register(urlService.registerHandler(this)); @@ -67,37 +70,39 @@ export class PromptUrlHandler extends Disposable implements IWorkbenchContributi try { const query = decodeURIComponent(uri.query); if (!query || !query.startsWith('url=')) { - return false; + return true; } const urlString = query.substring(4); const url = URI.parse(urlString); if (url.scheme !== Schemas.https && url.scheme !== Schemas.http) { this.logService.error(`[PromptUrlHandler] Invalid URL: ${urlString}`); - return false; + return true; } + await this.hostService.focus(mainWindow); + if (await this.shouldBlockInstall(promptType, url)) { - return false; + return true; } const result = await this.requestService.request({ type: 'GET', url: urlString }, CancellationToken.None); if (result.res.statusCode !== 200) { this.logService.error(`[PromptUrlHandler] Failed to fetch URL: ${urlString}`); this.notificationService.error(localize('failed', 'Failed to fetch URL: {0}', urlString)); - return false; + return true; } const responseData = (await streamToBuffer(result.stream)).toString(); const newFolder = await this.instantiationService.invokeFunction(askForPromptSourceFolder, promptType); if (!newFolder) { - return false; + return true; } const newName = await this.instantiationService.invokeFunction(askForPromptFileName, promptType, newFolder.uri, getCleanPromptName(url)); if (!newName) { - return false; + return true; } const promptUri = URI.joinPath(newFolder.uri, newName); @@ -110,7 +115,7 @@ export class PromptUrlHandler extends Disposable implements IWorkbenchContributi } catch (error) { this.logService.error(`Error handling prompt URL ${uri.toString()}`, error); - return false; + return true; } } @@ -129,13 +134,12 @@ export class PromptUrlHandler extends Disposable implements IWorkbenchContributi const detail = new MarkdownString('', { supportHtml: true }); detail.appendMarkdown(localize('confirmOpenDetail2', "This will access {0}.\n\n", `[${uriLabel}](${url.toString()})`)); - detail.appendMarkdown(localize('confirmOpenDetail1', "Do you want to continue by selecting a destination folder and name?\n\n")); detail.appendMarkdown(localize('confirmOpenDetail3', "If you did not initiate this request, it may represent an attempted attack on your system. Unless you took an explicit action to initiate this request, you should press 'Cancel'")); let message: string; switch (promptType) { case PromptsType.prompt: - message = localize('confirmInstallPrompt', "An external application wants to create a prompt file with content from a URL."); + message = localize('confirmInstallPrompt', "An external application wants to create a prompt file with content from a URL. Do you want to continue by selecting a destination folder and name?"); break; case PromptsType.instructions: message = localize('confirmInstallInstructions', "An external application wants to create an instructions file with content from a URL."); @@ -147,9 +151,10 @@ export class PromptUrlHandler extends Disposable implements IWorkbenchContributi const { confirmed, checkboxChecked } = await this.dialogService.confirm({ type: 'warning', - primaryButton: localize({ key: 'confirmOpenButton', comment: ['&& denotes a mnemonic'] }, "&&Continue"), + primaryButton: localize({ key: 'yesButton', comment: ['&& denotes a mnemonic'] }, "&&Yes"), + cancelButton: localize('noButton', "No"), message, - checkbox: { label: localize('confirmOpenDoNotAskAgain', "Do not show this message again for files from '{0}'", location) }, + checkbox: { label: localize('confirmOpenDoNotAskAgain', "Allow creating a prompt file without asking from '{0}'", location) }, custom: { markdownDetails: [{ markdown: detail From b2d86aa98d0c2307c98dafb1a6f372fe67fa3e95 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 3 Jul 2025 11:55:59 -0400 Subject: [PATCH 080/306] rm testing command (#253889) fix #253032 --- .../browser/terminal.suggest.contribution.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index f7727e42202..40ef5072964 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -306,20 +306,6 @@ registerTerminalAction({ } }); -registerActiveInstanceAction({ - id: TerminalSuggestCommandId.ResetDiscoverability, - title: localize2('workbench.action.terminal.resetDiscoverability', 'Reset Suggest Discoverability'), - f1: true, - precondition: ContextKeyExpr.and( - ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - TerminalContextKeys.isOpen, - ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.Enabled}`, true) - ), - run: (activeInstance) => { - TerminalSuggestContribution.get(activeInstance)?.addon?.resetDiscoverability(); - } -}); - registerActiveInstanceAction({ id: TerminalSuggestCommandId.RequestCompletions, title: localize2('workbench.action.terminal.requestCompletions', 'Request Completions'), From cc1eecb9f03971791fdd00720c5c4c46dd98b475 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 3 Jul 2025 17:56:13 +0200 Subject: [PATCH 081/306] fix #253187 (#253927) --- src/vs/platform/mcp/common/mcpManagementService.ts | 8 ++++---- .../browser/userDataSyncWorkbenchService.ts | 12 +----------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/vs/platform/mcp/common/mcpManagementService.ts b/src/vs/platform/mcp/common/mcpManagementService.ts index 34239c0fd25..566c43ead1e 100644 --- a/src/vs/platform/mcp/common/mcpManagementService.ts +++ b/src/vs/platform/mcp/common/mcpManagementService.ts @@ -82,16 +82,16 @@ export abstract class AbstractMcpResourceManagementService extends Disposable im private initialize(): Promise { if (!this.initializePromise) { this.initializePromise = (async () => { - this.local = await this.populateLocalServer(); + this.local = await this.populateLocalServers(); this.startWatching(); })(); } return this.initializePromise; } - private async populateLocalServer(): Promise> { + private async populateLocalServers(): Promise> { + this.logService.trace('AbstractMcpResourceManagementService#populateLocalServers', this.mcpResource.toString()); const local = new Map(); - this.logService.info('MCP Management Service: fetchInstalled', this.mcpResource.toString()); try { const scannedMcpServers = await this.mcpResourceScannerService.scanMcpServers(this.mcpResource, this.target); if (scannedMcpServers.servers) { @@ -118,7 +118,7 @@ export abstract class AbstractMcpResourceManagementService extends Disposable im protected async updateLocal(): Promise { try { - const current = await this.populateLocalServer(); + const current = await this.populateLocalServers(); const added: ILocalMcpServer[] = []; const updated: ILocalMcpServer[] = []; diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index e8abfebf49e..28905f3c93f 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -258,9 +258,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat let value: { token: string; authenticationProviderId: string } | undefined = undefined; if (current) { try { - this.logService.trace('Settings Sync: Updating the token for the account', current.accountName); const token = current.token; - this.traceOrInfo('Settings Sync: Token updated for the account', current.accountName); value = { token, authenticationProviderId: current.authenticationProviderId }; } catch (e) { this.logService.error(e); @@ -269,19 +267,11 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat await this.userDataSyncAccountService.updateAccount(value); } - private traceOrInfo(msg: string, ...args: any[]): void { - if (this.environmentService.isBuilt) { - this.logService.info(msg, ...args); - } else { - this.logService.trace(msg, ...args); - } - } - private updateAccountStatus(accountStatus: AccountStatus): void { this.logService.trace(`Settings Sync: Updating the account status to ${accountStatus}`); if (this._accountStatus !== accountStatus) { const previous = this._accountStatus; - this.traceOrInfo(`Settings Sync: Account status changed from ${previous} to ${accountStatus}`); + this.logService.info(`Settings Sync: Account status changed from ${previous} to ${accountStatus}`); this._accountStatus = accountStatus; this.accountStatusContext.set(accountStatus); From c6f916e443c79259a99c040d8021e7ea470abe42 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 3 Jul 2025 17:56:30 +0200 Subject: [PATCH 082/306] Moves ARC telemetry tracking to core (#253919) --- .../base/common/observableInternal/index.ts | 2 +- .../browser/services/editorWorkerService.ts | 13 + src/vs/editor/common/core/edits/edit.ts | 20 +- src/vs/editor/common/core/edits/stringEdit.ts | 374 +++++++++++---- .../common/core/text/positionToOffset.ts | 5 + src/vs/editor/common/diff/rangeMapping.ts | 11 + .../editor/common/services/editorWebWorker.ts | 21 + src/vs/editor/common/services/editorWorker.ts | 3 + .../services/testEditorWorkerService.ts | 5 + src/vs/platform/telemetry/common/telemetry.ts | 4 + .../editTelemetry/browser/arcTracker.ts | 62 +++ .../browser/documentWithAnnotatedEdits.ts | 424 ++++++++++++++++ .../browser/editSourceTrackingFeature.ts | 241 ++++++++++ .../browser/editSourceTrackingImpl.ts | 451 ++++++++++++++++++ .../browser/editTelemetry.contribution.ts | 41 ++ .../browser/editTelemetryService.ts | 39 ++ .../editTelemetry/browser/editTracker.ts | 96 ++++ .../browser/observableWorkspace.ts | 94 ++++ .../contrib/editTelemetry/browser/settings.ts | 8 + .../browser/vscodeObservableWorkspace.ts | 91 ++++ src/vs/workbench/workbench.common.main.ts | 2 + 21 files changed, 1922 insertions(+), 85 deletions(-) create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/arcTracker.ts create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/documentWithAnnotatedEdits.ts create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingFeature.ts create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/editTelemetryService.ts create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/editTracker.ts create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/observableWorkspace.ts create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/settings.ts create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/vscodeObservableWorkspace.ts diff --git a/src/vs/base/common/observableInternal/index.ts b/src/vs/base/common/observableInternal/index.ts index df8df29a80c..635d25d4ec2 100644 --- a/src/vs/base/common/observableInternal/index.ts +++ b/src/vs/base/common/observableInternal/index.ts @@ -27,7 +27,7 @@ export { observableFromEventOpts } from './observables/observableFromEvent.js'; export { observableSignalFromEvent } from './observables/observableSignalFromEvent.js'; export { asyncTransaction, globalTransaction, subtransaction, transaction, TransactionImpl } from './transaction.js'; export { observableFromValueWithChangeEvent, ValueWithChangeEventFromObservable } from './utils/valueWithChangeEvent.js'; -export { runOnChange, runOnChangeWithCancellationToken, runOnChangeWithStore } from './utils/runOnChange.js'; +export { runOnChange, runOnChangeWithCancellationToken, runOnChangeWithStore, type RemoveUndefined } from './utils/runOnChange.js'; export { derivedConstOnceDefined, latestChangedValue } from './experimental/utils.js'; export { observableFromEvent } from './observables/observableFromEvent.js'; export { observableValue } from './observables/observableValue.js'; diff --git a/src/vs/editor/browser/services/editorWorkerService.ts b/src/vs/editor/browser/services/editorWorkerService.ts index 77cc168f558..7c0d74d79c6 100644 --- a/src/vs/editor/browser/services/editorWorkerService.ts +++ b/src/vs/editor/browser/services/editorWorkerService.ts @@ -33,6 +33,8 @@ import { mainWindow } from '../../../base/browser/window.js'; import { WindowIntervalTimer } from '../../../base/browser/dom.js'; import { WorkerTextModelSyncClient } from '../../common/services/textModelSync/textModelSync.impl.js'; import { EditorWorkerHost } from '../../common/services/editorWorkerHost.js'; +import { StringEdit } from '../../common/core/edits/stringEdit.js'; +import { OffsetRange } from '../../common/core/ranges/offsetRange.js'; /** * Stop the worker if it was not needed for 5 min. @@ -180,6 +182,17 @@ export abstract class EditorWorkerService extends Disposable implements IEditorW } } + public async computeStringEditFromDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): Promise { + try { + const worker = await this._workerWithResources([]); + const edit = await worker.$computeStringDiff(original, modified, options, algorithm); + return StringEdit.fromJson(edit); + } catch (e) { + onUnexpectedError(e); + return StringEdit.replace(OffsetRange.ofLength(original.length), modified); // approximation + } + } + public canNavigateValueSet(resource: URI): boolean { return (canSyncModel(this._modelService, resource)); } diff --git a/src/vs/editor/common/core/edits/edit.ts b/src/vs/editor/common/core/edits/edit.ts index 75a8cb5d263..80cbdb4aa10 100644 --- a/src/vs/editor/common/core/edits/edit.ts +++ b/src/vs/editor/common/core/edits/edit.ts @@ -48,7 +48,7 @@ export abstract class BaseEdit, TEdit extends BaseE * Normalizes the edit by removing empty replacements and joining touching replacements (if the replacements allow joining). * Two edits have an equal normalized edit if and only if they have the same effect on any input. * - * ![](./docs/BaseEdit_normalize.dio.png) + * ![](https://raw.githubusercontent.com/microsoft/vscode/refs/heads/main/src/vs/editor/common/core/edits/docs/BaseEdit_normalize.drawio.png) * * Invariant: * ``` @@ -90,7 +90,7 @@ export abstract class BaseEdit, TEdit extends BaseE /** * Combines two edits into one with the same effect. * - * ![](./docs/BaseEdit_compose.dio.png) + * ![](https://raw.githubusercontent.com/microsoft/vscode/refs/heads/main/src/vs/editor/common/core/edits/docs/BaseEdit_compose.drawio.png) * * Invariant: * ``` @@ -183,6 +183,22 @@ export abstract class BaseEdit, TEdit extends BaseE return this._createNew(result).normalize(); } + public decomposeSplit(shouldBeInE1: (repl: T) => boolean): { e1: TEdit; e2: TEdit } { + const e1: T[] = []; + const e2: T[] = []; + + let e2delta = 0; + for (const edit of this.replacements) { + if (shouldBeInE1(edit)) { + e1.push(edit); + e2delta += edit.getNewLength() - edit.replaceRange.length; + } else { + e2.push(edit.slice(edit.replaceRange.delta(e2delta), new OffsetRange(0, edit.getNewLength()))); + } + } + return { e1: this._createNew(e1), e2: this._createNew(e2) }; + } + /** * Returns the range of each replacement in the applied value. */ diff --git a/src/vs/editor/common/core/edits/stringEdit.ts b/src/vs/editor/common/core/edits/stringEdit.ts index f7763163166..ba29bdfd6d8 100644 --- a/src/vs/editor/common/core/edits/stringEdit.ts +++ b/src/vs/editor/common/core/edits/stringEdit.ts @@ -5,58 +5,26 @@ import { commonPrefixLength, commonSuffixLength } from '../../../../base/common/strings.js'; import { OffsetRange } from '../ranges/offsetRange.js'; +import { StringText } from '../text/abstractText.js'; import { BaseEdit, BaseReplacement } from './edit.js'; -/** - * Represents a set of replacements to a string. - * All these replacements are applied at once. -*/ -export class StringEdit extends BaseEdit { - public static readonly empty = new StringEdit([]); - public static create(replacements: readonly StringReplacement[]): StringEdit { - return new StringEdit(replacements); +export abstract class BaseStringEdit = BaseStringReplacement, TEdit extends BaseStringEdit = BaseStringEdit> extends BaseEdit { + get TReplacement(): T { + throw new Error('TReplacement is not defined for BaseStringEdit'); } - public static single(replacement: StringReplacement): StringEdit { - return new StringEdit([replacement]); - } - - public static replace(range: OffsetRange, replacement: string): StringEdit { - return new StringEdit([new StringReplacement(range, replacement)]); - } - - public static insert(offset: number, replacement: string): StringEdit { - return new StringEdit([new StringReplacement(OffsetRange.emptyAt(offset), replacement)]); - } - - public static delete(range: OffsetRange): StringEdit { - return new StringEdit([new StringReplacement(range, '')]); - } - - public static fromJson(data: ISerializedStringEdit): StringEdit { - return new StringEdit(data.map(StringReplacement.fromJson)); - } - - public static compose(edits: readonly StringEdit[]): StringEdit { + public static composeOrUndefined(edits: readonly T[]): T | undefined { if (edits.length === 0) { - return StringEdit.empty; + return undefined; } let result = edits[0]; for (let i = 1; i < edits.length; i++) { - result = result.compose(edits[i]); + result = result.compose(edits[i]) as any; } return result; } - constructor(replacements: readonly StringReplacement[]) { - super(replacements); - } - - protected override _createNew(replacements: readonly StringReplacement[]): StringEdit { - return new StringEdit(replacements); - } - public apply(base: string): string { const resultText: string[] = []; let pos = 0; @@ -162,39 +130,41 @@ export class StringEdit extends BaseEdit { public normalizeEOL(eol: '\r\n' | '\n'): StringEdit { return new StringEdit(this.replacements.map(edit => edit.normalizeEOL(eol))); } + + /** + * If `e1.apply(source) === e2.apply(source)`, then `e1.normalizeOnSource(source).equals(e2.normalizeOnSource(source))`. + */ + public normalizeOnSource(source: string): StringEdit { + const result = this.apply(source); + + const edit = StringReplacement.replace(OffsetRange.ofLength(source.length), result); + const e = edit.removeCommonSuffixAndPrefix(source); + if (e.isEmpty) { + return StringEdit.empty; + } + return e.toEdit(); + } + + removeCommonSuffixAndPrefix(source: string): TEdit { + return this._createNew(this.replacements.map(e => e.removeCommonSuffixAndPrefix(source))).normalize(); + } + + applyOnText(docContents: StringText): StringText { + return new StringText(this.apply(docContents.value)); + } + + public mapData>(f: (replacement: T) => TData): AnnotatedStringEdit { + return new AnnotatedStringEdit( + this.replacements.map(e => new AnnotatedStringReplacement( + e.replaceRange, + e.newText, + f(e) + )) + ); + } } -/** - * Warning: Be careful when changing this type, as it is used for serialization! -*/ -export type ISerializedStringEdit = ISerializedStringReplacement[]; - -/** - * Warning: Be careful when changing this type, as it is used for serialization! -*/ -export interface ISerializedStringReplacement { - txt: string; - pos: number; - len: number; -} - -export class StringReplacement extends BaseReplacement { - public static insert(offset: number, text: string): StringReplacement { - return new StringReplacement(OffsetRange.emptyAt(offset), text); - } - - public static replace(range: OffsetRange, text: string): StringReplacement { - return new StringReplacement(range, text); - } - - public static delete(range: OffsetRange): StringReplacement { - return new StringReplacement(range, ''); - } - - public static fromJson(data: ISerializedStringReplacement): StringReplacement { - return new StringReplacement(OffsetRange.ofStartAndLength(data.pos, data.len), data.txt); - } - +export abstract class BaseStringReplacement = BaseStringReplacement> extends BaseReplacement { constructor( range: OffsetRange, public readonly newText: string, @@ -202,20 +172,8 @@ export class StringReplacement extends BaseReplacement { super(range); } - override equals(other: StringReplacement): boolean { - return this.replaceRange.equals(other.replaceRange) && this.newText === other.newText; - } - getNewLength(): number { return this.newText.length; } - tryJoinTouching(other: StringReplacement): StringReplacement | undefined { - return new StringReplacement(this.replaceRange.joinRightTouching(other.replaceRange), this.newText + other.newText); - } - - slice(range: OffsetRange, rangeInReplacement: OffsetRange): StringReplacement { - return new StringReplacement(range, rangeInReplacement.substring(this.newText)); - } - override toString(): string { return `${this.replaceRange} -> "${this.newText}"`; } @@ -254,6 +212,155 @@ export class StringReplacement extends BaseReplacement { const newText = this.newText.replace(/\r\n|\n/g, eol); return new StringReplacement(this.replaceRange, newText); } + + public removeCommonSuffixAndPrefix(source: string): T { + return this.removeCommonSuffix(source).removeCommonPrefix(source); + } + + public removeCommonPrefix(source: string): T { + const oldText = this.replaceRange.substring(source); + + const prefixLen = commonPrefixLength(oldText, this.newText); + if (prefixLen === 0) { + return this as unknown as T; + } + + return this.slice(this.replaceRange.deltaStart(prefixLen), new OffsetRange(prefixLen, this.newText.length)); + } + + public removeCommonSuffix(source: string): T { + const oldText = this.replaceRange.substring(source); + + const suffixLen = commonSuffixLength(oldText, this.newText); + if (suffixLen === 0) { + return this as unknown as T; + } + return this.slice(this.replaceRange.deltaEnd(-suffixLen), new OffsetRange(0, this.newText.length - suffixLen)); + } + + public toEdit(): StringEdit { + return new StringEdit([this]); + } +} + + +/** + * Represents a set of replacements to a string. + * All these replacements are applied at once. +*/ +export class StringEdit extends BaseStringEdit { + public static readonly empty = new StringEdit([]); + + public static create(replacements: readonly StringReplacement[]): StringEdit { + return new StringEdit(replacements); + } + + public static single(replacement: StringReplacement): StringEdit { + return new StringEdit([replacement]); + } + + public static replace(range: OffsetRange, replacement: string): StringEdit { + return new StringEdit([new StringReplacement(range, replacement)]); + } + + public static insert(offset: number, replacement: string): StringEdit { + return new StringEdit([new StringReplacement(OffsetRange.emptyAt(offset), replacement)]); + } + + public static delete(range: OffsetRange): StringEdit { + return new StringEdit([new StringReplacement(range, '')]); + } + + public static fromJson(data: ISerializedStringEdit): StringEdit { + return new StringEdit(data.map(StringReplacement.fromJson)); + } + + public static compose(edits: readonly StringEdit[]): StringEdit { + if (edits.length === 0) { + return StringEdit.empty; + } + let result = edits[0]; + for (let i = 1; i < edits.length; i++) { + result = result.compose(edits[i]); + } + return result; + } + + /** + * The replacements are applied in order! + * Equals `StringEdit.compose(replacements.map(r => r.toEdit()))`, but is much more performant. + */ + public static composeSequentialReplacements(replacements: readonly StringReplacement[]): StringEdit { + let edit = StringEdit.empty; + let curEditReplacements: StringReplacement[] = []; // These are reverse sorted + + for (const r of replacements) { + const last = curEditReplacements.at(-1); + if (!last || r.replaceRange.isBefore(last.replaceRange)) { + // Detect subsequences of reverse sorted replacements + curEditReplacements.push(r); + } else { + // Once the subsequence is broken, compose the current replacements and look for a new subsequence. + edit = edit.compose(StringEdit.create(curEditReplacements.reverse())); + curEditReplacements = [r]; + } + } + + edit = edit.compose(StringEdit.create(curEditReplacements.reverse())); + return edit; + } + + constructor(replacements: readonly StringReplacement[]) { + super(replacements); + } + + protected override _createNew(replacements: readonly StringReplacement[]): StringEdit { + return new StringEdit(replacements); + } +} + +/** + * Warning: Be careful when changing this type, as it is used for serialization! +*/ +export type ISerializedStringEdit = ISerializedStringReplacement[]; + +/** + * Warning: Be careful when changing this type, as it is used for serialization! +*/ +export interface ISerializedStringReplacement { + txt: string; + pos: number; + len: number; +} + +export class StringReplacement extends BaseStringReplacement { + public static insert(offset: number, text: string): StringReplacement { + return new StringReplacement(OffsetRange.emptyAt(offset), text); + } + + public static replace(range: OffsetRange, text: string): StringReplacement { + return new StringReplacement(range, text); + } + + public static delete(range: OffsetRange): StringReplacement { + return new StringReplacement(range, ''); + } + + public static fromJson(data: ISerializedStringReplacement): StringReplacement { + return new StringReplacement(OffsetRange.ofStartAndLength(data.pos, data.len), data.txt); + } + + override equals(other: StringReplacement): boolean { + return this.replaceRange.equals(other.replaceRange) && this.newText === other.newText; + } + + override tryJoinTouching(other: StringReplacement): StringReplacement | undefined { + return new StringReplacement(this.replaceRange.joinRightTouching(other.replaceRange), this.newText + other.newText); + } + + override slice(range: OffsetRange, rangeInReplacement: OffsetRange): StringReplacement { + return new StringReplacement(range, rangeInReplacement.substring(this.newText)); + } } export function applyEditsToRanges(sortedRanges: OffsetRange[], edit: StringEdit): OffsetRange[] { @@ -322,3 +429,106 @@ export function applyEditsToRanges(sortedRanges: OffsetRange[], edit: StringEdit return result; } + +/** + * Represents data associated to a single edit, which survives certain edit operations. +*/ +export interface IEditData { + join(other: T): T | undefined; +} + +export class VoidEditData implements IEditData { + join(other: VoidEditData): VoidEditData | undefined { + return this; + } +} + +/** + * Represents a set of replacements to a string. + * All these replacements are applied at once. +*/ +export class AnnotatedStringEdit> extends BaseStringEdit, AnnotatedStringEdit> { + public static readonly empty = new AnnotatedStringEdit([]); + + public static create>(replacements: readonly AnnotatedStringReplacement[]): AnnotatedStringEdit { + return new AnnotatedStringEdit(replacements); + } + + public static single>(replacement: AnnotatedStringReplacement): AnnotatedStringEdit { + return new AnnotatedStringEdit([replacement]); + } + + public static replace>(range: OffsetRange, replacement: string, data: T): AnnotatedStringEdit { + return new AnnotatedStringEdit([new AnnotatedStringReplacement(range, replacement, data)]); + } + + public static insert>(offset: number, replacement: string, data: T): AnnotatedStringEdit { + return new AnnotatedStringEdit([new AnnotatedStringReplacement(OffsetRange.emptyAt(offset), replacement, data)]); + } + + public static delete>(range: OffsetRange, data: T): AnnotatedStringEdit { + return new AnnotatedStringEdit([new AnnotatedStringReplacement(range, '', data)]); + } + + public static compose>(edits: readonly AnnotatedStringEdit[]): AnnotatedStringEdit { + if (edits.length === 0) { + return AnnotatedStringEdit.empty; + } + let result = edits[0]; + for (let i = 1; i < edits.length; i++) { + result = result.compose(edits[i]); + } + return result; + } + + constructor(replacements: readonly AnnotatedStringReplacement[]) { + super(replacements); + } + + protected override _createNew(replacements: readonly AnnotatedStringReplacement[]): AnnotatedStringEdit { + return new AnnotatedStringEdit(replacements); + } + + toStringEdit(): StringEdit { + return new StringEdit(this.replacements.map(e => new StringReplacement(e.replaceRange, e.newText))); + } +} + +export class AnnotatedStringReplacement> extends BaseStringReplacement> { + public static insert>(offset: number, text: string, data: T): AnnotatedStringReplacement { + return new AnnotatedStringReplacement(OffsetRange.emptyAt(offset), text, data); + } + + public static replace>(range: OffsetRange, text: string, data: T): AnnotatedStringReplacement { + return new AnnotatedStringReplacement(range, text, data); + } + + public static delete>(range: OffsetRange, data: T): AnnotatedStringReplacement { + return new AnnotatedStringReplacement(range, '', data); + } + + constructor( + range: OffsetRange, + newText: string, + public readonly data: T + ) { + super(range, newText); + } + + override equals(other: AnnotatedStringReplacement): boolean { + return this.replaceRange.equals(other.replaceRange) && this.newText === other.newText && this.data === other.data; + } + + tryJoinTouching(other: AnnotatedStringReplacement): AnnotatedStringReplacement | undefined { + const joined = this.data.join(other.data); + if (joined === undefined) { + return undefined; + } + return new AnnotatedStringReplacement(this.replaceRange.joinRightTouching(other.replaceRange), this.newText + other.newText, joined); + } + + slice(range: OffsetRange, rangeInReplacement?: OffsetRange): AnnotatedStringReplacement { + return new AnnotatedStringReplacement(range, rangeInReplacement ? rangeInReplacement.substring(this.newText) : this.newText, this.data); + } +} + diff --git a/src/vs/editor/common/core/text/positionToOffset.ts b/src/vs/editor/common/core/text/positionToOffset.ts index 97d6ee3bf7c..07447565fd6 100644 --- a/src/vs/editor/common/core/text/positionToOffset.ts +++ b/src/vs/editor/common/core/text/positionToOffset.ts @@ -17,3 +17,8 @@ _setPositionOffsetTransformerDependencies({ TextEdit: TextEdit, TextLength: TextLength, }); + +// TODO@hediet this is dept and needs to go. See https://github.com/microsoft/vscode/issues/251126. +export function ensureDependenciesAreSet(): void { + // Noop +} diff --git a/src/vs/editor/common/diff/rangeMapping.ts b/src/vs/editor/common/diff/rangeMapping.ts index c5a66c6a8d9..487ec42833b 100644 --- a/src/vs/editor/common/diff/rangeMapping.ts +++ b/src/vs/editor/common/diff/rangeMapping.ts @@ -194,6 +194,17 @@ function isValidLineNumber(lineNumber: number, lines: string[]): boolean { * Also contains inner range mappings. */ export class DetailedLineRangeMapping extends LineRangeMapping { + public static toTextEdit(mapping: readonly DetailedLineRangeMapping[], modified: AbstractText): TextEdit { + const replacements: TextReplacement[] = []; + for (const m of mapping) { + for (const r of m.innerChanges ?? []) { + const replacement = r.toTextEdit(modified); + replacements.push(replacement); + } + } + return new TextEdit(replacements); + } + public static fromRangeMappings(rangeMappings: RangeMapping[]): DetailedLineRangeMapping { const originalRange = LineRange.join(rangeMappings.map(r => LineRange.fromRangeInclusive(r.originalRange))); const modifiedRange = LineRange.join(rangeMappings.map(r => LineRange.fromRangeInclusive(r.modifiedRange))); diff --git a/src/vs/editor/common/services/editorWebWorker.ts b/src/vs/editor/common/services/editorWebWorker.ts index c5e8ea184f8..ca63478efbd 100644 --- a/src/vs/editor/common/services/editorWebWorker.ts +++ b/src/vs/editor/common/services/editorWebWorker.ts @@ -28,6 +28,9 @@ import { computeDefaultDocumentColors } from '../languages/defaultDocumentColors import { FindSectionHeaderOptions, SectionHeader, findSectionHeaders } from './findSectionHeaders.js'; import { IRawModelData, IWorkerTextModelSyncChannelServer } from './textModelSync/textModelSync.protocol.js'; import { ICommonModel, WorkerTextModelSyncServer } from './textModelSync/textModelSync.impl.js'; +import { ISerializedStringEdit } from '../core/edits/stringEdit.js'; +import { StringText } from '../core/text/abstractText.js'; +import { ensureDependenciesAreSet } from '../core/text/positionToOffset.js'; export interface IMirrorModel extends IMirrorTextModel { readonly uri: URI; @@ -201,6 +204,24 @@ export class EditorWorker implements IDisposable, IWorkerTextModelSyncChannelSer return diffComputer.computeDiff().changes; } + public $computeStringDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): ISerializedStringEdit { + const diffAlgorithm: ILinesDiffComputer = algorithm === 'advanced' ? linesDiffComputers.getDefault() : linesDiffComputers.getLegacy(); + + ensureDependenciesAreSet(); + + const originalText = new StringText(original); + const originalLines = originalText.getLines(); + const modifiedText = new StringText(modified); + const modifiedLines = modifiedText.getLines(); + + const result = diffAlgorithm.computeDiff(originalLines, modifiedLines, { ignoreTrimWhitespace: false, maxComputationTimeMs: options.maxComputationTimeMs, computeMoves: false, extendToSubwords: false }); + + const textEdit = DetailedLineRangeMapping.toTextEdit(result.changes, modifiedText); + const strEdit = originalText.getTransformer().getStringEdit(textEdit); + + return strEdit.toJson(); + } + // ---- END diff -------------------------------------------------------------------------- diff --git a/src/vs/editor/common/services/editorWorker.ts b/src/vs/editor/common/services/editorWorker.ts index fc0f44fa458..6b0720d60ff 100644 --- a/src/vs/editor/common/services/editorWorker.ts +++ b/src/vs/editor/common/services/editorWorker.ts @@ -12,6 +12,7 @@ import { UnicodeHighlighterOptions } from './unicodeTextModelHighlighter.js'; import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; import type { EditorWorker } from './editorWebWorker.js'; import { SectionHeader, FindSectionHeaderOptions } from './findSectionHeaders.js'; +import { StringEdit } from '../core/edits/stringEdit.js'; export const IEditorWorkerService = createDecorator('editorWorkerService'); @@ -32,6 +33,8 @@ export interface IEditorWorkerService { computeMoreMinimalEdits(resource: URI, edits: TextEdit[] | null | undefined, pretty?: boolean): Promise; computeHumanReadableDiff(resource: URI, edits: TextEdit[] | null | undefined): Promise; + computeStringEditFromDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): Promise; + canComputeWordRanges(resource: URI): boolean; computeWordRanges(resource: URI, range: IRange): Promise<{ [word: string]: IRange[] } | null>; diff --git a/src/vs/editor/test/common/services/testEditorWorkerService.ts b/src/vs/editor/test/common/services/testEditorWorkerService.ts index 44a9d5fffc3..a03cd461405 100644 --- a/src/vs/editor/test/common/services/testEditorWorkerService.ts +++ b/src/vs/editor/test/common/services/testEditorWorkerService.ts @@ -10,6 +10,7 @@ import { TextEdit, IInplaceReplaceSupportResult, IColorInformation } from '../.. import { IDocumentDiff, IDocumentDiffProviderOptions } from '../../../common/diff/documentDiffProvider.js'; import { IChange } from '../../../common/diff/legacyLinesDiffComputer.js'; import { SectionHeader } from '../../../common/services/findSectionHeaders.js'; +import { StringEdit } from '../../../common/core/edits/stringEdit.js'; export class TestEditorWorkerService implements IEditorWorkerService { @@ -28,4 +29,8 @@ export class TestEditorWorkerService implements IEditorWorkerService { async navigateValueSet(resource: URI, range: IRange, up: boolean): Promise { return null; } async findSectionHeaders(uri: URI): Promise { return []; } async computeDefaultDocumentColors(uri: URI): Promise { return null; } + + computeStringEditFromDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/src/vs/platform/telemetry/common/telemetry.ts b/src/vs/platform/telemetry/common/telemetry.ts index 6bfcb9159cd..5f82501a81e 100644 --- a/src/vs/platform/telemetry/common/telemetry.ts +++ b/src/vs/platform/telemetry/common/telemetry.ts @@ -53,6 +53,10 @@ export interface ITelemetryService { setExperimentProperty(name: string, value: string): void; } +export function telemetryLevelEnabled(service: ITelemetryService, level: TelemetryLevel): boolean { + return service.telemetryLevel >= level; +} + export interface ITelemetryEndpoint { id: string; aiKey: string; diff --git a/src/vs/workbench/contrib/editTelemetry/browser/arcTracker.ts b/src/vs/workbench/contrib/editTelemetry/browser/arcTracker.ts new file mode 100644 index 00000000000..2b1dcb7bc0a --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/arcTracker.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { sumBy } from '../../../../base/common/arrays.js'; +import { AnnotatedStringEdit, BaseStringEdit, IEditData } from '../../../../editor/common/core/edits/stringEdit.js'; + +/** + * The ARC (accepted and retained characters) counts how many characters inserted by the initial suggestion (trackedEdit) + * stay unmodified after a certain amount of time after acceptance. +*/ +export class ArcTracker { + private _updatedTrackedEdit: AnnotatedStringEdit; + + constructor( + public readonly originalText: string, + private readonly _trackedEdit: BaseStringEdit, + ) { + const eNormalized = _trackedEdit.removeCommonSuffixPrefix(originalText); + this._updatedTrackedEdit = eNormalized.mapData(() => new IsTrackedEditData(true)); + } + + handleEdits(edit: BaseStringEdit): void { + const e = edit.mapData(_d => new IsTrackedEditData(false)); + const composedEdit = this._updatedTrackedEdit.compose(e); + const onlyTrackedEdit = composedEdit.decomposeSplit(e => !e.data.isTrackedEdit).e2; + this._updatedTrackedEdit = onlyTrackedEdit; + } + + getAcceptedRestrainedCharactersCount(): number { + const s = sumBy(this._updatedTrackedEdit.replacements, e => e.getNewLength()); + return s; + } + + getOriginalCharacterCount(): number { + return sumBy(this._trackedEdit.replacements, e => e.getNewLength()); + } + + getDebugState(): unknown { + return { + edits: this._updatedTrackedEdit.replacements.map(e => ({ + range: e.replaceRange.toString(), + newText: e.newText, + isTrackedEdit: e.data.isTrackedEdit, + })) + }; + } +} + +export class IsTrackedEditData implements IEditData { + constructor( + public readonly isTrackedEdit: boolean + ) { } + + join(data: IsTrackedEditData): IsTrackedEditData | undefined { + if (this.isTrackedEdit !== data.isTrackedEdit) { + return undefined; + } + return this; + } +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/documentWithAnnotatedEdits.ts b/src/vs/workbench/contrib/editTelemetry/browser/documentWithAnnotatedEdits.ts new file mode 100644 index 00000000000..08ddfb6a33c --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/documentWithAnnotatedEdits.ts @@ -0,0 +1,424 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AsyncIterableObject, raceTimeout } from '../../../../base/common/async.js'; +import { CachedFunction } from '../../../../base/common/cache.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; +import { IObservableWithChange, ISettableObservable, observableValue, RemoveUndefined, runOnChange } from '../../../../base/common/observable.js'; +import { AnnotatedStringEdit, IEditData } from '../../../../editor/common/core/edits/stringEdit.js'; +import { StringText } from '../../../../editor/common/core/text/abstractText.js'; +import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; +import { TextModelEditReason } from '../../../../editor/common/textModelEditReason.js'; +import { IObservableDocument } from './observableWorkspace.js'; + +export interface IDocumentWithAnnotatedEdits = EditSourceData> { + readonly value: IObservableWithChange }>; + waitForQueue(): Promise; +} + +/** + * Creates a document that is a delayed copy of the original document, + * but with edits annotated with the source of the edit. +*/ +export class DocumentWithAnnotatedEdits extends Disposable implements IDocumentWithAnnotatedEdits { + public readonly value: IObservableWithChange }>; + + constructor(private readonly _originalDoc: IObservableDocument) { + super(); + + const v = this.value = observableValue(this, _originalDoc.value.get()); + + this._register(runOnChange(this._originalDoc.value, (val, _prevVal, edits) => { + const eComposed = AnnotatedStringEdit.compose(edits.map(e => { + const editSourceData = new EditReasonData(e.reason); + return e.mapData(() => editSourceData); + })); + + v.set(val, undefined, { edit: eComposed }); + })); + } + + public waitForQueue(): Promise { + return Promise.resolve(); + } +} + +/** + * Only joins touching edits if the source and the metadata is the same. +*/ +export class EditReasonData implements IEditData { + public readonly source; + public readonly key; + + constructor( + public readonly editReason: TextModelEditReason + ) { + this.key = this.editReason.toKey(1); + this.source = EditSourceBase.create(this.editReason); + } + + join(data: EditReasonData): EditReasonData | undefined { + if (this.editReason !== data.editReason) { + return undefined; + } + return this; + } + + toEditSourceData(): EditSourceData { + return new EditSourceData(this.key, this.source); + } +} + +export class EditSourceData implements IEditData { + constructor( + public readonly key: string, + public readonly source: EditSource, + ) { } + + join(data: EditSourceData): EditSourceData | undefined { + if (this.key !== data.key) { + return undefined; + } + if (this.source !== data.source) { + return undefined; + } + return this; + } +} + +export abstract class EditSourceBase { + private static _cache = new CachedFunction({ getCacheKey: v => v.toString() }, (arg: EditSource) => arg); + + public static create(reason: TextModelEditReason): EditSource { + const data = reason.metadata; + switch (data.source) { + case 'reloadFromDisk': + return this._cache.get(new ExternalEditSource()); + case 'inlineCompletionPartialAccept': + case 'inlineCompletionAccept': { + const type = 'type' in data ? data.type : undefined; + if ('$nes' in data && data.$nes) { + return this._cache.get(new InlineSuggestEditSource('nes', data.$extensionId ?? '', type)); + } + return this._cache.get(new InlineSuggestEditSource('completion', data.$extensionId ?? '', type)); + } + case 'snippet': + return this._cache.get(new IdeEditSource('suggest')); + case 'unknown': + if (!data.name) { + return this._cache.get(new UnknownEditSource()); + } + switch (data.name) { + case 'formatEditsCommand': + return this._cache.get(new IdeEditSource('format')); + } + return this._cache.get(new UnknownEditSource()); + + case 'Chat.applyEdits': + return this._cache.get(new ChatEditSource('sidebar')); + case 'inlineChat.applyEdits': + return this._cache.get(new ChatEditSource('inline')); + case 'cursor': + return this._cache.get(new UserEditSource()); + default: + return this._cache.get(new UnknownEditSource()); + } + } + + public abstract getColor(): string; +} + +export type EditSource = InlineSuggestEditSource | ChatEditSource | IdeEditSource | UserEditSource | UnknownEditSource | ExternalEditSource; + +export class InlineSuggestEditSource extends EditSourceBase { + public readonly category = 'ai'; + public readonly feature = 'inlineSuggest'; + constructor( + public readonly kind: 'completion' | 'nes', + public readonly extensionId: string, + public readonly type: 'word' | 'line' | undefined, + ) { super(); } + + override toString() { return `${this.category}/${this.feature}/${this.kind}/${this.extensionId}/${this.type}`; } + + public getColor(): string { return '#00ff0033'; } +} + +class ChatEditSource extends EditSourceBase { + public readonly category = 'ai'; + public readonly feature = 'chat'; + constructor( + public readonly kind: 'sidebar' | 'inline', + ) { super(); } + + override toString() { return `${this.category}/${this.feature}/${this.kind}`; } + + public getColor(): string { return '#00ff0066'; } +} + +class IdeEditSource extends EditSourceBase { + public readonly category = 'ide'; + constructor( + public readonly feature: 'suggest' | 'format' | string, + ) { super(); } + + override toString() { return `${this.category}/${this.feature}`; } + + public getColor(): string { return this.feature === 'format' ? '#0000ff33' : '#80808033'; } +} + +class UserEditSource extends EditSourceBase { + public readonly category = 'user'; + constructor() { super(); } + + override toString() { return this.category; } + + public getColor(): string { return '#d3d3d333'; } +} + +/** Caused by external tools that trigger a reload from disk */ +class ExternalEditSource extends EditSourceBase { + public readonly category = 'external'; + constructor() { super(); } + + override toString() { return this.category; } + + public getColor(): string { return '#009ab254'; } +} + +class UnknownEditSource extends EditSourceBase { + public readonly category = 'unknown'; + constructor() { super(); } + + override toString() { return this.category; } + + public getColor(): string { return '#ff000033'; } +} + +export class CombineStreamedChanges> extends Disposable implements IDocumentWithAnnotatedEdits { + private readonly _value: ISettableObservable }>; + readonly value: IObservableWithChange }>; + private readonly _runStore = this._register(new DisposableStore()); + private _runQueue: Promise = Promise.resolve(); + + constructor( + private readonly _originalDoc: IDocumentWithAnnotatedEdits, + @IEditorWorkerService private readonly _diffService: IEditorWorkerService, + ) { + super(); + + this.value = this._value = observableValue(this, _originalDoc.value.get()); + this._restart(); + + this._diffService.computeStringEditFromDiff('foo', 'last.value.value', { maxComputationTimeMs: 500 }, 'advanced'); + } + + async _restart(): Promise { + this._runStore.clear(); + const iterator = iterateChangesFromObservable(this._originalDoc.value, this._runStore)[Symbol.asyncIterator](); + const p = this._runQueue; + this._runQueue = this._runQueue.then(() => this._run(iterator)); + await p; + } + + private async _run(iterator: AsyncIterator<{ value: StringText; prevValue: StringText; change: { edit: AnnotatedStringEdit }[] }, any, any>) { + const reader = new AsyncReader(iterator); + while (true) { + let peeked = await reader.peek(); + if (peeked === AsyncReaderEndOfStream) { + return; + } else if (isChatEdit(peeked)) { + const first = peeked; + + let last = first; + let chatEdit = AnnotatedStringEdit.empty as AnnotatedStringEdit; + + do { + reader.readSyncOrThrow(); + last = peeked; + chatEdit = chatEdit.compose(AnnotatedStringEdit.compose(peeked.change.map(c => c.edit))); + if (!await reader.waitForBufferTimeout(1000)) { + break; + } + peeked = reader.peekSyncOrThrow(); + } while (peeked !== AsyncReaderEndOfStream && isChatEdit(peeked)); + + if (!chatEdit.isEmpty()) { + const data = chatEdit.replacements[0].data; + const diffEdit = await this._diffService.computeStringEditFromDiff(first.prevValue.value, last.value.value, { maxComputationTimeMs: 500 }, 'advanced'); + const edit = diffEdit.mapData(_e => data); + this._value.set(last.value, undefined, { edit }); + } + } else { + reader.readSyncOrThrow(); + const e = AnnotatedStringEdit.compose(peeked.change.map(c => c.edit)); + this._value.set(peeked.value, undefined, { edit: e }); + } + } + } + + async waitForQueue(): Promise { + await this._originalDoc.waitForQueue(); + await this._restart(); + } +} + +function isChatEdit(next: { value: StringText; change: { edit: AnnotatedStringEdit }[] }) { + return next.change.every(c => c.edit.replacements.every(e => { + if (e.data.source.category === 'ai' && e.data.source.feature === 'chat') { + return true; + } + return false; + })); +} + +function iterateChangesFromObservable(obs: IObservableWithChange, store: DisposableStore): AsyncIterable<{ value: T; prevValue: T; change: RemoveUndefined[] }> { + return new AsyncIterableObject<{ value: T; prevValue: T; change: RemoveUndefined[] }>((e) => { + store.add(runOnChange(obs, (value, prevValue, change) => { + e.emitOne({ value, prevValue, change: change }); + })); + + return new Promise((res) => { + store.add(toDisposable(() => { + res(undefined); + })); + }); + }); +} + +export class MinimizeEditsProcessor> extends Disposable implements IDocumentWithAnnotatedEdits { + readonly value: IObservableWithChange }>; + + constructor( + private readonly _originalDoc: IDocumentWithAnnotatedEdits, + ) { + super(); + + const v = this.value = observableValue(this, _originalDoc.value.get()); + + let prevValue: string = this._originalDoc.value.get().value; + this._register(runOnChange(this._originalDoc.value, (val, _prevVal, edits) => { + const eComposed = AnnotatedStringEdit.compose(edits.map(e => e.edit)); + + const e = eComposed.removeCommonSuffixAndPrefix(prevValue); + prevValue = val.value; + + v.set(val, undefined, { edit: e }); + })); + } + + async waitForQueue(): Promise { + await this._originalDoc.waitForQueue(); + } +} + +export const AsyncReaderEndOfStream = Symbol('AsyncReaderEndOfStream'); + +export class AsyncReader { + private _buffer: T[] = []; + private _atEnd = false; + + public get endOfStream(): boolean { return this._buffer.length === 0 && this._atEnd; } + + constructor( + private readonly _source: AsyncIterator + ) { + } + + private async _extendBuffer(): Promise { + if (this._atEnd) { + return; + } + const { value, done } = await this._source.next(); + if (done) { + this._atEnd = true; + } else { + this._buffer.push(value); + } + } + + public async peek(): Promise { + if (this._buffer.length === 0 && !this._atEnd) { + await this._extendBuffer(); + } + if (this._buffer.length === 0) { + return AsyncReaderEndOfStream; + } + return this._buffer[0]; + } + + public peekSyncOrThrow(): T | typeof AsyncReaderEndOfStream { + if (this._buffer.length === 0) { + if (this._atEnd) { + return AsyncReaderEndOfStream; + } + throw new Error('No more elements'); + } + + return this._buffer[0]; + } + + public readSyncOrThrow(): T | typeof AsyncReaderEndOfStream { + if (this._buffer.length === 0) { + if (this._atEnd) { + return AsyncReaderEndOfStream; + } + throw new Error('No more elements'); + } + + return this._buffer.shift()!; + } + + public async peekNextTimeout(timeoutMs: number): Promise { + if (this._buffer.length === 0 && !this._atEnd) { + await raceTimeout(this._extendBuffer(), timeoutMs); + } + if (this._atEnd) { + return AsyncReaderEndOfStream; + } + if (this._buffer.length === 0) { + return undefined; + } + return this._buffer[0]; + } + + public async waitForBufferTimeout(timeoutMs: number): Promise { + if (this._buffer.length > 0 || this._atEnd) { + return true; + } + const result = await raceTimeout(this._extendBuffer().then(() => true), timeoutMs); + return result !== undefined; + } + + public async read(): Promise { + if (this._buffer.length === 0 && !this._atEnd) { + await this._extendBuffer(); + } + if (this._buffer.length === 0) { + return AsyncReaderEndOfStream; + } + return this._buffer.shift()!; + } + + public async readWhile(predicate: (value: T) => boolean, callback: (element: T) => unknown): Promise { + do { + const piece = await this.peek(); + if (piece === AsyncReaderEndOfStream) { + break; + } + if (!predicate(piece)) { + break; + } + await this.read(); // consume + await callback(piece); + } while (true); + } + + public async consumeToEnd(): Promise { + while (!this.endOfStream) { + await this.read(); + } + } +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingFeature.ts b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingFeature.ts new file mode 100644 index 00000000000..7d437dd8d5f --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingFeature.ts @@ -0,0 +1,241 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { CachedFunction } from '../../../../base/common/cache.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun, mapObservableArrayCached, derived, IObservable, ISettableObservable, observableValue, derivedWithSetter, observableSignalFromEvent, observableFromEvent } from '../../../../base/common/observable.js'; +import { isDefined } from '../../../../base/common/types.js'; +import { URI } from '../../../../base/common/uri.js'; +import { DynamicCssRules } from '../../../../editor/browser/editorDom.js'; +import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; +import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { IModelDeltaDecoration } from '../../../../editor/common/model.js'; +import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { EditorResourceAccessor } from '../../../common/editor.js'; +import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js'; +import { EditSource } from './documentWithAnnotatedEdits.js'; +import { EditSourceTrackingImpl } from './editSourceTrackingImpl.js'; +import { EDIT_TELEMETRY_SHOW_DECORATIONS, EDIT_TELEMETRY_SHOW_STATUS_BAR } from './settings.js'; +import { VSCodeWorkspace } from './vscodeObservableWorkspace.js'; + +export class EditTrackingFeature extends Disposable { + + private readonly _editSourceTrackingShowDecorations; + private readonly _editSourceTrackingShowStatusBar; + private readonly _showStateInMarkdownDoc = 'editTelemetry.showDebugDetails'; + private readonly _toggleDecorations = 'editTelemetry.toggleDebugDecorations'; + + constructor( + private readonly _workspace: VSCodeWorkspace, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IStatusbarService private readonly _statusbarService: IStatusbarService, + @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, + @IEditorService private readonly _editorService: IEditorService, + ) { + super(); + + this._editSourceTrackingShowDecorations = makeSettable(observableConfigValue(EDIT_TELEMETRY_SHOW_DECORATIONS, false, this._configurationService)); + this._editSourceTrackingShowStatusBar = observableConfigValue(EDIT_TELEMETRY_SHOW_STATUS_BAR, false, this._configurationService); + + const onDidAddGroupSignal = observableSignalFromEvent(this, this._editorGroupsService.onDidAddGroup); + const onDidRemoveGroupSignal = observableSignalFromEvent(this, this._editorGroupsService.onDidRemoveGroup); + const groups = derived(this, reader => { + onDidAddGroupSignal.read(reader); + onDidRemoveGroupSignal.read(reader); + return this._editorGroupsService.groups; + }); + const visibleUris: IObservable> = mapObservableArrayCached(this, groups, g => { + const editors = observableFromEvent(this, g.onDidModelChange, () => g.editors); + return editors.map(e => e.map(editor => EditorResourceAccessor.getCanonicalUri(editor))); + }).map((editors, reader) => { + const map = new Map(); + for (const urisObs of editors) { + for (const uri of urisObs.read(reader)) { + if (isDefined(uri)) { + map.set(uri.toString(), uri); + } + } + } + return map; + }); + + const impl = this._register(this._instantiationService.createInstance(EditSourceTrackingImpl, this._workspace, (doc, reader) => { + const map = visibleUris.read(reader); + return map.get(doc.uri.toString()) !== undefined; + })); + + this._register(autorun((reader) => { + if (!this._editSourceTrackingShowDecorations.read(reader)) { + return; + } + + const visibleEditors = observableFromEvent(this, this._editorService.onDidVisibleEditorsChange, () => this._editorService.visibleTextEditorControls); + + mapObservableArrayCached(this, visibleEditors, (editor, store) => { + if (editor instanceof CodeEditorWidget) { + const obsEditor = observableCodeEditor(editor); + + const cssStyles = new DynamicCssRules(editor); + const decorations = new CachedFunction((source: EditSource) => { + const r = store.add(cssStyles.createClassNameRef({ + backgroundColor: source.getColor(), + })); + return r.className; + }); + + store.add(obsEditor.setDecorations(derived(reader => { + const uri = obsEditor.model.read(reader)?.uri; + if (!uri) { return []; } + const doc = this._workspace.getDocument(uri); + if (!doc) { return []; } + const docsState = impl.docsState.read(reader).get(doc); + if (!docsState) { return []; } + + const ranges = (docsState.longtermTracker.read(reader)?.getTrackedRanges(reader)) ?? []; + + return ranges.map(r => ({ + range: doc.value.get().getTransformer().getRange(r.range), + options: { + description: 'editSourceTracking', + inlineClassName: decorations.get(r.source), + } + })); + }))); + } + }).recomputeInitiallyAndOnChange(reader.store); + })); + + this._register(autorun(reader => { + if (!this._editSourceTrackingShowStatusBar.read(reader)) { + return; + } + + const statusBarItem = reader.store.add(this._statusbarService.addEntry( + { + name: '', + text: '', + command: this._showStateInMarkdownDoc, + tooltip: 'Edit Source Tracking', + ariaLabel: '', + }, + 'editTelemetry', + StatusbarAlignment.RIGHT, + 100 + )); + + const sumChangedCharacters = derived(reader => { + const docs = impl.docsState.read(reader); + let sum = 0; + for (const state of docs.values()) { + const t = state.longtermTracker.read(reader); + if (!t) { continue; } + const d = state.getTelemetryData(t.getTrackedRanges(reader)); + sum += d.totalModifiedCharactersInFinalState; + } + return sum; + }); + + const tooltipMarkdownString = derived(reader => { + const docs = impl.docsState.read(reader); + const docsDataInTooltip: string[] = []; + const editSources: EditSource[] = []; + for (const [doc, state] of docs) { + const tracker = state.longtermTracker.read(reader); + if (!tracker) { + continue; + } + const trackedRanges = tracker.getTrackedRanges(reader); + const data = state.getTelemetryData(trackedRanges); + if (data.totalModifiedCharactersInFinalState === 0) { + continue; // Don't include unmodified documents in tooltip + } + + editSources.push(...trackedRanges.map(r => r.source)); + + // Filter out unmodified properties as these are not interesting to see in the hover + const filteredData = Object.fromEntries( + Object.entries(data).filter(([_, value]) => !(typeof value === 'number') || value !== 0) + ); + + docsDataInTooltip.push([ + `### ${doc.uri.fsPath}`, + '```json', + JSON.stringify(filteredData, undefined, '\t'), + '```', + '\n' + ].join('\n')); + } + + let tooltipContent: string; + if (docsDataInTooltip.length === 0) { + tooltipContent = 'No modified documents'; + } else if (docsDataInTooltip.length <= 3) { + tooltipContent = docsDataInTooltip.join('\n\n'); + } else { + const lastThree = docsDataInTooltip.slice(-3); + tooltipContent = '...\n\n' + lastThree.join('\n\n'); + } + + const agenda = this._createEditSourceAgenda(editSources); + + const tooltipWithCommand = new MarkdownString(tooltipContent + '\n\n[View Details](command:' + this._showStateInMarkdownDoc + ')'); + tooltipWithCommand.appendMarkdown('\n\n' + agenda + '\n\nToggle decorations: [Click here](command:' + this._toggleDecorations + ')'); + tooltipWithCommand.isTrusted = { enabledCommands: [this._toggleDecorations] }; + tooltipWithCommand.supportHtml = true; + + return tooltipWithCommand; + }); + + reader.store.add(autorun(reader => { + statusBarItem.update({ + name: 'editTelemetry', + text: `$(edit) ${sumChangedCharacters.read(reader)} chars inserted`, + ariaLabel: `Edit Source Tracking: ${sumChangedCharacters.read(reader)} modified characters`, + tooltip: tooltipMarkdownString.read(reader), + command: this._showStateInMarkdownDoc, + }); + })); + + reader.store.add(CommandsRegistry.registerCommand(this._toggleDecorations, () => { + this._editSourceTrackingShowDecorations.set(!this._editSourceTrackingShowDecorations.get(), undefined); + })); + })); + } + + private _createEditSourceAgenda(editSources: EditSource[]): string { + // Collect all edit sources from the tracked documents + const editSourcesSeen = new Set(); + const editSourceInfo = []; + for (const editSource of editSources) { + if (!editSourcesSeen.has(editSource.toString())) { + editSourcesSeen.add(editSource.toString()); + editSourceInfo.push({ name: editSource.toString(), color: editSource.getColor() }); + } + } + + const agendaItems = editSourceInfo.map(info => + `${info.name}` + ); + + return agendaItems.join(' '); + } +} + +export function makeSettable(obs: IObservable): ISettableObservable { + const overrideObs = observableValue('overrideObs', undefined); + return derivedWithSetter(overrideObs, (reader) => { + return overrideObs.read(reader) ?? obs.read(reader); + }, (value, tx) => { + overrideObs.set(value, tx); + }); +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts new file mode 100644 index 00000000000..edadf7750b5 --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts @@ -0,0 +1,451 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { reverseOrder, compareBy, numberComparator, sumBy } from '../../../../base/common/arrays.js'; +import { IntervalTimer, TimeoutTimer } from '../../../../base/common/async.js'; +import { toDisposable, DisposableStore, Disposable } from '../../../../base/common/lifecycle.js'; +import { mapObservableArrayCached, derived, IReader, IObservable, observableSignal, runOnChange, IObservableWithChange, observableValue, transaction, derivedObservableWithCache } from '../../../../base/common/observable.js'; +import { isDefined } from '../../../../base/common/types.js'; +import { URI } from '../../../../base/common/uri.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { AnnotatedStringEdit, BaseStringEdit } from '../../../../editor/common/core/edits/stringEdit.js'; +import { StringText } from '../../../../editor/common/core/text/abstractText.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { ISCMRepository, ISCMService } from '../../scm/common/scm.js'; +import { ArcTracker } from './arcTracker.js'; +import { CombineStreamedChanges, DocumentWithAnnotatedEdits, EditReasonData, EditSource, EditSourceData, IDocumentWithAnnotatedEdits, MinimizeEditsProcessor } from './documentWithAnnotatedEdits.js'; +import { DocumentEditSourceTracker, TrackedEdit } from './editTracker.js'; +import { ObservableWorkspace, IObservableDocument } from './observableWorkspace.js'; + +export class EditSourceTrackingImpl extends Disposable { + public readonly docsState; + + constructor( + private readonly _workspace: ObservableWorkspace, + private readonly _docIsVisible: (doc: IObservableDocument, reader: IReader) => boolean, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + + const scmBridge = this._instantiationService.createInstance(ScmBridge); + + this.docsState = mapObservableArrayCached(this, this._workspace.documents, (doc, store) => { + const docIsVisible = derived(reader => this._docIsVisible(doc, reader)); + const wasEverVisible = derivedObservableWithCache(this, (reader, lastVal) => lastVal || docIsVisible.read(reader)); + return wasEverVisible.map(v => v ? [doc, store.add(this._instantiationService.createInstance(TrackedDocumentInfo, doc, docIsVisible, scmBridge))] as const : undefined); + }).recomputeInitiallyAndOnChange(this._store).map((entries, reader) => new Map(entries.map(e => e.read(reader)).filter(isDefined))); + } +} + +class ScmBridge { + constructor( + @ISCMService private readonly _scmService: ISCMService + ) { } + + public async getRepo(uri: URI): Promise { + const repo = this._scmService.getRepository(uri); + if (!repo) { + return undefined; + } + return new ScmRepoBridge(repo); + } +} + +class ScmRepoBridge { + public readonly headBranchNameObs: IObservable = derived(reader => this._repo.provider.historyProvider.read(reader)?.historyItemRef.read(reader)?.name); + public readonly headCommitHashObs: IObservable = derived(reader => this._repo.provider.historyProvider.read(reader)?.historyItemRef.read(reader)?.revision); + + constructor( + private readonly _repo: ISCMRepository, + ) { + } + + async isIgnored(uri: URI): Promise { + return false; + } +} + +class TrackedDocumentInfo extends Disposable { + public readonly longtermTracker: IObservable | undefined>; + public readonly windowedTracker: IObservable | undefined>; + + private readonly _repo: Promise; + + constructor( + private readonly _doc: IObservableDocument, + docIsVisible: IObservable, + private readonly _scm: ScmBridge, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ITelemetryService private readonly _telemetryService: ITelemetryService + ) { + super(); + + // Use the listener service and special events from core to annotate where an edit came from (is async) + let processedDoc: IDocumentWithAnnotatedEdits = this._store.add(new DocumentWithAnnotatedEdits(_doc)); + // Combine streaming edits into one and make edit smaller + processedDoc = this._store.add(this._instantiationService.createInstance((CombineStreamedChanges), processedDoc)); + // Remove common suffix and prefix from edits + processedDoc = this._store.add(new MinimizeEditsProcessor(processedDoc)); + + const docWithJustReason = createDocWithJustReason(processedDoc, this._store); + + const longtermResetSignal = observableSignal('resetSignal'); + + this.longtermTracker = derived((reader) => { + longtermResetSignal.read(reader); + + const t = reader.store.add(new DocumentEditSourceTracker(docWithJustReason, undefined)); + reader.store.add(toDisposable(() => { + // send long term document telemetry + if (!t.isEmpty()) { + this.sendTelemetry('longterm', t.getTrackedRanges()); + } + t.dispose(); + })); + return t; + }).recomputeInitiallyAndOnChange(this._store); + + this._store.add(new IntervalTimer()).cancelAndSet(() => { + // Reset after 10 hours + longtermResetSignal.trigger(undefined); + }, 10 * 60 * 60 * 1000); + + (async () => { + const repo = await this._scm.getRepo(_doc.uri); + if (this._store.isDisposed) { + return; + } + // Reset on branch change or commit + if (repo) { + this._store.add(runOnChange(repo.headCommitHashObs, () => { + longtermResetSignal.trigger(undefined); + })); + this._store.add(runOnChange(repo.headBranchNameObs, () => { + longtermResetSignal.trigger(undefined); + })); + } + + this._store.add(this._instantiationService.createInstance(ArcTelemetrySender, processedDoc, repo)); + })(); + + const resetSignal = observableSignal('resetSignal'); + + this.windowedTracker = derived((reader) => { + if (!docIsVisible.read(reader)) { + return undefined; + } + resetSignal.read(reader); + + reader.store.add(new TimeoutTimer(() => { + // Reset after 5 minutes + resetSignal.trigger(undefined); + }, 5 * 60 * 1000)); + + const t = reader.store.add(new DocumentEditSourceTracker(docWithJustReason, undefined)); + reader.store.add(toDisposable(async () => { + // send long term document telemetry + this.sendTelemetry('5minWindow', t.getTrackedRanges()); + t.dispose(); + })); + + return t; + }).recomputeInitiallyAndOnChange(this._store); + + this._repo = this._scm.getRepo(_doc.uri); + } + + async sendTelemetry(mode: 'longterm' | '5minWindow', ranges: readonly TrackedEdit[]) { + if (ranges.length === 0) { + return; + } + + const data = this.getTelemetryData(ranges); + const isTrackedByGit = await data.isTrackedByGit; + + const statsUuid = generateUuid(); + + this._telemetryService.publicLog2<{ + mode: string; + languageId: string; + statsUuid: string; + nesModifiedCount: number; + inlineCompletionsCopilotModifiedCount: number; + inlineCompletionsNESModifiedCount: number; + otherAIModifiedCount: number; + unknownModifiedCount: number; + userModifiedCount: number; + ideModifiedCount: number; + totalModifiedCharacters: number; + externalModifiedCount: number; + isTrackedByGit: number; + }, { + owner: 'hediet'; + comment: 'Reports distribution of AI vs user edited characters.'; + + mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'longterm or 5minWindow' }; + languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' }; + statsUuid: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The unique identifier for the telemetry event.' }; + + nesModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of nes modified characters'; isMeasurement: true }; + inlineCompletionsCopilotModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of inline completions copilot modified characters'; isMeasurement: true }; + inlineCompletionsNESModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of inline completions nes modified characters'; isMeasurement: true }; + otherAIModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of other AI modified characters'; isMeasurement: true }; + unknownModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of unknown modified characters'; isMeasurement: true }; + userModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of user modified characters'; isMeasurement: true }; + ideModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of IDE modified characters'; isMeasurement: true }; + totalModifiedCharacters: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Total modified characters'; isMeasurement: true }; + externalModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of external modified characters'; isMeasurement: true }; + isTrackedByGit: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Indicates if the document is tracked by git.' }; + }>('editTelemetry.editSources.stats', { + mode, + languageId: this._doc.languageId.get(), + statsUuid: statsUuid, + nesModifiedCount: data.nesModifiedCount, + inlineCompletionsCopilotModifiedCount: data.inlineCompletionsCopilotModifiedCount, + inlineCompletionsNESModifiedCount: data.inlineCompletionsNESModifiedCount, + otherAIModifiedCount: data.otherAIModifiedCount, + unknownModifiedCount: data.unknownModifiedCount, + userModifiedCount: data.userModifiedCount, + ideModifiedCount: data.ideModifiedCount, + totalModifiedCharacters: data.totalModifiedCharactersInFinalState, + externalModifiedCount: data.externalModifiedCount, + isTrackedByGit: isTrackedByGit ? 1 : 0, + }); + + + const sums = sumByCategory(ranges, r => r.range.length, r => r.sourceKey); + const entries = Object.entries(sums).filter(([key, value]) => value !== undefined); + entries.sort(reverseOrder(compareBy(([key, value]) => value!, numberComparator))); + entries.length = mode === 'longterm' ? 30 : 10; + + for (const [key, value] of Object.entries(sums)) { + if (value === undefined) { + continue; + } + this._telemetryService.publicLog2<{ + mode: string; + reasonKey: string; + languageId: string; + statsUuid: string; + modifiedCount: number; + totalModifiedCount: number; + }, { + owner: 'hediet'; + comment: 'Reports distribution of various edit kinds.'; + + reasonKey: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The reason for the edit.' }; + mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'longterm or 5minWindow' }; + languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' }; + statsUuid: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The unique identifier for the telemetry event.' }; + + modifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of nes modified characters'; isMeasurement: true }; + totalModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Total number of characters'; isMeasurement: true }; + }>('editTelemetry.editSources.details', { + mode, + reasonKey: key, + languageId: this._doc.languageId.get(), + statsUuid: statsUuid, + modifiedCount: value, + totalModifiedCount: data.totalModifiedCharactersInFinalState, + }); + } + } + + getTelemetryData(ranges: readonly TrackedEdit[]) { + const getEditCategory = (source: EditSource) => { + if (source.category === 'ai' && source.kind === 'nes') { return 'nes'; } + if (source.category === 'ai' && source.kind === 'completion' && source.extensionId === 'github.copilot') { return 'inlineCompletionsCopilot'; } + if (source.category === 'ai' && source.kind === 'completion' && source.extensionId === 'github.copilot-chat') { return 'inlineCompletionsNES'; } + if (source.category === 'ai' && source.kind === 'completion') { return 'inlineCompletionsOther'; } + if (source.category === 'ai') { return 'otherAI'; } + if (source.category === 'user') { return 'user'; } + if (source.category === 'ide') { return 'ide'; } + if (source.category === 'external') { return 'external'; } + if (source.category === 'unknown') { return 'unknown'; } + + return 'unknown'; + }; + + const sums = sumByCategory(ranges, r => r.range.length, r => getEditCategory(r.source)); + const totalModifiedCharactersInFinalState = sumBy(ranges, r => r.range.length); + + return { + nesModifiedCount: sums.nes ?? 0, + inlineCompletionsCopilotModifiedCount: sums.inlineCompletionsCopilot ?? 0, + inlineCompletionsNESModifiedCount: sums.inlineCompletionsNES ?? 0, + otherAIModifiedCount: sums.otherAI ?? 0, + userModifiedCount: sums.user ?? 0, + ideModifiedCount: sums.ide ?? 0, + unknownModifiedCount: sums.unknown ?? 0, + externalModifiedCount: sums.external ?? 0, + totalModifiedCharactersInFinalState, + languageId: this._doc.languageId.get(), + isTrackedByGit: this._repo.then(async (repo) => !!repo && !await repo.isIgnored(this._doc.uri)), + }; + } +} + + +function mapObservableDelta(obs: IObservableWithChange, mapFn: (value: TDelta) => TDeltaNew, store: DisposableStore): IObservableWithChange { + const obsResult = observableValue('mapped', obs.get()); + store.add(runOnChange(obs, (value, _prevValue, changes) => { + transaction(tx => { + for (const c of changes) { + obsResult.set(value, tx, mapFn(c)); + } + }); + })); + return obsResult; +} + +/** + * Removing the metadata allows touching edits from the same source to merged, even if they were caused by different actions (e.g. two user edits). + */ +function createDocWithJustReason(docWithAnnotatedEdits: IDocumentWithAnnotatedEdits, store: DisposableStore): IDocumentWithAnnotatedEdits { + const docWithJustReason: IDocumentWithAnnotatedEdits = { + value: mapObservableDelta(docWithAnnotatedEdits.value, edit => ({ edit: edit.edit.mapData(d => d.data.toEditSourceData()) }), store), + waitForQueue: () => docWithAnnotatedEdits.waitForQueue(), + }; + return docWithJustReason; +} + +class ArcTelemetrySender extends Disposable { + constructor( + docWithAnnotatedEdits: IDocumentWithAnnotatedEdits, + scmRepoBridge: ScmRepoBridge | undefined, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + + this._register(runOnChange(docWithAnnotatedEdits.value, (_val, _prev, changes) => { + const edit = AnnotatedStringEdit.compose(changes.map(c => c.edit)); + if (edit.replacements.length !== 1) { + return; + } + const singleEdit = edit.replacements[0]; + const data = singleEdit.data.editReason.metadata; + if (data?.source !== 'inlineCompletionAccept') { + return; + } + + const docWithJustReason = createDocWithJustReason(docWithAnnotatedEdits, this._store); + const reporter = this._instantiationService.createInstance(ArcTelemetryReporter, docWithJustReason, scmRepoBridge, singleEdit.toEdit(), res => { + + res.telemetryService.publicLog2<{ + extensionId: string; + opportunityId: string; + didBranchChange: number; + timeDelayMs: number; + arc: number; + originalCharCount: number; + }, { + owner: 'hediet'; + comment: 'Reports the accepted and retained character count for an inline completion/edit.'; + + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id (copilot or copilot-chat); which provided this inline completion.' }; + opportunityId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Unique identifier for an opportunity to show an inline completion or NES.' }; + + didBranchChange: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Indicates if the branch changed in the meantime. If the branch changed (value is 1); this event should probably be ignored.' }; + timeDelayMs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The time delay between the user accepting the edit and measuring the survival rate.' }; + arc: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The accepted and restrained character count.' }; + originalCharCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The original character count before any edits.' }; + }>('editTelemetry.reportInlineEditArc', { + extensionId: data.$extensionId ?? '', + opportunityId: data.$$requestUuid ?? 'unknown', + didBranchChange: res.didBranchChange ? 1 : 0, + timeDelayMs: res.timeDelayMs, + arc: res.arc, + originalCharCount: res.originalCharCount, + }); + }); + + this._register(toDisposable(() => { + reporter.cancel(); + })); + })); + } +} + +export interface EditTelemetryData { + telemetryService: ITelemetryService; + timeDelayMs: number; + didBranchChange: boolean; + arc: number; + originalCharCount: number; +} + +export class ArcTelemetryReporter { + private readonly _store = new DisposableStore(); + private readonly _arcTracker; + private readonly _initialBranchName: string | undefined; + + constructor( + private readonly _document: { value: IObservableWithChange }, + // _markedEdits -> document.value + private readonly _gitRepo: ScmRepoBridge | undefined, + private readonly _trackedEdit: BaseStringEdit, + private readonly _sendTelemetryEvent: (res: EditTelemetryData) => void, + + @ITelemetryService private readonly _telemetryService: ITelemetryService + ) { + this._arcTracker = new ArcTracker(this._document.value.get().value, this._trackedEdit); + + this._store.add(runOnChange(this._document.value, (_val, _prevVal, changes) => { + const edit = BaseStringEdit.composeOrUndefined(changes.map(c => c.edit)); + if (edit) { + this._arcTracker.handleEdits(edit); + } + })); + + this._initialBranchName = this._gitRepo?.headBranchNameObs.get(); + + // This aligns with github inline completions + this._reportAfter(30 * 1000); + this._reportAfter(120 * 1000); + this._reportAfter(300 * 1000); + this._reportAfter(600 * 1000); + // track up to 15min to allow for slower edit responses from legacy SD endpoint + this._reportAfter(900 * 1000, () => { + this._store.dispose(); + }); + } + + private _reportAfter(timeoutMs: number, cb?: () => void) { + const timer = new TimeoutTimer(() => { + this._report(timeoutMs); + timer.dispose(); + if (cb) { + cb(); + } + }, timeoutMs); + this._store.add(timer); + } + + private _report(timeMs: number): void { + const currentBranch = this._gitRepo?.headBranchNameObs.get(); + const didBranchChange = currentBranch !== this._initialBranchName; + + this._sendTelemetryEvent({ + telemetryService: this._telemetryService, + timeDelayMs: timeMs, + didBranchChange, + arc: this._arcTracker.getAcceptedRestrainedCharactersCount(), + originalCharCount: this._arcTracker.getOriginalCharacterCount(), + }); + } + + public cancel(): void { + this._store.dispose(); + } +} + +function sumByCategory(items: readonly T[], getValue: (item: T) => number, getCategory: (item: T) => TCategory): Record { + return items.reduce((acc, item) => { + const category = getCategory(item); + acc[category] = (acc[category] || 0) + getValue(item); + return acc; + }, {} as any as Record); +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts new file mode 100644 index 00000000000..4e9310cb8e8 --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { EditTelemetryService } from './editTelemetryService.js'; +import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { localize } from '../../../../nls.js'; +import { EDIT_TELEMETRY_SETTING_ID, EDIT_TELEMETRY_SHOW_DECORATIONS, EDIT_TELEMETRY_SHOW_STATUS_BAR } from './settings.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; + +registerWorkbenchContribution2('EditTelemetryService', EditTelemetryService, WorkbenchPhase.AfterRestored); + +const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); +configurationRegistry.registerConfiguration({ + id: 'task', + order: 100, + title: localize('editTelemetry', "Edit Telemetry"), + type: 'object', + properties: { + [EDIT_TELEMETRY_SETTING_ID]: { + markdownDescription: localize('telemetry.editStats.enabled', "Controls whether to enable telemetry for edit statistics (only sends statistics if general telemetry is enabled)."), + type: 'boolean', + default: true, + tags: ['experimental'], + }, + [EDIT_TELEMETRY_SHOW_STATUS_BAR]: { + markdownDescription: localize('telemetry.editStats.showStatusBar', "Controls whether to show the status bar for edit telemetry."), + type: 'boolean', + default: false, + tags: ['experimental'], + }, + [EDIT_TELEMETRY_SHOW_DECORATIONS]: { + markdownDescription: localize('telemetry.editStats.showDecorations', "Controls whether to show decorations for edit telemetry."), + type: 'boolean', + default: false, + tags: ['experimental'], + }, + } +}); diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetryService.ts b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetryService.ts new file mode 100644 index 00000000000..e77b9a3cb78 --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetryService.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../base/common/observable.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { ITelemetryService, TelemetryLevel, telemetryLevelEnabled } from '../../../../platform/telemetry/common/telemetry.js'; +import { EditTrackingFeature } from './editSourceTrackingFeature.js'; +import { EDIT_TELEMETRY_SETTING_ID } from './settings.js'; +import { VSCodeWorkspace } from './vscodeObservableWorkspace.js'; + +export class EditTelemetryService extends Disposable { + private readonly _editSourceTrackingEnabled; + + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + ) { + super(); + + this._editSourceTrackingEnabled = observableConfigValue(EDIT_TELEMETRY_SETTING_ID, true, this._configurationService); + + this._register(autorun(r => { + const enabled = this._editSourceTrackingEnabled.read(r); + if (!enabled || !telemetryLevelEnabled(this._telemetryService, TelemetryLevel.USAGE)) { + return; + } + + const workspace = this._instantiationService.createInstance(VSCodeWorkspace); + + r.store.add(this._instantiationService.createInstance(EditTrackingFeature, workspace)); + })); + } +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editTracker.ts b/src/vs/workbench/contrib/editTelemetry/browser/editTracker.ts new file mode 100644 index 00000000000..6311aee4ee4 --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/editTracker.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { observableSignal, runOnChange, IReader } from '../../../../base/common/observable.js'; +import { AnnotatedStringEdit } from '../../../../editor/common/core/edits/stringEdit.js'; +import { OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js'; +import { IDocumentWithAnnotatedEdits, EditSourceData, EditSource } from './documentWithAnnotatedEdits.js'; + +/** + * Tracks a single document. +*/ +export class DocumentEditSourceTracker extends Disposable { + private _edits: AnnotatedStringEdit = AnnotatedStringEdit.empty; + private _pendingExternalEdits: AnnotatedStringEdit = AnnotatedStringEdit.empty; + + private readonly _update = observableSignal(this); + + constructor( + private readonly _doc: IDocumentWithAnnotatedEdits, + public readonly data: T, + ) { + super(); + + this._register(runOnChange(this._doc.value, (_val, _prevVal, edits) => { + const eComposed = AnnotatedStringEdit.compose(edits.map(e => e.edit)); + if (eComposed.replacements.every(e => e.data.source.category === 'external')) { + if (this._edits.isEmpty()) { + // Ignore initial external edits + } else { + // queue pending external edits + this._pendingExternalEdits = this._pendingExternalEdits.compose(eComposed); + } + } else { + if (!this._pendingExternalEdits.isEmpty()) { + this._edits = this._edits.compose(this._pendingExternalEdits); + this._pendingExternalEdits = AnnotatedStringEdit.empty; + } + this._edits = this._edits.compose(eComposed); + } + + this._update.trigger(undefined); + })); + } + + async waitForQueue(): Promise { + await this._doc.waitForQueue(); + } + + getTrackedRanges(reader?: IReader): TrackedEdit[] { + this._update.read(reader); + const ranges = this._edits.getNewRanges(); + return ranges.map((r, idx) => { + const e = this._edits.replacements[idx]; + const reason = e.data.source; + const te = new TrackedEdit(e.replaceRange, r, reason, e.data.key); + return te; + }); + } + + isEmpty(): boolean { + return this._edits.isEmpty(); + } + + public reset(): void { + this._edits = AnnotatedStringEdit.empty; + } + + public _getDebugVisualization() { + const ranges = this.getTrackedRanges(); + const txt = this._doc.value.get().value; + + return { + ...{ $fileExtension: 'text.w' }, + 'value': txt, + 'decorations': ranges.map(r => { + return { + range: [r.range.start, r.range.endExclusive], + color: r.source.getColor(), + }; + }) + }; + } +} + +export class TrackedEdit { + constructor( + public readonly originalRange: OffsetRange, + public readonly range: OffsetRange, + public readonly source: EditSource, + public readonly sourceKey: string, + ) { } +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/observableWorkspace.ts b/src/vs/workbench/contrib/editTelemetry/browser/observableWorkspace.ts new file mode 100644 index 00000000000..537b9e00ccf --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/observableWorkspace.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IObservableWithChange, derivedHandleChanges, derivedWithStore, observableValue, autorunWithStore, runOnChange, IObservable } from '../../../../base/common/observable.js'; +import { URI } from '../../../../base/common/uri.js'; +import { StringEdit } from '../../../../editor/common/core/edits/stringEdit.js'; +import { StringText } from '../../../../editor/common/core/text/abstractText.js'; +import { TextModelEditReason } from '../../../../editor/common/textModelEditReason.js'; + +export abstract class ObservableWorkspace { + abstract get documents(): IObservableWithChange; + + + getFirstOpenDocument(): IObservableDocument | undefined { + return this.documents.get()[0]; + } + + getDocument(documentId: URI): IObservableDocument | undefined { + return this.documents.get().find(d => d.uri.toString() === documentId.toString()); + } + + private _version = 0; + + /** + * Is fired when any open document changes. + */ + public readonly onDidOpenDocumentChange = derivedHandleChanges({ + owner: this, + changeTracker: { + createChangeSummary: () => ({ didChange: false }), + handleChange: (ctx, changeSummary) => { + if (!ctx.didChange(this.documents)) { + changeSummary.didChange = true; // A document changed + } + return true; + } + } + }, (reader, changeSummary) => { + const docs = this.documents.read(reader); + for (const d of docs) { + d.value.read(reader); // add dependency + } + if (changeSummary.didChange) { + this._version++; // to force a change + } + return this._version; + + // TODO@hediet make this work: + /* + const docs = this.openDocuments.read(reader); + for (const d of docs) { + if (reader.readChangesSinceLastRun(d.value).length > 0) { + reader.reportChange(d); + } + } + return undefined; + */ + }); + + public readonly lastActiveDocument = derivedWithStore((_reader, store) => { + const obs = observableValue('lastActiveDocument', undefined as IObservableDocument | undefined); + store.add(autorunWithStore((reader, store) => { + const docs = this.documents.read(reader); + for (const d of docs) { + store.add(runOnChange(d.value, () => { + obs.set(d, undefined); + })); + } + })); + return obs; + }).flatten(); +} + +export interface IObservableDocument { + readonly uri: URI; + readonly value: IObservableWithChange; + + /** + * Increases whenever the value changes. Is also used to reference document states from the past. + */ + readonly version: IObservable; + readonly languageId: IObservable; +} + +export class StringEditWithReason extends StringEdit { + constructor( + replacements: StringEdit['replacements'], + public readonly reason: TextModelEditReason, + ) { + super(replacements); + } +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/settings.ts b/src/vs/workbench/contrib/editTelemetry/browser/settings.ts new file mode 100644 index 00000000000..e337e5e734f --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/settings.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const EDIT_TELEMETRY_SETTING_ID = 'telemetry.editStats.enabled'; +export const EDIT_TELEMETRY_SHOW_DECORATIONS = 'telemetry.editStats.showDecorations'; +export const EDIT_TELEMETRY_SHOW_STATUS_BAR = 'telemetry.editStats.showStatusBar'; diff --git a/src/vs/workbench/contrib/editTelemetry/browser/vscodeObservableWorkspace.ts b/src/vs/workbench/contrib/editTelemetry/browser/vscodeObservableWorkspace.ts new file mode 100644 index 00000000000..ea0afcac958 --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/vscodeObservableWorkspace.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { onUnexpectedError } from '../../../../base/common/errors.js'; +import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { derived, IObservable, IObservableWithChange, mapObservableArrayCached, observableSignalFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; +import { isDefined } from '../../../../base/common/types.js'; +import { URI } from '../../../../base/common/uri.js'; +import { StringText } from '../../../../editor/common/core/text/abstractText.js'; +import { ITextModel } from '../../../../editor/common/model.js'; +import { offsetEditFromContentChanges } from '../../../../editor/common/model/textModelStringEdit.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { IObservableDocument, ObservableWorkspace, StringEditWithReason } from './observableWorkspace.js'; + +export class VSCodeWorkspace extends ObservableWorkspace implements IDisposable { + private readonly _documents; + public get documents() { return this._documents; } + + private readonly _store = new DisposableStore(); + + constructor( + @IModelService private readonly _textModelService: IModelService, + ) { + super(); + + const onModelAdded = observableSignalFromEvent(this, this._textModelService.onModelAdded); + const onModelRemoved = observableSignalFromEvent(this, this._textModelService.onModelRemoved); + + const models = derived(this, reader => { + onModelAdded.read(reader); + onModelRemoved.read(reader); + const models = this._textModelService.getModels(); + return models; + }); + + const documents = mapObservableArrayCached(this, models, (m, store) => { + if (m.isTooLargeForSyncing()) { + return undefined; + } + return store.add(new VSCodeDocument(m)); + }).recomputeInitiallyAndOnChange(this._store).map(d => d.filter(isDefined)); + + this._documents = documents; + } + + dispose(): void { + this._store.dispose(); + } +} + +export class VSCodeDocument extends Disposable implements IObservableDocument { + get uri(): URI { return this.textModel.uri; } + private readonly _value; + private readonly _version; + private readonly _languageId; + get value(): IObservableWithChange { return this._value; } + get version(): IObservable { return this._version; } + get languageId(): IObservable { return this._languageId; } + + constructor( + public readonly textModel: ITextModel, + ) { + super(); + + this._value = observableValue(this, new StringText(this.textModel.getValue())); + this._version = observableValue(this, this.textModel.getVersionId()); + this._languageId = observableValue(this, this.textModel.getLanguageId()); + + this._register(this.textModel.onDidChangeContent((e) => { + transaction(tx => { + const edit = offsetEditFromContentChanges(e.changes); + if (e.detailedReasons.length !== 1) { + onUnexpectedError(new Error(`Unexpected number of detailed reasons: ${e.detailedReasons.length}`)); + } + + const change = new StringEditWithReason(edit.replacements, e.detailedReasons[0]); + + this._value.set(new StringText(this.textModel.getValue()), tx, change); + this._version.set(this.textModel.getVersionId(), tx); + }); + })); + + this._register(this.textModel.onDidChangeLanguage(e => { + transaction(tx => { + this._languageId.set(this.textModel.getLanguageId(), tx); + }); + })); + } +} diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 27d5c5a4b13..750087f2e72 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -417,6 +417,8 @@ import './contrib/inlineCompletions/browser/inlineCompletions.contribution.js'; // Drop or paste into import './contrib/dropOrPasteInto/browser/dropOrPasteInto.contribution.js'; +// Edit Telemetry +import './contrib/editTelemetry/browser/editTelemetry.contribution.js'; //#endregion From c6f3a3c93b1eb87eef926c0a876f8b7b1b85b59b Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 3 Jul 2025 17:56:49 +0200 Subject: [PATCH 083/306] fix #253911 (#253926) --- src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts index ddf3a31ae1d..a7de8e57ff6 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts @@ -302,6 +302,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ local.local = i; return local; }); + this._onChange.fire(undefined); return [...this.local]; } From 45f07cbad3a9dcbfd27cedf9cadbd97e21a08913 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 15:56:56 +0000 Subject: [PATCH 084/306] Fix MCP prompt flow cancellation when optional arguments are left empty (#253084) * Initial plan * Fix null pointer exception in MCP prompt argument picker Co-authored-by: connor4312 <2230985+connor4312@users.noreply.github.com> * Fix undefined value handling in MCP prompt argument picker Fixed the case where pressing Enter without selecting an item would cancel the flow instead of proceeding with empty value for optional arguments. Now properly handles: - Required arguments: shows validation message when empty - Optional arguments: returns empty text action instead of canceling Co-authored-by: connor4312 <2230985+connor4312@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: connor4312 <2230985+connor4312@users.noreply.github.com> --- .../workbench/contrib/mcp/browser/mcpPromptArgumentPick.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpPromptArgumentPick.ts b/src/vs/workbench/contrib/mcp/browser/mcpPromptArgumentPick.ts index f261f122024..05044e81fa5 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpPromptArgumentPick.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpPromptArgumentPick.ts @@ -168,10 +168,13 @@ export class McpPromptArgumentPick extends Disposable { })); store.add(quickPick.onDidAccept(() => { const item = quickPick.selectedItems[0]; - if (!quickPick.value && arg.required && (item.action === 'text' || item.action === 'command')) { + if (!quickPick.value && arg.required && (!item || item.action === 'text' || item.action === 'command')) { quickPick.validationMessage = localize('mcp.arg.required', "This argument is required"); + } else if (!item) { + // For optional arguments when no item is selected, return empty text action + resolve({ id: 'insert-text', label: '', action: 'text' }); } else { - resolve(quickPick.selectedItems[0]); + resolve(item); } })); store.add(quickPick.onDidTriggerButton(() => { From 8693de592f0b2504add218f0c95fc5a348d17491 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Thu, 3 Jul 2025 08:57:14 -0700 Subject: [PATCH 085/306] tweaks to remote coding agent (#253713) * make action verbiage more clear * move prompt file contribution behind dedicated context key * drop Copilot so that the text wraps nicely at default text size --- .../contrib/chat/browser/actions/chatExecuteActions.ts | 8 ++++---- .../promptSyntax/promptCodingAgentActionOverlay.ts | 5 +++-- src/vs/workbench/contrib/chat/common/chatContextKeys.ts | 1 + 3 files changed, 8 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 73b6a898546..47954785a56 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -504,14 +504,14 @@ export class CreateRemoteAgentJobAction extends Action2 { super({ id: CreateRemoteAgentJobAction.ID, - // TODO(joshspicer): Generalize title - title: localize2('actions.chat.createRemoteJob', "Push to Copilot coding agent"), + // TODO(joshspicer): Generalize title, pull from contribution + title: localize2('actions.chat.createRemoteJob', "Delegate to coding agent"), icon: Codicon.cloudUpload, precondition, toggled: { condition: ChatContextKeys.remoteJobCreating, icon: Codicon.sync, - tooltip: localize('remoteJobCreating', "Pushing to Copilot coding agent"), + tooltip: localize('remoteJobCreating', "Delegating to coding agent"), }, menu: { id: MenuId.ChatExecute, @@ -617,7 +617,7 @@ export class CreateRemoteAgentJobAction extends Action2 { chatModel.acceptResponseProgress(addedRequest, { kind: 'progressMessage', content: new MarkdownString( - localize('creatingRemoteJob', "Pushing state to coding agent"), + localize('creatingRemoteJob', "Delegating to coding agent"), CreateRemoteAgentJobAction.markdownStringTrustedOptions ) }); diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptCodingAgentActionOverlay.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptCodingAgentActionOverlay.ts index a848944936e..9ef18bf66fb 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptCodingAgentActionOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptCodingAgentActionOverlay.ts @@ -40,7 +40,7 @@ export class PromptCodingAgentActionOverlayWidget extends Disposable implements this._button.element.style.background = 'var(--vscode-button-background)'; this._button.element.style.color = 'var(--vscode-button-foreground)'; - this._button.label = localize('runWithCodingAgent.label', "{0} Push to Copilot coding agent", '$(cloud-upload)'); + this._button.label = localize('runWithCodingAgent.label', "{0} Delegate to Copilot coding agent", '$(cloud-upload)'); this._register(this._button.onDidClick(async () => { await this._execute(); @@ -80,10 +80,11 @@ export class PromptCodingAgentActionOverlayWidget extends Disposable implements } private _updateVisibility(): void { + const enableRemoteCodingAgentPromptFileOverlay = ChatContextKeys.enableRemoteCodingAgentPromptFileOverlay.getValue(this._contextKeyService); const hasRemoteCodingAgent = ChatContextKeys.hasRemoteCodingAgent.getValue(this._contextKeyService); const model = this._editor.getModel(); const isPromptFile = model?.getLanguageId() === PROMPT_LANGUAGE_ID; - const shouldBeVisible = !!(hasRemoteCodingAgent && isPromptFile); + const shouldBeVisible = !!(isPromptFile && enableRemoteCodingAgentPromptFileOverlay && hasRemoteCodingAgent); if (shouldBeVisible !== this._isVisible) { this._isVisible = shouldBeVisible; diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 86f5327ae6d..764ad70e395 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -56,6 +56,7 @@ export namespace ChatContextKeys { export const remoteJobCreating = new RawContextKey('chatRemoteJobCreating', false, { type: 'boolean', description: localize('chatRemoteJobCreating', "True when a remote coding agent job is being created.") }); export const hasRemoteCodingAgent = new RawContextKey('hasRemoteCodingAgent', false, localize('hasRemoteCodingAgent', "Whether any remote coding agent is available")); + export const enableRemoteCodingAgentPromptFileOverlay = new RawContextKey('enableRemoteCodingAgentPromptFileOverlay', false, localize('enableRemoteCodingAgentPromptFileOverlay', "Whether the remote coding agent prompt file overlay feature is enabled")); export const Setup = { hidden: new RawContextKey('chatSetupHidden', false, true), // True when chat setup is explicitly hidden. From 6ac7f77fdf23e106bacc6ce81442343d1759efd2 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Thu, 3 Jul 2025 08:57:30 -0700 Subject: [PATCH 086/306] Refactor chat widget welcome view to include additional messages and improve styling (#253720) * Refactor chat widget welcome view to include additional messages and improve styling * Update experiment configurations to adjust target percentages and group allocations --- .../contrib/chat/browser/chatWidget.ts | 51 +++++-------- .../chat/browser/media/chatViewWelcome.css | 56 +++++--------- .../viewsWelcome/chatViewWelcomeController.ts | 73 +++++-------------- 3 files changed, 56 insertions(+), 124 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 8faac4fb00f..bd917a0fd9a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -11,7 +11,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { FuzzyScore } from '../../../../base/common/filters.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { combinedDisposable, Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../base/common/map.js'; @@ -64,7 +64,7 @@ import { ChatEditorOptions } from './chatOptions.js'; import './media/chat.css'; import './media/chatAgentHover.css'; import './media/chatViewWelcome.css'; -import { ChatViewWelcomePart, IChatSuggestedPrompts, IChatViewWelcomeContent } from './viewsWelcome/chatViewWelcomeController.js'; +import { ChatViewWelcomePart, IChatViewWelcomeContent } from './viewsWelcome/chatViewWelcomeController.js'; import { MicrotaskDelay } from '../../../../base/common/symbols.js'; import { IChatRequestVariableEntry, ChatRequestVariableSet as ChatRequestVariableSet, isPromptFileVariableEntry, toPromptFileVariableEntry, PromptFileVariableKind } from '../common/chatVariableEntries.js'; import { PromptsConfig } from '../common/promptSyntax/config/config.js'; @@ -734,9 +734,6 @@ export class ChatWidget extends Disposable implements IChatWidget { const numItems = this.viewModel?.getItems().length ?? 0; if (!numItems) { dom.clearNode(this.welcomeMessageContainer); - const defaultAgent = this.chatAgentService.getDefaultAgent(this.location, this.input.currentModeKind); - const additionalMessage = defaultAgent?.metadata.additionalWelcomeMessage; - const startupExpValue = startupExpContext.getValue(this.contextKeyService); let welcomeContent: IChatViewWelcomeContent; if (startupExpValue === StartupExperimentGroup.MaximizedChat @@ -746,15 +743,17 @@ export class ChatWidget extends Disposable implements IChatWidget { this.container.classList.add('experimental-welcome-view'); } else { + const defaultAgent = this.chatAgentService.getDefaultAgent(this.location, this.input.currentModeKind); + const additionalMessage = defaultAgent?.metadata.additionalWelcomeMessage; const tips = this.input.currentModeKind === ChatModeKind.Ask ? new MarkdownString(localize('chatWidget.tips', "{0} or type {1} to attach context\n\n{2} to chat with extensions\n\nType {3} to use commands", '$(attach)', '#', '$(mention)', '/'), { supportThemeIcons: true }) : new MarkdownString(localize('chatWidget.tips.withoutParticipants', "{0} or type {1} to attach context", '$(attach)', '#'), { supportThemeIcons: true }); - welcomeContent = this.getWelcomeViewContent(); + welcomeContent = this.getWelcomeViewContent(additionalMessage); welcomeContent.tips = tips; } this.welcomePart.value = this.instantiationService.createInstance( ChatViewWelcomePart, - { ...welcomeContent, additionalMessage }, + welcomeContent, { location: this.location, isWidgetAgentWelcomeViewContent: this.input?.currentModeKind === ChatModeKind.Agent @@ -769,58 +768,44 @@ export class ChatWidget extends Disposable implements IChatWidget { } } - private getWelcomeViewContent(): IChatViewWelcomeContent { + private getWelcomeViewContent(additionalMessage: string | IMarkdownString | undefined): IChatViewWelcomeContent { const baseMessage = localize('chatMessage', "Copilot is powered by AI, so mistakes are possible. Review output carefully before use."); if (this.input.currentModeKind === ChatModeKind.Ask) { return { title: localize('chatDescription', "Ask Copilot"), message: new MarkdownString(baseMessage), - icon: Codicon.copilotLarge + icon: Codicon.copilotLarge, + additionalMessage, }; } else if (this.input.currentModeKind === ChatModeKind.Edit) { return { title: localize('editsTitle', "Edit with Copilot"), message: new MarkdownString(localize('editsMessage', "Start your editing session by defining a set of files that you want to work with. Then ask Copilot for the changes you want to make.") + `\n\n${baseMessage}`), - icon: Codicon.copilotLarge + icon: Codicon.copilotLarge, + additionalMessage }; } else { return { title: localize('editsTitle', "Edit with Copilot"), message: new MarkdownString(localize('agentMessage', "Ask Copilot to edit your files in [agent mode]({0}). Copilot will automatically use multiple requests to pick files to edit, run terminal commands, and iterate on errors.", 'https://aka.ms/vscode-copilot-agent') + `\n\n${baseMessage}`), - icon: Codicon.copilotLarge + icon: Codicon.copilotLarge, + additionalMessage }; } } private getExpWelcomeViewContent(): IChatViewWelcomeContent { - const baseMessage = localize('chatMessage', "Copilot is powered by AI, so mistakes are possible. Review output carefully before use."); - const welcomeContent = { - title: localize('expChatTitle', 'Get Started with VS Code'), - message: new MarkdownString(baseMessage), + const welcomeContent: IChatViewWelcomeContent = { + title: localize('expChatTitle', 'Welcome to Copilot'), + message: new MarkdownString(localize('expchatMessage', "Let's get started")), icon: Codicon.copilotLarge, - suggestedPrompts: this.getExpSuggestedPrompts(), inputPart: this.inputPart.element, + additionalMessage: localize('expChatAdditionalMessage', "Review output carefully before use."), + isExperimental: true }; return welcomeContent; } - private getExpSuggestedPrompts(): IChatSuggestedPrompts[] { - - return [ - { - icon: Codicon.vscode, - label: localize('chatWidget.suggestedPrompts.gettingStarted', "Ask @vscode"), - prompt: '@vscode Help me get started with VS Code?', - }, - { - icon: Codicon.newFolder, - label: localize('chatWidget.suggestedPrompts.newProject', "Create a #new Project"), - prompt: '#new Create a new project for me', - } - ]; - } - - private async renderChatEditingSessionState() { if (!this.input) { return; diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css index 23b7ce84566..872583659e4 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css @@ -23,6 +23,11 @@ .interactive-session .experimental-welcome-view & > .chat-welcome-view-input-part { max-width: 650px; + margin-bottom: 48px; +} + +.interactive-session.experimental-welcome-view .chat-input-toolbars > .chat-input-toolbar > div { + display: none; } /* Container for ChatViewPane welcome view */ @@ -102,44 +107,21 @@ div.chat-welcome-view { } } - & > .chat-welcome-view-suggested-prompts { - display: flex; - flex-wrap: wrap; - gap: 10px; - justify-content: center; - margin-top: 15px; + & > .chat-welcome-experimental-view-message { + text-align: center; + max-width: 350px; + padding: 0 20px 32px; + font-size: 16px; - > .chat-welcome-view-suggested-prompt { - display: flex; - align-items: center; - padding: 0 4px; - border-radius: 8px; - background-color: var(--vscode-editorWidget-background); - cursor: pointer; - border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); - border-radius: 4px; - gap: 2px; - max-width: 100%; - width: fit-content; - - > .chat-welcome-view-suggested-prompt-icon { - display: flex; - align-items: center; - margin-right: 8px; - font-size: 4px; - color: var(--vscode-icon-foreground) !important; - align-items: center; - } - - > .chat-welcome-view-suggested-prompt-label { - font-size: 14px; - color: var(--vscode-editorWidget-foreground); - } - } - - > .chat-welcome-view-suggested-prompt:hover { - background-color: var(--vscode-list-hoverBackground); - border-color: var(--vscode-focusBorder); + a { + color: var(--vscode-descriptionForeground); } } + + & > .chat-welcome-view-experimental-additional-message { + font-size: 12px; + color: var(--vscode-disabledForeground); + text-align: center; + max-width: 400px; + } } diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index 7b63aae3a9f..fc1bdc60794 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -19,8 +19,6 @@ import { IOpenerService } from '../../../../../platform/opener/common/opener.js' import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { chatViewsWelcomeRegistry, IChatViewsWelcomeDescriptor } from './chatViewsWelcome.js'; -import { IChatWidgetService } from '../chat.js'; -import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; const $ = dom.$; @@ -115,13 +113,7 @@ export interface IChatViewWelcomeContent { additionalMessage?: string | IMarkdownString; tips?: IMarkdownString; inputPart?: HTMLElement; - suggestedPrompts?: IChatSuggestedPrompts[]; -} - -export interface IChatSuggestedPrompts { - icon?: ThemeIcon; - label: string; - prompt: string; + isExperimental?: boolean; } export interface IChatViewWelcomeRenderOptions { @@ -139,8 +131,6 @@ export class ChatViewWelcomePart extends Disposable { @IOpenerService private openerService: IOpenerService, @IInstantiationService private instantiationService: IInstantiationService, @ILogService private logService: ILogService, - @IChatWidgetService private chatWidgetService: IChatWidgetService, - @ITelemetryService private telemetryService: ITelemetryService, ) { super(); this.element = dom.$('.chat-welcome-view'); @@ -165,57 +155,32 @@ export class ChatViewWelcomePart extends Disposable { } // Message - const message = dom.append(this.element, $('.chat-welcome-view-message')); + const message = dom.append(this.element, content.isExperimental ? $('.chat-welcome-experimental-view-message') : $('.chat-welcome-view-message')); if (typeof content.message === 'function') { dom.append(message, content.message(this._register(new DisposableStore()))); } else { const messageResult = this.renderMarkdownMessageContent(renderer, content.message, options); dom.append(message, messageResult.element); - } - // Additional message - if (typeof content.additionalMessage === 'string') { - const element = $(''); - element.textContent = content.additionalMessage; - dom.append(message, element); - } else if (content.additionalMessage) { - const additionalMessageResult = this.renderMarkdownMessageContent(renderer, content.additionalMessage, options); - dom.append(message, additionalMessageResult.element); - } - - if (content.inputPart) { + if (content.isExperimental && content.inputPart) { + content.inputPart.querySelector('.chat-attachments-container')?.remove(); dom.append(this.element, content.inputPart); - } - - if (content.suggestedPrompts && content.suggestedPrompts.length) { - - // create a tile with icon and label for each suggested promot - const suggestedPromptsContainer = dom.append(this.element, $('.chat-welcome-view-suggested-prompts')); - for (const prompt of content.suggestedPrompts) { - const promptElement = dom.append(suggestedPromptsContainer, $('.chat-welcome-view-suggested-prompt')); - if (prompt.icon) { - const iconElement = dom.append(promptElement, $('.chat-welcome-view-suggested-prompt-icon')); - iconElement.appendChild(renderIcon(prompt.icon)); - } - const labelElement = dom.append(promptElement, $('.chat-welcome-view-suggested-prompt-label')); - labelElement.textContent = prompt.label; - this._register(dom.addDisposableListener(promptElement, dom.EventType.CLICK, () => { - - type SuggestedPromptClickEvent = { suggestedPrompt: string }; - - type SuggestedPromptClickData = { - owner: 'bhavyaus'; - comment: 'Event used to gain insights into when suggested prompts are clicked.'; - suggestedPrompt: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The suggested prompt clicked.' }; - }; - - this.telemetryService.publicLog2('chat.clickedSuggestedPrompt', { - suggestedPrompt: prompt.prompt, - }); - - this.chatWidgetService.lastFocusedWidget?.setInput(prompt.prompt); - })); + if (typeof content.additionalMessage === 'string') { + const additionalMsg = $('.chat-welcome-view-experimental-additional-message'); + additionalMsg.textContent = content.additionalMessage; + dom.append(this.element, additionalMsg); + } + // also append telemetry message if available + } else { + // Additional message + if (typeof content.additionalMessage === 'string') { + const element = $(''); + element.textContent = content.additionalMessage; + dom.append(message, element); + } else if (content.additionalMessage) { + const additionalMessageResult = this.renderMarkdownMessageContent(renderer, content.additionalMessage, options); + dom.append(message, additionalMessageResult.element); } } From 128e677bf0fd3b762a77e745a5bce796049323e2 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 3 Jul 2025 11:57:46 -0400 Subject: [PATCH 087/306] prefer `Tab` kb over `Enter` so it shows in terminal suggest status bar (#253902) fix #253068 --- .../suggest/browser/terminal.suggest.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index 40ef5072964..ee00603dd43 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -427,7 +427,7 @@ registerActiveInstanceAction({ keybinding: [{ primary: KeyCode.Tab, // Tab is bound to other workbench keybindings that this needs to beat - weight: KeybindingWeight.WorkbenchContrib + 1 + weight: KeybindingWeight.WorkbenchContrib + 2 }, { primary: KeyCode.Enter, From 4e630c5ef40e2edfdcb924c2b4b28c2040b5ab50 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 3 Jul 2025 17:58:07 +0200 Subject: [PATCH 088/306] prompt file: add space when completing after colon (#253700) --- .../languageProviders/promptHeaderAutocompletion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e211947fe38..a932abbd6e6 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -171,7 +171,7 @@ export class PromptHeaderAutocompletion extends Disposable implements Completion const item: CompletionItem = { label: value, kind: CompletionItemKind.Value, - insertText: value, + insertText: whilespaceAfterColon === 0 ? ` ${value}` : value, range: new Range(position.lineNumber, colonPosition.column + whilespaceAfterColon + 1, position.lineNumber, model.getLineMaxColumn(position.lineNumber)), }; suggestions.push(item); From 7839464f8bcfc3083b48a25403743492080a7787 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 3 Jul 2025 17:58:25 +0200 Subject: [PATCH 089/306] show warning on tools when not used in agent mode (#253701) --- .../promptSyntax/parsers/promptHeader/promptHeader.ts | 2 +- .../promptSyntax/parsers/textModelPromptParser.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/promptHeader.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/promptHeader.ts index 5ec6de8e5c4..19185ee8961 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/promptHeader.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/promptHeader.ts @@ -133,7 +133,7 @@ export class PromptHeader extends HeaderBase { this.issues.push( new PromptMetadataWarning( - mode.range, + tools.range, localize( 'prompt.header.metadata.mode.diagnostics.incompatible-with-tools', "Tools can only be used when in 'agent' mode, but the mode is set to '{0}'. The tools will be ignored.", diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts index f010c192d64..cc9f4fe6629 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts @@ -691,7 +691,7 @@ suite('TextModelPromptParser', () => { 'Duplicate tool name \'tool_name2\'.', ), new ExpectedDiagnosticWarning( - new Range(3, 2, 3, 2 + 11), + new Range(5, 1, 5, 84), `Tools can only be used when in 'agent' mode, but the mode is set to 'ask'. The tools will be ignored.`, ), new ExpectedDiagnosticWarning( @@ -1173,7 +1173,7 @@ suite('TextModelPromptParser', () => { await test.validateHeaderDiagnostics([ new ExpectedDiagnosticWarning( - new Range(3, 1, 3, 1 + 11), + new Range(2, 1, 2, 38), 'Tools can only be used when in \'agent\' mode, but the mode is set to \'ask\'. The tools will be ignored.', ), ]); @@ -1220,7 +1220,7 @@ suite('TextModelPromptParser', () => { await test.validateHeaderDiagnostics([ new ExpectedDiagnosticWarning( - new Range(3, 1, 3, 1 + 12), + new Range(2, 1, 2, 38), 'Tools can only be used when in \'agent\' mode, but the mode is set to \'edit\'. The tools will be ignored.', ), ]); From 0179b5c08aee0e1a796fb90db11ad2068562e886 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 3 Jul 2025 17:58:53 +0200 Subject: [PATCH 090/306] fix showing installed server command (#253894) --- src/vs/workbench/contrib/mcp/browser/mcpCommands.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index 8d54d9fa28b..120d0e34fcf 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -55,6 +55,7 @@ import { VSBuffer } from '../../../../base/common/buffer.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { CHAT_CONFIG_MENU_ID } from '../../chat/browser/actions/chatActions.js'; +import { VIEW_CONTAINER } from '../../extensions/browser/extensions.contribution.js'; // acroynms do not get localized const category: ILocalizedString = { @@ -740,7 +741,12 @@ export class ShowInstalledMcpServersCommand extends Action2 { } async run(accessor: ServicesAccessor) { - accessor.get(IViewsService).openView(InstalledMcpServersViewId, true); + const viewsService = accessor.get(IViewsService); + const view = await viewsService.openView(InstalledMcpServersViewId, true); + if (!view) { + await viewsService.openViewContainer(VIEW_CONTAINER.id); + await viewsService.openView(InstalledMcpServersViewId, true); + } } } From 13eb13b9ebab0927484eb3910bd403b4c69b8783 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 3 Jul 2025 11:59:17 -0400 Subject: [PATCH 091/306] don't add `inlineCompletionItem` to model unless it's supported (#253886) fix #252367 --- .../suggest/browser/terminalSuggestAddon.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 1f5365fb007..54457166471 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -342,11 +342,13 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } const lineContext = new LineContext(normalizedLeadingLineContent, this._cursorIndexDelta); + const items = completions.filter(c => !!c.label).map(c => new TerminalCompletionItem(c)); + if (isInlineCompletionSupported(this.shellType)) { + items.push(this._inlineCompletionItem); + } + const model = new TerminalCompletionModel( - [ - ...completions.filter(c => !!c.label).map(c => new TerminalCompletionItem(c)), - this._inlineCompletionItem, - ], + items, lineContext ); if (token.isCancellationRequested) { From 963660fc93d737b1f8da1349acf53897f15f582b Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 3 Jul 2025 18:04:50 +0200 Subject: [PATCH 092/306] Remove blocking comment from data classification (#253931) remove comment which is blocking classifying data --- src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index ec0b527c801..01cbab7d060 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -1311,7 +1311,6 @@ type InlineCompletionEndOfLifeEvent = { superseded: boolean; editorType: string; viewKind: string | undefined; - // render info cursorColumnDistance: number | undefined; cursorLineDistance: number | undefined; lineCountOriginal: number | undefined; From 9d28256bf954d520e86f3c49e7858163be0d7f66 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 3 Jul 2025 18:22:02 +0200 Subject: [PATCH 093/306] Add localization for cancel snooze action and tweak css (#253940) missing localization --- src/vs/workbench/contrib/chat/browser/chatStatus.ts | 2 +- src/vs/workbench/contrib/chat/browser/media/chatStatus.css | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus.ts b/src/vs/workbench/contrib/chat/browser/chatStatus.ts index caa45e15333..af3d0523f82 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus.ts @@ -732,7 +732,7 @@ class ChatStatusDashboard extends Disposable { const toolbar = disposables.add(new ActionBar(actionBar, { hoverDelegate: nativeHoverDelegate })); const cancelAction = toAction({ id: 'workbench.action.cancelSnoozeStatusBarLink', - label: 'Cancel Snooze', + label: localize('cancelSnooze', "Cancel Snooze"), run: () => this.inlineCompletionsService.cancelSnooze(), class: ThemeIcon.asClassName(Codicon.stopCircle) }); diff --git a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css index 3cdad3c5980..d47c9b79565 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css @@ -138,6 +138,8 @@ text-overflow: ellipsis; text-wrap: nowrap; padding: 2px 8px; + user-select: none; + -webkit-user-select: none; } .chat-status-bar-entry-tooltip .snooze-completions .snooze-label { @@ -147,6 +149,7 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + font-variant-numeric: tabular-nums; } .chat-status-bar-entry-tooltip .snooze-completions.disabled .snooze-label { From 1d33fc1ba78ce6eed2bc834394aa1e00785d4fa9 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 3 Jul 2025 09:27:08 -0700 Subject: [PATCH 094/306] debug: bump js-deug to 1.102 (#253937) --- product.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/product.json b/product.json index 91140d9a483..a0719353135 100644 --- a/product.json +++ b/product.json @@ -52,8 +52,8 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.100.1", - "sha256": "8c2218df3422d45b95e96d9d28cdc4aa4426a2799aaaedd862d3f60ecab03844", + "version": "1.102.0", + "sha256": "0e8ed27ba2d707bcfb008e89e490c2d287d9537d84893b0792a4ee418274fa0b", "repo": "https://github.com/microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", From b6b750cd8c4a67a7e6c48f42e6820901d61a08a0 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 3 Jul 2025 18:34:16 +0200 Subject: [PATCH 095/306] Checkbox cut off (fix #253221) (#253912) --- src/vs/base/browser/ui/dialog/dialog.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css index 9a15ab6a2d9..2260cee2b66 100644 --- a/src/vs/base/browser/ui/dialog/dialog.css +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -143,6 +143,7 @@ cursor: pointer; user-select: none; -webkit-user-select: none; + flex: 1; } /** Dialog: Input */ From b08c9eb3575490659995224126340bbd7ad96205 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Thu, 3 Jul 2025 09:50:15 -0700 Subject: [PATCH 096/306] Restore experiment from storage if exists (#253943) * Restore experiment from storage if exists * Update test --- .../welcomeGettingStarted/browser/gettingStarted.ts | 1 - .../welcomeGettingStarted/browser/startupPage.ts | 1 - .../common/coreExperimentationService.ts | 12 ++++++++++-- .../test/browser/coreExperimentationService.test.ts | 4 ++-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 0302e33400c..77a3a36c528 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -962,7 +962,6 @@ export class GettingStartedPage extends EditorPane { const startupExpValue = startupExpContext.getValue(this.contextService); if (fistContentBehaviour === 'openToFirstCategory' && ((!startupExpValue || startupExpValue === '' || startupExpValue === StartupExperimentGroup.Control))) { - startupExpContext.bindTo(this.contextService).reset(); const first = this.gettingStartedCategories.filter(c => !c.when || this.contextService.contextMatchesRules(c.when))[0]; if (first) { this.hasScrolledToFirstCategory = true; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts index 8a974a0dff0..e11dd8dc01d 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts @@ -148,7 +148,6 @@ export class StartupPageRunnerContribution extends Disposable implements IWorkbe if (this.storageService.isNew(StorageScope.APPLICATION)) { const startupExpValue = startupExpContext.getValue(this.contextKeyService); if (startupExpValue === StartupExperimentGroup.MaximizedChat || startupExpValue === StartupExperimentGroup.SplitEmptyEditorChat) { - startupExpContext.bindTo(this.contextKeyService).reset(); return; } } diff --git a/src/vs/workbench/services/coreExperimentation/common/coreExperimentationService.ts b/src/vs/workbench/services/coreExperimentation/common/coreExperimentationService.ts index 5db22d7ae7a..531c20ca388 100644 --- a/src/vs/workbench/services/coreExperimentation/common/coreExperimentationService.ts +++ b/src/vs/workbench/services/coreExperimentation/common/coreExperimentationService.ts @@ -107,7 +107,15 @@ export class CoreExperimentationService extends Disposable implements ICoreExper const storageKey = `coreExperimentation.${experimentConfig.experimentName}`; const storedExperiment = this.storageService.get(storageKey, StorageScope.APPLICATION); if (storedExperiment) { - return; + try { + const parsedExperiment: IExperiment = JSON.parse(storedExperiment); + this.experiments.set(experimentConfig.experimentName, parsedExperiment); + startupExpContext.bindTo(this.contextKeyService).set(parsedExperiment.experimentGroup); + return; + } catch (e) { + this.storageService.remove(storageKey, StorageScope.APPLICATION); + return; + } } const experiment = this.createStartupExperiment(experimentConfig.experimentName, experimentConfig); @@ -196,4 +204,4 @@ export class CoreExperimentationService extends Disposable implements ICoreExper } } -registerSingleton(ICoreExperimentationService, CoreExperimentationService, InstantiationType.Delayed); +registerSingleton(ICoreExperimentationService, CoreExperimentationService, InstantiationType.Eager); diff --git a/src/vs/workbench/services/coreExperimentation/test/browser/coreExperimentationService.test.ts b/src/vs/workbench/services/coreExperimentation/test/browser/coreExperimentationService.test.ts index c35ebae816d..f15b136c3d9 100644 --- a/src/vs/workbench/services/coreExperimentation/test/browser/coreExperimentationService.test.ts +++ b/src/vs/workbench/services/coreExperimentation/test/browser/coreExperimentationService.test.ts @@ -80,7 +80,7 @@ suite('CoreExperimentationService', () => { contextKeyService = new MockContextKeyService(); }); - test('should not initialize experiment if user has already seen startup experience (found in storage)', () => { + test('should return experiment from storage if it exists', () => { storageService.store(firstSessionDateStorageKey, new Date().toUTCString(), StorageScope.APPLICATION, StorageTarget.MACHINE); // Set that user has already seen the experiment @@ -101,7 +101,7 @@ suite('CoreExperimentationService', () => { )); // Should not return experiment again - assert.strictEqual(service.getExperiment(), undefined); + assert.deepStrictEqual(service.getExperiment(), existingExperiment); // No telemetry should be sent for new experiment assert.strictEqual(telemetryService.events.length, 0); From f79494158437524b82cc9bb7444594e68b50b45d Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Thu, 3 Jul 2025 10:02:16 -0700 Subject: [PATCH 097/306] Show new welcome exp to existing users (#253696) * Show new welcome exp to existing users * Enhance chat widget welcome experience with context key checks --------- Co-authored-by: Benjamin Pasero --- .../workbench/contrib/chat/browser/chatWidget.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index bd917a0fd9a..d551719d75c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -25,7 +25,7 @@ import { ICodeEditorService } from '../../../../editor/browser/services/codeEdit import { localize } from '../../../../nls.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { ITextResourceEditorInput } from '../../../../platform/editor/common/editor.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -734,11 +734,21 @@ export class ChatWidget extends Disposable implements IChatWidget { const numItems = this.viewModel?.getItems().length ?? 0; if (!numItems) { dom.clearNode(this.welcomeMessageContainer); + // TODO@havyaus remove this startup experiment once settled const startupExpValue = startupExpContext.getValue(this.contextKeyService); + const configuration = this.configurationService.inspect('workbench.secondarySideBar.defaultVisibility'); + const expIsActive = configuration.defaultValue !== 'hidden'; + + const chatSetupTriggerContext = ContextKeyExpr.or( + ChatContextKeys.Setup.installed.negate(), + ChatContextKeys.Entitlement.canSignUp + ); + let welcomeContent: IChatViewWelcomeContent; - if (startupExpValue === StartupExperimentGroup.MaximizedChat + if ((startupExpValue === StartupExperimentGroup.MaximizedChat || startupExpValue === StartupExperimentGroup.SplitEmptyEditorChat - || startupExpValue === StartupExperimentGroup.SplitWelcomeChat) { + || startupExpValue === StartupExperimentGroup.SplitWelcomeChat + || expIsActive) && this.contextKeyService.contextMatchesRules(chatSetupTriggerContext)) { welcomeContent = this.getExpWelcomeViewContent(); this.container.classList.add('experimental-welcome-view'); } From ccb78d8ac756e623439383da112c3c9574c9251c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 18:12:49 +0000 Subject: [PATCH 098/306] Initial plan From 7e17b1a77c80b783ef7c19c48cc434b828a0daaf Mon Sep 17 00:00:00 2001 From: Robo Date: Fri, 4 Jul 2025 03:13:55 +0900 Subject: [PATCH 099/306] fix: cleanup web worker when worker client gets disposed (#253967) * fix: cleanup web worker when worker client gets disposed * simplify --------- Co-authored-by: Benjamin Pasero --- src/vs/base/common/worker/webWorker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/common/worker/webWorker.ts b/src/vs/base/common/worker/webWorker.ts index 666bd15aa03..999f490c590 100644 --- a/src/vs/base/common/worker/webWorker.ts +++ b/src/vs/base/common/worker/webWorker.ts @@ -315,7 +315,7 @@ export class WebWorkerClient extends Disposable implements IWe ) { super(); - this._worker = worker; + this._worker = this._register(worker); this._register(this._worker.onMessage((msg) => { this._protocol.handleMessage(msg); })); From 7e059a1cd7f021fdbb0f1bc0294a01e51ed0e318 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 18:19:21 +0000 Subject: [PATCH 100/306] Fix hover button text selection issue by preventing user-select on button elements Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com> --- src/vs/base/browser/ui/hover/hoverWidget.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/vs/base/browser/ui/hover/hoverWidget.css b/src/vs/base/browser/ui/hover/hoverWidget.css index c8138b37c16..755dec23531 100644 --- a/src/vs/base/browser/ui/hover/hoverWidget.css +++ b/src/vs/base/browser/ui/hover/hoverWidget.css @@ -207,3 +207,13 @@ opacity: 0.4; cursor: default; } + +/* Prevent text selection in all button-like elements within hovers */ +.monaco-hover .action-container, +.monaco-hover button, +.monaco-hover .monaco-button, +.monaco-hover .monaco-text-button, +.monaco-hover [role="button"] { + -webkit-user-select: none; + user-select: none; +} From 8470700d866d8a132ccdd3f4a97ca8816004124b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 18:23:09 +0000 Subject: [PATCH 101/306] Add .action selector to prevent text selection in all action elements Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com> --- src/vs/base/browser/ui/hover/hoverWidget.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/base/browser/ui/hover/hoverWidget.css b/src/vs/base/browser/ui/hover/hoverWidget.css index 755dec23531..ccaac1ed9b2 100644 --- a/src/vs/base/browser/ui/hover/hoverWidget.css +++ b/src/vs/base/browser/ui/hover/hoverWidget.css @@ -210,6 +210,7 @@ /* Prevent text selection in all button-like elements within hovers */ .monaco-hover .action-container, +.monaco-hover .action, .monaco-hover button, .monaco-hover .monaco-button, .monaco-hover .monaco-text-button, From ebe7d20b38b4eee2cc8a7fecd33493f5dfcf6d7f Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 3 Jul 2025 14:32:24 -0400 Subject: [PATCH 102/306] update `Learn More` keybinding (#253891) fix #253110 --- .../suggest/browser/terminal.suggest.contribution.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index ee00603dd43..6345d50bdee 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -297,9 +297,9 @@ registerTerminalAction({ order: 1 }, keybinding: { - primary: KeyMod.CtrlCmd | KeyCode.Slash, - mac: { primary: KeyMod.WinCtrl | KeyCode.KeyK }, - weight: KeybindingWeight.WorkbenchContrib + 1 + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL, + weight: KeybindingWeight.WorkbenchContrib + 1, + when: TerminalContextKeys.suggestWidgetVisible }, run: (c, accessor) => { (accessor.get(IOpenerService)).open('https://aka.ms/vscode-terminal-intellisense'); From 1741b45cc278baeaf29e9d8c14ba5059797e1ae1 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 18:57:11 +0000 Subject: [PATCH 103/306] Fix welcome view padding to ensure adequate spacing when resized (#253959) * Initial plan * Fix welcome view padding to ensure adequate spacing when resized Co-authored-by: bhavyaus <25044782+bhavyaus@users.noreply.github.com> * Fix welcome view content padding for consistent spacing * Fix padding values for chat welcome view for consistent layout --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bhavyaus <25044782+bhavyaus@users.noreply.github.com> Co-authored-by: bhavyaus --- src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css index 872583659e4..a62e9e686ab 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css @@ -58,6 +58,7 @@ div.chat-welcome-view { font-weight: 500; text-align: center; line-height: normal; + padding: 0 8px; } & > .chat-welcome-view-indicator-container { From 63a74fcb5de7aa6259ce7d4ca0a6157b3fa2099b Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 3 Jul 2025 12:09:44 -0700 Subject: [PATCH 104/306] fix padding on first element (#253711) Co-authored-by: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> --- src/vs/workbench/contrib/chat/browser/media/chat.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 2f657d8aea3..a99fa653618 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -1975,7 +1975,7 @@ have to be updated for changes to the rules above, or to support more deeply nes } div[data-index="0"] .monaco-tl-contents { - .interactive-item-container.interactive-request { + .interactive-item-container.interactive-request:not(.editing) { padding-top: 19px; } From 2fd47d88fc2c12b15ec4da06515070a940c0953b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 3 Jul 2025 21:14:22 +0200 Subject: [PATCH 105/306] Revert "layout - never show secondary sidebar by default if empty (fix #253855)" (#253976) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "layout - never show secondary sidebar by default if empty (fix #25385…" This reverts commit 4950db1f74217e09ceecb05fa51e826b69627251. --- src/vs/workbench/browser/layout.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 352b7df5b1a..ec7757a570e 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -633,13 +633,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private initLayoutState(lifecycleService: ILifecycleService, fileService: IFileService, coreExperimentationService: ICoreExperimentationService): void { this._mainContainerDimension = getClientArea(this.parent, DEFAULT_WINDOW_DIMENSIONS); // running with fallback to ensure no error is thrown (https://github.com/microsoft/vscode/issues/240242) - this.stateModel = new LayoutStateModel(this.storageService, this.configurationService, this.contextService, coreExperimentationService); + this.stateModel = new LayoutStateModel(this.storageService, this.configurationService, this.contextService, coreExperimentationService, this.environmentService); this.stateModel.load({ mainContainerDimension: this._mainContainerDimension, - resetLayout: Boolean(this.layoutOptions?.resetLayout), - isAuxiliaryBarEmpty: this.viewDescriptorService - .getViewContainersByLocation(ViewContainerLocation.AuxiliaryBar) - .find(viewContainer => this.hasViews(viewContainer.id))?.id !== undefined + resetLayout: Boolean(this.layoutOptions?.resetLayout) }); this._register(this.stateModel.onDidChangeState(change => { @@ -2792,7 +2789,6 @@ enum LegacyWorkbenchLayoutSettings { interface ILayoutStateLoadConfiguration { readonly mainContainerDimension: IDimension; readonly resetLayout: boolean; - readonly isAuxiliaryBarEmpty: boolean; } class LayoutStateModel extends Disposable { @@ -2808,7 +2804,8 @@ class LayoutStateModel extends Disposable { private readonly storageService: IStorageService, private readonly configurationService: IConfigurationService, private readonly contextService: IWorkspaceContextService, - private readonly coreExperimentationService: ICoreExperimentationService + private readonly coreExperimentationService: ICoreExperimentationService, + private readonly environmentService: IBrowserWorkbenchEnvironmentService ) { super(); @@ -2871,8 +2868,9 @@ class LayoutStateModel extends Disposable { LayoutStateKeys.SIDEBAR_HIDDEN.defaultValue = workbenchState === WorkbenchState.EMPTY; LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, mainContainerDimension.width / 4); LayoutStateKeys.AUXILIARYBAR_HIDDEN.defaultValue = (() => { - if (configuration.isAuxiliaryBarEmpty) { - return true; // require a view in the auxiliary bar to show it by default + const configuration = this.configurationService.inspect(WorkbenchLayoutSettings.AUXILIARYBAR_DEFAULT_VISIBILITY); + if (configuration.defaultValue !== 'hidden' && isWeb && !this.environmentService.remoteAuthority) { + return true; // TODO@bpasero revisit this when Chat is available in serverless web } switch (this.configurationService.getValue(WorkbenchLayoutSettings.AUXILIARYBAR_DEFAULT_VISIBILITY)) { From 6986b1c85614bf78d0f2241a5c91f85df1bbb6fd Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 3 Jul 2025 21:19:27 +0200 Subject: [PATCH 106/306] register missing services (#253747) Co-authored-by: Josh Spicer <23246594+joshspicer@users.noreply.github.com> --- src/vs/code/node/cliProcessMain.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index ad1cc2ae608..6c569ca4c8a 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -68,9 +68,10 @@ import { AllowedExtensionsService } from '../../platform/extensionManagement/com import { McpManagementCli } from '../../platform/mcp/common/mcpManagementCli.js'; import { IExtensionGalleryManifestService } from '../../platform/extensionManagement/common/extensionGalleryManifest.js'; import { ExtensionGalleryManifestService } from '../../platform/extensionManagement/common/extensionGalleryManifestService.js'; -import { IMcpManagementService } from '../../platform/mcp/common/mcpManagement.js'; +import { IMcpGalleryService, IMcpManagementService } from '../../platform/mcp/common/mcpManagement.js'; import { McpManagementService } from '../../platform/mcp/common/mcpManagementService.js'; import { IMcpResourceScannerService, McpResourceScannerService } from '../../platform/mcp/common/mcpResourceScannerService.js'; +import { McpGalleryService } from '../../platform/mcp/common/mcpGalleryService.js'; class CliMain extends Disposable { @@ -229,6 +230,7 @@ class CliMain extends Disposable { // MCP services.set(IMcpResourceScannerService, new SyncDescriptor(McpResourceScannerService, undefined, true)); + services.set(IMcpGalleryService, new SyncDescriptor(McpGalleryService, undefined, true)); services.set(IMcpManagementService, new SyncDescriptor(McpManagementService, undefined, true)); // Telemetry From 68196ad36bee379db8aa100fc90bda5367224818 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 3 Jul 2025 12:26:00 -0700 Subject: [PATCH 107/306] add a bit more padding to container in inline chat (#253772) add a bit more padding to container --- .../workbench/contrib/inlineChat/browser/media/inlineChat.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index 01c83a45b7b..74d57ab1ce2 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -359,6 +359,10 @@ opacity: 1; } +.monaco-workbench .inline-chat .chat-attached-context { + padding: 3px 0px; +} + /* HINT */ From f663acad336e79613285b495494d3f8132003abd Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Thu, 3 Jul 2025 12:51:39 -0700 Subject: [PATCH 108/306] Add suggested prompts to chat widget welcome view and update experiment configurations (#253975) --- .../contrib/chat/browser/chatWidget.ts | 43 +++++++++++++++++-- .../chat/browser/media/chatViewWelcome.css | 42 ++++++++++++++++++ .../viewsWelcome/chatViewWelcomeController.ts | 43 ++++++++++++++++++- 3 files changed, 123 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 25e470e30a2..4398ce1a6fe 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -64,13 +64,14 @@ import { ChatEditorOptions } from './chatOptions.js'; import './media/chat.css'; import './media/chatAgentHover.css'; import './media/chatViewWelcome.css'; -import { ChatViewWelcomePart, IChatViewWelcomeContent } from './viewsWelcome/chatViewWelcomeController.js'; +import { ChatViewWelcomePart, IChatSuggestedPrompts, IChatViewWelcomeContent } from './viewsWelcome/chatViewWelcomeController.js'; import { MicrotaskDelay } from '../../../../base/common/symbols.js'; import { IChatRequestVariableEntry, ChatRequestVariableSet as ChatRequestVariableSet, isPromptFileVariableEntry, toPromptFileVariableEntry, PromptFileVariableKind } from '../common/chatVariableEntries.js'; import { PromptsConfig } from '../common/promptSyntax/config/config.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ComputeAutomaticInstructions } from '../common/promptSyntax/computeAutomaticInstructions.js'; import { startupExpContext, StartupExperimentGroup } from '../../../services/coreExperimentation/common/coreExperimentationService.js'; +import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; const $ = dom.$; @@ -291,6 +292,7 @@ export class ChatWidget extends Disposable implements IChatWidget { @ITelemetryService private readonly telemetryService: ITelemetryService, @IPromptsService private readonly promptsService: IPromptsService, @ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService ) { super(); @@ -734,7 +736,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const numItems = this.viewModel?.getItems().length ?? 0; if (!numItems) { dom.clearNode(this.welcomeMessageContainer); - // TODO@havyaus remove this startup experiment once settled + // TODO@bhavyaus remove this startup experiment once settled const startupExpValue = startupExpContext.getValue(this.contextKeyService); const configuration = this.configurationService.inspect('workbench.secondarySideBar.defaultVisibility'); const expIsActive = configuration.defaultValue !== 'hidden'; @@ -810,12 +812,45 @@ export class ChatWidget extends Disposable implements IChatWidget { message: new MarkdownString(localize('expchatMessage', "Let's get started")), icon: Codicon.copilotLarge, inputPart: this.inputPart.element, - additionalMessage: localize('expChatAdditionalMessage', "Review output carefully before use."), - isExperimental: true + additionalMessage: localize('expChatAdditionalMessage', "Review AI output carefully before use."), + isExperimental: true, + suggestedPrompts: this.getExpSuggestedPrompts(), }; return welcomeContent; } + private getExpSuggestedPrompts(): IChatSuggestedPrompts[] { + // Check if the workbench is empty + const isEmpty = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY; + if (isEmpty) { + return [ + { + icon: Codicon.vscode, + label: localize('chatWidget.suggestedPrompts.gettingStarted', "Ask @vscode"), + prompt: localize('chatWidget.suggestedPrompts.gettingStartedPrompt', "@vscode How do I change the theme to light mode?"), + }, + { + icon: Codicon.newFolder, + label: localize('chatWidget.suggestedPrompts.newProject', "Create project"), + prompt: localize('chatWidget.suggestedPrompts.newProjectPrompt', "Create a #new Hello World project in TypeScript"), + } + ]; + } else { + return [ + { + icon: Codicon.debugAlt, + label: localize('chatWidget.suggestedPrompts.buildWorkspace', "Build workspace"), + prompt: localize('chatWidget.suggestedPrompts.buildWorkspacePrompt', "How do I build this workspace?"), + }, + { + icon: Codicon.gear, + label: localize('chatWidget.suggestedPrompts.findConfig', "Show project config"), + prompt: localize('chatWidget.suggestedPrompts.findConfigPrompt', "Where is the configuration for this project defined?"), + } + ]; + } + } + private async renderChatEditingSessionState() { if (!this.input) { return; diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css index a62e9e686ab..b1bbaff2195 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css @@ -124,5 +124,47 @@ div.chat-welcome-view { color: var(--vscode-disabledForeground); text-align: center; max-width: 400px; + margin-top: 8px; + } + + & > .chat-welcome-view-suggested-prompts { + display: flex; + flex-wrap: wrap; + justify-content: center; + margin-top: 4px; + + > .chat-welcome-view-suggested-prompt { + display: flex; + align-items: center; + padding: 2px; + border-radius: 8px; + background-color: var(--vscode-editorWidget-background); + cursor: pointer; + border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); + border-radius: 4px; + max-width: 100%; + width: fit-content; + margin: 0 4px; + + > .chat-welcome-view-suggested-prompt-icon { + display: flex; + align-items: center; + font-size: 4px; + color: var(--vscode-icon-foreground) !important; + align-items: center; + padding: 4px; + } + + > .chat-welcome-view-suggested-prompt-label { + font-size: 14px; + color: var(--vscode-editorWidget-foreground); + padding: 4px 4px 4px 0; + } + } + + > .chat-welcome-view-suggested-prompt:hover { + background-color: var(--vscode-list-hoverBackground); + border-color: var(--vscode-focusBorder); + } } } diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index fc1bdc60794..5f501338086 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -16,8 +16,10 @@ import { IContextKeyService } from '../../../../../platform/contextkey/common/co import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; +import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { ChatAgentLocation } from '../../common/constants.js'; +import { IChatWidgetService } from '../chat.js'; import { chatViewsWelcomeRegistry, IChatViewsWelcomeDescriptor } from './chatViewsWelcome.js'; const $ = dom.$; @@ -114,6 +116,13 @@ export interface IChatViewWelcomeContent { tips?: IMarkdownString; inputPart?: HTMLElement; isExperimental?: boolean; + suggestedPrompts?: IChatSuggestedPrompts[]; +} + +export interface IChatSuggestedPrompts { + icon?: ThemeIcon; + label: string; + prompt: string; } export interface IChatViewWelcomeRenderOptions { @@ -131,6 +140,8 @@ export class ChatViewWelcomePart extends Disposable { @IOpenerService private openerService: IOpenerService, @IInstantiationService private instantiationService: IInstantiationService, @ILogService private logService: ILogService, + @IChatWidgetService private chatWidgetService: IChatWidgetService, + @ITelemetryService private telemetryService: ITelemetryService, ) { super(); this.element = dom.$('.chat-welcome-view'); @@ -166,12 +177,42 @@ export class ChatViewWelcomePart extends Disposable { if (content.isExperimental && content.inputPart) { content.inputPart.querySelector('.chat-attachments-container')?.remove(); dom.append(this.element, content.inputPart); + + if (content.suggestedPrompts && content.suggestedPrompts.length) { + // create a tile with icon and label for each suggested promot + const suggestedPromptsContainer = dom.append(this.element, $('.chat-welcome-view-suggested-prompts')); + for (const prompt of content.suggestedPrompts) { + const promptElement = dom.append(suggestedPromptsContainer, $('.chat-welcome-view-suggested-prompt')); + if (prompt.icon) { + const iconElement = dom.append(promptElement, $('.chat-welcome-view-suggested-prompt-icon')); + iconElement.appendChild(renderIcon(prompt.icon)); + } + const labelElement = dom.append(promptElement, $('.chat-welcome-view-suggested-prompt-label')); + labelElement.textContent = prompt.label; + this._register(dom.addDisposableListener(promptElement, dom.EventType.CLICK, () => { + + type SuggestedPromptClickEvent = { suggestedPrompt: string }; + + type SuggestedPromptClickData = { + owner: 'bhavyaus'; + comment: 'Event used to gain insights into when suggested prompts are clicked.'; + suggestedPrompt: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The suggested prompt clicked.' }; + }; + + this.telemetryService.publicLog2('chat.clickedSuggestedPrompt', { + suggestedPrompt: prompt.prompt, + }); + + this.chatWidgetService.lastFocusedWidget?.setInput(prompt.prompt); + })); + } + } + if (typeof content.additionalMessage === 'string') { const additionalMsg = $('.chat-welcome-view-experimental-additional-message'); additionalMsg.textContent = content.additionalMessage; dom.append(this.element, additionalMsg); } - // also append telemetry message if available } else { // Additional message if (typeof content.additionalMessage === 'string') { From 264329bf8e8e1d623fe41b350e22ca2f88594212 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 3 Jul 2025 22:39:36 +0200 Subject: [PATCH 109/306] use the snooze icon (#253950) --- src/vs/workbench/contrib/chat/browser/chatStatus.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus.ts b/src/vs/workbench/contrib/chat/browser/chatStatus.ts index af3d0523f82..259381a28fd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus.ts @@ -119,6 +119,7 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu @IStatusbarService private readonly statusbarService: IStatusbarService, @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IInlineCompletionsService private readonly completionsService: IInlineCompletionsService, ) { super(); @@ -143,6 +144,7 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu this._register(this.chatEntitlementService.onDidChangeQuotaExceeded(() => this.update())); this._register(this.chatEntitlementService.onDidChangeSentiment(() => this.update())); this._register(this.chatEntitlementService.onDidChangeEntitlement(() => this.update())); + this._register(this.completionsService.onDidChangeIsSnoozing(() => this.update())); this._register(this.editorService.onDidActiveEditorChange(() => this.onDidActiveEditorChange())); @@ -228,6 +230,12 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu text = `$(copilot-unavailable)`; ariaLabel = localize('completionsDisabledStatus', "Code completions disabled"); } + + // Completions Snoozed + else if (this.completionsService.isSnoozing()) { + text = `$(copilot-snooze)`; + ariaLabel = localize('completionsSnoozedStatus', "Code completions snoozed"); + } } return { From 4b6db63faa71640db1dabe06ba6501a2de37b9d2 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Thu, 3 Jul 2025 13:44:04 -0700 Subject: [PATCH 110/306] refactor: update tool registration check to filter by 'copilot_' prefix (#253983) * refactor: update tool registration check to filter by 'copilot_' prefix * refactor: simplify tool ID check by removing redundant type check --- src/vs/workbench/contrib/chat/browser/chatSetup.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 3c510fcd7ec..22d6de21673 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -353,14 +353,14 @@ class SetupAgent extends Disposable implements IChatAgentImplementation { // check that tools other than setup. and internal tools are registered. for (const tool of languageModelToolsService.getTools()) { - if (tool.source.type !== 'internal') { + if (tool.id.startsWith('copilot_')) { return; // we have tools! } } return Event.toPromise(Event.filter(languageModelToolsService.onDidChangeTools, () => { for (const tool of languageModelToolsService.getTools()) { - if (tool.source.type !== 'internal') { + if (tool.id.startsWith('copilot_')) { return true; // we have tools! } } From 219f8f19184671f1a7b2e9f7adcedec4c99b5c25 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 3 Jul 2025 13:44:44 -0700 Subject: [PATCH 111/306] never show prompt file (originally #253691) (#253973) never show prompt file (oroginally #253691) --- .../chatContentParts/chatAttachmentsContentPart.ts | 4 ++-- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 2 +- .../contrib/chat/common/chatVariableEntries.ts | 12 ++++++------ .../promptSyntax/computeAutomaticInstructions.ts | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index e03563b06c1..b2f292e7614 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -60,12 +60,12 @@ export class ChatAttachmentsContentPart extends Disposable { attachment.omittedState = isAttachmentPartialOrOmitted ? OmittedState.Full : attachment.omittedState; widget = this.instantiationService.createInstance(ImageAttachmentWidget, resource, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); } else if (isPromptFileVariableEntry(attachment)) { - if (attachment.isHidden) { + if (attachment.automaticallyAdded) { continue; } widget = this.instantiationService.createInstance(PromptFileAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); } else if (isPromptTextVariableEntry(attachment)) { - if (attachment.isHidden) { + if (attachment.automaticallyAdded) { continue; } widget = this.instantiationService.createInstance(PromptTextAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 4398ce1a6fe..caca8418b45 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1562,7 +1562,7 @@ export class ChatWidget extends Disposable implements IChatWidget { parseResult = await this.promptsService.resolvePromptSlashCommand(agentSlashPromptPart.slashPromptCommand, CancellationToken.None); if (parseResult) { // add the prompt file to the context, but not sticky - requestInput.attachedContext.insertFirst(toPromptFileVariableEntry(parseResult.uri, PromptFileVariableKind.PromptFile)); + requestInput.attachedContext.insertFirst(toPromptFileVariableEntry(parseResult.uri, PromptFileVariableKind.PromptFile, undefined, true)); // remove the slash command from the input requestInput.input = this.parsedInput.parts.filter(part => !(part instanceof ChatRequestSlashPromptPart)).map(part => part.text).join('').trim(); diff --git a/src/vs/workbench/contrib/chat/common/chatVariableEntries.ts b/src/vs/workbench/contrib/chat/common/chatVariableEntries.ts index cd074ad4d8f..90d5f479c92 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariableEntries.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariableEntries.ts @@ -187,7 +187,7 @@ export interface IPromptFileVariableEntry extends IBaseChatRequestVariableEntry readonly isRoot: boolean; readonly originLabel?: string; readonly modelDescription: string; - readonly isHidden: boolean; + readonly automaticallyAdded: boolean; } export interface IPromptTextVariableEntry extends IBaseChatRequestVariableEntry { @@ -195,7 +195,7 @@ export interface IPromptTextVariableEntry extends IBaseChatRequestVariableEntry readonly value: string; readonly settingId?: string; readonly modelDescription: string; - readonly isHidden: boolean; + readonly automaticallyAdded: boolean; } export interface ISCMHistoryItemVariableEntry extends IBaseChatRequestVariableEntry { @@ -291,7 +291,7 @@ export enum PromptFileVariableKind { * @param uri A resource URI that points to a prompt instructions file. * @param kind The kind of the prompt file variable entry. */ -export function toPromptFileVariableEntry(uri: URI, kind: PromptFileVariableKind, originLabel?: string): IPromptFileVariableEntry { +export function toPromptFileVariableEntry(uri: URI, kind: PromptFileVariableKind, originLabel?: string, automaticallyAdded = false): IPromptFileVariableEntry { // `id` for all `prompt files` starts with the well-defined part that the copilot extension(or other chatbot) can rely on return { id: `${kind}__${uri.toString()}`, @@ -301,11 +301,11 @@ export function toPromptFileVariableEntry(uri: URI, kind: PromptFileVariableKind modelDescription: 'Prompt instructions file', isRoot: kind !== PromptFileVariableKind.InstructionReference, originLabel, - isHidden: kind === PromptFileVariableKind.PromptFile + automaticallyAdded }; } -export function toPromptTextVariableEntry(content: string, settingId?: string): IPromptTextVariableEntry { +export function toPromptTextVariableEntry(content: string, settingId?: string, automaticallyAdded = false): IPromptTextVariableEntry { return { id: `vscode.prompt.instructions.text${settingId ? `.${settingId}` : ''}`, name: `prompt:text`, @@ -313,7 +313,7 @@ export function toPromptTextVariableEntry(content: string, settingId?: string): settingId, kind: 'promptText', modelDescription: 'Prompt instructions text', - isHidden: true, // do not show in the UI + automaticallyAdded }; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index 22dab49c4b2..07529626522 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -74,7 +74,7 @@ export class ComputeAutomaticInstructions { const instructionsWithPatternsList = await this._getInstructionsWithPatternsList(instructionFiles, variables, token); if (instructionsWithPatternsList.length > 0) { const text = instructionsWithPatternsList.join('\n'); - variables.add(toPromptTextVariableEntry(text, PromptsConfig.COPILOT_INSTRUCTIONS)); + variables.add(toPromptTextVariableEntry(text, PromptsConfig.COPILOT_INSTRUCTIONS, true)); } // add all instructions for all instruction files that are in the context await this._addReferencedInstructions(variables, token); @@ -114,7 +114,7 @@ export class ComputeAutomaticInstructions { localize('instruction.file.reason.specificFile', 'Automatically attached as pattern {0} matches {1}', applyTo, this._labelService.getUriLabel(match.file, { relative: true })); - autoAddedInstructions.push(toPromptFileVariableEntry(uri, PromptFileVariableKind.Instruction, reason)); + autoAddedInstructions.push(toPromptFileVariableEntry(uri, PromptFileVariableKind.Instruction, reason, true)); } else { this._logService.trace(`[InstructionsContextComputer] No match for ${uri} with ${applyTo}`); } @@ -153,7 +153,7 @@ export class ComputeAutomaticInstructions { for (const instructionFilePath of instructionFiles) { const file = joinPath(folder.uri, instructionFilePath); if (await this._fileService.exists(file)) { - entries.push(toPromptFileVariableEntry(file, PromptFileVariableKind.Instruction, localize('instruction.file.reason.copilot', 'Automatically attached as setting {0} is enabled', PromptsConfig.USE_COPILOT_INSTRUCTION_FILES))); + entries.push(toPromptFileVariableEntry(file, PromptFileVariableKind.Instruction, localize('instruction.file.reason.copilot', 'Automatically attached as setting {0} is enabled', PromptsConfig.USE_COPILOT_INSTRUCTION_FILES), true)); } } } @@ -260,7 +260,7 @@ export class ComputeAutomaticInstructions { if (stat.success && stat.stat?.isFile) { todo.push(uri); const reason = localize('instruction.file.reason.referenced', 'Referenced by {0}', basename(next)); - attachedContext.add(toPromptFileVariableEntry(uri, PromptFileVariableKind.InstructionReference, reason)); + attachedContext.add(toPromptFileVariableEntry(uri, PromptFileVariableKind.InstructionReference, reason, true)); } } } From 149c27734e30421e96173ea04dec6967a905f2aa Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 3 Jul 2025 22:56:04 +0200 Subject: [PATCH 112/306] Referenced Copilot Instructions File not Referenced by Copilot (#253861) --- .../common/promptSyntax/computeAutomaticInstructions.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index 07529626522..97b28420f83 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -247,7 +247,8 @@ export class ComputeAutomaticInstructions { const result = await this._parseInstructionsFile(next, token); const refsToCheck: { resource: URI }[] = []; for (const ref of result.references) { - if (!seen.has(ref) && isPromptOrInstructionsFile(ref)) { + if (!seen.has(ref) && (isPromptOrInstructionsFile(ref) || this._workspaceService.getWorkspaceFolder(ref) !== undefined)) { + // only add references that are either prompt or instruction files or are part of the workspace refsToCheck.push({ resource: ref }); seen.add(ref); } @@ -258,7 +259,10 @@ export class ComputeAutomaticInstructions { const stat = stats[i]; const uri = refsToCheck[i].resource; if (stat.success && stat.stat?.isFile) { - todo.push(uri); + if (isPromptOrInstructionsFile(uri)) { + // only recursivly parse instruction files + todo.push(uri); + } const reason = localize('instruction.file.reason.referenced', 'Referenced by {0}', basename(next)); attachedContext.add(toPromptFileVariableEntry(uri, PromptFileVariableKind.InstructionReference, reason, true)); } From 4fff4d204de5e6a8f0e7187e002ccf03ded16944 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 3 Jul 2025 23:08:03 +0200 Subject: [PATCH 113/306] Modes not updated properly when changing (#253986) --- .../filePromptContentsProvider.ts | 40 ++++++++++--------- .../promptContentsProviderBase.ts | 5 +++ .../textModelContentsProvider.ts | 6 +-- .../service/promptsServiceImpl.ts | 6 +-- .../filePromptContentsProvider.test.ts | 6 +-- .../parsers/textModelPromptParser.test.ts | 2 +- .../promptSyntax/promptFileReference.test.ts | 2 +- 7 files changed, 37 insertions(+), 30 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts index ea70d0137d1..47705213282 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts @@ -57,26 +57,28 @@ export class FilePromptContentProvider extends PromptContentsProviderBase { - // if file was added or updated, forward the event to - // the `getContentsStream()` produce a new stream for file contents - if (event.contains(this.uri, FileChangeType.ADDED, FileChangeType.UPDATED)) { - // we support only full file parsing right now because - // the event doesn't contain a list of changed lines - this.onChangeEmitter.fire('full'); - return; - } + if (options.updateOnChange) { + // make sure the object is updated on file changes + this._register( + this.fileService.onDidFilesChange((event) => { + // if file was added or updated, forward the event to + // the `getContentsStream()` produce a new stream for file contents + if (event.contains(this.uri, FileChangeType.ADDED, FileChangeType.UPDATED)) { + // we support only full file parsing right now because + // the event doesn't contain a list of changed lines + this.onChangeEmitter.fire('full'); + return; + } - // if file was deleted, forward the event to - // the `getContentsStream()` produce an error - if (event.contains(this.uri, FileChangeType.DELETED)) { - this.onChangeEmitter.fire(event); - return; - } - }), - ); + // if file was deleted, forward the event to + // the `getContentsStream()` produce an error + if (event.contains(this.uri, FileChangeType.DELETED)) { + this.onChangeEmitter.fire(event); + return; + } + }), + ); + } } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts index 73eaaae6a1f..7c9e58dc3d3 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts @@ -29,6 +29,11 @@ export interface IPromptContentsProviderOptions { * Language ID to use for the prompt contents. If not set, the language ID will be inferred from the file. */ readonly languageId: string | undefined; + + /** + * If set to `true`, the contents provider will listen for updates and retrigger a parse. + */ + readonly updateOnChange: boolean; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts index fc9b0714814..e083b773206 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts @@ -42,9 +42,9 @@ export class TextModelContentsProvider extends PromptContentsProviderBase { const contentsProvider = testDisposables.add(instantiationService.createInstance( FilePromptContentProvider, fileUri, - { allowNonPromptFiles: true, languageId: undefined }, + { allowNonPromptFiles: true, languageId: undefined, updateOnChange: true }, )); let streamOrError: ReadableStream | Error | undefined; @@ -128,7 +128,7 @@ suite('FilePromptContentsProvider', () => { const contentsProvider = testDisposables.add(instantiationService.createInstance( FilePromptContentProvider, fileUri, - { allowNonPromptFiles: true, languageId: undefined }, + { allowNonPromptFiles: true, languageId: undefined, updateOnChange: true }, )); let streamOrError: ReadableStream | Error | undefined; @@ -184,7 +184,7 @@ suite('FilePromptContentsProvider', () => { const contentsProvider = testDisposables.add(instantiationService.createInstance( FilePromptContentProvider, fileUri, - { allowNonPromptFiles: false, languageId: undefined }, + { allowNonPromptFiles: false, languageId: undefined, updateOnChange: true }, )); let streamOrError: ReadableStream | Error | undefined; diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts index cc9f4fe6629..7ff0ae1e359 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts @@ -71,7 +71,7 @@ class TextModelPromptParserTest extends Disposable { // create the parser instance this.parser = this._register( - instantiationService.createInstance(TextModelPromptParser, this.model, { allowNonPromptFiles: true, languageId: undefined }), + instantiationService.createInstance(TextModelPromptParser, this.model, { allowNonPromptFiles: true, languageId: undefined, updateOnChange: true }), ).start(); } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts index d810ed8299b..f3198003a3a 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts @@ -104,7 +104,7 @@ class TestPromptFileReference extends Disposable { this.instantiationService.createInstance( FilePromptParser, this.rootFileUri, - { allowNonPromptFiles: true, languageId: undefined }, + { allowNonPromptFiles: true, languageId: undefined, updateOnChange: true }, ), ).start(); From 61bb9b414f62646618cc3a52fd5adcda3f9303c6 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 3 Jul 2025 14:23:38 -0700 Subject: [PATCH 114/306] mcp: fix changes to MCP servers in mcp.json are not picked up (#253982) --- .../common/mcpWorkbenchManagementService.ts | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts b/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts index cc447afc203..d01429d53c0 100644 --- a/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts +++ b/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts @@ -148,14 +148,7 @@ class WorkbenchMcpManagementService extends Disposable implements IWorkbenchMcpM })); this._register(this.workspaceMcpManagementService.onDidInstallMcpServers(async e => { - const mcpServerInstallResult: IWorkbenchMcpServerInstallResult[] = []; - for (const result of e) { - const workbenchResult = { - ...result, - local: result.local ? this.toWorkspaceMcpServer(result.local, LocalMcpServerScope.Workspace) : undefined - }; - mcpServerInstallResult.push(workbenchResult); - } + const { mcpServerInstallResult } = this.createInstallMcpServerResultsFromEvent(e); this._onDidInstallMcpServersInCurrentProfile.fire(mcpServerInstallResult); this._onDidInstallMcpServersInCurrentProfile.fire(mcpServerInstallResult); })); @@ -170,6 +163,12 @@ class WorkbenchMcpManagementService extends Disposable implements IWorkbenchMcpM this._onDidUninstallMcpServerInCurrentProfile.fire(e); })); + this._register(this.workspaceMcpManagementService.onDidUpdateMcpServers(e => { + const { mcpServerInstallResult } = this.createInstallMcpServerResultsFromEvent(e); + this._onDidUpdateMcpServersInCurrentProfile.fire(mcpServerInstallResult); + this._onDidUpdateMcpServersInCurrentProfile.fire(mcpServerInstallResult); + })); + if (this.remoteMcpManagementService) { this._register(this.remoteMcpManagementService.onInstallMcpServer(async e => { this._onInstallMcpServer.fire(e); @@ -206,7 +205,7 @@ class WorkbenchMcpManagementService extends Disposable implements IWorkbenchMcpM })); } - private handleInstallMcpServerResultsFromEvent(e: readonly InstallMcpServerResult[], emitter: Emitter, currentProfileEmitter: Emitter): void { + private createInstallMcpServerResultsFromEvent(e: readonly InstallMcpServerResult[]) { const mcpServerInstallResult: IWorkbenchMcpServerInstallResult[] = []; const mcpServerInstallResultInCurrentProfile: IWorkbenchMcpServerInstallResult[] = []; for (const result of e) { @@ -220,6 +219,11 @@ class WorkbenchMcpManagementService extends Disposable implements IWorkbenchMcpM } } + return { mcpServerInstallResult, mcpServerInstallResultInCurrentProfile }; + } + + private handleInstallMcpServerResultsFromEvent(e: readonly InstallMcpServerResult[], emitter: Emitter, currentProfileEmitter: Emitter): void { + const { mcpServerInstallResult, mcpServerInstallResultInCurrentProfile } = this.createInstallMcpServerResultsFromEvent(e); emitter.fire(mcpServerInstallResult); if (mcpServerInstallResultInCurrentProfile.length) { currentProfileEmitter.fire(mcpServerInstallResultInCurrentProfile); From eebf50256e9aafec43755d3a25d1835d2bc9e8ec Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 3 Jul 2025 23:29:22 +0200 Subject: [PATCH 115/306] Change 'Completions' to 'Code Completions' (#253992) fixes https://github.com/microsoft/vscode/issues/253724 --- src/vs/workbench/contrib/chat/browser/chatStatus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus.ts b/src/vs/workbench/contrib/chat/browser/chatStatus.ts index 259381a28fd..53e416911b0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus.ts @@ -420,7 +420,7 @@ class ChatStatusDashboard extends Disposable { // Settings { const chatSentiment = this.chatEntitlementService.sentiment; - addSeparator(localize('completionsAndNES', "Completions"), chatSentiment.installed && !chatSentiment.disabled && !chatSentiment.untrusted ? toAction({ + addSeparator(localize('codeCompletions', "Code Completions"), chatSentiment.installed && !chatSentiment.disabled && !chatSentiment.untrusted ? toAction({ id: 'workbench.action.openChatSettings', label: localize('settingsLabel', "Settings"), tooltip: localize('settingsTooltip', "Open Settings"), From 4bfa684c0655067cce36f859be56f83b5ca8f059 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 3 Jul 2025 23:39:12 +0200 Subject: [PATCH 116/306] minor fixes, 3rd try to get PRs merged (#253999) 3rd try to get the prs in --- src/vs/base/browser/ui/toggle/toggle.ts | 14 +++++++++++--- .../workbench/contrib/chat/browser/chatStatus.ts | 10 ++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/vs/base/browser/ui/toggle/toggle.ts b/src/vs/base/browser/ui/toggle/toggle.ts index 5e0aabe51c8..287c44c94d1 100644 --- a/src/vs/base/browser/ui/toggle/toggle.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -132,7 +132,7 @@ export class Toggle extends Widget { readonly domNode: HTMLElement; private _checked: boolean; - private _hover: IManagedHover; + private _hover?: IManagedHover; constructor(opts: IToggleOpts) { super(); @@ -153,7 +153,11 @@ export class Toggle extends Widget { } this.domNode = document.createElement('div'); - this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(opts.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this.domNode, this._opts.title)); + if (this._opts.hoverDelegate?.showNativeHover) { + this.domNode.title = this._opts.title; + } else { + this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(opts.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this.domNode, this._opts.title)); + } this.domNode.classList.add(...classes); if (!this._opts.notFocusable) { this.domNode.tabIndex = 0; @@ -245,7 +249,11 @@ export class Toggle extends Widget { } setTitle(newTitle: string): void { - this._hover.update(newTitle); + if (this._hover) { + this._hover.update(newTitle); + } else { + this.domNode.title = newTitle; + } this.domNode.setAttribute('aria-label', newTitle); } diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus.ts b/src/vs/workbench/contrib/chat/browser/chatStatus.ts index 53e416911b0..b6c34cf3b77 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus.ts @@ -751,9 +751,10 @@ class ChatStatusDashboard extends Disposable { const timeLeftMs = this.inlineCompletionsService.snoozeTimeLeft; if (!isEnabled || timeLeftMs <= 0) { - timerDisplay.textContent = localize('completions.snooze5minutesTitle', "Hide completions for 5 mins"); + timerDisplay.textContent = localize('completions.snooze5minutesTitle', "Hide completions for 5 min"); + timerDisplay.title = ''; button.label = label; - button.setTitle(localize('completions.snooze5minutes', "Hide completions and NES for 5 mins")); + button.setTitle(localize('completions.snooze5minutes', "Hide completions and NES for 5 min")); return true; } @@ -762,8 +763,9 @@ class ChatStatusDashboard extends Disposable { const seconds = timeLeftSeconds % 60; timerDisplay.textContent = `${minutes}:${seconds < 10 ? '0' : ''}${seconds} ${localize('completions.remainingTime', "remaining")}`; - button.label = localize('completions.plus5mins', "+5 mins"); - button.setTitle(localize('completions.snoozeAdditional5minutes', "Hide additional 5 mins")); + timerDisplay.title = localize('completions.snoozeTimeDescription', "Completions are hidden for the remaining duration"); + button.label = localize('completions.plus5min', "+5 min"); + button.setTitle(localize('completions.snoozeAdditional5minutes', "Snooze additional 5 min")); toolbar.push([cancelAction], { icon: true, label: false }); return false; From 25ee562cba29e4256a144af0b1f3af9b2f8f7f04 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Thu, 3 Jul 2025 14:42:14 -0700 Subject: [PATCH 117/306] Add cli override to control startupExp group (#253155) * Add startup experiment group support to environment services * refactor: streamline startup experiment group handling and remove unused code * feat: add first session date check to initialize experiments --- src/vs/platform/environment/common/argv.ts | 1 + src/vs/platform/environment/node/argv.ts | 1 + .../common/coreExperimentationService.ts | 20 +++++++++++++- .../coreExperimentationService.test.ts | 27 +++++++++++++------ .../environment/common/environmentService.ts | 1 + .../electron-browser/environmentService.ts | 9 +++++++ 6 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 185ce5ad1b1..d4937b2d4ec 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -138,6 +138,7 @@ export interface NativeParsedArgs { 'unresponsive-sample-period'?: string; 'enable-rdp-display-tracking'?: boolean; 'disable-layout-restore'?: boolean; + 'startup-experiment-group'?: string; // chromium command line args: https://electronjs.org/docs/all#supported-chrome-command-line-switches 'no-proxy-server'?: boolean; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index b5958ee0ec3..2f30fbe8cd2 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -200,6 +200,7 @@ export const OPTIONS: OptionDescriptions> = { 'unresponsive-sample-period': { type: 'string' }, 'enable-rdp-display-tracking': { type: 'boolean' }, 'disable-layout-restore': { type: 'boolean' }, + 'startup-experiment-group': { type: 'string', cat: 't', args: 'control|maximizedChat|splitEmptyEditorChat|splitWelcomeChat', description: localize('startupExperimentGroup', "Override the startup experiment group.") }, // chromium flags 'no-proxy-server': { type: 'boolean' }, diff --git a/src/vs/workbench/services/coreExperimentation/common/coreExperimentationService.ts b/src/vs/workbench/services/coreExperimentation/common/coreExperimentationService.ts index 531c20ca388..f14dd96a9a3 100644 --- a/src/vs/workbench/services/coreExperimentation/common/coreExperimentationService.ts +++ b/src/vs/workbench/services/coreExperimentation/common/coreExperimentationService.ts @@ -10,6 +10,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { firstSessionDateStorageKey, ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; export const ICoreExperimentationService = createDecorator('coreExperimentationService'); export const startupExpContext = new RawContextKey('coreExperimentation.startupExpGroup', ''); @@ -83,7 +84,8 @@ export class CoreExperimentationService extends Disposable implements ICoreExper @IStorageService private readonly storageService: IStorageService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IProductService private readonly productService: IProductService, - @IContextKeyService private readonly contextKeyService: IContextKeyService + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, ) { super(); this.initializeExperiments(); @@ -141,6 +143,22 @@ export class CoreExperimentationService extends Disposable implements ICoreExper } private createStartupExperiment(experimentName: string, experimentConfig: ExperimentConfiguration): IExperiment | undefined { + const startupExpGroupOverride = this.environmentService.startupExperimentGroup; + if (startupExpGroupOverride) { + // If the user has an override, we use that directly + const group = experimentConfig.groups.find(g => g.name === startupExpGroupOverride); + if (group) { + return { + cohort: 1, + subCohort: 1, + experimentGroup: group.name, + iteration: group.iteration, + isInExperiment: true + }; + } + return undefined; + } + const cohort = Math.random(); if (cohort >= experimentConfig.targetPercentage / 100) { diff --git a/src/vs/workbench/services/coreExperimentation/test/browser/coreExperimentationService.test.ts b/src/vs/workbench/services/coreExperimentation/test/browser/coreExperimentationService.test.ts index f15b136c3d9..77e44c5668d 100644 --- a/src/vs/workbench/services/coreExperimentation/test/browser/coreExperimentationService.test.ts +++ b/src/vs/workbench/services/coreExperimentation/test/browser/coreExperimentationService.test.ts @@ -11,6 +11,7 @@ import { firstSessionDateStorageKey, ITelemetryService, ITelemetryData, Telemetr import { StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { TestStorageService } from '../../../../test/common/workbenchTestServices.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; +import { IWorkbenchEnvironmentService } from '../../../environment/common/environmentService.js'; interface ITelemetryEvent { eventName: string; @@ -72,12 +73,14 @@ suite('CoreExperimentationService', () => { let telemetryService: MockTelemetryService; let productService: MockProductService; let contextKeyService: MockContextKeyService; + let environmentService: IWorkbenchEnvironmentService; setup(() => { storageService = disposables.add(new TestStorageService()); telemetryService = new MockTelemetryService(); productService = new MockProductService(); contextKeyService = new MockContextKeyService(); + environmentService = {} as IWorkbenchEnvironmentService; }); test('should return experiment from storage if it exists', () => { @@ -97,7 +100,8 @@ suite('CoreExperimentationService', () => { storageService, telemetryService, productService, - contextKeyService + contextKeyService, + environmentService )); // Should not return experiment again @@ -120,7 +124,8 @@ suite('CoreExperimentationService', () => { storageService, telemetryService, productService, - contextKeyService + contextKeyService, + environmentService )); // Should create experiment @@ -154,7 +159,8 @@ suite('CoreExperimentationService', () => { storageService, telemetryService, productService, - contextKeyService + contextKeyService, + environmentService )); const experiment = service.getExperiment(); @@ -191,7 +197,8 @@ suite('CoreExperimentationService', () => { storageService, telemetryService, productService, - contextKeyService + contextKeyService, + environmentService )); // Should not create experiment @@ -229,7 +236,8 @@ suite('CoreExperimentationService', () => { storageService, telemetryService, productService, - contextKeyService + contextKeyService, + environmentService )); const experiment = service.getExperiment(); @@ -254,7 +262,8 @@ suite('CoreExperimentationService', () => { storageService, telemetryService, productService, - contextKeyService + contextKeyService, + environmentService )); const experiment = service.getExperiment(); @@ -285,7 +294,8 @@ suite('CoreExperimentationService', () => { storageService, telemetryService, productService, - contextKeyService + contextKeyService, + environmentService )); const experiment = service.getExperiment(); @@ -309,7 +319,8 @@ suite('CoreExperimentationService', () => { storageService, telemetryService, productService, - contextKeyService + contextKeyService, + environmentService )); const experiment = service.getExperiment(); diff --git a/src/vs/workbench/services/environment/common/environmentService.ts b/src/vs/workbench/services/environment/common/environmentService.ts index 5312892fe6f..69961cce91c 100644 --- a/src/vs/workbench/services/environment/common/environmentService.ts +++ b/src/vs/workbench/services/environment/common/environmentService.ts @@ -36,6 +36,7 @@ export interface IWorkbenchEnvironmentService extends IEnvironmentService { readonly skipWelcome: boolean; readonly disableWorkspaceTrust: boolean; readonly webviewExternalEndpoint: string; + readonly startupExperimentGroup?: string; // --- Development readonly debugRenderer: boolean; diff --git a/src/vs/workbench/services/environment/electron-browser/environmentService.ts b/src/vs/workbench/services/environment/electron-browser/environmentService.ts index 6cfa51701be..87b2df16ead 100644 --- a/src/vs/workbench/services/environment/electron-browser/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-browser/environmentService.ts @@ -147,6 +147,15 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment @memoize get filesToWait(): IPathsToWaitFor | undefined { return this.configuration.filesToWait; } + @memoize + get startupExperimentGroup(): string | undefined { + const group = this.args['startup-experiment-group']; + if (typeof group === 'string') { + return group; + } + return undefined; + } + constructor( private readonly configuration: INativeWindowConfiguration, productService: IProductService From 5da12b49827c0a037453d4672bd7d7a8447f9d22 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 3 Jul 2025 15:02:24 -0700 Subject: [PATCH 118/306] mcp: correctly fix authority-less MCP resources cannot be read by VS Code (#254003) Closes https://github.com/microsoft/vscode/issues/250905 --- .../mcp/browser/mcpResourceQuickAccess.ts | 2 +- .../mcp/common/mcpResourceFilesystem.ts | 24 +++++++++---------- .../workbench/contrib/mcp/common/mcpTypes.ts | 15 +++++++----- .../contrib/mcp/test/common/mcpTypes.test.ts | 5 +++- 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpResourceQuickAccess.ts b/src/vs/workbench/contrib/mcp/browser/mcpResourceQuickAccess.ts index d084e5064ca..5af938ba637 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpResourceQuickAccess.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpResourceQuickAccess.ts @@ -130,7 +130,7 @@ export class McpResourcePickHelper { return uri; } - this._notificationService.warn(localize('mcp.resource.template.notFound', "The resource {0} was not found.", McpResourceURI.toServer(uri).resourceURI.toString())); + this._notificationService.warn(localize('mcp.resource.template.notFound', "The resource {0} was not found.", McpResourceURI.toServer(uri).resourceURL.toString())); return undefined; } diff --git a/src/vs/workbench/contrib/mcp/common/mcpResourceFilesystem.ts b/src/vs/workbench/contrib/mcp/common/mcpResourceFilesystem.ts index 19ca63170cb..19392dfdd2f 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpResourceFilesystem.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpResourceFilesystem.ts @@ -108,11 +108,11 @@ export class McpResourceFilesystem extends Disposable implements IWorkbenchContr watchedOnHandler = handler; const token = callCts.value.token; - handler.subscribe({ uri: resourceURI.toString(true) }, token).then( + handler.subscribe({ uri: resourceURI.toString() }, token).then( () => { if (!token.isCancellationRequested) { watchListener.value = handler.onDidUpdateResource(e => { - if (equalsUriPath(e.params.uri, resourceURI)) { + if (equalsUrlPath(e.params.uri, resourceURI)) { this._onDidChangeFile.fire([{ resource: uri, type: FileChangeType.UPDATED }]); } }); @@ -147,7 +147,7 @@ export class McpResourceFilesystem extends Disposable implements IWorkbenchContr throw createFileSystemProviderError(`File is not a directory`, FileSystemProviderErrorCode.FileNotADirectory); } - const resourcePathParts = resourceURI.path.split('/'); + const resourcePathParts = resourceURI.pathname.split('/'); const output = new Map(); for (const content of contents) { @@ -207,15 +207,15 @@ export class McpResourceFilesystem extends Disposable implements IWorkbenchContr private _decodeURI(uri: URI) { let definitionId: string; - let resourceURI: URI; + let resourceURL: URL; try { - ({ definitionId, resourceURI } = McpResourceURI.toServer(uri)); + ({ definitionId, resourceURL } = McpResourceURI.toServer(uri)); } catch (e) { throw createFileSystemProviderError(String(e), FileSystemProviderErrorCode.FileNotFound); } - if (resourceURI.path.endsWith('/')) { - resourceURI = resourceURI.with({ path: resourceURI.path.slice(0, -1) }); + if (resourceURL.pathname.endsWith('/')) { + resourceURL.pathname = resourceURL.pathname.slice(0, -1); } const server = this._mcpService.servers.get().find(s => s.definition.id === definitionId); @@ -228,25 +228,25 @@ export class McpResourceFilesystem extends Disposable implements IWorkbenchContr throw createFileSystemProviderError(`MCP server ${definitionId} does not support resources`, FileSystemProviderErrorCode.FileNotFound); } - return { definitionId, resourceURI, server }; + return { definitionId, resourceURI: resourceURL, server }; } private async _readURI(uri: URI, token?: CancellationToken) { const { resourceURI, server } = this._decodeURI(uri); - const res = await McpServer.callOn(server, r => r.readResource({ uri: resourceURI.toString(true) }, token), token); + const res = await McpServer.callOn(server, r => r.readResource({ uri: resourceURI.toString() }, token), token); return { contents: res.contents, resourceURI, - forSameURI: res.contents.filter(c => equalsUriPath(c.uri, resourceURI)), + forSameURI: res.contents.filter(c => equalsUrlPath(c.uri, resourceURI)), }; } } -function equalsUriPath(a: string, b: URI): boolean { +function equalsUrlPath(a: string, b: URL): boolean { // MCP doesn't specify either way, but underlying systems may can be case-sensitive. // It's better to treat case-sensitive paths as case-insensitive than vise-versa. - return equalsIgnoreCase(URI.parse(a).path, b.path); + return equalsIgnoreCase(new URL(a).pathname, b.pathname); } function contentToBuffer(content: MCP.TextResourceContents | MCP.BlobResourceContents): Uint8Array { diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 1d9f3f1af2c..97756f2bced 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -678,7 +678,7 @@ export namespace McpResourceURI { }); } - export function toServer(uri: URI | string): { definitionId: string; resourceURI: URI } { + export function toServer(uri: URI | string): { definitionId: string; resourceURL: URL } { if (typeof uri === 'string') { uri = URI.parse(uri); } @@ -690,13 +690,16 @@ export namespace McpResourceURI { throw new Error(`Invalid MCP resource URI: ${uri.toString()}`); } const [, serverScheme, authority, ...path] = parts; + + // URI cannot correctly stringify empty authorities (#250905) so we use URL instead to construct + const url = new URL(`${serverScheme}://${authority.toLowerCase() === emptyAuthorityPlaceholder ? '' : authority}`); + url.pathname = path.length ? ('/' + path.join('/')) : ''; + url.search = uri.query; + url.hash = uri.fragment; + return { definitionId: decodeHex(uri.authority).toString(), - resourceURI: uri.with({ - scheme: serverScheme, - authority: authority.toLowerCase() === emptyAuthorityPlaceholder ? '' : authority, - path: path.length ? ('/' + path.join('/')) : '', - }), + resourceURL: url, }; } diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpTypes.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpTypes.test.ts index ca832e81c44..b308d02c3c1 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpTypes.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpTypes.test.ts @@ -15,7 +15,7 @@ suite('MCP Types', () => { const from = McpResourceURI.fromServer({ label: '', id: 'my-id' }, uri); const to = McpResourceURI.toServer(from); assert.strictEqual(to.definitionId, 'my-id'); - assert.strictEqual(to.resourceURI.toString(true), uri, `expected to round trip ${uri}`); + assert.strictEqual(to.resourceURL.toString(), uri, `expected to round trip ${uri}`); }; roundTrip('file:///path/to/file.txt'); @@ -23,5 +23,8 @@ suite('MCP Types', () => { roundTrip('custom-scheme://my-path'); roundTrip('custom-scheme://my-path/'); roundTrip('custom-scheme://my-path/?with=query¶ms=here'); + + roundTrip('custom-scheme:///my-path'); + roundTrip('custom-scheme:///my-path/foo/?with=query¶ms=here'); }); }); From 2fda72e83b8366530a4d455bd167f50026194054 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 4 Jul 2025 00:10:20 +0200 Subject: [PATCH 119/306] Use existing setting icons for configure chat (#253984) --- src/vs/workbench/contrib/chat/browser/actions/chatActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 864bdf1f9c5..38257061335 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -875,7 +875,7 @@ Update \`.github/copilot-instructions.md\` for the user, then ask for feedback o title: localize2('config.label', "Configure Chat..."), group: 'navigation', when: ContextKeyExpr.equals('view', ChatViewId), - icon: Codicon.settings, + icon: Codicon.settingsGear, order: 6 }); } From d9f1bf8fed9ccec3f04ddb7b8644f16656f613fb Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 4 Jul 2025 00:44:17 +0200 Subject: [PATCH 120/306] fix #253356 (#253705) --- .../contrib/mcp/browser/mcpServerEditor.ts | 39 +++++++++++++++++-- .../mcp/browser/mcpWorkbenchService.ts | 8 ++-- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts index be635988841..77626709d7e 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts @@ -37,7 +37,7 @@ import { IWebview, IWebviewService } from '../../webview/browser/webview.js'; import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { IMcpServerEditorOptions, IWorkbenchMcpServer, McpServerContainers } from '../common/mcpTypes.js'; +import { IMcpServerEditorOptions, IMcpWorkbenchService, IWorkbenchMcpServer, McpServerContainers } from '../common/mcpTypes.js'; import { InstallCountWidget, McpServerIconWidget, McpServerWidget, onClick, PublisherWidget, RatingsWidget } from './mcpServerWidgets.js'; import { DropDownAction, InstallAction, InstallingLabelAction, ManageMcpServerAction, UninstallAction } from './mcpServerActions.js'; import { McpServerEditorInput } from './mcpServerEditorInput.js'; @@ -73,19 +73,34 @@ class NavBar extends Disposable { this.actionbar = this._register(new ActionBar(element)); } - push(id: string, label: string, tooltip: string): void { + push(id: string, label: string, tooltip: string, index?: number): void { const action = new Action(id, label, undefined, true, () => this.update(id, true)); action.tooltip = tooltip; - this.actions.push(action); - this.actionbar.push(action); + if (typeof index === 'number') { + this.actions.splice(index, 0, action); + } else { + this.actions.push(action); + } + this.actionbar.push(action, { index }); if (this.actions.length === 1) { this.update(id); } } + remove(id: string): void { + const index = this.actions.findIndex(action => action.id === id); + if (index !== -1) { + this.actions.splice(index, 1); + this.actionbar.pull(index); + if (this._currentId === id) { + this.switch(this.actions[0]?.id); + } + } + } + clear(): void { this.actions = dispose(this.actions); this.actionbar.clear(); @@ -100,6 +115,10 @@ class NavBar extends Disposable { return false; } + has(id: string): boolean { + return this.actions.some(action => action.id === id); + } + private update(id: string, focus?: boolean): void { this._currentId = id; this._onChange.fire({ id, focus: !!focus }); @@ -165,6 +184,7 @@ export class McpServerEditor extends EditorPane { @IWebviewService private readonly webviewService: IWebviewService, @ILanguageService private readonly languageService: ILanguageService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService, @IHoverService private readonly hoverService: IHoverService, ) { super(McpServerEditor.ID, group, telemetryService, themeService, storageService); @@ -341,6 +361,17 @@ export class McpServerEditor extends EditorPane { template.navbar.push(McpServerEditorTab.Manifest, localize('manifest', "Manifest"), localize('manifesttooltip', "Server manifest details")); } + this.transientDisposables.add(this.mcpWorkbenchService.onChange(e => { + if (e === extension) { + if (e.config && !template.navbar.has(McpServerEditorTab.Configuration)) { + template.navbar.push(McpServerEditorTab.Configuration, localize('configuration', "Configuration"), localize('configurationtooltip', "Server configuration details"), extension.readmeUrl ? 1 : 0); + } + if (!e.config && template.navbar.has(McpServerEditorTab.Configuration)) { + template.navbar.remove(McpServerEditorTab.Configuration); + } + } + })); + if ((this.options)?.tab) { template.navbar.switch((this.options).tab!); } diff --git a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts index a7de8e57ff6..0ba9db36d5b 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts @@ -209,13 +209,15 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ } private onDidInstallMcpServer(local: IWorkbenchLocalMcpServer, gallery?: IGalleryMcpServer): IWorkbenchMcpServer { - let server = this._local.find(server => server.local?.name === local.name); + let server = this.installing.find(server => server.name === local.name); + this.installing = server ? this.installing.filter(e => e !== server) : this.installing; if (server) { server.local = local; } else { server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), local, gallery, undefined); - this._local.push(server); } + this._local = this._local.filter(e => e.name === local.name); + this._local.push(server); this._onChange.fire(server); return server; } @@ -334,7 +336,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ private async doInstall(server: McpWorkbenchServer, installTask: () => Promise): Promise { this.installing.push(server); this._onChange.fire(server); - await installTask().finally(() => this.installing = this.installing.filter(s => s !== server)); + await installTask(); return this.waitAndGetInstalledMcpServer(server); } From 9d9234fd33540be56aadb17f1fcec47ea2d81c2f Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:39:46 -0700 Subject: [PATCH 121/306] Cloud button nits (#254015) * acceptInput so that it goes to history * hide stop button when remotejobCreating --- .../chat/browser/actions/chatExecuteActions.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 47954785a56..0df0aa10499 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -543,13 +543,19 @@ export class CreateRemoteAgentJobAction extends Action2 { return; } - const userPrompt = widget.getInput(); - widget.setInput(); const chatModel = widget.viewModel?.model; if (!chatModel) { return; } + + const userPrompt = widget.getInput(); + if (!userPrompt) { + return; + } + + widget.input.acceptInput(true); + const chatRequests = chatModel.getRequests(); const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Panel); @@ -775,7 +781,11 @@ export class CancelAction extends Action2 { icon: Codicon.stopCircle, menu: [{ id: MenuId.ChatExecute, - when: ContextKeyExpr.and(ChatContextKeys.isRequestPaused.negate(), ChatContextKeys.requestInProgress), + when: ContextKeyExpr.and( + ChatContextKeys.isRequestPaused.negate(), + ChatContextKeys.requestInProgress, + ChatContextKeys.remoteJobCreating.negate() + ), order: 4, group: 'navigation', }, From 39ca06bcdf4506b778cf2f7cc630c018ef1ba371 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 3 Jul 2025 16:56:10 -0700 Subject: [PATCH 122/306] Add "open mode picker" action (#253965) * Add "open mode picker" action * poke ci --------- Co-authored-by: Josh Spicer <23246594+joshspicer@users.noreply.github.com> --- .../browser/actions/chatExecuteActions.ts | 23 +++++++++++++++++++ .../contrib/chat/browser/chatInputPart.ts | 7 +++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 0df0aa10499..8fcb946111c 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -377,6 +377,28 @@ class OpenModelPickerAction extends Action2 { } } +class OpenModePickerAction extends Action2 { + static readonly ID = 'workbench.action.chat.openModePicker'; + + constructor() { + super({ + id: OpenModePickerAction.ID, + title: localize2('interactive.openModePicker.label', "Open Mode Picker"), + category: CHAT_CATEGORY, + f1: false, + precondition: ChatContextKeys.enabled, + }); + } + + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + const widgetService = accessor.get(IChatWidgetService); + const widget = widgetService.lastFocusedWidget; + if (widget) { + widget.input.openModePicker(); + } + } +} + export const ChangeChatModelActionId = 'workbench.action.chat.changeModel'; class ChangeChatModelAction extends Action2 { static readonly ID = ChangeChatModelActionId; @@ -868,6 +890,7 @@ export function registerChatExecuteActions() { registerAction2(ToggleRequestPausedAction); registerAction2(SwitchToNextModelAction); registerAction2(OpenModelPickerAction); + registerAction2(OpenModePickerAction); registerAction2(ChangeChatModelAction); registerAction2(CancelEdit); } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index cfc2154c3b4..061f59eb695 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -271,6 +271,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private chatModeKindKey: IContextKey; private modelWidget: ModelPickerActionItem | undefined; + private modeWidget: ModePickerActionItem | undefined; private readonly _waitForPersistedLanguageModel: MutableDisposable; private _onDidChangeCurrentLanguageModel: Emitter; @@ -540,6 +541,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.modelWidget?.show(); } + public openModePicker(): void { + this.modeWidget?.show(); + } + private checkModelSupported(): void { if (this._currentLanguageModel && !this.modelSupportedForDefaultAgent(this._currentLanguageModel)) { this.setCurrentLanguageModelToDefault(); @@ -1149,7 +1154,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const delegate: IModePickerDelegate = { currentMode: this._currentModeObservable }; - return this.instantiationService.createInstance(ModePickerActionItem, action, delegate); + return this.modeWidget = this.instantiationService.createInstance(ModePickerActionItem, action, delegate); } return undefined; From aeabf149f94cccc45c02f7745ab313e46dcda2b4 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 4 Jul 2025 02:00:54 +0200 Subject: [PATCH 123/306] Unselected servers/tools in agent mode are not restored (#254004) --- .../contrib/chat/browser/chatSelectedTools.ts | 96 +++++++------------ 1 file changed, 37 insertions(+), 59 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts b/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts index 6bf867e7e38..5b73948d32c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts @@ -6,7 +6,6 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { derived, IObservable, observableFromEvent, ObservableMap } from '../../../../base/common/observable.js'; -import { isObject } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ObservableMemento, observableMemento } from '../../../../platform/observable/common/observableMemento.js'; @@ -37,7 +36,7 @@ export enum ToolsScope { export class ChatSelectedTools extends Disposable { - private readonly _selectedTools: ObservableMemento; + private readonly _selectedTools: ObservableMemento; private readonly _sessionStates = new ObservableMap(); @@ -64,48 +63,8 @@ export class ChatSelectedTools extends Disposable { ) { super(); - const storedTools = observableMemento({ - defaultValue: new Map(), - toStorage: (value) => { - const data = { - disabledToolSets: [] as string[], - disabledTools: [] as string[], - }; - for (const [item, enabled] of value) { - if (!enabled) { - if (item instanceof ToolSet) { - data.disabledToolSets.push(item.id); - } else { - data.disabledTools.push(item.id); - } - } - } - return JSON.stringify(data); - }, - fromStorage: (value) => { - const obj = JSON.parse(value) as StoredData; - const map = new Map(); - if (!obj || !isObject(obj)) { - return map; - } - if (Array.isArray(obj.disabledToolSets)) { - for (const toolSetId of obj.disabledToolSets) { - const toolset = this._toolsService.getToolSet(toolSetId); - if (toolset) { - map.set(toolset, false); - } - } - } - if (Array.isArray(obj.disabledTools)) { - for (const toolId of obj.disabledTools) { - const tool = this._toolsService.getTool(toolId); - if (tool) { - map.set(tool, false); - } - } - } - return map; - }, + const storedTools = observableMemento({ + defaultValue: { disabledToolSets: [], disabledTools: [] }, key: 'chat/selectedTools', }); @@ -119,27 +78,36 @@ export class ChatSelectedTools extends Disposable { */ get entriesMap(): IObservable { return derived(r => { + const map = new Map(); + const currentMode = this._mode.read(r); let currentMap = this._sessionStates.get(currentMode.id); - let defaultEnablement = false; if (!currentMap && currentMode.kind === ChatModeKind.Agent && currentMode.customTools) { currentMap = this._toolsService.toToolAndToolSetEnablementMap(currentMode.customTools.read(r)); } - if (!currentMap) { - currentMap = this._selectedTools.read(r); - defaultEnablement = true; - } + if (currentMap) { + for (const tool of this._allTools.read(r)) { + if (tool.canBeReferencedInPrompt) { + map.set(tool, currentMap.get(tool) === true); // false if not present + } + } + for (const toolSet of this._toolsService.toolSets.read(r)) { + map.set(toolSet, currentMap.get(toolSet) === true); // false if not present + } + } else { + const currData = this._selectedTools.read(r); + const disabledToolSets = new Set(currData.disabledToolSets ?? []); + const disabledTools = new Set(currData.disabledTools ?? []); - // create a complete map of all tools and tool sets - const map = new Map(); - const tools = this._allTools.read(r).filter(t => t.canBeReferencedInPrompt); - for (const tool of tools) { - map.set(tool, currentMap.get(tool) ?? defaultEnablement); - } - const toolSets = this._toolsService.toolSets.read(r); - for (const toolSet of toolSets) { - map.set(toolSet, currentMap.get(toolSet) ?? defaultEnablement); + for (const tool of this._allTools.read(r)) { + if (tool.canBeReferencedInPrompt) { + map.set(tool, !disabledTools.has(tool.id)); + } + } + for (const toolSet of this._toolsService.toolSets.read(r)) { + map.set(toolSet, !disabledToolSets.has(toolSet.id)); + } } return map; }); @@ -180,7 +148,17 @@ export class ChatSelectedTools extends Disposable { this.updateCustomModeTools(mode.uri.get(), enablementMap); return; } - this._selectedTools.set(enablementMap, undefined); + const storedData = { disabledToolSets: [] as string[], disabledTools: [] as string[] }; + for (const [item, enabled] of enablementMap) { + if (!enabled) { + if (item instanceof ToolSet) { + storedData.disabledToolSets.push(item.id); + } else { + storedData.disabledTools.push(item.id); + } + } + } + this._selectedTools.set(storedData, undefined); } async updateCustomModeTools(uri: URI, enablementMap: IToolAndToolSetEnablementMap): Promise { From e7d03805842e0051d0230f516a0316feada4239e Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 3 Jul 2025 20:02:55 -0400 Subject: [PATCH 124/306] Add logs for completion resolution (#253974) --- .../browser/terminalCompletionService.ts | 21 +++++++++++++++---- .../browser/terminalCompletionService.test.ts | 2 ++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index ba7186834a7..3454720d795 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -19,6 +19,7 @@ import type { IProcessEnvironment } from '../../../../../base/common/platform.js import { timeout } from '../../../../../base/common/async.js'; import { gitBashToWindowsPath } from './terminalGitBashHelpers.js'; import { isEqual } from '../../../../../base/common/resources.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; export const ITerminalCompletionService = createDecorator('terminalCompletionService'); @@ -98,6 +99,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @IFileService private readonly _fileService: IFileService, + @ILogService private readonly _logService: ILogService ) { super(); } @@ -170,10 +172,21 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return undefined; } const timeoutMs = explicitlyInvoked ? 30000 : 5000; - const completions = await Promise.race([ - provider.provideCompletions(promptValue, cursorPosition, allowFallbackCompletions, token), - timeout(timeoutMs) - ]); + let timedOut = false; + let completions; + try { + completions = await Promise.race([ + provider.provideCompletions(promptValue, cursorPosition, allowFallbackCompletions, token), + (async () => { await timeout(timeoutMs); timedOut = true; return undefined; })() + ]); + } catch (e) { + this._logService.trace(`[TerminalCompletionService] Exception from provider '${provider.id}':`, e); + return undefined; + } + if (timedOut) { + this._logService.trace(`[TerminalCompletionService] Provider '${provider.id}' timed out after ${timeoutMs}ms. promptValue='${promptValue}', cursorPosition=${cursorPosition}, explicitlyInvoked=${explicitlyInvoked}`); + return undefined; + } if (!completions) { return undefined; } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index d157012624f..950ff80d949 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -20,6 +20,7 @@ import { ITerminalCompletion, TerminalCompletionItemKind } from '../../browser/t import { count } from '../../../../../../base/common/strings.js'; import { WindowsShellType } from '../../../../../../platform/terminal/common/terminal.js'; import { gitBashToWindowsPath, windowsToGitBashPath } from '../../browser/terminalGitBashHelpers.js'; +import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; const pathSeparator = isWindows ? '\\' : '/'; @@ -105,6 +106,7 @@ suite('TerminalCompletionService', () => { setup(() => { instantiationService = store.add(new TestInstantiationService()); configurationService = new TestConfigurationService(); + instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(IConfigurationService, configurationService); instantiationService.stub(IFileService, { async stat(resource) { From bcf133c1a7ee846a6d0a9bc58d2f516061dda55c Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Thu, 3 Jul 2025 17:09:36 -0700 Subject: [PATCH 125/306] fix: settings toolbar lacks margin-right (#253716) --- .../contrib/preferences/browser/media/settingsEditor2.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 5aae7a8df36..b34ede68376 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -40,13 +40,13 @@ .settings-editor > .settings-header > .search-container > .settings-count-widget { position: absolute; - right: 48px; + right: 49px; top: 0px; margin: 2.5px 0px; } .settings-editor > .settings-header > .search-container.with-ai-toggle > .settings-count-widget { - right: 69px; + right: 70px; } .settings-editor > .settings-header > .search-container > .settings-count-widget:empty { @@ -60,6 +60,7 @@ top: 0; right: 0; height: 100%; + margin-right: 1px; } .settings-editor > .settings-header > .search-container > .settings-clear-widget .action-label { From 51aa46f271d60b8ef5f4f28a34ecb67c772c2af1 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 4 Jul 2025 02:13:48 +0200 Subject: [PATCH 126/306] Constant flickering when typing in mode file (#253941) Co-authored-by: Josh Spicer <23246594+joshspicer@users.noreply.github.com> --- .../promptSyntax/promptFileRewriter.ts | 11 +++++--- .../promptToolsCodeLensProvider.ts | 11 +++++--- .../promptSyntax/codecs/base/baseDecoder.ts | 12 ++++++--- .../promptHeaderAutocompletion.ts | 5 +++- .../promptHeaderDiagnosticsProvider.ts | 16 +++++++----- .../languageProviders/promptHeaderHovers.ts | 5 +++- .../languageProviders/promptLinkProvider.ts | 16 +++++------- .../promptPathAutocompletion.ts | 14 ++++------- .../promptSyntax/parsers/basePromptParser.ts | 25 +++++++------------ .../parsers/promptHeader/headerBase.ts | 2 +- .../service/promptsServiceImpl.ts | 10 ++++++-- .../parsers/textModelPromptParser.test.ts | 9 ++++--- .../promptSyntax/promptFileReference.test.ts | 2 +- 13 files changed, 76 insertions(+), 62 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptFileRewriter.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptFileRewriter.ts index 9b186f987b5..695b89f1cbd 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptFileRewriter.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptFileRewriter.ts @@ -27,12 +27,17 @@ export class PromptFileRewriter { const model = editor.getModel(); const parser = this._promptsService.getSyntaxParserFor(model); - const { header } = await parser.start(token).settled(); - - if ((header === undefined) || token.isCancellationRequested) { + await parser.start(token).settled(); + const { header } = parser; + if (header === undefined) { return undefined; } + const completed = await header.settled; + if (!completed || token.isCancellationRequested) { + return; + } + if (('tools' in header.metadataUtility) === false) { return undefined; } diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts index 3d904edb187..904ff2dff6f 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts @@ -48,11 +48,14 @@ class PromptToolsCodeLensProvider extends Disposable implements CodeLensProvider const parser = this.promptsService.getSyntaxParserFor(model); - const { header } = await parser - .start(token) - .settled(); + await parser.start(token).settled(); + const { header } = parser; + if (!header) { + return undefined; + } - if ((header === undefined) || token.isCancellationRequested) { + const completed = await header.settled; + if (!completed || token.isCancellationRequested) { return undefined; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/baseDecoder.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/baseDecoder.ts index 2d0780e7d7c..76d1745d3c6 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/baseDecoder.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/baseDecoder.ts @@ -67,18 +67,22 @@ export abstract class BaseDecoder< * Promise that resolves when the stream has ended, either by * receiving the `end` event or by a disposal, but not when * the `error` event is received alone. + * The promise is true if the stream has ended, and false + * if the stream has been disposed without ending. */ - private settledPromise = new DeferredPromise(); + private settledPromise = new DeferredPromise(); /** * Promise that resolves when the stream has ended, either by * receiving the `end` event or by a disposal, but not when * the `error` event is received alone. + * The promise is true if the stream has ended, and false + * if the stream has been disposed without ending. * * @throws If the stream was not yet started to prevent this * promise to block the consumer calls indefinitely. */ - public get settled(): Promise { + public get settled(): Promise { // if the stream has not started yet, the promise might // block the consumer calls indefinitely if they forget // to call the `start()` method, or if the call happens @@ -296,7 +300,7 @@ export abstract class BaseDecoder< this._ended = true; this._onEnd.fire(); - this.settledPromise.complete(); + this.settledPromise.complete(this._ended); } /** @@ -347,7 +351,7 @@ export abstract class BaseDecoder< } public override dispose(): void { - this.settledPromise.complete(); + this.settledPromise.complete(this.ended); this._listeners.clearAndDisposeAll(); this.stream.destroy(); 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 a932abbd6e6..0e231c27eee 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -71,7 +71,10 @@ export class PromptHeaderAutocompletion extends Disposable implements Completion return undefined; } - await header.settled; + const completed = await header.settled; + if (!completed || token.isCancellationRequested) { + return undefined; + } const fullHeaderRange = parser.header.range; const headerRange = new Range(fullHeaderRange.startLineNumber + 1, 0, fullHeaderRange.endLineNumber - 1, model.getLineMaxColumn(fullHeaderRange.endLineNumber - 1),); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderDiagnosticsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderDiagnosticsProvider.ts index 35f6296171d..256f6340b24 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderDiagnosticsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderDiagnosticsProvider.ts @@ -53,22 +53,20 @@ class PromptHeaderDiagnosticsProvider extends ProviderInstanceBase { _error: Error | undefined, token: CancellationToken, ): Promise { - // clean up all previously added markers - this.markerService.remove(MARKERS_OWNER_ID, [this.model.uri]); const { header } = this.parser; if (header === undefined) { + this.markerService.remove(MARKERS_OWNER_ID, [this.model.uri]); return; } // header parsing process is separate from the prompt parsing one, hence // apply markers only after the header is settled and so has diagnostics - await header.settled; - // by the time the promise finishes, the token might have been cancelled - // already due to a new 'onSettle' event, hence don't apply outdated markers - if (token.isCancellationRequested) { + const completed = await header.settled; + if (!completed || token.isCancellationRequested) { return; } + console.log(`Prompt header diagnostics for ${this.model.uri.toString()}:`); const markers: IMarkerData[] = []; for (const diagnostic of header.diagnostics) { @@ -84,6 +82,12 @@ class PromptHeaderDiagnosticsProvider extends ProviderInstanceBase { } + if (markers.length === 0) { + console.warn(`No markers for ${this.model.uri.toString()}`); + this.markerService.remove(MARKERS_OWNER_ID, [this.model.uri]); + return; + } + this.markerService.changeOne( MARKERS_OWNER_ID, this.model.uri, diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderHovers.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderHovers.ts index e9d12ef8916..032fbb079d0 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderHovers.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderHovers.ts @@ -70,7 +70,10 @@ export class PromptHeaderHoverProvider extends Disposable implements HoverProvid return undefined; } - await header.settled; + const completed = await header.settled; + if (!completed || token.isCancellationRequested) { + return undefined; + } if (header instanceof InstructionsHeader) { const descriptionRange = header.metadataUtility.description?.range; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptLinkProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptLinkProvider.ts index 766af1f2383..22b498d8b3a 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptLinkProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptLinkProvider.ts @@ -26,7 +26,7 @@ export class PromptLinkProvider implements LinkProvider { public async provideLinks( model: ITextModel, token: CancellationToken, - ): Promise { + ): Promise { assert( !token.isCancellationRequested, new CancellationError(), @@ -40,15 +40,11 @@ export class PromptLinkProvider implements LinkProvider { // start the parser in case it was not started yet, // and wait for it to settle to a final result - const { references } = await parser - .start(token) - .settled(); - - // validate that the cancellation was not yet requested - assert( - !token.isCancellationRequested, - new CancellationError(), - ); + const completed = await parser.start(token).settled(); + if (!completed || token.isCancellationRequested) { + return undefined; + } + const { references } = parser; // filter out references that are not valid links const links: ILink[] = references diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptPathAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptPathAutocompletion.ts index f61e625523f..b3e9f429cb8 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptPathAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptPathAutocompletion.ts @@ -137,15 +137,11 @@ export class PromptPathAutocompletion extends Disposable implements CompletionIt // start the parser in case it was not started yet, // and wait for it to settle to a final result - const { references } = await parser - .start(token) - .settled(); - - // validate that the cancellation was not yet requested - assert( - !token.isCancellationRequested, - new CancellationError(), - ); + const completed = await parser.start(token).settled(); + if (!completed || token.isCancellationRequested) { + return undefined; + } + const { references } = parser; const fileReference = findFileReference(references, position); if (!fileReference) { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index a5bb792f5ba..156859802ef 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -191,7 +191,7 @@ export class BasePromptParser * block until the latest prompt contents parsing logic is settled * (e.g., for every `onContentChanged` event of the prompt source). */ - public async settled(): Promise { + public async settled(): Promise { assert( this.started, 'Cannot wait on the parser that did not start yet.', @@ -200,13 +200,13 @@ export class BasePromptParser await this.firstParseResult.promise; if (this.errorCondition) { - return this; + return false; } // by the time when the `firstParseResult` promise is resolved, // this object may have been already disposed, hence noop if (this.isDisposed) { - return this; + return false; } assertDefined( @@ -214,24 +214,17 @@ export class BasePromptParser 'No stream reference found.', ); - await this.stream.settled; + const completed = await this.stream.settled; // if prompt header exists, also wait for it to be settled if (this.promptHeader) { - await this.promptHeader.settled; + const headerCompleted = await this.promptHeader.settled; + if (!headerCompleted) { + return false; + } } - return this; - } - - /** - * Same as {@link settled} but also waits for all possible - * nested child prompt references and their children to be settled. - */ - public async allSettled(): Promise { - await this.settled(); - - return this; + return completed; } constructor( diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/headerBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/headerBase.ts index 0dc491b8ba1..eedebeabeed 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/headerBase.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/headerBase.ts @@ -249,7 +249,7 @@ export abstract class HeaderBase< * Promise that resolves when parsing process of * the prompt header completes. */ - public get settled(): Promise { + public get settled(): Promise { return this.stream.settled; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 912209978d8..1446b7c0639 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -235,7 +235,10 @@ export class PromptsService extends Disposable implements IPromptsService { { allowNonPromptFiles: true, languageId: MODE_LANGUAGE_ID, updateOnChange: false }, ).start(token); - await parser.settled(); + const completed = await parser.settled(); + if (!completed) { + throw new Error(localize('promptParser.notCompleted', "Prompt parser for {0} did not complete.", uri.toString())); + } const { description, model, tools } = parser.metadata as TModeMetadata; const body = await parser.getBody(); @@ -255,7 +258,10 @@ export class PromptsService extends Disposable implements IPromptsService { try { const languageId = getLanguageIdForPromptsType(type); parser = this.instantiationService.createInstance(PromptParser, uri, { allowNonPromptFiles: true, languageId, updateOnChange: false }).start(token); - await parser.settled(); + const completed = await parser.settled(); + if (!completed) { + throw new Error(localize('promptParser.notCompleted', "Prompt parser for {0} did not complete.", uri.toString())); + } // make a copy, to avoid leaking the parser instance return { uri: parser.uri, diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts index 7ff0ae1e359..35f77dfd78e 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts @@ -78,8 +78,9 @@ class TextModelPromptParserTest extends Disposable { /** * Wait for the prompt parsing/resolve process to finish. */ - public allSettled(): Promise { - return this.parser.allSettled(); + public async allSettled(): Promise { + await this.parser.settled(); + return this.parser; } /** @@ -88,7 +89,7 @@ class TextModelPromptParserTest extends Disposable { public async validateReferences( expectedReferences: readonly ExpectedReference[], ) { - await this.parser.allSettled(); + await this.parser.settled(); const { references } = this.parser; for (let i = 0; i < expectedReferences.length; i++) { @@ -115,7 +116,7 @@ class TextModelPromptParserTest extends Disposable { public async validateHeaderDiagnostics( expectedDiagnostics: readonly TExpectedDiagnostic[], ) { - await this.parser.allSettled(); + await this.parser.settled(); const { header } = this.parser; assertDefined( diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts index f3198003a3a..c1cfdd00f0b 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts @@ -109,7 +109,7 @@ class TestPromptFileReference extends Disposable { ).start(); // wait until entire prompts tree is resolved - await rootReference.allSettled(); + await rootReference.settled(); // resolve the root file reference including all nested references const resolvedReferences: readonly (TPromptReference | undefined)[] = rootReference.references; From 81989afd86a286184c41ce6845d58dbdc8daa59a Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 3 Jul 2025 21:08:37 -0400 Subject: [PATCH 127/306] ensure there's a space before reset char (#253925) fix #253190 --- extensions/terminal-suggest/src/tokens.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/terminal-suggest/src/tokens.ts b/extensions/terminal-suggest/src/tokens.ts index ee39314b6f9..cadab15ba64 100644 --- a/extensions/terminal-suggest/src/tokens.ts +++ b/extensions/terminal-suggest/src/tokens.ts @@ -14,7 +14,7 @@ export const enum TokenType { export const shellTypeResetChars = new Map([ [TerminalShellType.Bash, ['>', '>>', '<', '2>', '2>>', '&>', '&>>', '|', '|&', '&&', '||', '&', ';', '(', '{', '<<']], [TerminalShellType.Zsh, ['>', '>>', '<', '2>', '2>>', '&>', '&>>', '<>', '|', '|&', '&&', '||', '&', ';', '(', '{', '<<', '<<<', '<(']], - [TerminalShellType.PowerShell, ['>', '>>', '<', '2>', '2>>', '*>', '*>>', '|', ';', '-and', '-or', '-not', '!', '&', '-eq', '-ne', '-gt', '-lt', '-ge', '-le', '-like', '-notlike', '-match', '-notmatch', '-contains', '-notcontains', '-in', '-notin']] + [TerminalShellType.PowerShell, ['>', '>>', '<', '2>', '2>>', '*>', '*>>', '|', ';', ' -and ', ' -or ', ' -not ', '!', '&', ' -eq ', ' -ne ', ' -gt ', ' -lt ', ' -ge ', ' -le ', ' -like ', ' -notlike ', ' -match ', ' -notmatch ', ' -contains ', ' -notcontains ', ' -in ', ' -notin ']] ]); export const defaultShellTypeResetChars = shellTypeResetChars.get(TerminalShellType.Bash)!; @@ -31,7 +31,7 @@ export function getTokenType(ctx: { commandLine: string; cursorPosition: number // Look for " " before the word for (const resetChar of commandResetChars) { - const pattern = ` ${resetChar} `; + const pattern = shellType === TerminalShellType.PowerShell ? `${resetChar}` : ` ${resetChar} `; if (beforeWord.endsWith(pattern)) { return TokenType.Command; } From 751a38769af1a73f2f0f465f50a0e93257890ce9 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 4 Jul 2025 01:29:11 -0400 Subject: [PATCH 128/306] fix suggest bug when in `selectionMode:never` (#253887) fix #253183 --- .../suggest/browser/terminal.suggest.contribution.ts | 3 ++- .../terminalContrib/suggest/browser/terminalSuggestAddon.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index 6345d50bdee..9623d11ea6e 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -427,7 +427,8 @@ registerActiveInstanceAction({ keybinding: [{ primary: KeyCode.Tab, // Tab is bound to other workbench keybindings that this needs to beat - weight: KeybindingWeight.WorkbenchContrib + 2 + weight: KeybindingWeight.WorkbenchContrib + 2, + when: ContextKeyExpr.and(SimpleSuggestContext.HasFocusedSuggestion) }, { primary: KeyCode.Enter, diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 54457166471..aaf7bbc6a9c 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -816,8 +816,9 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest if (!suggestion) { suggestion = this._suggestWidget?.getFocusedItem(); } + const initialPromptInputState = this._mostRecentPromptInputState; - if (!suggestion || !initialPromptInputState || this._leadingLineContent === undefined || !this._model) { + if (!suggestion?.item || !initialPromptInputState || this._leadingLineContent === undefined || !this._model) { this._suggestTelemetry?.acceptCompletion(this._sessionId, undefined, this._mostRecentPromptInputState?.value); return; } From a758f9e8c846c2484c59e3242cacd28ed84e1cca Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 4 Jul 2025 08:04:42 +0200 Subject: [PATCH 129/306] Bump version (#254031) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f86132d242a..51c2dbe2b30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-oss-dev", - "version": "1.102.0", + "version": "1.103.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-oss-dev", - "version": "1.102.0", + "version": "1.103.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index dea32398691..b1a4469038c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "code-oss-dev", - "version": "1.102.0", + "version": "1.103.0", "distro": "f79d65a2e4f2caf1099ed08b494763e63710c2bc", "author": { "name": "Microsoft Corporation" From e93d38445442f8d707cc40cdc44f37fd2614a7f6 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 4 Jul 2025 09:30:48 +0200 Subject: [PATCH 130/306] perf - start to track aux sidebar (#254048) --- .../performance/browser/perfviewEditor.ts | 3 ++- .../services/timer/browser/timerService.ts | 21 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts b/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts index 20518d6be9b..3f45789a32f 100644 --- a/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts +++ b/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts @@ -211,7 +211,8 @@ class PerfModelContentProvider implements ITextModelContentProvider { table.push(['init keybindings, snippets & extensions from settings sync service', metrics.timers.ellapsedOtherUserDataInit, '[renderer]', undefined]); } table.push(['register extensions & spawn extension host', metrics.timers.ellapsedExtensions, '[renderer]', undefined]); - table.push(['restore viewlet', metrics.timers.ellapsedViewletRestore, '[renderer]', metrics.viewletId]); + table.push(['restore primary viewlet', metrics.timers.ellapsedViewletRestore, '[renderer]', metrics.viewletId]); + table.push(['restore secondary viewlet', metrics.timers.ellapsedAuxiliaryViewletRestore, '[renderer]', metrics.auxiliaryViewletId]); table.push(['restore panel', metrics.timers.ellapsedPanelRestore, '[renderer]', metrics.panelId]); table.push(['restore & resolve visible editors', metrics.timers.ellapsedEditorRestore, '[renderer]', `${metrics.editorIds.length}: ${metrics.editorIds.join(', ')}`]); table.push(['create workbench contributions', metrics.timers.ellapsedWorkbenchContributions, '[renderer]', `${(contribTimings.get(LifecyclePhase.Starting)?.length ?? 0) + (contribTimings.get(LifecyclePhase.Starting)?.length ?? 0)} blocking startup`]); diff --git a/src/vs/workbench/services/timer/browser/timerService.ts b/src/vs/workbench/services/timer/browser/timerService.ts index 9b2a06f7a90..dd80dce0eb4 100644 --- a/src/vs/workbench/services/timer/browser/timerService.ts +++ b/src/vs/workbench/services/timer/browser/timerService.ts @@ -64,6 +64,7 @@ export interface IMemoryInfo { "timers.ellapsedExtensions" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "timers.ellapsedExtensionsReady" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "timers.ellapsedViewletRestore" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "timers.ellapsedAuxiliaryViewletRestore" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "timers.ellapsedPanelRestore" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "timers.ellapsedEditorRestore" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "timers.ellapsedWorkbenchContributions" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, @@ -123,6 +124,11 @@ export interface IStartupMetrics { */ readonly viewletId?: string; + /** + * The active auxiliary viewlet id or `undedined` + */ + readonly auxiliaryViewletId?: string; + /** * The active panel id or `undefined` */ @@ -338,7 +344,7 @@ export interface IStartupMetrics { readonly ellapsedExtensionsReady: number; /** - * The time it took to restore the viewlet. + * The time it took to restore the primary sidebar viewlet. * * * Happens in the renderer-process * * Measured with the `willRestoreViewlet` and `didRestoreViewlet` performance marks. @@ -347,6 +353,16 @@ export interface IStartupMetrics { */ readonly ellapsedViewletRestore: number; + /** + * The time it took to restore the auxiliary bar viewlet. + * + * * Happens in the renderer-process + * * Measured with the `willRestoreAuxiliaryBar` and `didRestoreAuxiliaryBar` performance marks. + * * This should be looked at per viewlet-type/id. + * * Happens in parallel to other things, depends on async timing + */ + readonly ellapsedAuxiliaryViewletRestore: number; + /** * The time it took to restore the panel. * @@ -676,6 +692,7 @@ export abstract class AbstractTimerService implements ITimerService { } const activeViewlet = this._paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar); + const activeAuxiliaryViewlet = this._paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar); const activePanel = this._paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel); const info: Writeable = { @@ -687,6 +704,7 @@ export abstract class AbstractTimerService implements ITimerService { windowKind: this._lifecycleService.startupKind, windowCount: await this._getWindowCount(), viewletId: activeViewlet?.getId(), + auxiliaryViewletId: activeAuxiliaryViewlet?.getId(), editorIds: this._editorService.visibleEditors.map(input => input.typeId), panelId: activePanel ? activePanel.getId() : undefined, @@ -714,6 +732,7 @@ export abstract class AbstractTimerService implements ITimerService { ellapsedExtensions: this._marks.getDuration('code/willLoadExtensions', 'code/didLoadExtensions'), ellapsedEditorRestore: this._marks.getDuration('code/willRestoreEditors', 'code/didRestoreEditors'), ellapsedViewletRestore: this._marks.getDuration('code/willRestoreViewlet', 'code/didRestoreViewlet'), + ellapsedAuxiliaryViewletRestore: this._marks.getDuration('code/willRestoreAuxiliaryBar', 'code/didRestoreAuxiliaryBar'), ellapsedPanelRestore: this._marks.getDuration('code/willRestorePanel', 'code/didRestorePanel'), ellapsedWorkbenchContributions: this._marks.getDuration('code/willCreateWorkbenchContributions/1', 'code/didCreateWorkbenchContributions/2'), ellapsedWorkbench: this._marks.getDuration('code/willStartWorkbench', 'code/didStartWorkbench'), From 4d7c56ee824119599d2b8dc212bbc05da9c711ad Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 4 Jul 2025 08:21:56 +0000 Subject: [PATCH 131/306] Engineering - Add GitHub action for pull requests (#254056) * Test - handle running tests as part of a GitHub action * Add GitHub action files --- .github/workflows/pr-darwin-test.yml | 242 +++++++++++++++ .github/workflows/pr-linux-cli-test.yml | 46 +++ .github/workflows/pr-linux-test.yml | 288 ++++++++++++++++++ .github/workflows/pr-win32-test.yml | 279 +++++++++++++++++ .github/workflows/pr.yml | 148 +++++++++ .vscode-test.js | 7 +- build/lib/fetch.js | 2 +- build/lib/fetch.ts | 2 +- .../configuration-editing/src/test/index.ts | 6 +- .../server/test/index.js | 6 +- extensions/emmet/src/test/index.ts | 6 +- extensions/git/src/test/index.ts | 6 +- extensions/github/src/test/index.ts | 6 +- .../server/test/index.js | 6 +- extensions/ipynb/src/test/index.ts | 6 +- .../src/test/index.ts | 6 +- .../notebook-renderers/src/test/index.ts | 6 +- .../src/singlefolder-tests/index.ts | 6 +- .../src/workspace-tests/index.ts | 6 +- .../vscode-colorize-perf-tests/src/index.ts | 6 +- extensions/vscode-colorize-tests/src/index.ts | 4 +- src/vs/base/common/platform.ts | 2 +- test/smoke/test/index.js | 7 +- test/unit/browser/index.js | 5 +- test/unit/electron/index.js | 3 +- test/unit/electron/renderer.js | 2 +- 26 files changed, 1072 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/pr-darwin-test.yml create mode 100644 .github/workflows/pr-linux-cli-test.yml create mode 100644 .github/workflows/pr-linux-test.yml create mode 100644 .github/workflows/pr-win32-test.yml create mode 100644 .github/workflows/pr.yml diff --git a/.github/workflows/pr-darwin-test.yml b/.github/workflows/pr-darwin-test.yml new file mode 100644 index 00000000000..0f536f4cb36 --- /dev/null +++ b/.github/workflows/pr-darwin-test.yml @@ -0,0 +1,242 @@ +on: + workflow_call: + inputs: + job_name: + type: string + required: true + electron_tests: + type: boolean + default: false + browser_tests: + type: boolean + default: false + remote_tests: + type: boolean + default: false + +jobs: + macOS-test: + name: ${{ inputs.job_name }} + runs-on: macos-14-xlarge + env: + ARTIFACT_NAME: ${{ (inputs.electron_tests && 'electron') || (inputs.browser_tests && 'browser') || (inputs.remote_tests && 'remote') || 'unknown' }} + NPM_ARCH: arm64 + VSCODE_ARCH: arm64 + steps: + - name: Checkout microsoft/vscode + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + env: + NODEJS_ORG_MIRROR: https://github.com/joaomoreno/node-mirror/releases/download + + - name: Prepare node_modules cache key + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js darwin $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash + + - name: Restore node_modules cache + id: cache-node-modules + uses: actions/cache@v4 + with: + path: .build/node_modules_cache + key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" + + - name: Extract node_modules cache + if: steps.cache-node-modules.outputs.cache-hit == 'true' + run: tar -xzf .build/node_modules_cache/cache.tgz + + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + c++ --version + xcode-select -print-path + python3 -m pip install --break-system-packages setuptools + + for i in {1..5}; do # try 5 times + npm ci && break + if [ $i -eq 5 ]; then + echo "Npm install failed too many times" >&2 + exit 1 + fi + echo "Npm install failed $i, trying again..." + done + env: + npm_config_arch: ${{ env.NPM_ARCH }} + VSCODE_ARCH: ${{ env.VSCODE_ARCH }} + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Avoid using dlopen to load Kerberos on macOS which can cause missing libraries + # https://github.com/mongodb-js/kerberos/commit/04044d2814ad1d01e77f1ce87f26b03d86692cf2 + # flipped the default to support legacy linux distros which shouldn't happen + # on macOS. + GYP_DEFINES: "kerberos_use_rtld=false" + + - name: Create node_modules archive + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + + - name: Create .build folder + run: mkdir -p .build + + - name: Prepare built-in extensions cache key + run: node build/azure-pipelines/common/computeBuiltInDepsCacheKey.js > .build/builtindepshash + + - name: Restore built-in extensions cache + id: cache-builtin-extensions + uses: actions/cache@v4 + with: + path: .build/builtInExtensions + key: "builtin-extensions-${{ hashFiles('.build/builtindepshash') }}" + + - name: Download built-in extensions + if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' + run: node build/lib/builtInExtensions.js + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Transpile client and extensions + run: npm run gulp transpile-client-esbuild transpile-extensions + + - name: Download Electron and Playwright + run: | + set -e + + for i in {1..3}; do # try 3 times (matching retryCountOnTaskFailure: 3) + if npm exec -- npm-run-all -lp "electron ${{ env.VSCODE_ARCH }}" "playwright-install"; then + echo "Download successful on attempt $i" + break + fi + + if [ $i -eq 3 ]; then + echo "Download failed after 3 attempts" >&2 + exit 1 + fi + + echo "Download failed on attempt $i, retrying..." + sleep 5 # optional: add a small delay between retries + done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: 🧪 Run unit tests (Electron) + if: ${{ inputs.electron_tests }} + timeout-minutes: 15 + run: ./scripts/test.sh --tfs "Unit Tests" + + - name: 🧪 Run unit tests (node.js) + if: ${{ inputs.electron_tests }} + timeout-minutes: 15 + run: npm run test-node + + - name: 🧪 Run unit tests (Browser, Webkit) + if: ${{ inputs.browser_tests }} + timeout-minutes: 30 + run: npm run test-browser-no-install -- --browser webkit --tfs "Browser Unit Tests" + env: + DEBUG: "*browser*" + + - name: Build integration tests + run: | + set -e + npm run gulp \ + compile-extension:configuration-editing \ + compile-extension:css-language-features-server \ + compile-extension:emmet \ + compile-extension:git \ + compile-extension:github-authentication \ + compile-extension:html-language-features-server \ + compile-extension:ipynb \ + compile-extension:notebook-renderers \ + compile-extension:json-language-features-server \ + compile-extension:markdown-language-features \ + compile-extension-media \ + compile-extension:microsoft-authentication \ + compile-extension:typescript-language-features \ + compile-extension:vscode-api-tests \ + compile-extension:vscode-colorize-tests \ + compile-extension:vscode-colorize-perf-tests \ + compile-extension:vscode-test-resolver + + - name: 🧪 Run integration tests (Electron) + if: ${{ inputs.electron_tests }} + timeout-minutes: 20 + run: ./scripts/test-integration.sh --tfs "Integration Tests" + + - name: 🧪 Run integration tests (Browser, Webkit) + if: ${{ inputs.browser_tests }} + timeout-minutes: 20 + run: ./scripts/test-web-integration.sh --browser webkit + + - name: 🧪 Run integration tests (Remote) + if: ${{ inputs.remote_tests }} + timeout-minutes: 20 + run: ./scripts/test-remote-integration.sh + + - name: Compile smoke tests + working-directory: test/smoke + run: npm run compile + + - name: Compile extensions for smoke tests + run: npm run gulp compile-extension-media + + - name: Diagnostics before smoke test run + run: ps -ef + continue-on-error: true + if: always() + + - name: 🧪 Run smoke tests (Electron) + if: ${{ inputs.electron_tests }} + timeout-minutes: 20 + run: npm run smoketest-no-compile -- --tracing + + - name: 🧪 Run smoke tests (Browser, Chromium) + if: ${{ inputs.browser_tests }} + timeout-minutes: 20 + run: npm run smoketest-no-compile -- --web --tracing --headless + + - name: 🧪 Run smoke tests (Remote) + if: ${{ inputs.remote_tests }} + timeout-minutes: 20 + run: npm run smoketest-no-compile -- --remote --tracing + + - name: Diagnostics after smoke test run + run: ps -ef + continue-on-error: true + if: always() + + - name: Publish Crash Reports + uses: actions/upload-artifact@v4 + if: failure() + continue-on-error: true + with: + name: ${{ format('crash-dump-macos-{0}-{1}-{2}', env.VSCODE_ARCH, env.ARTIFACT_NAME, github.run_attempt) }} + path: .build/crashes + if-no-files-found: ignore + + # In order to properly symbolify above crash reports + # (if any), we need the compiled native modules too + - name: Publish Node Modules + uses: actions/upload-artifact@v4 + if: failure() + continue-on-error: true + with: + name: ${{ format('node-modules-macos-{0}-{1}-{2}', env.VSCODE_ARCH, env.ARTIFACT_NAME, github.run_attempt) }} + path: node_modules + if-no-files-found: ignore + + - name: Publish Log Files + uses: actions/upload-artifact@v4 + if: always() + continue-on-error: true + with: + name: ${{ format('logs-macos-{0}-{1}-{2}', env.VSCODE_ARCH, env.ARTIFACT_NAME, github.run_attempt) }} + path: .build/logs + if-no-files-found: ignore diff --git a/.github/workflows/pr-linux-cli-test.yml b/.github/workflows/pr-linux-cli-test.yml new file mode 100644 index 00000000000..7466c639cae --- /dev/null +++ b/.github/workflows/pr-linux-cli-test.yml @@ -0,0 +1,46 @@ +on: + workflow_call: + inputs: + job_name: + type: string + required: true + rustup_toolchain: + type: string + required: true + +jobs: + linux-cli-test: + name: ${{ inputs.job_name }} + runs-on: [ self-hosted, 1ES.Pool=1es-vscode-oss-ubuntu-22.04-x64 ] + env: + RUSTUP_TOOLCHAIN: ${{ inputs.rustup_toolchain }} + steps: + - name: Checkout microsoft/vscode + uses: actions/checkout@v4 + + - name: Install Rust + run: | + set -e + curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain $RUSTUP_TOOLCHAIN + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Set Rust version + run: | + set -e + rustup default $RUSTUP_TOOLCHAIN + rustup update $RUSTUP_TOOLCHAIN + rustup component add clippy + + - name: Check Rust versions + run: | + set -e + rustc --version + cargo --version + + - name: Clippy lint + run: cargo clippy -- -D warnings + working-directory: cli + + - name: 🧪 Run unit tests + run: cargo test + working-directory: cli diff --git a/.github/workflows/pr-linux-test.yml b/.github/workflows/pr-linux-test.yml new file mode 100644 index 00000000000..6e75ada8b84 --- /dev/null +++ b/.github/workflows/pr-linux-test.yml @@ -0,0 +1,288 @@ +on: + workflow_call: + inputs: + job_name: + type: string + required: true + electron_tests: + type: boolean + default: false + browser_tests: + type: boolean + default: false + remote_tests: + type: boolean + default: false + +jobs: + linux-test: + name: ${{ inputs.job_name }} + runs-on: [ self-hosted, 1ES.Pool=1es-vscode-oss-ubuntu-22.04-x64 ] + env: + ARTIFACT_NAME: ${{ (inputs.electron_tests && 'electron') || (inputs.browser_tests && 'browser') || (inputs.remote_tests && 'remote') || 'unknown' }} + NPM_ARCH: x64 + VSCODE_ARCH: x64 + steps: + - name: Checkout microsoft/vscode + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + env: + NODEJS_ORG_MIRROR: https://github.com/joaomoreno/node-mirror/releases/download + + - name: Setup system services + run: | + set -e + # Start X server + ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get update + ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get install -y pkg-config \ + xvfb \ + libgtk-3-0 \ + libxkbfile-dev \ + libkrb5-dev \ + libgbm1 \ + rpm + sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb + sudo chmod +x /etc/init.d/xvfb + sudo update-rc.d xvfb defaults + sudo service xvfb start + + - name: Prepare node_modules cache key + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js linux $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash + + - name: Restore node_modules cache + id: cache-node-modules + uses: actions/cache@v4 + with: + path: .build/node_modules_cache + key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" + + - name: Extract node_modules cache + if: steps.cache-node-modules.outputs.cache-hit == 'true' + run: tar -xzf .build/node_modules_cache/cache.tgz + + - name: Install build dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + working-directory: build + run: | + set -e + + for i in {1..5}; do # try 5 times + npm ci && break + if [ $i -eq 5 ]; then + echo "Npm install failed too many times" >&2 + exit 1 + fi + echo "Npm install failed $i, trying again..." + done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + + source ./build/azure-pipelines/linux/setup-env.sh + + for i in {1..5}; do # try 5 times + npm ci && break + if [ $i -eq 5 ]; then + echo "Npm install failed too many times" >&2 + exit 1 + fi + echo "Npm install failed $i, trying again..." + done + env: + npm_config_arch: ${{ env.NPM_ARCH }} + VSCODE_ARCH: ${{ env.VSCODE_ARCH }} + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create node_modules archive + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + + - name: Create .build folder + run: mkdir -p .build + + - name: Prepare built-in extensions cache key + run: node build/azure-pipelines/common/computeBuiltInDepsCacheKey.js > .build/builtindepshash + + - name: Restore built-in extensions cache + id: cache-builtin-extensions + uses: actions/cache@v4 + with: + path: .build/builtInExtensions + key: "builtin-extensions-${{ hashFiles('.build/builtindepshash') }}" + + - name: Download built-in extensions + if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' + run: node build/lib/builtInExtensions.js + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Transpile client and extensions + run: npm run gulp transpile-client-esbuild transpile-extensions + + - name: Download Electron and Playwright + run: | + set -e + + for i in {1..3}; do # try 3 times (matching retryCountOnTaskFailure: 3) + if npm exec -- npm-run-all -lp "electron ${{ env.VSCODE_ARCH }}" "playwright-install"; then + echo "Download successful on attempt $i" + break + fi + + if [ $i -eq 3 ]; then + echo "Download failed after 3 attempts" >&2 + exit 1 + fi + + echo "Download failed on attempt $i, retrying..." + sleep 5 # optional: add a small delay between retries + done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: 🧪 Run unit tests (Electron) + if: ${{ inputs.electron_tests }} + timeout-minutes: 15 + run: ./scripts/test.sh --tfs "Unit Tests" + env: + DISPLAY: ":10" + + - name: 🧪 Run unit tests (node.js) + if: ${{ inputs.electron_tests }} + timeout-minutes: 15 + run: npm run test-node + + - name: 🧪 Run unit tests (Browser, Chromium) + if: ${{ inputs.browser_tests }} + timeout-minutes: 30 + run: npm run test-browser-no-install -- --browser chromium --tfs "Browser Unit Tests" + env: + DEBUG: "*browser*" + + - name: Build integration tests + run: | + set -e + npm run gulp \ + compile-extension:configuration-editing \ + compile-extension:css-language-features-server \ + compile-extension:emmet \ + compile-extension:git \ + compile-extension:github-authentication \ + compile-extension:html-language-features-server \ + compile-extension:ipynb \ + compile-extension:notebook-renderers \ + compile-extension:json-language-features-server \ + compile-extension:markdown-language-features \ + compile-extension-media \ + compile-extension:microsoft-authentication \ + compile-extension:typescript-language-features \ + compile-extension:vscode-api-tests \ + compile-extension:vscode-colorize-tests \ + compile-extension:vscode-colorize-perf-tests \ + compile-extension:vscode-test-resolver + + - name: 🧪 Run integration tests (Electron) + if: ${{ inputs.electron_tests }} + timeout-minutes: 20 + run: ./scripts/test-integration.sh --tfs "Integration Tests" + env: + DISPLAY: ":10" + + - name: 🧪 Run integration tests (Browser, Chromium) + if: ${{ inputs.browser_tests }} + timeout-minutes: 20 + run: ./scripts/test-web-integration.sh --browser chromium + + - name: 🧪 Run integration tests (Remote) + if: ${{ inputs.remote_tests }} + timeout-minutes: 20 + run: ./scripts/test-remote-integration.sh + env: + DISPLAY: ":10" + + - name: Compile smoke tests + working-directory: test/smoke + run: npm run compile + + - name: Compile extensions for smoke tests + run: npm run gulp compile-extension-media + + - name: Diagnostics before smoke test run (processes, max_user_watches, number of opened file handles) + run: | + set -e + ps -ef + cat /proc/sys/fs/inotify/max_user_watches + lsof | wc -l + continue-on-error: true + if: always() + + - name: 🧪 Run smoke tests (Electron) + if: ${{ inputs.electron_tests }} + timeout-minutes: 20 + run: npm run smoketest-no-compile -- --tracing + env: + DISPLAY: ":10" + + - name: 🧪 Run smoke tests (Browser, Chromium) + if: ${{ inputs.browser_tests }} + timeout-minutes: 20 + run: npm run smoketest-no-compile -- --web --tracing --headless + + - name: 🧪 Run smoke tests (Remote) + if: ${{ inputs.remote_tests }} + timeout-minutes: 20 + run: npm run smoketest-no-compile -- --remote --tracing + env: + DISPLAY: ":10" + + - name: Diagnostics after smoke test run (processes, max_user_watches, number of opened file handles) + run: | + set -e + ps -ef + cat /proc/sys/fs/inotify/max_user_watches + lsof | wc -l + continue-on-error: true + if: always() + + - name: Publish Crash Reports + uses: actions/upload-artifact@v4 + if: failure() + continue-on-error: true + with: + name: ${{ format('crash-dump-linux-{0}-{1}-{2}', env.VSCODE_ARCH, env.ARTIFACT_NAME, github.run_attempt) }} + path: .build/crashes + if-no-files-found: ignore + + # In order to properly symbolify above crash reports + # (if any), we need the compiled native modules too + - name: Publish Node Modules + uses: actions/upload-artifact@v4 + if: failure() + continue-on-error: true + with: + name: ${{ format('node-modules-linux-{0}-{1}-{2}', env.VSCODE_ARCH, env.ARTIFACT_NAME, github.run_attempt) }} + path: node_modules + if-no-files-found: ignore + + - name: Publish Log Files + uses: actions/upload-artifact@v4 + if: always() + continue-on-error: true + with: + name: ${{ format('logs-linux-{0}-{1}-{2}', env.VSCODE_ARCH, env.ARTIFACT_NAME, github.run_attempt) }} + path: .build/logs + if-no-files-found: ignore diff --git a/.github/workflows/pr-win32-test.yml b/.github/workflows/pr-win32-test.yml new file mode 100644 index 00000000000..35b39311be5 --- /dev/null +++ b/.github/workflows/pr-win32-test.yml @@ -0,0 +1,279 @@ +on: + workflow_call: + inputs: + job_name: + type: string + required: true + electron_tests: + type: boolean + default: false + browser_tests: + type: boolean + default: false + remote_tests: + type: boolean + default: false + +jobs: + windows-test: + name: ${{ inputs.job_name }} + runs-on: [ self-hosted, 1ES.Pool=1es-vscode-oss-windows-2022-x64 ] + env: + ARTIFACT_NAME: ${{ (inputs.electron_tests && 'electron') || (inputs.browser_tests && 'browser') || (inputs.remote_tests && 'remote') || 'unknown' }} + NPM_ARCH: x64 + VSCODE_ARCH: x64 + steps: + - name: Checkout microsoft/vscode + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + env: + NODEJS_ORG_MIRROR: https://github.com/joaomoreno/node-mirror/releases/download + + - name: Prepare node_modules cache key + shell: pwsh + run: | + mkdir .build -ea 0 + node build/azure-pipelines/common/computeNodeModulesCacheKey.js win32 ${{ env.VSCODE_ARCH }} $(node -p process.arch) > .build/packagelockhash + + - name: Restore node_modules cache + uses: actions/cache@v4 + id: node-modules-cache + with: + path: .build/node_modules_cache + key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" + + - 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 + + - name: Install dependencies + if: steps.node-modules-cache.outputs.cache-hit != 'true' + shell: pwsh + run: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + + for ($i = 1; $i -le 5; $i++) { + try { + exec { npm ci } + break + } + catch { + if ($i -eq 5) { + Write-Error "npm ci failed after 5 attempts" + throw + } + Write-Host "npm ci failed attempt $i, retrying..." + Start-Sleep -Seconds 2 + } + } + env: + npm_config_arch: ${{ env.NPM_ARCH }} + npm_config_foreground_scripts: "true" + VSCODE_ARCH: ${{ env.VSCODE_ARCH }} + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create node_modules archive + if: steps.node-modules-cache.outputs.cache-hit != 'true' + shell: pwsh + run: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { node build/azure-pipelines/common/listNodeModules.js .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 } + + - name: Create .build folder + shell: pwsh + run: mkdir .build -ea 0 + + - name: Prepare built-in extensions cache key + shell: pwsh + run: node build/azure-pipelines/common/computeBuiltInDepsCacheKey.js > .build/builtindepshash + + - name: Restore built-in extensions cache + id: cache-builtin-extensions + uses: actions/cache@v4 + with: + path: .build/builtInExtensions + key: "builtin-extensions-${{ hashFiles('.build/builtindepshash') }}" + + - name: Download built-in extensions + if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' + run: node build/lib/builtInExtensions.js + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Transpile client and extensions + shell: pwsh + run: npm run gulp "transpile-client-esbuild" "transpile-extensions" + + - name: Download Electron and Playwright + shell: pwsh + run: | + for ($i = 1; $i -le 3; $i++) { + try { + npm exec -- -- npm-run-all -lp "electron ${{ env.VSCODE_ARCH }}" "playwright-install" + break + } + catch { + if ($i -eq 3) { + Write-Error "Download failed after 3 attempts" + throw + } + Write-Host "Download failed attempt $i, retrying..." + Start-Sleep -Seconds 2 + } + } + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: 🧪 Run unit tests (Electron) + if: ${{ inputs.electron_tests }} + shell: pwsh + run: .\scripts\test.bat --tfs "Unit Tests" + timeout-minutes: 15 + + - name: 🧪 Run unit tests (node.js) + if: ${{ inputs.electron_tests }} + shell: pwsh + run: npm run test-node + timeout-minutes: 15 + + - name: 🧪 Run unit tests (Browser, Chromium) + if: ${{ inputs.browser_tests }} + shell: pwsh + run: node test/unit/browser/index.js --browser chromium --tfs "Browser Unit Tests" + env: + DEBUG: "*browser*" + timeout-minutes: 20 + + - name: Build integration tests + shell: pwsh + run: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { npm run gulp ` + compile-extension:configuration-editing ` + compile-extension:css-language-features-server ` + compile-extension:emmet ` + compile-extension:git ` + compile-extension:github-authentication ` + compile-extension:html-language-features-server ` + compile-extension:ipynb ` + compile-extension:notebook-renderers ` + compile-extension:json-language-features-server ` + compile-extension:markdown-language-features ` + compile-extension-media ` + compile-extension:microsoft-authentication ` + compile-extension:typescript-language-features ` + compile-extension:vscode-api-tests ` + compile-extension:vscode-colorize-tests ` + compile-extension:vscode-colorize-perf-tests ` + compile-extension:vscode-test-resolver ` + } + + - name: Diagnostics before integration test runs + shell: pwsh + run: .\build\azure-pipelines\win32\listprocesses.bat + continue-on-error: true + if: always() + + - name: 🧪 Run integration tests (Electron) + if: ${{ inputs.electron_tests }} + shell: pwsh + run: .\scripts\test-integration.bat --tfs "Integration Tests" + timeout-minutes: 20 + + - name: 🧪 Run integration tests (Browser, Chromium) + if: ${{ inputs.browser_tests }} + shell: pwsh + run: .\scripts\test-web-integration.bat --browser chromium + timeout-minutes: 20 + + - name: 🧪 Run integration tests (Remote) + if: ${{ inputs.remote_tests }} + shell: pwsh + run: .\scripts\test-remote-integration.bat + timeout-minutes: 20 + + - name: Diagnostics after integration test runs + shell: pwsh + run: .\build\azure-pipelines\win32\listprocesses.bat + continue-on-error: true + if: always() + + - name: Diagnostics before smoke test run + shell: pwsh + run: .\build\azure-pipelines\win32\listprocesses.bat + continue-on-error: true + if: always() + + - name: Compile smoke tests + working-directory: test/smoke + shell: pwsh + run: npm run compile + + - name: Compile extensions for smoke tests + shell: pwsh + run: npm run gulp compile-extension-media + + - name: 🧪 Run smoke tests (Electron) + if: ${{ inputs.electron_tests }} + timeout-minutes: 20 + shell: pwsh + run: npm run smoketest-no-compile -- -- --tracing + + - name: 🧪 Run smoke tests (Browser, Chromium) + if: ${{ inputs.browser_tests }} + timeout-minutes: 20 + shell: pwsh + run: npm run smoketest-no-compile -- -- --web --tracing --headless + + - name: 🧪 Run smoke tests (Remote) + if: ${{ inputs.remote_tests }} + timeout-minutes: 20 + shell: pwsh + run: npm run smoketest-no-compile -- -- --remote --tracing + + - name: Diagnostics after smoke test run + shell: pwsh + run: .\build\azure-pipelines\win32\listprocesses.bat + continue-on-error: true + if: always() + + - name: Publish Crash Reports + uses: actions/upload-artifact@v4 + if: failure() + continue-on-error: true + with: + name: ${{ format('crash-dump-windows-{0}-{1}-{2}', env.VSCODE_ARCH, env.ARTIFACT_NAME, github.run_attempt) }} + path: .build/crashes + if-no-files-found: ignore + + # In order to properly symbolify above crash reports + # (if any), we need the compiled native modules too + - name: Publish Node Modules + uses: actions/upload-artifact@v4 + if: failure() + continue-on-error: true + with: + name: ${{ format('node-modules-windows-{0}-{1}-{2}', env.VSCODE_ARCH, env.ARTIFACT_NAME, github.run_attempt) }} + path: node_modules + if-no-files-found: ignore + + - name: Publish Log Files + uses: actions/upload-artifact@v4 + if: always() + continue-on-error: true + with: + name: ${{ format('logs-windows-{0}-{1}-{2}', env.VSCODE_ARCH, env.ARTIFACT_NAME, github.run_attempt) }} + path: .build/logs + if-no-files-found: ignore diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 00000000000..004e125ec04 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,148 @@ +name: Code OSS + +on: + pull_request: + branches: + - main + - 'release/*' + +permissions: {} + +jobs: + compile: + name: Compile & Hygiene + runs-on: [ self-hosted, 1ES.Pool=1es-vscode-oss-ubuntu-22.04-x64 ] + steps: + - name: Checkout microsoft/vscode + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + env: + NODEJS_ORG_MIRROR: https://github.com/joaomoreno/node-mirror/releases/download + + - name: Prepare node_modules cache key + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js compile $(node -p process.arch) > .build/packagelockhash + + - name: Restore node_modules cache + id: cache-node-modules + uses: actions/cache@v4 + with: + path: .build/node_modules_cache + key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" + + - name: Extract node_modules cache + if: steps.cache-node-modules.outputs.cache-hit == 'true' + run: tar -xzf .build/node_modules_cache/cache.tgz + + - name: Install build tools + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: sudo apt update -y && sudo apt install -y build-essential pkg-config libx11-dev libx11-xcb-dev libxkbfile-dev libnotify-bin libkrb5-dev + + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + + for i in {1..5}; do # try 5 times + npm ci && break + if [ $i -eq 5 ]; then + echo "Npm install failed too many times" >&2 + exit 1 + fi + echo "Npm install failed $i, trying again..." + done + env: + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create node_modules archive + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + + - name: Compile /build/ folder + run: npm run compile + working-directory: build + + - name: Check /build/ folder + run: .github/workflows/check-clean-git-state.sh + + - name: Compile & Hygiene + run: npm exec -- npm-run-all -lp core-ci-pr extensions-ci-pr hygiene eslint valid-layers-check define-class-fields-check vscode-dts-compile-check tsec-compile-check + + linux-cli-tests: + name: Linux + uses: ./.github/workflows/pr-linux-cli-test.yml + with: + job_name: CLI + rustup_toolchain: 1.85 + + linux-electron-tests: + name: Linux + uses: ./.github/workflows/pr-linux-test.yml + with: + job_name: Electron + electron_tests: true + + linux-browser-tests: + name: Linux + uses: ./.github/workflows/pr-linux-test.yml + with: + job_name: Browser + browser_tests: true + + linux-remote-tests: + name: Linux + uses: ./.github/workflows/pr-linux-test.yml + with: + job_name: Remote + remote_tests: true + + macos-electron-tests: + name: macOS + uses: ./.github/workflows/pr-darwin-test.yml + with: + job_name: Electron + electron_tests: true + + macos-browser-tests: + name: macOS + uses: ./.github/workflows/pr-darwin-test.yml + with: + job_name: Browser + browser_tests: true + + macos-remote-tests: + name: macOS + uses: ./.github/workflows/pr-darwin-test.yml + with: + job_name: Remote + remote_tests: true + + windows-electron-tests: + name: Windows + uses: ./.github/workflows/pr-win32-test.yml + with: + job_name: Electron + electron_tests: true + + windows-browser-tests: + name: Windows + uses: ./.github/workflows/pr-win32-test.yml + with: + job_name: Browser + browser_tests: true + + windows-remote-tests: + name: Windows + uses: ./.github/workflows/pr-win32-test.yml + with: + job_name: Remote + remote_tests: true diff --git a/.vscode-test.js b/.vscode-test.js index 823ef615f4f..e46484cb9df 100644 --- a/.vscode-test.js +++ b/.vscode-test.js @@ -97,7 +97,7 @@ const config = defineConfig(extensions.map(extension => { }; config.mocha ??= {}; - if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { + if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) { let suite = ''; if (process.env.VSCODE_BROWSER) { suite = `${process.env.VSCODE_BROWSER} Browser Integration ${config.label} tests`; @@ -112,7 +112,10 @@ const config = defineConfig(extensions.map(extension => { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join( + process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE || __dirname, + `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml` + ) } }; } diff --git a/build/lib/fetch.js b/build/lib/fetch.js index 078706cdd00..9f2b974b7ac 100644 --- a/build/lib/fetch.js +++ b/build/lib/fetch.js @@ -36,7 +36,7 @@ function fetchUrls(urls, options) { })); } async function fetchUrl(url, options, retries = 10, retryDelay = 1000) { - const verbose = !!options.verbose || !!process.env['CI'] || !!process.env['BUILD_ARTIFACTSTAGINGDIRECTORY']; + const verbose = !!options.verbose || !!process.env['CI'] || !!process.env['BUILD_ARTIFACTSTAGINGDIRECTORY'] || !!process.env['GITHUB_WORKSPACE']; try { let startTime = 0; if (verbose) { diff --git a/build/lib/fetch.ts b/build/lib/fetch.ts index 47a65b88fb5..f09b53e121c 100644 --- a/build/lib/fetch.ts +++ b/build/lib/fetch.ts @@ -42,7 +42,7 @@ export function fetchUrls(urls: string[] | string, options: IFetchOptions): es.T } export async function fetchUrl(url: string, options: IFetchOptions, retries = 10, retryDelay = 1000): Promise { - const verbose = !!options.verbose || !!process.env['CI'] || !!process.env['BUILD_ARTIFACTSTAGINGDIRECTORY']; + const verbose = !!options.verbose || !!process.env['CI'] || !!process.env['BUILD_ARTIFACTSTAGINGDIRECTORY'] || !!process.env['GITHUB_WORKSPACE']; try { let startTime = 0; if (verbose) { diff --git a/extensions/configuration-editing/src/test/index.ts b/extensions/configuration-editing/src/test/index.ts index ee8aeb0c6eb..c361b97928d 100644 --- a/extensions/configuration-editing/src/test/index.ts +++ b/extensions/configuration-editing/src/test/index.ts @@ -24,13 +24,15 @@ if (process.env.VSCODE_BROWSER) { suite = 'Integration Configuration-Editing Tests'; } -if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join( + process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE || __dirname, + `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/css-language-features/server/test/index.js b/extensions/css-language-features/server/test/index.js index 1699883a574..0c9be2d9710 100644 --- a/extensions/css-language-features/server/test/index.js +++ b/extensions/css-language-features/server/test/index.js @@ -15,13 +15,15 @@ const options = { timeout: 60000 }; -if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join( + process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE || __dirname, + `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/emmet/src/test/index.ts b/extensions/emmet/src/test/index.ts index e36bb6f7c77..51b4929af06 100644 --- a/extensions/emmet/src/test/index.ts +++ b/extensions/emmet/src/test/index.ts @@ -24,13 +24,15 @@ if (process.env.VSCODE_BROWSER) { suite = 'Integration Emmet Tests'; } -if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join( + process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE || __dirname, + `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/git/src/test/index.ts b/extensions/git/src/test/index.ts index c18561aaa66..da526f4d4c7 100644 --- a/extensions/git/src/test/index.ts +++ b/extensions/git/src/test/index.ts @@ -24,13 +24,15 @@ if (process.env.VSCODE_BROWSER) { suite = 'Integration Git Tests'; } -if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join( + process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE || __dirname, + `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/github/src/test/index.ts b/extensions/github/src/test/index.ts index 6573ab1daa4..382e728bfb2 100644 --- a/extensions/github/src/test/index.ts +++ b/extensions/github/src/test/index.ts @@ -14,13 +14,15 @@ const options: import('mocha').MochaOptions = { timeout: 60000 }; -if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join( + process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE || __dirname, + `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/html-language-features/server/test/index.js b/extensions/html-language-features/server/test/index.js index 50e250b78b8..93ffe7d6c09 100644 --- a/extensions/html-language-features/server/test/index.js +++ b/extensions/html-language-features/server/test/index.js @@ -15,13 +15,15 @@ const options = { timeout: 60000 }; -if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join( + process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE || __dirname, + `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/ipynb/src/test/index.ts b/extensions/ipynb/src/test/index.ts index 290194bfb2f..d70cf2e0628 100644 --- a/extensions/ipynb/src/test/index.ts +++ b/extensions/ipynb/src/test/index.ts @@ -24,13 +24,15 @@ if (process.env.VSCODE_BROWSER) { suite = 'Integration .ipynb Tests'; } -if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join( + process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE || __dirname, + `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/markdown-language-features/src/test/index.ts b/extensions/markdown-language-features/src/test/index.ts index 91e28d38e95..f9bd44aab27 100644 --- a/extensions/markdown-language-features/src/test/index.ts +++ b/extensions/markdown-language-features/src/test/index.ts @@ -24,13 +24,15 @@ if (process.env.VSCODE_BROWSER) { suite = 'Integration Markdown Tests'; } -if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join( + process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE || __dirname, + `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/notebook-renderers/src/test/index.ts b/extensions/notebook-renderers/src/test/index.ts index c7a818354b7..ff61996ea9c 100644 --- a/extensions/notebook-renderers/src/test/index.ts +++ b/extensions/notebook-renderers/src/test/index.ts @@ -24,13 +24,15 @@ if (process.env.VSCODE_BROWSER) { suite = 'Integration notebook output renderer Tests'; } -if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join( + process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE || __dirname, + `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/index.ts b/extensions/vscode-api-tests/src/singlefolder-tests/index.ts index 6798fc5a1e0..d7cce06ea62 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/index.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/index.ts @@ -24,13 +24,15 @@ if (process.env.VSCODE_BROWSER) { suite = 'Integration Single Folder Tests'; } -if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join( + process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE || __dirname, + `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/vscode-api-tests/src/workspace-tests/index.ts b/extensions/vscode-api-tests/src/workspace-tests/index.ts index 314a2e0d8f4..2010b075565 100644 --- a/extensions/vscode-api-tests/src/workspace-tests/index.ts +++ b/extensions/vscode-api-tests/src/workspace-tests/index.ts @@ -24,13 +24,15 @@ if (process.env.VSCODE_BROWSER) { suite = 'Integration Workspace Tests'; } -if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join( + process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE || __dirname, + `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/vscode-colorize-perf-tests/src/index.ts b/extensions/vscode-colorize-perf-tests/src/index.ts index 4376f31accd..65554f64ceb 100644 --- a/extensions/vscode-colorize-perf-tests/src/index.ts +++ b/extensions/vscode-colorize-perf-tests/src/index.ts @@ -14,13 +14,15 @@ const options: import('mocha').MochaOptions = { timeout: 60000 }; -if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join( + process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE || __dirname, + `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/vscode-colorize-tests/src/index.ts b/extensions/vscode-colorize-tests/src/index.ts index 51634213040..ea6a3357a63 100644 --- a/extensions/vscode-colorize-tests/src/index.ts +++ b/extensions/vscode-colorize-tests/src/index.ts @@ -20,7 +20,9 @@ if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join( + process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE || __dirname, + `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index 4bc8f61532b..cf76f7ce0e1 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -77,7 +77,7 @@ if (typeof nodeProcess === 'object') { _isLinux = (nodeProcess.platform === 'linux'); _isLinuxSnap = _isLinux && !!nodeProcess.env['SNAP'] && !!nodeProcess.env['SNAP_REVISION']; _isElectron = isElectronProcess; - _isCI = !!nodeProcess.env['CI'] || !!nodeProcess.env['BUILD_ARTIFACTSTAGINGDIRECTORY']; + _isCI = !!nodeProcess.env['CI'] || !!nodeProcess.env['BUILD_ARTIFACTSTAGINGDIRECTORY'] || !!nodeProcess.env['GITHUB_WORKSPACE']; _locale = LANGUAGE_DEFAULT; _language = LANGUAGE_DEFAULT; const rawNlsConfig = nodeProcess.env['VSCODE_NLS_CONFIG']; diff --git a/test/smoke/test/index.js b/test/smoke/test/index.js index 8e9646c0d65..b7c1277a6c6 100644 --- a/test/smoke/test/index.js +++ b/test/smoke/test/index.js @@ -25,13 +25,14 @@ const options = { grep: opts['f'] || opts['g'] }; -if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE || __dirname, + `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } @@ -45,7 +46,7 @@ mocha.run(failures => { const rootPath = join(__dirname, '..', '..', '..'); const logPath = join(rootPath, '.build', 'logs'); - if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { + if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) { console.log(` ################################################################### # # diff --git a/test/unit/browser/index.js b/test/unit/browser/index.js index d112d64ea80..7789165f19e 100644 --- a/test/unit/browser/index.js +++ b/test/unit/browser/index.js @@ -89,12 +89,13 @@ const isDebug = !!args.debug; const withReporter = (function () { if (args.tfs) { { + const testResultsRoot = process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE; return (browserType, runner) => { new mocha.reporters.Spec(runner); new MochaJUnitReporter(runner, { reporterOptions: { testsuitesTitle: `${args.tfs} ${process.platform}`, - mochaFile: process.env.BUILD_ARTIFACTSTAGINGDIRECTORY ? path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${browserType}-${args.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined + mochaFile: testResultsRoot ? path.join(testResultsRoot, `test-results/${process.platform}-${process.arch}-${browserType}-${args.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined } }); }; @@ -249,7 +250,7 @@ async function runTestsInBrowser(testModules, browserType, browserChannel) { if (args.build) { target.searchParams.set('build', 'true'); } - if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { + if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) { target.searchParams.set('ci', 'true'); } diff --git a/test/unit/electron/index.js b/test/unit/electron/index.js index f69104799ca..2072d9c2bde 100644 --- a/test/unit/electron/index.js +++ b/test/unit/electron/index.js @@ -326,12 +326,13 @@ app.on('ready', () => { const reporters = []; if (args.tfs) { + const testResultsRoot = process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE; reporters.push( new mocha.reporters.Spec(runner), new MochaJUnitReporter(runner, { reporterOptions: { testsuitesTitle: `${args.tfs} ${process.platform}`, - mochaFile: process.env.BUILD_ARTIFACTSTAGINGDIRECTORY ? path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${args.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined + mochaFile: testResultsRoot ? path.join(testResultsRoot, `test-results/${process.platform}-${process.arch}-${args.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined } }), ); diff --git a/test/unit/electron/renderer.js b/test/unit/electron/renderer.js index 661be873561..af451fbab8e 100644 --- a/test/unit/electron/renderer.js +++ b/test/unit/electron/renderer.js @@ -88,7 +88,7 @@ Object.assign(globalThis, { __mkdirPInTests: path => fs.promises.mkdir(path, { recursive: true }), }); -const IS_CI = !!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY; +const IS_CI = !!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || !!process.env.GITHUB_WORKSPACE; const _tests_glob = '**/test/**/*.test.js'; From 5b03be390e7646d0f6b880c4ec013f6d2c306d48 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 4 Jul 2025 10:24:09 +0200 Subject: [PATCH 132/306] editors - give better action label to unmaximize group (#254050) --- src/vs/workbench/browser/parts/editor/editorActions.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index d7452326923..6d9baf325b2 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -1178,7 +1178,10 @@ export class ToggleMaximizeEditorGroupAction extends Action2 { when: EditorPartMaximizedEditorGroupContext }], icon: Codicon.screenFull, - toggled: EditorPartMaximizedEditorGroupContext, + toggled: { + condition: EditorPartMaximizedEditorGroupContext, + title: localize('unmaximizeGroup', "Unmaximize Group") + }, }); } From 93858ca3600058322e0d86d2383708e06845e95f Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 4 Jul 2025 09:05:45 +0000 Subject: [PATCH 133/306] Engineering - cancel PR build when a new commit is pushed (#254064) --- .github/workflows/pr.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 004e125ec04..d1689793e0e 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -6,6 +6,10 @@ on: - main - 'release/*' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: {} jobs: From 367fc5f2437bf18e769b1add856f4d8d88db6b3b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 4 Jul 2025 11:17:56 +0200 Subject: [PATCH 134/306] exp - introduce and use flag to disable experiments (#254057) * exp - introduce and use flag to disable experiments * fix --- .vscode-test.js | 2 +- extensions/vscode-test-resolver/src/extension.ts | 2 +- scripts/test-integration.bat | 2 +- scripts/test-integration.sh | 2 +- scripts/test-remote-integration.bat | 2 +- scripts/test-remote-integration.sh | 2 +- src/vs/editor/standalone/browser/standaloneServices.ts | 1 + src/vs/platform/environment/common/argv.ts | 1 + src/vs/platform/environment/common/environment.ts | 3 ++- src/vs/platform/environment/common/environmentService.ts | 3 +++ src/vs/platform/environment/node/argv.ts | 1 + src/vs/server/node/serverEnvironmentService.ts | 2 ++ .../services/assignment/common/assignmentService.ts | 2 +- .../coreExperimentation/common/coreExperimentationService.ts | 5 +++++ .../services/environment/browser/environmentService.ts | 3 +++ test/automation/src/electron.ts | 1 + test/automation/src/playwrightBrowser.ts | 1 + test/integration/browser/src/index.ts | 2 +- 18 files changed, 28 insertions(+), 9 deletions(-) diff --git a/.vscode-test.js b/.vscode-test.js index e46484cb9df..2e49c90126b 100644 --- a/.vscode-test.js +++ b/.vscode-test.js @@ -84,7 +84,7 @@ const extensions = [ const defaultLaunchArgs = process.env.API_TESTS_EXTRA_ARGS?.split(' ') || [ - '--disable-telemetry', '--skip-welcome', '--skip-release-notes', `--crash-reporter-directory=${__dirname}/.build/crashes`, `--logsPath=${__dirname}/.build/logs/integration-tests`, '--no-cached-data', '--disable-updates', '--use-inmemory-secretstorage', '--disable-extensions', '--disable-workspace-trust' + '--disable-telemetry', '--disable-experiments', '--skip-welcome', '--skip-release-notes', `--crash-reporter-directory=${__dirname}/.build/crashes`, `--logsPath=${__dirname}/.build/logs/integration-tests`, '--no-cached-data', '--disable-updates', '--use-inmemory-secretstorage', '--disable-extensions', '--disable-workspace-trust' ]; const config = defineConfig(extensions.map(extension => { diff --git a/extensions/vscode-test-resolver/src/extension.ts b/extensions/vscode-test-resolver/src/extension.ts index 2fab3ec306a..74d25c65ae0 100644 --- a/extensions/vscode-test-resolver/src/extension.ts +++ b/extensions/vscode-test-resolver/src/extension.ts @@ -142,7 +142,7 @@ export function activate(context: vscode.ExtensionContext) { } const { updateUrl, commit, quality, serverDataFolderName, serverApplicationName, dataFolderName } = getProductConfiguration(); - const commandArgs = ['--host=127.0.0.1', '--port=0', '--disable-telemetry', '--use-host-proxy', '--accept-server-license-terms']; + const commandArgs = ['--host=127.0.0.1', '--port=0', '--disable-telemetry', '--disable-experiments', '--use-host-proxy', '--accept-server-license-terms']; const env = getNewEnv(); const remoteDataDir = process.env['TESTRESOLVER_DATA_FOLDER'] || path.join(os.homedir(), `${serverDataFolderName || dataFolderName}-testresolver`); const logsDir = process.env['TESTRESOLVER_LOGS_FOLDER']; diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index b59cceada07..50c14ff8d3f 100644 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -35,7 +35,7 @@ if %errorlevel% neq 0 exit /b %errorlevel% :: Tests in the extension host -set API_TESTS_EXTRA_ARGS=--disable-telemetry --skip-welcome --skip-release-notes --crash-reporter-directory=%VSCODECRASHDIR% --logsPath=%VSCODELOGSDIR% --no-cached-data --disable-updates --use-inmemory-secretstorage --disable-extensions --disable-workspace-trust --user-data-dir=%VSCODEUSERDATADIR% +set API_TESTS_EXTRA_ARGS=--disable-telemetry --disable-experiments --skip-welcome --skip-release-notes --crash-reporter-directory=%VSCODECRASHDIR% --logsPath=%VSCODELOGSDIR% --no-cached-data --disable-updates --use-inmemory-secretstorage --disable-extensions --disable-workspace-trust --user-data-dir=%VSCODEUSERDATADIR% echo. echo ### API tests (folder) diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index 33d00615359..3e26bb17a17 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -41,7 +41,7 @@ echo # Tests in the extension host -API_TESTS_EXTRA_ARGS="--disable-telemetry --skip-welcome --skip-release-notes --crash-reporter-directory=$VSCODECRASHDIR --logsPath=$VSCODELOGSDIR --no-cached-data --disable-updates --use-inmemory-secretstorage --disable-extensions --disable-workspace-trust --user-data-dir=$VSCODEUSERDATADIR" +API_TESTS_EXTRA_ARGS="--disable-telemetry --disable-experiments --skip-welcome --skip-release-notes --crash-reporter-directory=$VSCODECRASHDIR --logsPath=$VSCODELOGSDIR --no-cached-data --disable-updates --use-inmemory-secretstorage --disable-extensions --disable-workspace-trust --user-data-dir=$VSCODEUSERDATADIR" if [ -z "$INTEGRATION_TEST_APP_NAME" ]; then kill_app() { true; } diff --git a/scripts/test-remote-integration.bat b/scripts/test-remote-integration.bat index e79466424db..96288d35886 100644 --- a/scripts/test-remote-integration.bat +++ b/scripts/test-remote-integration.bat @@ -55,7 +55,7 @@ echo Storing log files into '%VSCODELOGSDIR%' :: Tests in the extension host -set API_TESTS_EXTRA_ARGS=--disable-telemetry --skip-welcome --skip-release-notes --crash-reporter-directory=%VSCODECRASHDIR% --logsPath=%VSCODELOGSDIR% --no-cached-data --disable-updates --use-inmemory-secretstorage --disable-inspect --disable-workspace-trust --user-data-dir=%VSCODEUSERDATADIR% +set API_TESTS_EXTRA_ARGS=--disable-telemetry --disable-experiments --skip-welcome --skip-release-notes --crash-reporter-directory=%VSCODECRASHDIR% --logsPath=%VSCODELOGSDIR% --no-cached-data --disable-updates --use-inmemory-secretstorage --disable-inspect --disable-workspace-trust --user-data-dir=%VSCODEUSERDATADIR% echo. echo ### API tests (folder) diff --git a/scripts/test-remote-integration.sh b/scripts/test-remote-integration.sh index 7325757418e..7224216b2c0 100755 --- a/scripts/test-remote-integration.sh +++ b/scripts/test-remote-integration.sh @@ -65,7 +65,7 @@ else kill_app() { killall $INTEGRATION_TEST_APP_NAME || true; } fi -API_TESTS_EXTRA_ARGS="--disable-telemetry --skip-welcome --skip-release-notes --crash-reporter-directory=$VSCODECRASHDIR --logsPath=$VSCODELOGSDIR --no-cached-data --disable-updates --use-inmemory-secretstorage --disable-workspace-trust --user-data-dir=$VSCODEUSERDATADIR" +API_TESTS_EXTRA_ARGS="--disable-telemetry --disable-experiments --skip-welcome --skip-release-notes --crash-reporter-directory=$VSCODECRASHDIR --logsPath=$VSCODELOGSDIR --no-cached-data --disable-updates --use-inmemory-secretstorage --disable-workspace-trust --user-data-dir=$VSCODEUSERDATADIR" echo "Storing crash reports into '$VSCODECRASHDIR'." echo "Storing log files into '$VSCODELOGSDIR'." diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 89e3a9dd3d7..0703f81e39a 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -229,6 +229,7 @@ class StandaloneEnvironmentService implements IEnvironmentService { readonly debugExtensionHost: IExtensionHostDebugParams = { port: null, break: false }; readonly isExtensionDevelopment: boolean = false; readonly disableExtensions: boolean | string[] = false; + readonly disableExperiments: boolean = false; readonly enableExtensions?: readonly string[] | undefined = undefined; readonly extensionDevelopmentLocationURI?: URI[] | undefined = undefined; readonly extensionDevelopmentKind?: ExtensionKind[] | undefined = undefined; diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index d4937b2d4ec..2ffbb9773bc 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -139,6 +139,7 @@ export interface NativeParsedArgs { 'enable-rdp-display-tracking'?: boolean; 'disable-layout-restore'?: boolean; 'startup-experiment-group'?: string; + 'disable-experiments'?: boolean; // chromium command line args: https://electronjs.org/docs/all#supported-chrome-command-line-switches 'no-proxy-server'?: boolean; diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index b6e945496d9..c5f10d53040 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -83,8 +83,9 @@ export interface IEnvironmentService { verbose: boolean; isBuilt: boolean; - // --- telemetry + // --- telemetry/exp disableTelemetry: boolean; + disableExperiments: boolean; serviceMachineIdResource: URI; // --- Policy diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index f60efc16786..e60b83f27d4 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -230,6 +230,9 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron @memoize get disableTelemetry(): boolean { return !!this.args['disable-telemetry']; } + @memoize + get disableExperiments(): boolean { return !!this.args['disable-experiments']; } + @memoize get disableWorkspaceTrust(): boolean { return !!this.args['disable-workspace-trust']; } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 2f30fbe8cd2..342718df668 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -200,6 +200,7 @@ export const OPTIONS: OptionDescriptions> = { 'unresponsive-sample-period': { type: 'string' }, 'enable-rdp-display-tracking': { type: 'boolean' }, 'disable-layout-restore': { type: 'boolean' }, + 'disable-experiments': { type: 'boolean' }, 'startup-experiment-group': { type: 'string', cat: 't', args: 'control|maximizedChat|splitEmptyEditorChat|splitWelcomeChat', description: localize('startupExperimentGroup', "Override the startup experiment group.") }, // chromium flags diff --git a/src/vs/server/node/serverEnvironmentService.ts b/src/vs/server/node/serverEnvironmentService.ts index eb588059c16..092618c6846 100644 --- a/src/vs/server/node/serverEnvironmentService.ts +++ b/src/vs/server/node/serverEnvironmentService.ts @@ -37,6 +37,7 @@ export const serverOptions: OptionDescriptions> = { 'user-data-dir': OPTIONS['user-data-dir'], 'enable-smoke-test-driver': OPTIONS['enable-smoke-test-driver'], 'disable-telemetry': OPTIONS['disable-telemetry'], + 'disable-experiments': OPTIONS['disable-experiments'], 'disable-workspace-trust': OPTIONS['disable-workspace-trust'], 'file-watcher-polling': { type: 'string', deprecates: ['fileWatcherPolling'] }, 'log': OPTIONS['log'], @@ -160,6 +161,7 @@ export interface ServerParsedArgs { 'enable-smoke-test-driver'?: boolean; 'disable-telemetry'?: boolean; + 'disable-experiments'?: boolean; 'file-watcher-polling'?: string; 'log'?: string[]; diff --git a/src/vs/workbench/services/assignment/common/assignmentService.ts b/src/vs/workbench/services/assignment/common/assignmentService.ts index 8e7aa27c14c..d18437e1bca 100644 --- a/src/vs/workbench/services/assignment/common/assignmentService.ts +++ b/src/vs/workbench/services/assignment/common/assignmentService.ts @@ -100,7 +100,7 @@ export class WorkbenchAssignmentService extends BaseAssignmentService { } protected override get experimentsEnabled(): boolean { - return this.configurationService.getValue('workbench.enableExperiments') === true; + return !this.environmentService.disableExperiments && this.configurationService.getValue('workbench.enableExperiments') === true; } override async getTreatment(name: string): Promise { diff --git a/src/vs/workbench/services/coreExperimentation/common/coreExperimentationService.ts b/src/vs/workbench/services/coreExperimentation/common/coreExperimentationService.ts index f14dd96a9a3..944e9bee9de 100644 --- a/src/vs/workbench/services/coreExperimentation/common/coreExperimentationService.ts +++ b/src/vs/workbench/services/coreExperimentation/common/coreExperimentationService.ts @@ -88,6 +88,11 @@ export class CoreExperimentationService extends Disposable implements ICoreExper @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, ) { super(); + + if (environmentService.disableExperiments) { + return; // explicitly disabled + } + this.initializeExperiments(); } diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index 7223b54c3ad..a50ad52689b 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -236,6 +236,9 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi @memoize get disableTelemetry(): boolean { return false; } + @memoize + get disableExperiments(): boolean { return false; } + @memoize get verbose(): boolean { return this.payload?.get('verbose') === 'true'; } diff --git a/test/automation/src/electron.ts b/test/automation/src/electron.ts index 7d162daf1d2..7935aea1dd1 100644 --- a/test/automation/src/electron.ts +++ b/test/automation/src/electron.ts @@ -27,6 +27,7 @@ export async function resolveElectronConfiguration(options: LaunchOptions): Prom '--skip-release-notes', '--skip-welcome', '--disable-telemetry', + '--disable-experiments', '--no-cached-data', '--disable-updates', '--use-inmemory-secretstorage', diff --git a/test/automation/src/playwrightBrowser.ts b/test/automation/src/playwrightBrowser.ts index f4f63875b2a..6a2334fdc78 100644 --- a/test/automation/src/playwrightBrowser.ts +++ b/test/automation/src/playwrightBrowser.ts @@ -44,6 +44,7 @@ async function launchServer(options: LaunchOptions) { const args = [ '--disable-telemetry', + '--disable-experiments', '--disable-workspace-trust', `--port=${port++}`, '--enable-smoke-test-driver', diff --git a/test/integration/browser/src/index.ts b/test/integration/browser/src/index.ts index 0c3cd8efd32..47ac09d9f8c 100644 --- a/test/integration/browser/src/index.ts +++ b/test/integration/browser/src/index.ts @@ -169,7 +169,7 @@ async function launchServer(browserType: BrowserType, browserChannel: BrowserCha ...process.env }; - const serverArgs = ['--enable-proposed-api', '--disable-telemetry', '--server-data-dir', userDataDir, '--accept-server-license-terms', '--disable-workspace-trust']; + const serverArgs = ['--enable-proposed-api', '--disable-telemetry', '--disable-experiments', '--server-data-dir', userDataDir, '--accept-server-license-terms', '--disable-workspace-trust']; let serverLocation: string; if (process.env.VSCODE_REMOTE_SERVER_PATH) { From 74f72e008d2bef1c9c7d7dd5573c7a14dcec783a Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 4 Jul 2025 09:27:58 +0000 Subject: [PATCH 135/306] Engineering - add GitHub action to maintain node_modules cache (#254069) --- .github/workflows/pr-node-modules.yml | 228 ++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 .github/workflows/pr-node-modules.yml diff --git a/.github/workflows/pr-node-modules.yml b/.github/workflows/pr-node-modules.yml new file mode 100644 index 00000000000..a35033197b9 --- /dev/null +++ b/.github/workflows/pr-node-modules.yml @@ -0,0 +1,228 @@ +name: Code OSS (node_modules) + +on: + push: + branches: + - main + +permissions: {} + +jobs: + linux: + name: Linux + runs-on: [ self-hosted, 1ES.Pool=1es-vscode-oss-ubuntu-22.04-x64 ] + env: + NPM_ARCH: x64 + VSCODE_ARCH: x64 + steps: + - name: Checkout microsoft/vscode + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + env: + NODEJS_ORG_MIRROR: https://github.com/joaomoreno/node-mirror/releases/download + + - name: Prepare node_modules cache key + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js linux $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash + + - name: Restore node_modules cache + id: cache-node-modules + uses: actions/cache@v4 + with: + path: .build/node_modules_cache + key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" + + - name: Extract node_modules cache + if: steps.cache-node-modules.outputs.cache-hit == 'true' + run: tar -xzf .build/node_modules_cache/cache.tgz + + - name: Install build dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + working-directory: build + run: | + set -e + + for i in {1..5}; do # try 5 times + npm ci && break + if [ $i -eq 5 ]; then + echo "Npm install failed too many times" >&2 + exit 1 + fi + echo "Npm install failed $i, trying again..." + done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + + source ./build/azure-pipelines/linux/setup-env.sh + + for i in {1..5}; do # try 5 times + npm ci && break + if [ $i -eq 5 ]; then + echo "Npm install failed too many times" >&2 + exit 1 + fi + echo "Npm install failed $i, trying again..." + done + env: + npm_config_arch: ${{ env.NPM_ARCH }} + VSCODE_ARCH: ${{ env.VSCODE_ARCH }} + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create node_modules archive + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + + macOS: + name: macOS + runs-on: macos-14-xlarge + env: + NPM_ARCH: arm64 + VSCODE_ARCH: arm64 + steps: + - name: Checkout microsoft/vscode + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + env: + NODEJS_ORG_MIRROR: https://github.com/joaomoreno/node-mirror/releases/download + + - name: Prepare node_modules cache key + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js darwin $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash + + - name: Restore node_modules cache + id: cache-node-modules + uses: actions/cache@v4 + with: + path: .build/node_modules_cache + key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" + + - name: Extract node_modules cache + if: steps.cache-node-modules.outputs.cache-hit == 'true' + run: tar -xzf .build/node_modules_cache/cache.tgz + + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + c++ --version + xcode-select -print-path + python3 -m pip install --break-system-packages setuptools + + for i in {1..5}; do # try 5 times + npm ci && break + if [ $i -eq 5 ]; then + echo "Npm install failed too many times" >&2 + exit 1 + fi + echo "Npm install failed $i, trying again..." + done + env: + npm_config_arch: ${{ env.NPM_ARCH }} + VSCODE_ARCH: ${{ env.VSCODE_ARCH }} + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Avoid using dlopen to load Kerberos on macOS which can cause missing libraries + # https://github.com/mongodb-js/kerberos/commit/04044d2814ad1d01e77f1ce87f26b03d86692cf2 + # flipped the default to support legacy linux distros which shouldn't happen + # on macOS. + GYP_DEFINES: "kerberos_use_rtld=false" + + - name: Create node_modules archive + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + + windows: + name: Windows + runs-on: [ self-hosted, 1ES.Pool=1es-vscode-oss-windows-2022-x64 ] + env: + NPM_ARCH: x64 + VSCODE_ARCH: x64 + steps: + - name: Checkout microsoft/vscode + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + env: + NODEJS_ORG_MIRROR: https://github.com/joaomoreno/node-mirror/releases/download + + - name: Prepare node_modules cache key + shell: pwsh + run: | + mkdir .build -ea 0 + node build/azure-pipelines/common/computeNodeModulesCacheKey.js win32 ${{ env.VSCODE_ARCH }} $(node -p process.arch) > .build/packagelockhash + + - name: Restore node_modules cache + uses: actions/cache@v4 + id: node-modules-cache + with: + path: .build/node_modules_cache + key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" + + - 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 + + - name: Install dependencies + if: steps.node-modules-cache.outputs.cache-hit != 'true' + shell: pwsh + run: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + + for ($i = 1; $i -le 5; $i++) { + try { + exec { npm ci } + break + } + catch { + if ($i -eq 5) { + Write-Error "npm ci failed after 5 attempts" + throw + } + Write-Host "npm ci failed attempt $i, retrying..." + Start-Sleep -Seconds 2 + } + } + env: + npm_config_arch: ${{ env.NPM_ARCH }} + npm_config_foreground_scripts: "true" + VSCODE_ARCH: ${{ env.VSCODE_ARCH }} + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create node_modules archive + if: steps.node-modules-cache.outputs.cache-hit != 'true' + shell: pwsh + run: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { node build/azure-pipelines/common/listNodeModules.js .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 } From b3010d6447fe94bffa76701aec407cdbd0b47bd9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 4 Jul 2025 11:30:38 +0200 Subject: [PATCH 136/306] chat - remove alternate fallback (#254070) --- src/vs/workbench/contrib/chat/browser/chatSetup.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 22d6de21673..76c2c513091 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -736,10 +736,6 @@ class ChatSetup { if (this.context.state.entitlement === ChatEntitlement.Unknown) { let alternateProvider: 'off' | 'monochrome' | 'colorful' | 'first' = 'off'; if (defaultChat.alternativeProviderId) { - if (this.configurationService.getValue('chat.setup.signInWithAlternateProvider')) { - alternateProvider = 'colorful'; // TODO@bpasero remove me soon - } - switch (variant) { case 'alternate-first': alternateProvider = 'first'; From 116cbcd6054c2de004c0b9ee2b72389a9b99031a Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 4 Jul 2025 09:42:56 +0000 Subject: [PATCH 137/306] Engineering - update CODEOWNERS (#254072) --- .github/CODEOWNERS | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8da51487c84..b74accb8908 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,11 @@ +# Ensure that Lad and Joao review changes to the Code OSS actions +.github/workflows/pr-darwin-test.yml @lszomoru @joaomoreno +.github/workflows/pr-linux-cli-test.yml @lszomoru @joaomoreno +.github/workflows/pr-linux-test.yml @lszomoru @joaomoreno +.github/workflows/pr-node-modules.yml @lszomoru @joaomoreno +.github/workflows/pr-win32-test.yml @lszomoru @joaomoreno +.github/workflows/pr.yml @lszomoru @joaomoreno + # ensure the API police is aware of changes to the vscode-dts file # this is only about the final API, not about proposed API changes src/vscode-dts/vscode.d.ts @jrieken @mjbvz From c809b74d173e305011fc1f7fcdde89a00e021138 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 4 Jul 2025 12:22:59 +0200 Subject: [PATCH 138/306] show iconUrl for mcp servers from gallery (#254081) --- src/vs/platform/mcp/common/mcpGalleryService.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/vs/platform/mcp/common/mcpGalleryService.ts b/src/vs/platform/mcp/common/mcpGalleryService.ts index 37b4dc17070..5f397f8d1f2 100644 --- a/src/vs/platform/mcp/common/mcpGalleryService.ts +++ b/src/vs/platform/mcp/common/mcpGalleryService.ts @@ -177,6 +177,22 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService } } + let icon: { light: string; dark: string } | undefined; + if (this.productService.extensionsGallery?.mcpUrl !== this.getMcpGalleryUrl()) { + if (item.iconUrl) { + icon = { + light: item.iconUrl, + dark: item.iconUrl + }; + } + if (item.iconUrlLight && item.iconUrlDark) { + icon = { + light: item.iconUrlLight, + dark: item.iconUrlDark + }; + } + } + return { id: item.id ?? item.name, name: item.name, @@ -187,6 +203,7 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService lastUpdated: item.version_detail ? Date.parse(item.version_detail.release_date) : undefined, repositoryUrl: item.repository?.url, codicon: item.codicon, + icon, readmeUrl: item.readmeUrl, manifestUrl: this.getManifestUrl(item), packageTypes: item.package_types ?? [], From 02506486470a2e1aab0efe537388e920866c0d2e Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 4 Jul 2025 03:23:34 -0700 Subject: [PATCH 139/306] promptUrlHandler: Simplify the confirmation dialog (#254077) promptUrlHandler: Simplify the confirmation dilaog --- .../browser/promptSyntax/promptUrlHandler.ts | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptUrlHandler.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptUrlHandler.ts index 436171fb529..2875e5bcdb7 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptUrlHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptUrlHandler.ts @@ -21,7 +21,6 @@ import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { localize } from '../../../../../nls.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { Schemas } from '../../../../../base/common/network.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { IHostService } from '../../../../services/host/browser/host.js'; @@ -33,8 +32,6 @@ export class PromptUrlHandler extends Disposable implements IWorkbenchContributi static readonly ID = 'workbench.contrib.promptUrlHandler'; - static readonly CONFIRM_INSTALL_STORAGE_KEY = 'security.promptForPromptProtocolHandling'; - constructor( @IURLService urlService: IURLService, @INotificationService private readonly notificationService: INotificationService, @@ -44,7 +41,7 @@ export class PromptUrlHandler extends Disposable implements IWorkbenchContributi @IOpenerService private readonly openerService: IOpenerService, @ILogService private readonly logService: ILogService, @IDialogService private readonly dialogService: IDialogService, - @IStorageService private readonly storageService: IStorageService, + @IHostService private readonly hostService: IHostService, ) { super(); @@ -120,13 +117,6 @@ export class PromptUrlHandler extends Disposable implements IWorkbenchContributi } private async shouldBlockInstall(promptType: PromptsType, url: URI): Promise { - const location = url.with({ path: url.path.substring(0, url.path.indexOf('/', 1) + 1), query: undefined, fragment: undefined }).toString(); - const key = PromptUrlHandler.CONFIRM_INSTALL_STORAGE_KEY + '-' + location; - - if (this.storageService.getBoolean(key, StorageScope.APPLICATION, false)) { - return false; - } - let uriLabel = url.toString(); if (uriLabel.length > 50) { uriLabel = `${uriLabel.substring(0, 35)}...${uriLabel.substring(uriLabel.length - 15)}`; @@ -134,7 +124,7 @@ export class PromptUrlHandler extends Disposable implements IWorkbenchContributi const detail = new MarkdownString('', { supportHtml: true }); detail.appendMarkdown(localize('confirmOpenDetail2', "This will access {0}.\n\n", `[${uriLabel}](${url.toString()})`)); - detail.appendMarkdown(localize('confirmOpenDetail3', "If you did not initiate this request, it may represent an attempted attack on your system. Unless you took an explicit action to initiate this request, you should press 'Cancel'")); + detail.appendMarkdown(localize('confirmOpenDetail3', "If you did not initiate this request, it may represent an attempted attack on your system. Unless you took an explicit action to initiate this request, you should press 'No'")); let message: string; switch (promptType) { @@ -142,19 +132,18 @@ export class PromptUrlHandler extends Disposable implements IWorkbenchContributi message = localize('confirmInstallPrompt', "An external application wants to create a prompt file with content from a URL. Do you want to continue by selecting a destination folder and name?"); break; case PromptsType.instructions: - message = localize('confirmInstallInstructions', "An external application wants to create an instructions file with content from a URL."); + message = localize('confirmInstallInstructions', "An external application wants to create an instructions file with content from a URL. Do you want to continue by selecting a destination folder and name?"); break; default: - message = localize('confirmInstallMode', "An external application wants to create a chat mode with content from a URL."); + message = localize('confirmInstallMode', "An external application wants to create a chat mode with content from a URL. Do you want to continue by selecting a destination folder and name?"); break; } - const { confirmed, checkboxChecked } = await this.dialogService.confirm({ + const { confirmed } = await this.dialogService.confirm({ type: 'warning', primaryButton: localize({ key: 'yesButton', comment: ['&& denotes a mnemonic'] }, "&&Yes"), cancelButton: localize('noButton', "No"), message, - checkbox: { label: localize('confirmOpenDoNotAskAgain', "Allow creating a prompt file without asking from '{0}'", location) }, custom: { markdownDetails: [{ markdown: detail @@ -162,9 +151,6 @@ export class PromptUrlHandler extends Disposable implements IWorkbenchContributi } }); - if (checkboxChecked) { - this.storageService.store(key, true, StorageScope.APPLICATION, StorageTarget.MACHINE); - } return !confirmed; } From 535bf22ea8a1229295a2c468250319b98a1c8e95 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 4 Jul 2025 12:36:50 +0200 Subject: [PATCH 140/306] move user mcp management to shared process (#254086) --- .../sharedProcess/sharedProcessMain.ts | 14 ++++++++ src/vs/code/node/cliProcessMain.ts | 2 +- .../mcp/common/mcpManagementService.ts | 18 +++++++--- .../platform/mcp/node/mcpManagementService.ts | 34 ++++++++++++++++++ src/vs/server/node/serverServices.ts | 2 +- .../browser/mcpWorkbenchManagementService.ts | 34 ++++++++++++++++++ .../common/mcpWorkbenchManagementService.ts | 19 +++++----- .../mcpWorkbenchManagementService.ts | 36 +++++++++++++++++++ src/vs/workbench/workbench.common.main.ts | 5 +-- src/vs/workbench/workbench.desktop.main.ts | 1 + .../workbench/workbench.web.main.internal.ts | 1 + 11 files changed, 147 insertions(+), 19 deletions(-) create mode 100644 src/vs/platform/mcp/node/mcpManagementService.ts create mode 100644 src/vs/workbench/services/mcp/browser/mcpWorkbenchManagementService.ts create mode 100644 src/vs/workbench/services/mcp/electron-browser/mcpWorkbenchManagementService.ts diff --git a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts index 87bc0f1cb16..ae32ea5de9b 100644 --- a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts @@ -124,6 +124,11 @@ import { IExtensionGalleryManifestService } from '../../../platform/extensionMan import { ExtensionGalleryManifestIPCService } from '../../../platform/extensionManagement/common/extensionGalleryManifestServiceIpc.js'; import { ISharedWebContentExtractorService } from '../../../platform/webContentExtractor/common/webContentExtractor.js'; import { SharedWebContentExtractorService } from '../../../platform/webContentExtractor/node/sharedWebContentExtractorService.js'; +import { McpManagementService } from '../../../platform/mcp/node/mcpManagementService.js'; +import { IMcpGalleryService, IMcpManagementService } from '../../../platform/mcp/common/mcpManagement.js'; +import { IMcpResourceScannerService, McpResourceScannerService } from '../../../platform/mcp/common/mcpResourceScannerService.js'; +import { McpGalleryService } from '../../../platform/mcp/common/mcpGalleryService.js'; +import { McpManagementChannel } from '../../../platform/mcp/common/mcpManagementIpc.js'; class SharedProcessMain extends Disposable implements IClientConnectionFilter { @@ -334,6 +339,11 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { services.set(IAllowedExtensionsService, new SyncDescriptor(AllowedExtensionsService, undefined, true)); services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService, undefined, true)); + // MCP Management + services.set(IMcpGalleryService, new SyncDescriptor(McpGalleryService, undefined, true)); + services.set(IMcpResourceScannerService, new SyncDescriptor(McpResourceScannerService, undefined, true)); + services.set(IMcpManagementService, new SyncDescriptor(McpManagementService, undefined, true)); + // Extension Gallery services.set(IExtensionGalleryManifestService, new ExtensionGalleryManifestIPCService(this.server, productService)); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService, undefined, true)); @@ -388,6 +398,10 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { const channel = new ExtensionManagementChannel(accessor.get(IExtensionManagementService), () => null); this.server.registerChannel('extensions', channel); + // Mcp Management + const mcpManagementChannel = new McpManagementChannel(accessor.get(IMcpManagementService), () => null); + this.server.registerChannel('mcpManagement', mcpManagementChannel); + // Language Packs const languagePacksChannel = ProxyChannel.fromService(accessor.get(ILanguagePackService), this._store); this.server.registerChannel('languagePacks', languagePacksChannel); diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 6c569ca4c8a..74a137b05b1 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -69,7 +69,7 @@ import { McpManagementCli } from '../../platform/mcp/common/mcpManagementCli.js' import { IExtensionGalleryManifestService } from '../../platform/extensionManagement/common/extensionGalleryManifest.js'; import { ExtensionGalleryManifestService } from '../../platform/extensionManagement/common/extensionGalleryManifestService.js'; import { IMcpGalleryService, IMcpManagementService } from '../../platform/mcp/common/mcpManagement.js'; -import { McpManagementService } from '../../platform/mcp/common/mcpManagementService.js'; +import { McpManagementService } from '../../platform/mcp/node/mcpManagementService.js'; import { IMcpResourceScannerService, McpResourceScannerService } from '../../platform/mcp/common/mcpResourceScannerService.js'; import { McpGalleryService } from '../../platform/mcp/common/mcpGalleryService.js'; diff --git a/src/vs/platform/mcp/common/mcpManagementService.ts b/src/vs/platform/mcp/common/mcpManagementService.ts index 566c43ead1e..84466d3893d 100644 --- a/src/vs/platform/mcp/common/mcpManagementService.ts +++ b/src/vs/platform/mcp/common/mcpManagementService.ts @@ -386,11 +386,12 @@ export abstract class AbstractMcpResourceManagementService extends Disposable im abstract installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise; abstract updateMetadata(local: ILocalMcpServer, server: IGalleryMcpServer, profileLocation: URI): Promise; protected abstract getLocalServerInfo(name: string, mcpServerConfig: IMcpServerConfiguration): Promise; + protected abstract installFromUri(uri: URI, options?: Omit): Promise; } export class McpUserResourceManagementService extends AbstractMcpResourceManagementService implements IMcpManagementService { - private readonly mcpLocation: URI; + protected readonly mcpLocation: URI; constructor( mcpResource: URI, @@ -498,13 +499,18 @@ export class McpUserResourceManagementService extends AbstractMcpResourceManagem return storedMcpServerInfo; } - private getLocation(name: string, version?: string): URI { + protected getLocation(name: string, version?: string): URI { name = name.replace('/', '.'); return this.uriIdentityService.extUri.joinPath(this.mcpLocation, version ? `${name}-${version}` : name); } + protected override installFromUri(uri: URI, options?: Omit): Promise { + throw new Error('Method not supported.'); + } + } + export class McpManagementService extends Disposable implements IMcpManagementService { readonly _serviceBrand: undefined; @@ -528,7 +534,7 @@ export class McpManagementService extends Disposable implements IMcpManagementSe constructor( @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, ) { super(); } @@ -537,7 +543,7 @@ export class McpManagementService extends Disposable implements IMcpManagementSe let mcpResourceManagementService = this.mcpResourceManagementServices.get(mcpResource); if (!mcpResourceManagementService) { const disposables = new DisposableStore(); - const service = disposables.add(this.instantiationService.createInstance(McpUserResourceManagementService, mcpResource)); + const service = disposables.add(this.createMcpResourceManagementService(mcpResource)); disposables.add(service.onInstallMcpServer(e => this._onInstallMcpServer.fire(e))); disposables.add(service.onDidInstallMcpServers(e => this._onDidInstallMcpServers.fire(e))); disposables.add(service.onDidUpdateMcpServers(e => this._onDidUpdateMcpServers.fire(e))); @@ -578,4 +584,8 @@ export class McpManagementService extends Disposable implements IMcpManagementSe super.dispose(); } + protected createMcpResourceManagementService(mcpResource: URI): McpUserResourceManagementService { + return this.instantiationService.createInstance(McpUserResourceManagementService, mcpResource); + } + } diff --git a/src/vs/platform/mcp/node/mcpManagementService.ts b/src/vs/platform/mcp/node/mcpManagementService.ts new file mode 100644 index 00000000000..13a417d03b2 --- /dev/null +++ b/src/vs/platform/mcp/node/mcpManagementService.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../base/common/uri.js'; +import { IEnvironmentService } from '../../environment/common/environment.js'; +import { IFileService } from '../../files/common/files.js'; +import { ILogService } from '../../log/common/log.js'; +import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; +import { IMcpGalleryService, IMcpManagementService } from '../common/mcpManagement.js'; +import { McpUserResourceManagementService as CommonMcpUserResourceManagementService, McpManagementService as CommonMcpManagementService } from '../common/mcpManagementService.js'; +import { IMcpResourceScannerService } from '../common/mcpResourceScannerService.js'; + + +export class McpUserResourceManagementService extends CommonMcpUserResourceManagementService { + constructor( + mcpResource: URI, + @IMcpGalleryService mcpGalleryService: IMcpGalleryService, + @IFileService fileService: IFileService, + @IUriIdentityService uriIdentityService: IUriIdentityService, + @ILogService logService: ILogService, + @IMcpResourceScannerService mcpResourceScannerService: IMcpResourceScannerService, + @IEnvironmentService environmentService: IEnvironmentService + ) { + super(mcpResource, mcpGalleryService, fileService, uriIdentityService, logService, mcpResourceScannerService, environmentService); + } +} + +export class McpManagementService extends CommonMcpManagementService implements IMcpManagementService { + protected override createMcpResourceManagementService(mcpResource: URI): McpUserResourceManagementService { + return this.instantiationService.createInstance(McpUserResourceManagementService, mcpResource); + } +} diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index bdb0dcb629f..c4f5bc2ce92 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -86,7 +86,7 @@ import { NativeMcpDiscoveryHelperService } from '../../platform/mcp/node/nativeM import { IExtensionGalleryManifestService } from '../../platform/extensionManagement/common/extensionGalleryManifest.js'; import { ExtensionGalleryManifestIPCService } from '../../platform/extensionManagement/common/extensionGalleryManifestServiceIpc.js'; import { IMcpGalleryService, IMcpManagementService } from '../../platform/mcp/common/mcpManagement.js'; -import { McpManagementService } from '../../platform/mcp/common/mcpManagementService.js'; +import { McpManagementService } from '../../platform/mcp/node/mcpManagementService.js'; import { McpGalleryService } from '../../platform/mcp/common/mcpGalleryService.js'; import { IMcpResourceScannerService, McpResourceScannerService } from '../../platform/mcp/common/mcpResourceScannerService.js'; import { McpManagementChannel } from '../../platform/mcp/common/mcpManagementIpc.js'; diff --git a/src/vs/workbench/services/mcp/browser/mcpWorkbenchManagementService.ts b/src/vs/workbench/services/mcp/browser/mcpWorkbenchManagementService.ts new file mode 100644 index 00000000000..1b7256ce155 --- /dev/null +++ b/src/vs/workbench/services/mcp/browser/mcpWorkbenchManagementService.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +import { IRemoteAgentService } from '../../remote/common/remoteAgentService.js'; +import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; +import { IRemoteUserDataProfilesService } from '../../userDataProfile/common/remoteUserDataProfiles.js'; +import { WorkbenchMcpManagementService as BaseWorkbenchMcpManagementService, IWorkbenchMcpManagementService } from '../common/mcpWorkbenchManagementService.js'; +import { McpManagementService } from '../../../../platform/mcp/common/mcpManagementService.js'; + +export class WorkbenchMcpManagementService extends BaseWorkbenchMcpManagementService { + + constructor( + @IUserDataProfileService userDataProfileService: IUserDataProfileService, + @IUriIdentityService uriIdentityService: IUriIdentityService, + @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, + @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @IRemoteUserDataProfilesService remoteUserDataProfilesService: IRemoteUserDataProfilesService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + const mMcpManagementService = instantiationService.createInstance(McpManagementService); + super(mMcpManagementService, userDataProfileService, uriIdentityService, workspaceContextService, remoteAgentService, userDataProfilesService, remoteUserDataProfilesService, instantiationService); + this._register(mMcpManagementService); + } +} + +registerSingleton(IWorkbenchMcpManagementService, WorkbenchMcpManagementService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts b/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts index d01429d53c0..f70a1ba04c5 100644 --- a/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts +++ b/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts @@ -5,9 +5,8 @@ import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; import { ILocalMcpServer, IMcpManagementService, IGalleryMcpServer, InstallOptions, InstallMcpServerEvent, UninstallMcpServerEvent, DidUninstallMcpServerEvent, InstallMcpServerResult, IInstallableMcpServer, IMcpGalleryService, UninstallOptions } from '../../../../platform/mcp/common/mcpManagement.js'; -import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IInstantiationService, refineServiceDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js'; -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { IMcpResourceScannerService, McpResourceTarget } from '../../../../platform/mcp/common/mcpResourceScannerService.js'; import { isWorkspaceFolder, IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from '../../../../platform/workspace/common/workspace.js'; @@ -43,7 +42,9 @@ export interface IWorkbenchMcpServerInstallResult extends InstallMcpServerResult readonly local?: IWorkbenchLocalMcpServer; } +export const IWorkbenchMcpManagementService = refineServiceDecorator(IMcpManagementService); export interface IWorkbenchMcpManagementService extends IMcpManagementService { + readonly _serviceBrand: undefined; readonly onDidInstallMcpServers: Event; @@ -55,12 +56,10 @@ export interface IWorkbenchMcpManagementService extends IMcpManagementService { readonly onDidChangeProfile: Event; getInstalled(): Promise; - install(server: IInstallableMcpServer, options?: IWorkbencMcpServerInstallOptions): Promise; + install(server: IInstallableMcpServer | URI, options?: IWorkbencMcpServerInstallOptions): Promise; } -export const IWorkbenchMcpManagementService = createDecorator('workbenchMcpManagementService'); - -class WorkbenchMcpManagementService extends Disposable implements IWorkbenchMcpManagementService { +export class WorkbenchMcpManagementService extends Disposable implements IWorkbenchMcpManagementService { readonly _serviceBrand: undefined; @@ -101,10 +100,10 @@ class WorkbenchMcpManagementService extends Disposable implements IWorkbenchMcpM private readonly remoteMcpManagementService: IMcpManagementService | undefined; constructor( + private readonly mcpManagementService: IMcpManagementService, @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IMcpManagementService private readonly mcpManagementService: IMcpManagementService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IRemoteUserDataProfilesService private readonly remoteUserDataProfilesService: IRemoteUserDataProfilesService, @@ -379,6 +378,10 @@ class WorkspaceMcpResourceManagementService extends AbstractMcpResourceManagemen throw new Error('Not supported'); } + protected override installFromUri(): Promise { + throw new Error('Not supported'); + } + protected override async getLocalServerInfo(): Promise { return undefined; } @@ -568,5 +571,3 @@ class WorkspaceMcpManagementService extends Disposable implements IMcpManagement super.dispose(); } } - -registerSingleton(IWorkbenchMcpManagementService, WorkbenchMcpManagementService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/mcp/electron-browser/mcpWorkbenchManagementService.ts b/src/vs/workbench/services/mcp/electron-browser/mcpWorkbenchManagementService.ts new file mode 100644 index 00000000000..6989b92cfd7 --- /dev/null +++ b/src/vs/workbench/services/mcp/electron-browser/mcpWorkbenchManagementService.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +import { IRemoteAgentService } from '../../remote/common/remoteAgentService.js'; +import { McpManagementChannelClient } from '../../../../platform/mcp/common/mcpManagementIpc.js'; +import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; +import { IRemoteUserDataProfilesService } from '../../userDataProfile/common/remoteUserDataProfiles.js'; +import { WorkbenchMcpManagementService as BaseWorkbenchMcpManagementService, IWorkbenchMcpManagementService } from '../common/mcpWorkbenchManagementService.js'; +import { ISharedProcessService } from '../../../../platform/ipc/electron-browser/services.js'; + +export class WorkbenchMcpManagementService extends BaseWorkbenchMcpManagementService { + + constructor( + @IUserDataProfileService userDataProfileService: IUserDataProfileService, + @IUriIdentityService uriIdentityService: IUriIdentityService, + @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, + @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @IRemoteUserDataProfilesService remoteUserDataProfilesService: IRemoteUserDataProfilesService, + @IInstantiationService instantiationService: IInstantiationService, + @ISharedProcessService sharedProcessService: ISharedProcessService, + ) { + const mcpManagementService = new McpManagementChannelClient(sharedProcessService.getChannel('mcpManagement')); + super(mcpManagementService, userDataProfileService, uriIdentityService, workspaceContextService, remoteAgentService, userDataProfilesService, remoteUserDataProfilesService, instantiationService); + this._register(mcpManagementService); + } +} + +registerSingleton(IWorkbenchMcpManagementService, WorkbenchMcpManagementService, InstantiationType.Delayed); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 750087f2e72..924f024d86e 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -83,7 +83,6 @@ import './services/notebook/common/notebookDocumentService.js'; import './services/commands/common/commandService.js'; import './services/themes/browser/workbenchThemeService.js'; import './services/label/common/labelService.js'; -import './services/mcp/common/mcpWorkbenchManagementService.js'; import './services/extensions/common/extensionManifestPropertiesService.js'; import './services/extensionManagement/common/extensionGalleryService.js'; import './services/extensionManagement/browser/extensionEnablementService.js'; @@ -157,9 +156,8 @@ import { ExtensionStorageService, IExtensionStorageService } from '../platform/e import { IUserDataSyncLogService } from '../platform/userDataSync/common/userDataSync.js'; import { UserDataSyncLogService } from '../platform/userDataSync/common/userDataSyncLog.js'; import { AllowedExtensionsService } from '../platform/extensionManagement/common/allowedExtensionsService.js'; -import { IMcpGalleryService, IMcpManagementService } from '../platform/mcp/common/mcpManagement.js'; +import { IMcpGalleryService } from '../platform/mcp/common/mcpManagement.js'; import { McpGalleryService } from '../platform/mcp/common/mcpGalleryService.js'; -import { McpManagementService } from '../platform/mcp/common/mcpManagementService.js'; registerSingleton(IUserDataSyncLogService, UserDataSyncLogService, InstantiationType.Delayed); registerSingleton(IAllowedExtensionsService, AllowedExtensionsService, InstantiationType.Delayed); @@ -176,7 +174,6 @@ registerSingleton(ITextResourceConfigurationService, TextResourceConfigurationSe registerSingleton(IDownloadService, DownloadService, InstantiationType.Delayed); registerSingleton(IOpenerService, OpenerService, InstantiationType.Delayed); registerSingleton(IMcpGalleryService, McpGalleryService, InstantiationType.Delayed); -registerSingleton(IMcpManagementService, McpManagementService, InstantiationType.Delayed); //#endregion diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index c9df215ea42..af84852ebd2 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -53,6 +53,7 @@ import './services/keybinding/electron-browser/nativeKeyboardLayout.js'; import './services/path/electron-browser/pathService.js'; import './services/themes/electron-browser/nativeHostColorSchemeService.js'; import './services/extensionManagement/electron-browser/extensionManagementService.js'; +import './services/mcp/electron-browser/mcpWorkbenchManagementService.js'; import './services/encryption/electron-browser/encryptionService.js'; import './services/browserElements/electron-browser/browserElementsService.js'; import './services/secrets/electron-browser/secretStorageService.js'; diff --git a/src/vs/workbench/workbench.web.main.internal.ts b/src/vs/workbench/workbench.web.main.internal.ts index 460c888e390..eae3a102dee 100644 --- a/src/vs/workbench/workbench.web.main.internal.ts +++ b/src/vs/workbench/workbench.web.main.internal.ts @@ -43,6 +43,7 @@ import './services/extensionManagement/browser/extensionsProfileScannerService.j import './services/extensions/browser/extensionsScannerService.js'; import './services/extensionManagement/browser/webExtensionsScannerService.js'; import './services/extensionManagement/common/extensionManagementServerService.js'; +import './services/mcp/browser/mcpWorkbenchManagementService.js'; import './services/extensionManagement/browser/extensionGalleryManifestService.js'; import './services/telemetry/browser/telemetryService.js'; import './services/url/browser/urlService.js'; From 69005e100266f9e8569aca6f9abea6f70b9bdcdd Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 4 Jul 2025 11:16:38 +0000 Subject: [PATCH 141/306] Engineering - improve node_modules cache action (#254076) --- .github/workflows/pr-node-modules.yml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.github/workflows/pr-node-modules.yml b/.github/workflows/pr-node-modules.yml index a35033197b9..61682ee0ace 100644 --- a/.github/workflows/pr-node-modules.yml +++ b/.github/workflows/pr-node-modules.yml @@ -35,10 +35,6 @@ jobs: path: .build/node_modules_cache key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" - - name: Extract node_modules cache - if: steps.cache-node-modules.outputs.cache-hit == 'true' - run: tar -xzf .build/node_modules_cache/cache.tgz - - name: Install build dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' working-directory: build @@ -113,10 +109,6 @@ jobs: path: .build/node_modules_cache key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" - - name: Extract node_modules cache - if: steps.cache-node-modules.outputs.cache-hit == 'true' - run: tar -xzf .build/node_modules_cache/cache.tgz - - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' run: | @@ -183,11 +175,6 @@ jobs: path: .build/node_modules_cache key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" - - 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 - - name: Install dependencies if: steps.node-modules-cache.outputs.cache-hit != 'true' shell: pwsh From 3319d505f35a909e6b568c353ebed385b34da272 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 4 Jul 2025 13:37:19 +0200 Subject: [PATCH 142/306] Stop showing secondary sidebar if its empty (fix #253855) (#254087) * Stop showing secondary sidebar if its empty (fix #253855) * . * . --- src/vs/workbench/browser/layout.ts | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index ec7757a570e..2c3c925cb14 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -15,7 +15,7 @@ import { PanelPart } from './parts/panel/panelPart.js'; import { Position, Parts, PartOpensMaximizedOptions, IWorkbenchLayoutService, positionFromString, positionToString, partOpensMaximizedFromString, PanelAlignment, ActivityBarPosition, LayoutSettings, MULTI_WINDOW_PARTS, SINGLE_WINDOW_PARTS, ZenModeSettings, EditorTabsMode, EditorActionsLocation, shouldShowCustomTitleBar, isHorizontal, isMultiWindowPart } from '../services/layout/browser/layoutService.js'; import { isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState } from '../../platform/workspace/common/workspace.js'; import { IStorageService, StorageScope, StorageTarget } from '../../platform/storage/common/storage.js'; -import { IConfigurationChangeEvent, IConfigurationService } from '../../platform/configuration/common/configuration.js'; +import { IConfigurationChangeEvent, IConfigurationService, isConfigured } from '../../platform/configuration/common/configuration.js'; import { ITitleService } from '../services/title/browser/titleService.js'; import { ServicesAccessor } from '../../platform/instantiation/common/instantiation.js'; import { StartupKind, ILifecycleService } from '../services/lifecycle/common/lifecycle.js'; @@ -633,7 +633,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private initLayoutState(lifecycleService: ILifecycleService, fileService: IFileService, coreExperimentationService: ICoreExperimentationService): void { this._mainContainerDimension = getClientArea(this.parent, DEFAULT_WINDOW_DIMENSIONS); // running with fallback to ensure no error is thrown (https://github.com/microsoft/vscode/issues/240242) - this.stateModel = new LayoutStateModel(this.storageService, this.configurationService, this.contextService, coreExperimentationService, this.environmentService); + this.stateModel = new LayoutStateModel(this.storageService, this.configurationService, this.contextService, coreExperimentationService, this.environmentService, this.viewDescriptorService); this.stateModel.load({ mainContainerDimension: this._mainContainerDimension, resetLayout: Boolean(this.layoutOptions?.resetLayout) @@ -2805,7 +2805,8 @@ class LayoutStateModel extends Disposable { private readonly configurationService: IConfigurationService, private readonly contextService: IWorkspaceContextService, private readonly coreExperimentationService: ICoreExperimentationService, - private readonly environmentService: IBrowserWorkbenchEnvironmentService + private readonly environmentService: IBrowserWorkbenchEnvironmentService, + private readonly viewDescriptorService: IViewDescriptorService ) { super(); @@ -2868,9 +2869,28 @@ class LayoutStateModel extends Disposable { LayoutStateKeys.SIDEBAR_HIDDEN.defaultValue = workbenchState === WorkbenchState.EMPTY; LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, mainContainerDimension.width / 4); LayoutStateKeys.AUXILIARYBAR_HIDDEN.defaultValue = (() => { + + // TODO@bpasero: lots of hacks here to not force open the auxiliary sidebar + // when no Chat view is present within: + // - revisit this when/if the default value of workbench.secondarySideBar.defaultVisibility changes + // - revisit this when Chat is available in serverless web + // - drop the need to probe for chat.setupContext + // - drop the need to probe for view location of workbench.panel.chat.view.copilot const configuration = this.configurationService.inspect(WorkbenchLayoutSettings.AUXILIARYBAR_DEFAULT_VISIBILITY); - if (configuration.defaultValue !== 'hidden' && isWeb && !this.environmentService.remoteAuthority) { - return true; // TODO@bpasero revisit this when Chat is available in serverless web + if (configuration.defaultValue !== 'hidden' && !isConfigured(configuration)) { + if (isWeb && !this.environmentService.remoteAuthority) { + return true; // Chat view is not enabled + } + + const context = this.storageService.getObject<{ hidden?: boolean; disabled?: boolean; installed?: boolean }>('chat.setupContext', StorageScope.PROFILE); + if (context && ((context.installed && context.disabled) || (!context.installed && context.hidden))) { + return true; // Chat view is hidden by user choice + } + + const location = this.viewDescriptorService.getViewLocationById('workbench.panel.chat.view.copilot'); + if (location === ViewContainerLocation.Sidebar || location === ViewContainerLocation.Panel) { + return true; // Chat view is not located in the auxiliary bar + } } switch (this.configurationService.getValue(WorkbenchLayoutSettings.AUXILIARYBAR_DEFAULT_VISIBILITY)) { From 2c76ee1b47c5e620ebe1d98cd4ebc1ab002c769b Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 4 Jul 2025 14:48:38 +0200 Subject: [PATCH 143/306] force auto update --- .../common/extensionGalleryService.ts | 6 +++-- .../common/extensionManagement.ts | 1 + .../common/unsupportedExtensionsMigration.ts | 24 +++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index fc54caa8a08..d23e8d441c6 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -540,6 +540,7 @@ interface IRawExtensionsControlManifest { additionalInfo?: string; }>; search?: ISearchPrefferedResults[]; + autoUpdate?: IStringDictionary; } export abstract class AbstractExtensionGalleryService implements IExtensionGalleryService { @@ -1793,7 +1794,7 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle } if (!this.extensionsControlUrl) { - return { malicious: [], deprecated: {}, search: [] }; + return { malicious: [], deprecated: {}, search: [], autoUpdate: {} }; } const context = await this.requestService.request({ @@ -1810,6 +1811,7 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle const malicious: Array = []; const deprecated: IStringDictionary = {}; const search: ISearchPrefferedResults[] = []; + const autoUpdate: IStringDictionary = result?.autoUpdate ?? {}; if (result) { for (const id of result.malicious) { if (!isString(id)) { @@ -1847,7 +1849,7 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle } } - return { malicious, deprecated, search }; + return { malicious, deprecated, search, autoUpdate }; } } diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index edcd714f880..2f8d1e83b99 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -357,6 +357,7 @@ export interface IExtensionsControlManifest { readonly malicious: ReadonlyArray; readonly deprecated: IStringDictionary; readonly search: ISearchPrefferedResults[]; + readonly autoUpdate?: IStringDictionary; } export const enum InstallOperation { diff --git a/src/vs/platform/extensionManagement/common/unsupportedExtensionsMigration.ts b/src/vs/platform/extensionManagement/common/unsupportedExtensionsMigration.ts index de38da03b0a..1e75ad85b3d 100644 --- a/src/vs/platform/extensionManagement/common/unsupportedExtensionsMigration.ts +++ b/src/vs/platform/extensionManagement/common/unsupportedExtensionsMigration.ts @@ -9,6 +9,7 @@ import { areSameExtensions, getExtensionId } from './extensionManagementUtil.js' import { IExtensionStorageService } from './extensionStorage.js'; import { ExtensionType } from '../../extensions/common/extensions.js'; import { ILogService } from '../../log/common/log.js'; +import * as semver from '../../../base/common/semver/semver.js'; /** * Migrates the installed unsupported nightly extension to a supported pre-release extension. It includes following: @@ -69,6 +70,29 @@ export async function migrateUnsupportedExtensions(extensionManagementService: I logService.error(error); } } + + if (extensionsControlManifest.autoUpdate) { + for (const [extensionId, version] of Object.entries(extensionsControlManifest.autoUpdate)) { + try { + const extensionToAutoUpdate = installed.find(i => areSameExtensions(i.identifier, { id: extensionId }) && semver.lte(i.manifest.version, version)); + if (!extensionToAutoUpdate) { + continue; + } + + const gallery = (await galleryService.getExtensions([{ id: extensionId, preRelease: extensionToAutoUpdate.preRelease }], { targetPlatform: await extensionManagementService.getTargetPlatform(), compatible: true }, CancellationToken.None))[0]; + if (!gallery) { + logService.info(`Skipping updating '${extensionToAutoUpdate.identifier.id}' extension because, the compatible target '${extensionId}' extension is not found`); + continue; + } + + await extensionManagementService.installFromGallery(gallery, { installPreReleaseVersion: extensionToAutoUpdate.preRelease, isMachineScoped: extensionToAutoUpdate.isMachineScoped, operation: InstallOperation.Update, context: { [EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT]: true } }); + logService.info(`Autoupdated '${extensionToAutoUpdate.identifier.id}' extension to '${gallery.version}' extension.`); + } catch (error) { + logService.error(error); + } + } + } + } catch (error) { logService.error(error); } From 48301d6c9e42c8b833ff1646d134ffe0d250647e Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 4 Jul 2025 15:28:09 +0200 Subject: [PATCH 144/306] Fixes https://github.com/microsoft/vscode/issues/254122 (#254123) --- src/vs/editor/common/core/edits/stringEdit.ts | 14 ++++++++----- .../browser/model/inlineCompletionsSource.ts | 21 +++++++++++++++++-- .../browser/model/inlineSuggestionItem.ts | 12 ++++++++++- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/common/core/edits/stringEdit.ts b/src/vs/editor/common/core/edits/stringEdit.ts index ba29bdfd6d8..db78f3bf8b6 100644 --- a/src/vs/editor/common/core/edits/stringEdit.ts +++ b/src/vs/editor/common/core/edits/stringEdit.ts @@ -105,11 +105,7 @@ export abstract class BaseStringEdit = BaseSt } public toJson(): ISerializedStringEdit { - return this.replacements.map(e => ({ - txt: e.newText, - pos: e.replaceRange.start, - len: e.replaceRange.length, - })); + return this.replacements.map(e => e.toJson()); } public isNeutralOn(text: string): boolean { @@ -241,6 +237,14 @@ export abstract class BaseStringReplacement = public toEdit(): StringEdit { return new StringEdit([this]); } + + public toJson(): ISerializedStringReplacement { + return ({ + txt: this.newText, + pos: this.replaceRange.start, + len: this.replaceRange.length, + }); + } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index 4d18532982a..0a318d79ff7 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { compareUndefinedSmallest, numberComparator } from '../../../../../base/common/arrays.js'; +import { booleanComparator, compareBy, compareUndefinedSmallest, numberComparator } from '../../../../../base/common/arrays.js'; import { findLastMax } from '../../../../../base/common/arraysFind.js'; import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { equalsIfDefined, itemEquals } from '../../../../../base/common/equals.js'; @@ -404,7 +404,7 @@ class InlineCompletionsState extends Disposable { // Otherwise: prefer inline completion if there is a visible one : updatedSuggestions.some(i => !i.isInlineEdit && i.isVisible(textModel, cursorPosition)); - const updatedItems: InlineSuggestionItem[] = []; + let updatedItems: InlineSuggestionItem[] = []; for (const i of updatedSuggestions) { const oldItem = this._findByHash(i.hash); let item; @@ -418,6 +418,10 @@ class InlineCompletionsState extends Disposable { updatedItems.push(item); } } + + updatedItems.sort(compareBy(i => i.showInlineEditMenu, booleanComparator)); + updatedItems = distinctByKey(updatedItems, i => i.semanticId); + return new InlineCompletionsState(updatedItems, request); } @@ -426,6 +430,19 @@ class InlineCompletionsState extends Disposable { } } +/** Keeps the first item in case of duplicates. */ +function distinctByKey(items: T[], key: (item: T) => unknown): T[] { + const seen = new Set(); + return items.filter(item => { + const k = key(item); + if (seen.has(k)) { + return false; + } + seen.add(k); + return true; + }); +} + function moveToFront(item: T, items: T[]): T[] { const index = items.indexOf(item); if (index > -1) { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts index 00035fb46f7..ba2a25bd559 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts @@ -196,17 +196,19 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { const insertText = data.insertText.replace(/\r\n|\r|\n/g, textModel.getEOL()); const edit = reshapeInlineCompletion(new StringReplacement(transformer.getOffsetRange(data.range), insertText), textModel); + const trimmedEdit = edit.removeCommonSuffixAndPrefix(textModel.getValue()); const textEdit = transformer.getSingleTextEdit(edit); const displayLocation = data.displayLocation ? InlineSuggestDisplayLocation.create(data.displayLocation, textModel) : undefined; - return new InlineCompletionItem(edit, textEdit, textEdit.range, data.snippetInfo, data.additionalTextEdits, data, identity, displayLocation); + return new InlineCompletionItem(edit, trimmedEdit, textEdit, textEdit.range, data.snippetInfo, data.additionalTextEdits, data, identity, displayLocation); } public readonly isInlineEdit = false; private constructor( private readonly _edit: StringReplacement, + private readonly _trimmedEdit: StringReplacement, private readonly _textEdit: TextReplacement, private readonly _originalRange: Range, public readonly snippetInfo: SnippetInfo | undefined, @@ -219,11 +221,16 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { super(data, identity, displayLocation); } + override get hash(): string { + return JSON.stringify(this._trimmedEdit.toJson()); + } + override getSingleTextEdit(): TextReplacement { return this._textEdit; } override withIdentity(identity: InlineSuggestionIdentity): InlineCompletionItem { return new InlineCompletionItem( this._edit, + this._trimmedEdit, this._textEdit, this._originalRange, this.snippetInfo, @@ -251,8 +258,11 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { } } + const trimmedEdit = newEdit.removeCommonSuffixAndPrefix(textModel.getValue()); + return new InlineCompletionItem( newEdit, + trimmedEdit, newTextEdit, this._originalRange, this.snippetInfo, From 6c0cbe007a3a9a658d8ee8782aab2efb0437eecc Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 4 Jul 2025 16:16:57 +0200 Subject: [PATCH 145/306] chat - disable custom welcome content for now (workaround endless loop) (#254129) --- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index caca8418b45..7cf409db576 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -747,7 +747,8 @@ export class ChatWidget extends Disposable implements IChatWidget { ); let welcomeContent: IChatViewWelcomeContent; - if ((startupExpValue === StartupExperimentGroup.MaximizedChat + const enabled = false; + if (enabled && (startupExpValue === StartupExperimentGroup.MaximizedChat || startupExpValue === StartupExperimentGroup.SplitEmptyEditorChat || startupExpValue === StartupExperimentGroup.SplitWelcomeChat || expIsActive) && this.contextKeyService.contextMatchesRules(chatSetupTriggerContext)) { From 2380d75ac839446a175f54b4ce8b8d8936d83afb Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 4 Jul 2025 14:17:51 +0000 Subject: [PATCH 146/306] Engineering - add job the maintain the node_modules cache for the Compile stage and builtin extensions cache (#254126) * Engineering - add job the maintain the node_modules cache for the Compile stage and builtin extensions cache * Fix typo --- .github/workflows/pr-node-modules.yml | 73 +++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/.github/workflows/pr-node-modules.yml b/.github/workflows/pr-node-modules.yml index 61682ee0ace..08bf54f22c2 100644 --- a/.github/workflows/pr-node-modules.yml +++ b/.github/workflows/pr-node-modules.yml @@ -8,6 +8,79 @@ on: permissions: {} jobs: + compile: + name: Compile + runs-on: [ self-hosted, 1ES.Pool=1es-vscode-oss-ubuntu-22.04-x64 ] + steps: + - name: Checkout microsoft/vscode + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + env: + NODEJS_ORG_MIRROR: https://github.com/joaomoreno/node-mirror/releases/download + + - name: Prepare node_modules cache key + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js compile $(node -p process.arch) > .build/packagelockhash + + - name: Restore node_modules cache + id: cache-node-modules + uses: actions/cache@v4 + with: + path: .build/node_modules_cache + key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" + + - name: Install build tools + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: sudo apt update -y && sudo apt install -y build-essential pkg-config libx11-dev libx11-xcb-dev libxkbfile-dev libnotify-bin libkrb5-dev + + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + + for i in {1..5}; do # try 5 times + npm ci && break + if [ $i -eq 5 ]; then + echo "Npm install failed too many times" >&2 + exit 1 + fi + echo "Npm install failed $i, trying again..." + done + env: + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create node_modules archive + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + + - name: Prepare built-in extensions cache key + run: | + set -e + mkdir -p .build + node build/azure-pipelines/common/computeBuiltInDepsCacheKey.js > .build/builtindepshash + + - name: Restore built-in extensions cache + id: cache-builtin-extensions + uses: actions/cache@v4 + with: + path: .build/builtInExtensions + key: "builtin-extensions-${{ hashFiles('.build/builtindepshash') }}" + + - name: Download built-in extensions + if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' + run: node build/lib/builtInExtensions.js + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + linux: name: Linux runs-on: [ self-hosted, 1ES.Pool=1es-vscode-oss-ubuntu-22.04-x64 ] From 449fd6e036fc7d65b8f2e8ef86ae843a41b8d5ec Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 4 Jul 2025 16:28:45 +0200 Subject: [PATCH 147/306] chat models in prompts: qualify name with family --- src/vs/workbench/api/common/extHostLanguageModels.ts | 3 +-- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 6 +++--- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 8 ++++---- .../chat/browser/modelPicker/modelPickerActionItem.ts | 3 ++- src/vs/workbench/contrib/chat/common/languageModels.ts | 10 +++++++++- .../languageProviders/promptHeaderAutocompletion.ts | 2 +- .../promptHeaderDiagnosticsProvider.ts | 2 +- .../languageProviders/promptHeaderHovers.ts | 4 ++-- .../contrib/chat/test/common/languageModels.test.ts | 4 ++-- 9 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index f2fcab92ffc..1ff04676524 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -26,7 +26,6 @@ import * as typeConvert from './extHostTypeConverters.js'; import * as extHostTypes from './extHostTypes.js'; import { SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js'; import { VSBuffer } from '../../../base/common/buffer.js'; -import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../contrib/chat/common/modelPicker/modelPickerWidget.js'; export interface IExtHostLanguageModels extends ExtHostLanguageModels { } @@ -200,7 +199,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { targetExtensions: metadata.extensions, isDefault: metadata.isDefault, isUserSelectable: metadata.isUserSelectable, - modelPickerCategory: metadata.category ?? DEFAULT_MODEL_PICKER_CATEGORY, + modelPickerCategory: metadata.category, capabilities: metadata.capabilities, }); diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 061f59eb695..4a687c90b2f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -453,7 +453,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const mode = this._currentModeObservable.read(r); const model = mode.model?.read(r); if (model) { - this.switchModelByName(model); + this.switchModelByQualifiedName(model); } })); } @@ -518,9 +518,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - public switchModelByName(modelName: string): boolean { + public switchModelByQualifiedName(qualifiedModelName: string): boolean { const models = this.getModels(); - const model = models.find(m => m.metadata.name === modelName); + const model = models.find(m => ILanguageModelChatMetadata.asQualifiedName(m.metadata) === qualifiedModelName); if (model) { this.setCurrentLanguageModel(model); return true; diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index caca8418b45..653abf32684 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1121,9 +1121,9 @@ export class ChatWidget extends Disposable implements IChatWidget { } this.inputPart.setChatMode(this.inlineInputPart.currentModeKind); - const currentModelName = this.inlineInputPart.selectedLanguageModel?.metadata.name; - if (currentModelName) { - this.inputPart.switchModelByName(currentModelName); + const currentModel = this.inlineInputPart.selectedLanguageModel; + if (currentModel) { + this.inputPart.switchModel(currentModel.metadata); } const isInput = this.configurationService.getValue('chat.editRequests') === 'input'; @@ -1957,7 +1957,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } if (model !== undefined) { - this.input.switchModelByName(model); + this.input.switchModelByQualifiedName(model); } } diff --git a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts index bccee94cbf4..2dce470eacf 100644 --- a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts @@ -19,6 +19,7 @@ import { getFlatActionBarActions } from '../../../../../platform/actions/browser import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ChatEntitlement, IChatEntitlementService } from '../../common/chatEntitlementService.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; +import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../common/modelPicker/modelPickerWidget.js'; export interface IModelPickerDelegate { readonly onDidChangeModel: Event; @@ -35,7 +36,7 @@ function modelDelegateToWidgetActionsProvider(delegate: IModelPickerDelegate): I id: model.metadata.id, enabled: true, checked: model.metadata.id === delegate.getCurrentModel()?.metadata.id, - category: model.metadata.modelPickerCategory, + category: model.metadata.modelPickerCategory || DEFAULT_MODEL_PICKER_CATEGORY, class: undefined, description: model.metadata.cost, tooltip: model.metadata.description ?? model.metadata.name, diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 99051b82c75..3cbf23c7263 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -134,7 +134,7 @@ export interface ILanguageModelChatMetadata { readonly isDefault?: boolean; readonly isUserSelectable?: boolean; - readonly modelPickerCategory: { label: string; order: number }; + readonly modelPickerCategory: { label: string; order: number } | undefined; readonly auth?: { readonly providerLabel: string; readonly accountLabel?: string; @@ -151,6 +151,14 @@ export namespace ILanguageModelChatMetadata { const supportsToolsAgent = typeof metadata.capabilities?.agentMode === 'undefined' || metadata.capabilities.agentMode; return supportsToolsAgent && !!metadata.capabilities?.toolCalling; } + + export function asQualifiedName(metadata: ILanguageModelChatMetadata): string { + if (metadata.modelPickerCategory === undefined) { + // in the others category + return `${metadata.name} (${metadata.family})`; + } + return metadata.name; + } } export interface ILanguageModelChatResponse { 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 0e231c27eee..8492d5b2d86 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -228,7 +228,7 @@ export class PromptHeaderAutocompletion extends Disposable implements Completion const metadata = this.languageModelsService.lookupLanguageModel(model); if (metadata && metadata.isUserSelectable !== false) { if (!agentModeOnly || ILanguageModelChatMetadata.suitableForAgentMode(metadata)) { - result.push(metadata.name); + result.push(ILanguageModelChatMetadata.asQualifiedName(metadata)); } } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderDiagnosticsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderDiagnosticsProvider.ts index 256f6340b24..9646fd38b68 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderDiagnosticsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderDiagnosticsProvider.ts @@ -123,7 +123,7 @@ class PromptHeaderDiagnosticsProvider extends ProviderInstanceBase { findModelByName(languageModes: string[], modelName: string): ILanguageModelChatMetadata | undefined { for (const model of languageModes) { const metadata = this.languageModelsService.lookupLanguageModel(model); - if (metadata && metadata.isUserSelectable !== false && metadata.name === modelName) { + if (metadata && metadata.isUserSelectable !== false && ILanguageModelChatMetadata.asQualifiedName(metadata) === modelName) { return metadata; } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderHovers.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderHovers.ts index 032fbb079d0..a8505faeb8e 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderHovers.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderHovers.ts @@ -12,7 +12,7 @@ import { Hover, HoverContext, HoverProvider } from '../../../../../../editor/com import { ITextModel } from '../../../../../../editor/common/model.js'; import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; import { localize } from '../../../../../../nls.js'; -import { ILanguageModelsService } from '../../languageModels.js'; +import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js'; import { ILanguageModelToolsService, ToolSet } from '../../languageModelToolsService.js'; import { InstructionsHeader } from '../parsers/promptHeader/instructionsHeader.js'; import { PromptModelMetadata } from '../parsers/promptHeader/metadata/model.js'; @@ -156,7 +156,7 @@ export class PromptHeaderHoverProvider extends Disposable implements HoverProvid if (modelName) { for (const id of this.languageModelsService.getLanguageModelIds()) { const meta = this.languageModelsService.lookupLanguageModel(id); - if (meta && meta.name === modelName) { + if (meta && ILanguageModelChatMetadata.asQualifiedName(meta) === modelName) { const lines: string[] = []; lines.push(baseMessage + '\n'); lines.push(localize('modelName', '- Name: {0}', meta.name)); diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts index 138d176f95b..8178d3e9274 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts @@ -51,8 +51,8 @@ suite('LanguageModels', function () { name: 'Pretty Name', vendor: 'test-vendor', family: 'test-family', - modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, version: 'test-version', + modelPickerCategory: undefined, id: 'test-id', maxInputTokens: 100, maxOutputTokens: 100, @@ -72,7 +72,7 @@ suite('LanguageModels', function () { vendor: 'test-vendor', family: 'test2-family', version: 'test2-version', - modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, + modelPickerCategory: undefined, id: 'test-id', maxInputTokens: 100, maxOutputTokens: 100, From 005f93e50aa4aa1a333b73cbeff2b4a07ef34a70 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 4 Jul 2025 16:37:48 +0200 Subject: [PATCH 148/306] Fixes https://github.com/microsoft/vscode-internalbacklog/issues/5602 (#254099) * Fixes https://github.com/microsoft/vscode-internalbacklog/issues/5602 * Aligns test --- .../inlineCompletions/browser/model/inlineCompletionsModel.ts | 1 + .../inlineCompletions/test/browser/inlineCompletions.test.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index ad4f401d00a..abb3ac944cb 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -172,6 +172,7 @@ export class InlineCompletionsModel extends Disposable { changeSummary.changeReason = detailedReasons.length > 0 ? detailedReasons[0].getType() : ''; changeSummary.textChange = true; } else if (ctx.didChange(this._forceUpdateExplicitlySignal)) { + changeSummary.preserveCurrentCompletion = true; changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit; } else if (ctx.didChange(this.dontRefetchSignal)) { changeSummary.dontRefetch = true; diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts index 1e4f0f7c934..a8e0d0ea8b0 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts @@ -340,6 +340,7 @@ suite('Inline Completions', () => { test('when accepting word by word', async function () { // The user types the text as suggested and the provider reports a different suggestion. + // Even when triggering explicitly, we want to keep the suggestion. const provider = new MockInlineCompletionsProvider(); await withAsyncTestCodeEditorAndInlineCompletionsModel('', @@ -356,7 +357,7 @@ suite('Inline Completions', () => { await ctx.model.triggerExplicitly(); // reset to provider truth await timeout(10000); - assert.deepStrictEqual(ctx.context.getAndClearViewStates(), (["foo[ baz]"])); + assert.deepStrictEqual(ctx.context.getAndClearViewStates(), ([])); } ); }); From 27ab41b18c4703b6f4895d5830a61beb1cc041f4 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 4 Jul 2025 17:34:58 +0200 Subject: [PATCH 149/306] Initializes fields at declaration when possible (#254149) --- .../observableInternal/changeTracker.ts | 39 + .../base/common/observableInternal/index.ts | 2 +- .../browser/model/inlineCompletionsModel.ts | 943 +++++++++--------- .../browser/model/inlineCompletionsSource.ts | 85 +- 4 files changed, 529 insertions(+), 540 deletions(-) diff --git a/src/vs/base/common/observableInternal/changeTracker.ts b/src/vs/base/common/observableInternal/changeTracker.ts index 3cbddc534a8..b29fb62ce40 100644 --- a/src/vs/base/common/observableInternal/changeTracker.ts +++ b/src/vs/base/common/observableInternal/changeTracker.ts @@ -53,3 +53,42 @@ export function recordChanges>>(getObs: () => TObs): + IChangeTracker<{ [TKey in keyof TObs]: ReturnType } + & { changes: readonly ({ [TKey in keyof TObs]: { key: TKey; change: TObs[TKey]['TChange'] } }[keyof TObs])[] }> { + let obs: TObs | undefined = undefined; + return { + createChangeSummary: (_previousChangeSummary) => { + return { + changes: [], + } as any; + }, + handleChange(ctx, changeSummary) { + if (!obs) { + obs = getObs(); + } + for (const key in obs) { + if (ctx.didChange(obs[key])) { + (changeSummary.changes as any).push({ key, change: ctx.change }); + } + } + return true; + }, + beforeUpdate(reader, changeSummary) { + if (!obs) { + obs = getObs(); + } + for (const key in obs) { + if (key === 'changes') { + throw new BugIndicatingError('property name "changes" is reserved for change tracking'); + } + changeSummary[key] = obs[key].read(reader); + } + } + }; +} diff --git a/src/vs/base/common/observableInternal/index.ts b/src/vs/base/common/observableInternal/index.ts index 635d25d4ec2..d54520780d1 100644 --- a/src/vs/base/common/observableInternal/index.ts +++ b/src/vs/base/common/observableInternal/index.ts @@ -20,7 +20,7 @@ export { signalFromObservable, wasEventTriggeredRecently, } from './utils/utils.js'; export { type DebugOwner } from './debugName.js'; -export { type IChangeContext, type IChangeTracker, recordChanges } from './changeTracker.js'; +export { type IChangeContext, type IChangeTracker, recordChanges, recordChangesLazy } from './changeTracker.js'; export { constObservable } from './observables/constObservable.js'; export { type IObservableSignal, observableSignal } from './observables/observableSignal.js'; export { observableFromEventOpts } from './observables/observableFromEvent.js'; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index abb3ac944cb..4d642e4dafc 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -50,22 +50,37 @@ import { IInlineCompletionsService } from '../../../../browser/services/inlineCo export class InlineCompletionsModel extends Disposable { private readonly _source; - private readonly _isActive; - private readonly _onlyRequestInlineEditsSignal; - private readonly _forceUpdateExplicitlySignal; - private readonly _noDelaySignal; + private readonly _isActive = observableValue(this, false); + private readonly _onlyRequestInlineEditsSignal = observableSignal(this); + private readonly _forceUpdateExplicitlySignal = observableSignal(this); + private readonly _noDelaySignal = observableSignal(this); - private readonly _fetchSpecificProviderSignal; + private readonly _fetchSpecificProviderSignal = observableSignal(this); // We use a semantic id to keep the same inline completion selected even if the provider reorders the completions. - private readonly _selectedInlineCompletionId; - public readonly primaryPosition; + private readonly _selectedInlineCompletionId = observableValue(this, undefined); + public readonly primaryPosition = derived(this, reader => this._positions.read(reader)[0] ?? new Position(1, 1)); - private _isAcceptingPartially; + private _isAcceptingPartially = false; + private readonly _appearedInsideViewport = derived(this, reader => { + const state = this.state.read(reader); + if (!state || !state.inlineCompletion) { + return false; + } + + const targetRange = state.inlineCompletion.targetRange; + const visibleRanges = this._editorObs.editor.getVisibleRanges(); + if (visibleRanges.length < 1) { + return false; + } + + const viewportRange = new Range(visibleRanges[0].startLineNumber, visibleRanges[0].startColumn, visibleRanges[visibleRanges.length - 1].endLineNumber, visibleRanges[visibleRanges.length - 1].endColumn); + return viewportRange.containsRange(targetRange); + }); public get isAcceptingPartially() { return this._isAcceptingPartially; } - private readonly _onDidAccept; - public readonly onDidAccept; + private readonly _onDidAccept = new Emitter(); + public readonly onDidAccept = this._onDidAccept.event; private readonly _editorObs; @@ -91,20 +106,12 @@ export class InlineCompletionsModel extends Disposable { @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, - @IInlineCompletionsService inlineCompletionsService: IInlineCompletionsService + @IInlineCompletionsService private readonly _inlineCompletionsService: IInlineCompletionsService ) { super(); - this.primaryPosition = derived(this, reader => this._positions.read(reader)[0] ?? new Position(1, 1)); this._source = this._register(this._instantiationService.createInstance(InlineCompletionsSource, this.textModel, this._textModelVersionId, this._debounceValue, this.primaryPosition)); - this._isActive = observableValue(this, false); - this._onlyRequestInlineEditsSignal = observableSignal(this); - this._forceUpdateExplicitlySignal = observableSignal(this); - this._noDelaySignal = observableSignal(this); - this._fetchSpecificProviderSignal = observableSignal(this); - this._selectedInlineCompletionId = observableValue(this, undefined); - this._isAcceptingPartially = false; - this._onDidAccept = new Emitter(); - this.onDidAccept = this._onDidAccept.event; + this.lastTriggerKind = this._source.inlineCompletions.map(this, v => v?.request?.context.triggerKind); + this._editorObs = observableCodeEditor(this._editor); this._suggestPreviewEnabled = this._editorObs.getOption(EditorOption.suggest).map(v => v.preview); this._suggestPreviewMode = this._editorObs.getOption(EditorOption.suggest).map(v => v.previewMode); @@ -113,453 +120,13 @@ export class InlineCompletionsModel extends Disposable { this._inlineEditsEnabled = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !!v.edits.enabled); this._inlineEditsShowCollapsedEnabled = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.showCollapsed); this._triggerCommandOnProviderChange = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.experimental.triggerCommandOnProviderChange); - this._register(inlineCompletionsService.onDidChangeIsSnoozing((isSnoozing) => { + + this._register(this._inlineCompletionsService.onDidChangeIsSnoozing((isSnoozing) => { if (isSnoozing) { this.stop(); } })); - this._lastShownInlineCompletionInfo = undefined; - this._lastAcceptedInlineCompletionInfo = undefined; - this._didUndoInlineEdits = derivedHandleChanges({ - owner: this, - changeTracker: { - createChangeSummary: () => ({ didUndo: false }), - handleChange: (ctx, changeSummary) => { - changeSummary.didUndo = ctx.didChange(this._textModelVersionId) && !!ctx.change?.isUndoing; - return true; - } - } - }, (reader, changeSummary) => { - const versionId = this._textModelVersionId.read(reader); - if (versionId !== null - && this._lastAcceptedInlineCompletionInfo - && this._lastAcceptedInlineCompletionInfo.textModelVersionIdAfter === versionId - 1 - && this._lastAcceptedInlineCompletionInfo.inlineCompletion.isInlineEdit - && changeSummary.didUndo - ) { - this._lastAcceptedInlineCompletionInfo = undefined; - return true; - } - return false; - }); - this._preserveCurrentCompletionReasons = new Set([ - VersionIdChangeReason.Redo, - VersionIdChangeReason.Undo, - VersionIdChangeReason.AcceptWord, - ]); - this.dontRefetchSignal = observableSignal(this); - this._fetchInlineCompletionsPromise = derivedHandleChanges({ - owner: this, - changeTracker: { - createChangeSummary: () => ({ - dontRefetch: false, - preserveCurrentCompletion: false, - inlineCompletionTriggerKind: InlineCompletionTriggerKind.Automatic, - onlyRequestInlineEdits: false, - shouldDebounce: true, - provider: undefined as InlineCompletionsProvider | undefined, - textChange: false, - changeReason: '', - }), - handleChange: (ctx, changeSummary) => { - /** @description fetch inline completions */ - if (ctx.didChange(this._textModelVersionId)) { - if (this._preserveCurrentCompletionReasons.has(this._getReason(ctx.change))) { - changeSummary.preserveCurrentCompletion = true; - } - const detailedReasons = ctx.change?.detailedReasons ?? []; - changeSummary.changeReason = detailedReasons.length > 0 ? detailedReasons[0].getType() : ''; - changeSummary.textChange = true; - } else if (ctx.didChange(this._forceUpdateExplicitlySignal)) { - changeSummary.preserveCurrentCompletion = true; - changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit; - } else if (ctx.didChange(this.dontRefetchSignal)) { - changeSummary.dontRefetch = true; - } else if (ctx.didChange(this._onlyRequestInlineEditsSignal)) { - changeSummary.onlyRequestInlineEdits = true; - } else if (ctx.didChange(this._fetchSpecificProviderSignal)) { - changeSummary.provider = ctx.change; - } - return true; - }, - }, - }, (reader, changeSummary) => { - this._source.clearOperationOnTextModelChange.read(reader); // Make sure the clear operation runs before the fetch operation - this._noDelaySignal.read(reader); - this.dontRefetchSignal.read(reader); - this._onlyRequestInlineEditsSignal.read(reader); - this._forceUpdateExplicitlySignal.read(reader); - this._fetchSpecificProviderSignal.read(reader); - const shouldUpdate = ((this._enabled.read(reader) && this._selectedSuggestItem.read(reader)) || this._isActive.read(reader)) - && (!inlineCompletionsService.isSnoozing() || changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit); - if (!shouldUpdate) { - this._source.cancelUpdate(); - return undefined; - } - - this._textModelVersionId.read(reader); // Refetch on text change - - const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.get(); - const suggestItem = this._selectedSuggestItem.read(reader); - if (suggestWidgetInlineCompletions && !suggestItem) { - this._source.seedInlineCompletionsWithSuggestWidget(); - } - - if (changeSummary.dontRefetch) { - return Promise.resolve(true); - } - - if (this._didUndoInlineEdits.read(reader) && changeSummary.inlineCompletionTriggerKind !== InlineCompletionTriggerKind.Explicit) { - transaction(tx => { - this._source.clear(tx); - }); - return undefined; - } - - let reason: string = ''; - if (changeSummary.provider) { - reason += 'providerOnDidChange'; - } else if (changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit) { - reason += 'explicit'; - } - if (changeSummary.changeReason) { - reason += reason.length > 0 ? `:${changeSummary.changeReason}` : changeSummary.changeReason; - } - - const requestInfo: InlineSuggestRequestInfo = { - editorType: this.editorType, - startTime: Date.now(), - languageId: this.textModel.getLanguageId(), - reason, - }; - - let context: InlineCompletionContextWithoutUuid = { - triggerKind: changeSummary.inlineCompletionTriggerKind, - selectedSuggestionInfo: suggestItem?.toSelectedSuggestionInfo(), - includeInlineCompletions: !changeSummary.onlyRequestInlineEdits, - includeInlineEdits: this._inlineEditsEnabled.read(reader), - }; - - if (context.triggerKind === InlineCompletionTriggerKind.Automatic && changeSummary.textChange) { - if (this.textModel.getAlternativeVersionId() === this._lastShownInlineCompletionInfo?.alternateTextModelVersionId) { - // When undoing back to a version where an inline edit/completion was shown, - // we want to show an inline edit (or completion) again if it was originally an inline edit (or completion). - context = { - ...context, - includeInlineCompletions: !this._lastShownInlineCompletionInfo.inlineCompletion.isInlineEdit, - includeInlineEdits: this._lastShownInlineCompletionInfo.inlineCompletion.isInlineEdit, - }; - } - } - - const itemToPreserveCandidate = this.selectedInlineCompletion.get() ?? this._inlineCompletionItems.get()?.inlineEdit; - const itemToPreserve = changeSummary.preserveCurrentCompletion || itemToPreserveCandidate?.forwardStable - ? itemToPreserveCandidate : undefined; - const userJumpedToActiveCompletion = this._jumpedToId.map(jumpedTo => !!jumpedTo && jumpedTo === this._inlineCompletionItems.get()?.inlineEdit?.semanticId); - - const providers = changeSummary.provider ? [changeSummary.provider] : this._languageFeaturesService.inlineCompletionsProvider.all(this.textModel); - const suppressedProviderGroupIds = this._suppressedInlineCompletionGroupIds.get(); - const availableProviders = providers.filter(provider => !(provider.groupId && suppressedProviderGroupIds.has(provider.groupId))); - - return this._source.fetch(availableProviders, context, itemToPreserve?.identity, changeSummary.shouldDebounce, userJumpedToActiveCompletion, !!changeSummary.provider, requestInfo); - }); - - this._inlineCompletionItems = derivedOpts({ owner: this }, reader => { - const c = this._source.inlineCompletions.read(reader); - if (!c) { return undefined; } - const cursorPosition = this.primaryPosition.read(reader); - let inlineEdit: InlineEditItem | undefined = undefined; - const visibleCompletions: InlineCompletionItem[] = []; - for (const completion of c.inlineCompletions) { - if (!completion.isInlineEdit) { - if (completion.isVisible(this.textModel, cursorPosition)) { - visibleCompletions.push(completion); - } - } else { - inlineEdit = completion; - } - } - - if (visibleCompletions.length !== 0) { - // Don't show the inline edit if there is a visible completion - inlineEdit = undefined; - } - - return { - inlineCompletions: visibleCompletions, - inlineEdit, - }; - }); - this._filteredInlineCompletionItems = derivedOpts({ owner: this, equalsFn: itemsEquals() }, reader => { - const c = this._inlineCompletionItems.read(reader); - return c?.inlineCompletions ?? []; - }); - this.selectedInlineCompletionIndex = derived(this, (reader) => { - const selectedInlineCompletionId = this._selectedInlineCompletionId.read(reader); - const filteredCompletions = this._filteredInlineCompletionItems.read(reader); - const idx = this._selectedInlineCompletionId === undefined ? -1 - : filteredCompletions.findIndex(v => v.semanticId === selectedInlineCompletionId); - if (idx === -1) { - // Reset the selection so that the selection does not jump back when it appears again - this._selectedInlineCompletionId.set(undefined, undefined); - return 0; - } - return idx; - }); - this.selectedInlineCompletion = derived(this, (reader) => { - const filteredCompletions = this._filteredInlineCompletionItems.read(reader); - const idx = this.selectedInlineCompletionIndex.read(reader); - return filteredCompletions[idx]; - }); - this.activeCommands = derivedOpts({ owner: this, equalsFn: itemsEquals() }, - r => this.selectedInlineCompletion.read(r)?.source.inlineSuggestions.commands ?? [] - ); - this.lastTriggerKind = this._source.inlineCompletions.map(this, v => v?.request?.context.triggerKind); - this.inlineCompletionsCount = derived(this, reader => { - if (this.lastTriggerKind.read(reader) === InlineCompletionTriggerKind.Explicit) { - return this._filteredInlineCompletionItems.read(reader).length; - } else { - return undefined; - } - }); - this._hasVisiblePeekWidgets = derived(this, reader => this._editorObs.openedPeekWidgets.read(reader) > 0); - this.state = derivedOpts<{ - kind: 'ghostText'; - edits: readonly TextReplacement[]; - primaryGhostText: GhostTextOrReplacement; - ghostTexts: readonly GhostTextOrReplacement[]; - suggestItem: SuggestItemInfo | undefined; - inlineCompletion: InlineCompletionItem | undefined; - } | { - kind: 'inlineEdit'; - edits: readonly TextReplacement[]; - inlineEdit: InlineEdit; - inlineCompletion: InlineEditItem; - cursorAtInlineEdit: IObservable; - } | undefined>({ - owner: this, - equalsFn: (a, b) => { - if (!a || !b) { return a === b; } - - if (a.kind === 'ghostText' && b.kind === 'ghostText') { - return ghostTextsOrReplacementsEqual(a.ghostTexts, b.ghostTexts) - && a.inlineCompletion === b.inlineCompletion - && a.suggestItem === b.suggestItem; - } else if (a.kind === 'inlineEdit' && b.kind === 'inlineEdit') { - return a.inlineEdit.equals(b.inlineEdit); - } - return false; - } - }, (reader) => { - const model = this.textModel; - - const item = this._inlineCompletionItems.read(reader); - const inlineEditResult = item?.inlineEdit; - if (inlineEditResult) { - if (this._hasVisiblePeekWidgets.read(reader)) { - return undefined; - } - let edit = inlineEditResult.getSingleTextEdit(); - edit = singleTextRemoveCommonPrefix(edit, model); - - const cursorAtInlineEdit = this.primaryPosition.map(cursorPos => LineRange.fromRangeInclusive(inlineEditResult.targetRange).addMargin(1, 1).contains(cursorPos.lineNumber)); - - const commands = inlineEditResult.source.inlineSuggestions.commands; - const inlineEdit = new InlineEdit(edit, commands ?? [], inlineEditResult); - - const edits = inlineEditResult.updatedEdit; - const e = edits ? TextEdit.fromStringEdit(edits, new TextModelText(this.textModel)).replacements : [edit]; - - return { kind: 'inlineEdit', inlineEdit, inlineCompletion: inlineEditResult, edits: e, cursorAtInlineEdit }; - } - - const suggestItem = this._selectedSuggestItem.read(reader); - if (suggestItem) { - const suggestCompletionEdit = singleTextRemoveCommonPrefix(suggestItem.getSingleTextEdit(), model); - const augmentation = this._computeAugmentation(suggestCompletionEdit, reader); - - const isSuggestionPreviewEnabled = this._suggestPreviewEnabled.read(reader); - if (!isSuggestionPreviewEnabled && !augmentation) { return undefined; } - - const fullEdit = augmentation?.edit ?? suggestCompletionEdit; - const fullEditPreviewLength = augmentation ? augmentation.edit.text.length - suggestCompletionEdit.text.length : 0; - - const mode = this._suggestPreviewMode.read(reader); - const positions = this._positions.read(reader); - const edits = [fullEdit, ...getSecondaryEdits(this.textModel, positions, fullEdit)]; - const ghostTexts = edits - .map((edit, idx) => computeGhostText(edit, model, mode, positions[idx], fullEditPreviewLength)) - .filter(isDefined); - const primaryGhostText = ghostTexts[0] ?? new GhostText(fullEdit.range.endLineNumber, []); - return { kind: 'ghostText', edits, primaryGhostText, ghostTexts, inlineCompletion: augmentation?.completion, suggestItem }; - } else { - if (!this._isActive.read(reader)) { return undefined; } - const inlineCompletion = this.selectedInlineCompletion.read(reader); - if (!inlineCompletion) { return undefined; } - - const replacement = inlineCompletion.getSingleTextEdit(); - const mode = this._inlineSuggestMode.read(reader); - const positions = this._positions.read(reader); - const edits = [replacement, ...getSecondaryEdits(this.textModel, positions, replacement)]; - const ghostTexts = edits - .map((edit, idx) => computeGhostText(edit, model, mode, positions[idx], 0)) - .filter(isDefined); - if (!ghostTexts[0]) { return undefined; } - return { kind: 'ghostText', edits, primaryGhostText: ghostTexts[0], ghostTexts, inlineCompletion, suggestItem: undefined }; - } - }); - this.status = derived(this, reader => { - if (this._source.loading.read(reader)) { return 'loading'; } - const s = this.state.read(reader); - if (s?.kind === 'ghostText') { return 'ghostText'; } - if (s?.kind === 'inlineEdit') { return 'inlineEdit'; } - return 'noSuggestion'; - }); - this.inlineCompletionState = derived(this, reader => { - const s = this.state.read(reader); - if (!s || s.kind !== 'ghostText') { - return undefined; - } - if (this._editorObs.inComposition.read(reader)) { - return undefined; - } - return s; - }); - this.inlineEditState = derived(this, reader => { - const s = this.state.read(reader); - if (!s || s.kind !== 'inlineEdit') { - return undefined; - } - return s; - }); - this.inlineEditAvailable = derived(this, reader => { - const s = this.inlineEditState.read(reader); - return !!s; - }); - this.warning = derived(this, reader => { - return this.inlineCompletionState.read(reader)?.inlineCompletion?.warning; - }); - this.ghostTexts = derivedOpts({ owner: this, equalsFn: ghostTextsOrReplacementsEqual }, reader => { - const v = this.inlineCompletionState.read(reader); - if (!v) { - return undefined; - } - return v.ghostTexts; - }); - this.primaryGhostText = derivedOpts({ owner: this, equalsFn: ghostTextOrReplacementEquals }, reader => { - const v = this.inlineCompletionState.read(reader); - if (!v) { - return undefined; - } - return v?.primaryGhostText; - }); - - this._jumpedToId = observableValue(this, undefined); - this._inAcceptFlow = observableValue(this, false); - this.inAcceptFlow = this._inAcceptFlow; - - // When the suggestion appeared, was it inside the view port or not - const appearedInsideViewport = derived(this, reader => { - const state = this.state.read(reader); - if (!state || !state.inlineCompletion) { - return false; - } - - const targetRange = state.inlineCompletion.targetRange; - const visibleRanges = this._editorObs.editor.getVisibleRanges(); - if (visibleRanges.length < 1) { - return false; - } - - const viewportRange = new Range(visibleRanges[0].startLineNumber, visibleRanges[0].startColumn, visibleRanges[visibleRanges.length - 1].endLineNumber, visibleRanges[visibleRanges.length - 1].endColumn); - return viewportRange.containsRange(targetRange); - }); - - this.showCollapsed = derived(this, reader => { - const state = this.state.read(reader); - if (!state || state.kind !== 'inlineEdit') { - return false; - } - - if (state.inlineCompletion.displayLocation) { - return false; - } - - const isCurrentModelVersion = state.inlineCompletion.updatedEditModelVersion === this._textModelVersionId.read(reader); - return (this._inlineEditsShowCollapsedEnabled.read(reader) || !isCurrentModelVersion) - && this._jumpedToId.read(reader) !== state.inlineCompletion.semanticId - && !this._inAcceptFlow.read(reader); - }); - this._tabShouldIndent = derived(this, reader => { - if (this._inAcceptFlow.read(reader)) { - return false; - } - - function isMultiLine(range: Range): boolean { - return range.startLineNumber !== range.endLineNumber; - } - - function getNonIndentationRange(model: ITextModel, lineNumber: number): Range { - const columnStart = model.getLineIndentColumn(lineNumber); - const lastNonWsColumn = model.getLineLastNonWhitespaceColumn(lineNumber); - const columnEnd = Math.max(lastNonWsColumn, columnStart); - return new Range(lineNumber, columnStart, lineNumber, columnEnd); - } - - const selections = this._editorObs.selections.read(reader); - return selections?.some(s => { - if (s.isEmpty()) { - return this.textModel.getLineLength(s.startLineNumber) === 0; - } else { - return isMultiLine(s) || s.containsRange(getNonIndentationRange(this.textModel, s.startLineNumber)); - } - }); - }); - this.tabShouldJumpToInlineEdit = derived(this, reader => { - if (this._tabShouldIndent.read(reader)) { - return false; - } - - const s = this.inlineEditState.read(reader); - if (!s) { - return false; - } - - if (this.showCollapsed.read(reader)) { - return true; - } - - if (this._inAcceptFlow.read(reader) && appearedInsideViewport.read(reader)) { - return false; - } - - return !s.cursorAtInlineEdit.read(reader); - }); - this.tabShouldAcceptInlineEdit = derived(this, reader => { - const s = this.inlineEditState.read(reader); - if (!s) { - return false; - } - if (this.showCollapsed.read(reader)) { - return false; - } - if (this._inAcceptFlow.read(reader) && appearedInsideViewport.read(reader)) { - return true; - } - if (s.inlineCompletion.targetRange.startLineNumber === this._editorObs.cursorLineNumber.read(reader)) { - return true; - } - if (this._jumpedToId.read(reader) === s.inlineCompletion.semanticId) { - return true; - } - if (this._tabShouldIndent.read(reader)) { - return false; - } - - return s.cursorAtInlineEdit.read(reader); - }); - { // Determine editor type const [diffEditor] = this._codeEditorService.listDiffEditors() .filter(d => @@ -638,9 +205,30 @@ export class InlineCompletionsModel extends Disposable { this._didUndoInlineEdits.recomputeInitiallyAndOnChange(this._store); } - private _lastShownInlineCompletionInfo: { alternateTextModelVersionId: number; /* already freed! */ inlineCompletion: InlineSuggestionItem } | undefined; - private _lastAcceptedInlineCompletionInfo: { textModelVersionIdAfter: number; /* already freed! */ inlineCompletion: InlineSuggestionItem } | undefined; - private readonly _didUndoInlineEdits; + private _lastShownInlineCompletionInfo: { alternateTextModelVersionId: number; /* already freed! */ inlineCompletion: InlineSuggestionItem } | undefined = undefined; + private _lastAcceptedInlineCompletionInfo: { textModelVersionIdAfter: number; /* already freed! */ inlineCompletion: InlineSuggestionItem } | undefined = undefined; + private readonly _didUndoInlineEdits = derivedHandleChanges({ + owner: this, + changeTracker: { + createChangeSummary: () => ({ didUndo: false }), + handleChange: (ctx, changeSummary) => { + changeSummary.didUndo = ctx.didChange(this._textModelVersionId) && !!ctx.change?.isUndoing; + return true; + } + } + }, (reader, changeSummary) => { + const versionId = this._textModelVersionId.read(reader); + if (versionId !== null + && this._lastAcceptedInlineCompletionInfo + && this._lastAcceptedInlineCompletionInfo.textModelVersionIdAfter === versionId - 1 + && this._lastAcceptedInlineCompletionInfo.inlineCompletion.isInlineEdit + && changeSummary.didUndo + ) { + this._lastAcceptedInlineCompletionInfo = undefined; + return true; + } + return false; + }); public debugGetSelectedSuggestItem(): IObservable { return this._selectedSuggestItem; @@ -676,7 +264,11 @@ export class InlineCompletionsModel extends Disposable { }; } - private readonly _preserveCurrentCompletionReasons; + private readonly _preserveCurrentCompletionReasons = new Set([ + VersionIdChangeReason.Redo, + VersionIdChangeReason.Undo, + VersionIdChangeReason.AcceptWord, + ]); private _getReason(e: IModelContentChangedEvent | undefined): VersionIdChangeReason { if (e?.isUndoing) { return VersionIdChangeReason.Undo; } @@ -685,9 +277,123 @@ export class InlineCompletionsModel extends Disposable { return VersionIdChangeReason.Other; } - public readonly dontRefetchSignal; + public readonly dontRefetchSignal = observableSignal(this); - private readonly _fetchInlineCompletionsPromise; + private readonly _fetchInlineCompletionsPromise = derivedHandleChanges({ + owner: this, + changeTracker: { + createChangeSummary: () => ({ + dontRefetch: false, + preserveCurrentCompletion: false, + inlineCompletionTriggerKind: InlineCompletionTriggerKind.Automatic, + onlyRequestInlineEdits: false, + shouldDebounce: true, + provider: undefined as InlineCompletionsProvider | undefined, + textChange: false, + changeReason: '', + }), + handleChange: (ctx, changeSummary) => { + /** @description fetch inline completions */ + if (ctx.didChange(this._textModelVersionId)) { + if (this._preserveCurrentCompletionReasons.has(this._getReason(ctx.change))) { + changeSummary.preserveCurrentCompletion = true; + } + const detailedReasons = ctx.change?.detailedReasons ?? []; + changeSummary.changeReason = detailedReasons.length > 0 ? detailedReasons[0].getType() : ''; + changeSummary.textChange = true; + } else if (ctx.didChange(this._forceUpdateExplicitlySignal)) { + changeSummary.preserveCurrentCompletion = true; + changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit; + } else if (ctx.didChange(this.dontRefetchSignal)) { + changeSummary.dontRefetch = true; + } else if (ctx.didChange(this._onlyRequestInlineEditsSignal)) { + changeSummary.onlyRequestInlineEdits = true; + } else if (ctx.didChange(this._fetchSpecificProviderSignal)) { + changeSummary.provider = ctx.change; + } + return true; + }, + }, + }, (reader, changeSummary) => { + this._source.clearOperationOnTextModelChange.read(reader); // Make sure the clear operation runs before the fetch operation + this._noDelaySignal.read(reader); + this.dontRefetchSignal.read(reader); + this._onlyRequestInlineEditsSignal.read(reader); + this._forceUpdateExplicitlySignal.read(reader); + this._fetchSpecificProviderSignal.read(reader); + const shouldUpdate = ((this._enabled.read(reader) && this._selectedSuggestItem.read(reader)) || this._isActive.read(reader)) + && (!this._inlineCompletionsService.isSnoozing() || changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit); + if (!shouldUpdate) { + this._source.cancelUpdate(); + return undefined; + } + + this._textModelVersionId.read(reader); // Refetch on text change + + const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.get(); + const suggestItem = this._selectedSuggestItem.read(reader); + if (suggestWidgetInlineCompletions && !suggestItem) { + this._source.seedInlineCompletionsWithSuggestWidget(); + } + + if (changeSummary.dontRefetch) { + return Promise.resolve(true); + } + + if (this._didUndoInlineEdits.read(reader) && changeSummary.inlineCompletionTriggerKind !== InlineCompletionTriggerKind.Explicit) { + transaction(tx => { + this._source.clear(tx); + }); + return undefined; + } + + let reason: string = ''; + if (changeSummary.provider) { + reason += 'providerOnDidChange'; + } else if (changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit) { + reason += 'explicit'; + } + if (changeSummary.changeReason) { + reason += reason.length > 0 ? `:${changeSummary.changeReason}` : changeSummary.changeReason; + } + + const requestInfo: InlineSuggestRequestInfo = { + editorType: this.editorType, + startTime: Date.now(), + languageId: this.textModel.getLanguageId(), + reason, + }; + + let context: InlineCompletionContextWithoutUuid = { + triggerKind: changeSummary.inlineCompletionTriggerKind, + selectedSuggestionInfo: suggestItem?.toSelectedSuggestionInfo(), + includeInlineCompletions: !changeSummary.onlyRequestInlineEdits, + includeInlineEdits: this._inlineEditsEnabled.read(reader), + }; + + if (context.triggerKind === InlineCompletionTriggerKind.Automatic && changeSummary.textChange) { + if (this.textModel.getAlternativeVersionId() === this._lastShownInlineCompletionInfo?.alternateTextModelVersionId) { + // When undoing back to a version where an inline edit/completion was shown, + // we want to show an inline edit (or completion) again if it was originally an inline edit (or completion). + context = { + ...context, + includeInlineCompletions: !this._lastShownInlineCompletionInfo.inlineCompletion.isInlineEdit, + includeInlineEdits: this._lastShownInlineCompletionInfo.inlineCompletion.isInlineEdit, + }; + } + } + + const itemToPreserveCandidate = this.selectedInlineCompletion.get() ?? this._inlineCompletionItems.get()?.inlineEdit; + const itemToPreserve = changeSummary.preserveCurrentCompletion || itemToPreserveCandidate?.forwardStable + ? itemToPreserveCandidate : undefined; + const userJumpedToActiveCompletion = this._jumpedToId.map(jumpedTo => !!jumpedTo && jumpedTo === this._inlineCompletionItems.get()?.inlineEdit?.semanticId); + + const providers = changeSummary.provider ? [changeSummary.provider] : this._languageFeaturesService.inlineCompletionsProvider.all(this.textModel); + const suppressedProviderGroupIds = this._suppressedInlineCompletionGroupIds.get(); + const availableProviders = providers.filter(provider => !(provider.groupId && suppressedProviderGroupIds.has(provider.groupId))); + + return this._source.fetch(availableProviders, context, itemToPreserve?.identity, changeSummary.shouldDebounce, userJumpedToActiveCompletion, !!changeSummary.provider, requestInfo); + }); public async trigger(tx?: ITransaction, options?: { onlyFetchInlineEdits?: boolean; noDelay?: boolean }): Promise { subtransaction(tx, tx => { @@ -728,32 +434,190 @@ export class InlineCompletionsModel extends Disposable { }); } - private readonly _inlineCompletionItems; + private readonly _inlineCompletionItems = derivedOpts({ owner: this }, reader => { + const c = this._source.inlineCompletions.read(reader); + if (!c) { return undefined; } + const cursorPosition = this.primaryPosition.read(reader); + let inlineEdit: InlineEditItem | undefined = undefined; + const visibleCompletions: InlineCompletionItem[] = []; + for (const completion of c.inlineCompletions) { + if (!completion.isInlineEdit) { + if (completion.isVisible(this.textModel, cursorPosition)) { + visibleCompletions.push(completion); + } + } else { + inlineEdit = completion; + } + } - private readonly _filteredInlineCompletionItems; + if (visibleCompletions.length !== 0) { + // Don't show the inline edit if there is a visible completion + inlineEdit = undefined; + } - public readonly selectedInlineCompletionIndex; + return { + inlineCompletions: visibleCompletions, + inlineEdit, + }; + }); - public readonly selectedInlineCompletion; + private readonly _filteredInlineCompletionItems = derivedOpts({ owner: this, equalsFn: itemsEquals() }, reader => { + const c = this._inlineCompletionItems.read(reader); + return c?.inlineCompletions ?? []; + }); - public readonly activeCommands; + public readonly selectedInlineCompletionIndex = derived(this, (reader) => { + const selectedInlineCompletionId = this._selectedInlineCompletionId.read(reader); + const filteredCompletions = this._filteredInlineCompletionItems.read(reader); + const idx = this._selectedInlineCompletionId === undefined ? -1 + : filteredCompletions.findIndex(v => v.semanticId === selectedInlineCompletionId); + if (idx === -1) { + // Reset the selection so that the selection does not jump back when it appears again + this._selectedInlineCompletionId.set(undefined, undefined); + return 0; + } + return idx; + }); - public readonly lastTriggerKind: IObservable - ; + public readonly selectedInlineCompletion = derived(this, (reader) => { + const filteredCompletions = this._filteredInlineCompletionItems.read(reader); + const idx = this.selectedInlineCompletionIndex.read(reader); + return filteredCompletions[idx]; + }); - public readonly inlineCompletionsCount; + public readonly activeCommands = derivedOpts({ owner: this, equalsFn: itemsEquals() }, + r => this.selectedInlineCompletion.read(r)?.source.inlineSuggestions.commands ?? [] + ); - private readonly _hasVisiblePeekWidgets; + public readonly lastTriggerKind: IObservable; - public readonly state; + public readonly inlineCompletionsCount = derived(this, reader => { + if (this.lastTriggerKind.read(reader) === InlineCompletionTriggerKind.Explicit) { + return this._filteredInlineCompletionItems.read(reader).length; + } else { + return undefined; + } + }); - public readonly status; + private readonly _hasVisiblePeekWidgets = derived(this, reader => this._editorObs.openedPeekWidgets.read(reader) > 0); - public readonly inlineCompletionState; + public readonly state = derivedOpts<{ + kind: 'ghostText'; + edits: readonly TextReplacement[]; + primaryGhostText: GhostTextOrReplacement; + ghostTexts: readonly GhostTextOrReplacement[]; + suggestItem: SuggestItemInfo | undefined; + inlineCompletion: InlineCompletionItem | undefined; + } | { + kind: 'inlineEdit'; + edits: readonly TextReplacement[]; + inlineEdit: InlineEdit; + inlineCompletion: InlineEditItem; + cursorAtInlineEdit: IObservable; + } | undefined>({ + owner: this, + equalsFn: (a, b) => { + if (!a || !b) { return a === b; } - public readonly inlineEditState; + if (a.kind === 'ghostText' && b.kind === 'ghostText') { + return ghostTextsOrReplacementsEqual(a.ghostTexts, b.ghostTexts) + && a.inlineCompletion === b.inlineCompletion + && a.suggestItem === b.suggestItem; + } else if (a.kind === 'inlineEdit' && b.kind === 'inlineEdit') { + return a.inlineEdit.equals(b.inlineEdit); + } + return false; + } + }, (reader) => { + const model = this.textModel; - public readonly inlineEditAvailable; + const item = this._inlineCompletionItems.read(reader); + const inlineEditResult = item?.inlineEdit; + if (inlineEditResult) { + if (this._hasVisiblePeekWidgets.read(reader)) { + return undefined; + } + let edit = inlineEditResult.getSingleTextEdit(); + edit = singleTextRemoveCommonPrefix(edit, model); + + const cursorAtInlineEdit = this.primaryPosition.map(cursorPos => LineRange.fromRangeInclusive(inlineEditResult.targetRange).addMargin(1, 1).contains(cursorPos.lineNumber)); + + const commands = inlineEditResult.source.inlineSuggestions.commands; + const inlineEdit = new InlineEdit(edit, commands ?? [], inlineEditResult); + + const edits = inlineEditResult.updatedEdit; + const e = edits ? TextEdit.fromStringEdit(edits, new TextModelText(this.textModel)).replacements : [edit]; + + return { kind: 'inlineEdit', inlineEdit, inlineCompletion: inlineEditResult, edits: e, cursorAtInlineEdit }; + } + + const suggestItem = this._selectedSuggestItem.read(reader); + if (suggestItem) { + const suggestCompletionEdit = singleTextRemoveCommonPrefix(suggestItem.getSingleTextEdit(), model); + const augmentation = this._computeAugmentation(suggestCompletionEdit, reader); + + const isSuggestionPreviewEnabled = this._suggestPreviewEnabled.read(reader); + if (!isSuggestionPreviewEnabled && !augmentation) { return undefined; } + + const fullEdit = augmentation?.edit ?? suggestCompletionEdit; + const fullEditPreviewLength = augmentation ? augmentation.edit.text.length - suggestCompletionEdit.text.length : 0; + + const mode = this._suggestPreviewMode.read(reader); + const positions = this._positions.read(reader); + const edits = [fullEdit, ...getSecondaryEdits(this.textModel, positions, fullEdit)]; + const ghostTexts = edits + .map((edit, idx) => computeGhostText(edit, model, mode, positions[idx], fullEditPreviewLength)) + .filter(isDefined); + const primaryGhostText = ghostTexts[0] ?? new GhostText(fullEdit.range.endLineNumber, []); + return { kind: 'ghostText', edits, primaryGhostText, ghostTexts, inlineCompletion: augmentation?.completion, suggestItem }; + } else { + if (!this._isActive.read(reader)) { return undefined; } + const inlineCompletion = this.selectedInlineCompletion.read(reader); + if (!inlineCompletion) { return undefined; } + + const replacement = inlineCompletion.getSingleTextEdit(); + const mode = this._inlineSuggestMode.read(reader); + const positions = this._positions.read(reader); + const edits = [replacement, ...getSecondaryEdits(this.textModel, positions, replacement)]; + const ghostTexts = edits + .map((edit, idx) => computeGhostText(edit, model, mode, positions[idx], 0)) + .filter(isDefined); + if (!ghostTexts[0]) { return undefined; } + return { kind: 'ghostText', edits, primaryGhostText: ghostTexts[0], ghostTexts, inlineCompletion, suggestItem: undefined }; + } + }); + + public readonly status = derived(this, reader => { + if (this._source.loading.read(reader)) { return 'loading'; } + const s = this.state.read(reader); + if (s?.kind === 'ghostText') { return 'ghostText'; } + if (s?.kind === 'inlineEdit') { return 'inlineEdit'; } + return 'noSuggestion'; + }); + + public readonly inlineCompletionState = derived(this, reader => { + const s = this.state.read(reader); + if (!s || s.kind !== 'ghostText') { + return undefined; + } + if (this._editorObs.inComposition.read(reader)) { + return undefined; + } + return s; + }); + + public readonly inlineEditState = derived(this, reader => { + const s = this.state.read(reader); + if (!s || s.kind !== 'inlineEdit') { + return undefined; + } + return s; + }); + + public readonly inlineEditAvailable = derived(this, reader => { + const s = this.inlineEditState.read(reader); + return !!s; + }); private _computeAugmentation(suggestCompletion: TextReplacement, reader: IReader | undefined) { const model = this.textModel; @@ -775,19 +639,112 @@ export class InlineCompletionsModel extends Disposable { return augmentedCompletion; } - public readonly warning; + public readonly warning = derived(this, reader => { + return this.inlineCompletionState.read(reader)?.inlineCompletion?.warning; + }); - public readonly ghostTexts; + public readonly ghostTexts = derivedOpts({ owner: this, equalsFn: ghostTextsOrReplacementsEqual }, reader => { + const v = this.inlineCompletionState.read(reader); + if (!v) { + return undefined; + } + return v.ghostTexts; + }); - public readonly primaryGhostText; + public readonly primaryGhostText = derivedOpts({ owner: this, equalsFn: ghostTextOrReplacementEquals }, reader => { + const v = this.inlineCompletionState.read(reader); + if (!v) { + return undefined; + } + return v?.primaryGhostText; + }); - public readonly showCollapsed; + public readonly showCollapsed = derived(this, reader => { + const state = this.state.read(reader); + if (!state || state.kind !== 'inlineEdit') { + return false; + } - private readonly _tabShouldIndent; + if (state.inlineCompletion.displayLocation) { + return false; + } - public readonly tabShouldJumpToInlineEdit; + const isCurrentModelVersion = state.inlineCompletion.updatedEditModelVersion === this._textModelVersionId.read(reader); + return (this._inlineEditsShowCollapsedEnabled.read(reader) || !isCurrentModelVersion) + && this._jumpedToId.read(reader) !== state.inlineCompletion.semanticId + && !this._inAcceptFlow.read(reader); + }); - public readonly tabShouldAcceptInlineEdit; + private readonly _tabShouldIndent = derived(this, reader => { + if (this._inAcceptFlow.read(reader)) { + return false; + } + + function isMultiLine(range: Range): boolean { + return range.startLineNumber !== range.endLineNumber; + } + + function getNonIndentationRange(model: ITextModel, lineNumber: number): Range { + const columnStart = model.getLineIndentColumn(lineNumber); + const lastNonWsColumn = model.getLineLastNonWhitespaceColumn(lineNumber); + const columnEnd = Math.max(lastNonWsColumn, columnStart); + return new Range(lineNumber, columnStart, lineNumber, columnEnd); + } + + const selections = this._editorObs.selections.read(reader); + return selections?.some(s => { + if (s.isEmpty()) { + return this.textModel.getLineLength(s.startLineNumber) === 0; + } else { + return isMultiLine(s) || s.containsRange(getNonIndentationRange(this.textModel, s.startLineNumber)); + } + }); + }); + + public readonly tabShouldJumpToInlineEdit = derived(this, reader => { + if (this._tabShouldIndent.read(reader)) { + return false; + } + + const s = this.inlineEditState.read(reader); + if (!s) { + return false; + } + + if (this.showCollapsed.read(reader)) { + return true; + } + + if (this._inAcceptFlow.read(reader) && this._appearedInsideViewport.read(reader)) { + return false; + } + + return !s.cursorAtInlineEdit.read(reader); + }); + + public readonly tabShouldAcceptInlineEdit = derived(this, reader => { + const s = this.inlineEditState.read(reader); + if (!s) { + return false; + } + if (this.showCollapsed.read(reader)) { + return false; + } + if (this._inAcceptFlow.read(reader) && this._appearedInsideViewport.read(reader)) { + return true; + } + if (s.inlineCompletion.targetRange.startLineNumber === this._editorObs.cursorLineNumber.read(reader)) { + return true; + } + if (this._jumpedToId.read(reader) === s.inlineCompletion.semanticId) { + return true; + } + if (this._tabShouldIndent.read(reader)) { + return false; + } + + return s.cursorAtInlineEdit.read(reader); + }); public readonly isInDiffEditor; @@ -1031,9 +988,9 @@ export class InlineCompletionsModel extends Disposable { }; } - private readonly _jumpedToId; - private readonly _inAcceptFlow; - public readonly inAcceptFlow: IObservable; + private readonly _jumpedToId = observableValue(this, undefined); + private readonly _inAcceptFlow = observableValue(this, false); + public readonly inAcceptFlow: IObservable = this._inAcceptFlow; public jump(): void { const s = this.inlineEditState.get(); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index 0a318d79ff7..b1fa5014d95 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -8,7 +8,7 @@ import { findLastMax } from '../../../../../base/common/arraysFind.js'; import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { equalsIfDefined, itemEquals } from '../../../../../base/common/equals.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { derived, IObservable, IObservableWithChange, ITransaction, observableValue, recordChanges, transaction } from '../../../../../base/common/observable.js'; +import { derived, IObservable, IObservableWithChange, ITransaction, observableValue, recordChangesLazy, transaction } from '../../../../../base/common/observable.js'; // eslint-disable-next-line local/code-no-deep-import-of-internal import { observableReducerSettable } from '../../../../../base/common/observableInternal/experimental/reducer.js'; import { isDefined } from '../../../../../base/common/types.js'; @@ -32,16 +32,42 @@ import { InlineCompletionContextWithoutUuid, InlineSuggestRequestInfo, provideIn export class InlineCompletionsSource extends Disposable { private static _requestId = 0; - private readonly _updateOperation; + private readonly _updateOperation = this._register(new MutableDisposable()); private readonly _loggingEnabled; private readonly _structuredFetchLogger; - private readonly _state; + private readonly _state = observableReducerSettable(this, { + initial: () => ({ + inlineCompletions: InlineCompletionsState.createEmpty(), + suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(), + }), + disposeFinal: (values) => { + values.inlineCompletions.dispose(); + values.suggestWidgetInlineCompletions.dispose(); + }, + changeTracker: recordChangesLazy(() => ({ versionId: this._versionId })), + update: (reader, previousValue, changes) => { + const edit = StringEdit.compose(changes.changes.map(c => c.change ? offsetEditFromContentChanges(c.change.changes) : StringEdit.empty).filter(isDefined)); - public readonly inlineCompletions; - public readonly suggestWidgetInlineCompletions; + if (edit.isEmpty()) { + return previousValue; + } + try { + return { + inlineCompletions: previousValue.inlineCompletions.createStateWithAppliedEdit(edit, this._textModel), + suggestWidgetInlineCompletions: previousValue.suggestWidgetInlineCompletions.createStateWithAppliedEdit(edit, this._textModel), + }; + } finally { + previousValue.inlineCompletions.dispose(); + previousValue.suggestWidgetInlineCompletions.dispose(); + } + } + }); + + public readonly inlineCompletions = this._state.map(this, v => v.inlineCompletions); + public readonly suggestWidgetInlineCompletions = this._state.map(this, v => v.suggestWidgetInlineCompletions); constructor( private readonly _textModel: ITextModel, @@ -51,10 +77,9 @@ export class InlineCompletionsSource extends Disposable { @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, @ILogService private readonly _logService: ILogService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); - this._updateOperation = this._register(new MutableDisposable()); this._loggingEnabled = observableConfigValue('editor.inlineSuggest.logFetch', false, this._configurationService).recomputeInitiallyAndOnChange(this._store); this._structuredFetchLogger = this._register(this._instantiationService.createInstance(StructuredLogger.cast< { kind: 'start'; requestId: number; context: unknown } & IRecordableEditorLogEntry @@ -62,47 +87,15 @@ export class InlineCompletionsSource extends Disposable { >(), 'editor.inlineSuggest.logFetch.commandId' )); - this._state = observableReducerSettable(this, { - initial: () => ({ - inlineCompletions: InlineCompletionsState.createEmpty(), - suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(), - }), - disposeFinal: (values) => { - values.inlineCompletions.dispose(); - values.suggestWidgetInlineCompletions.dispose(); - }, - changeTracker: recordChanges({ versionId: this._versionId }), - update: (reader, previousValue, changes) => { - const edit = StringEdit.compose(changes.changes.map(c => c.change ? offsetEditFromContentChanges(c.change.changes) : StringEdit.empty).filter(isDefined)); - - if (edit.isEmpty()) { - return previousValue; - } - try { - return { - inlineCompletions: previousValue.inlineCompletions.createStateWithAppliedEdit(edit, this._textModel), - suggestWidgetInlineCompletions: previousValue.suggestWidgetInlineCompletions.createStateWithAppliedEdit(edit, this._textModel), - }; - } finally { - previousValue.inlineCompletions.dispose(); - previousValue.suggestWidgetInlineCompletions.dispose(); - } - } - }); - this.inlineCompletions = this._state.map(this, v => v.inlineCompletions); - this.suggestWidgetInlineCompletions = this._state.map(this, v => v.suggestWidgetInlineCompletions); - this.clearOperationOnTextModelChange = derived(this, reader => { - this._versionId.read(reader); - this._updateOperation.clear(); - return undefined; // always constant - }); - this._loadingCount = observableValue(this, 0); - this.loading = this._loadingCount.map(this, v => v > 0); this.clearOperationOnTextModelChange.recomputeInitiallyAndOnChange(this._store); } - public readonly clearOperationOnTextModelChange; + public readonly clearOperationOnTextModelChange = derived(this, reader => { + this._versionId.read(reader); + this._updateOperation.clear(); + return undefined; // always constant + }); private _log(entry: { sourceId: string; kind: 'start'; requestId: number; context: unknown } & IRecordableEditorLogEntry @@ -114,8 +107,8 @@ export class InlineCompletionsSource extends Disposable { this._structuredFetchLogger.log(entry); } - private readonly _loadingCount; - public readonly loading; + private readonly _loadingCount = observableValue(this, 0); + public readonly loading = this._loadingCount.map(this, v => v > 0); public fetch(providers: InlineCompletionsProvider[], context: InlineCompletionContextWithoutUuid, activeInlineCompletion: InlineSuggestionIdentity | undefined, withDebounce: boolean, userJumpedToActiveCompletion: IObservable, providerhasChangedCompletion: boolean, requestInfo: InlineSuggestRequestInfo): Promise { const position = this._cursorPosition.get(); From e2d204cc35a68925bd2480525eeb6d7e03f54fe9 Mon Sep 17 00:00:00 2001 From: isidorn Date: Fri, 4 Jul 2025 18:39:19 +0200 Subject: [PATCH 150/306] update untitled editor placeholder --- .../emptyTextEditorHint.ts | 44 +++---------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts index aa18d550d2a..58487b1242e 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts @@ -18,9 +18,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { ConfigurationChangedEvent, EditorOption } from '../../../../../editor/common/config/editorOptions.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../../editor/browser/editorExtensions.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; -import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; import { IContentActionHandler, renderFormattedText } from '../../../../../base/browser/formattedTextRenderer.js'; -import { ApplyFileSnippetAction } from '../../../snippets/browser/commands/fileTemplateSnippets.js'; import { IInlineChatSessionService } from '../../../inlineChat/browser/inlineChatSessionService.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../../base/common/actions.js'; @@ -144,7 +142,6 @@ class EmptyTextEditorHintContentWidget extends Disposable implements IContentWid constructor( private readonly editor: ICodeEditor, - @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @ICommandService private readonly commandService: ICommandService, @IConfigurationService private readonly configurationService: IConfigurationService, @IKeybindingService private readonly keybindingService: IKeybindingService, @@ -213,12 +210,9 @@ class EmptyTextEditorHintContentWidget extends Disposable implements IContentWid hasInlineChatProvider ? askSomething(event.browserEvent) : languageOnClickOrTap(event.browserEvent); break; case '1': - hasInlineChatProvider ? languageOnClickOrTap(event.browserEvent) : snippetOnClickOrTap(event.browserEvent); + hasInlineChatProvider ? languageOnClickOrTap(event.browserEvent) : this.disableHint(); break; case '2': - hasInlineChatProvider ? snippetOnClickOrTap(event.browserEvent) : chooseEditorOnClickOrTap(event.browserEvent); - break; - case '3': this.disableHint(); break; } @@ -247,33 +241,7 @@ class EmptyTextEditorHintContentWidget extends Disposable implements IContentWid this.editor.focus(); }; - const snippetOnClickOrTap = async (e: UIEvent) => { - e.stopPropagation(); - - this.telemetryService.publicLog2('workbenchActionExecuted', { - id: ApplyFileSnippetAction.Id, - from: 'hint' - }); - await this.commandService.executeCommand(ApplyFileSnippetAction.Id); - }; - - const chooseEditorOnClickOrTap = async (e: UIEvent) => { - e.stopPropagation(); - - const activeEditorInput = this.editorGroupsService.activeGroup.activeEditor; - this.telemetryService.publicLog2('workbenchActionExecuted', { - id: 'welcome.showNewFileEntries', - from: 'hint' - }); - const newEditorSelected = await this.commandService.executeCommand('welcome.showNewFileEntries', { from: 'hint' }); - - // Close the active editor as long as it is untitled (swap the editors out) - if (newEditorSelected && activeEditorInput !== null && activeEditorInput.resource?.scheme === Schemas.untitled) { - this.editorGroupsService.activeGroup.closeEditor(activeEditorInput, { preserveFocus: true }); - } - }; - - const keybindingsLookup = hasInlineChatProvider ? [askSomethingCommandId, ChangeLanguageAction.ID, ApplyFileSnippetAction.Id] : [ChangeLanguageAction.ID, ApplyFileSnippetAction.Id, 'welcome.showNewFileEntries']; + const keybindingsLookup = [askSomethingCommandId, ChangeLanguageAction.ID]; const keybindingLabels = keybindingsLookup.map(id => this.keybindingService.lookupKeybinding(id)?.getLabel()); const hintMsg = (hasInlineChatProvider ? localize({ @@ -282,13 +250,13 @@ class EmptyTextEditorHintContentWidget extends Disposable implements IContentWid 'Preserve double-square brackets and their order', 'language refers to a programming language' ] - }, '[[Open chat]] ({0}), or [[select a language]] ({1}), or [[fill with template]] ({2}) to get started.\nStart typing to dismiss or [[don\'t show]] this again.', keybindingLabels.at(0) ?? '', keybindingLabels.at(1) ?? '', keybindingLabels.at(2) ?? '') : localize({ + }, '[[Generate code]] ({0}), or [[select a language]] ({1}). Start typing to dismiss or [[don\'t show]] this again.', keybindingLabels.at(0) ?? '', keybindingLabels.at(1) ?? '') : localize({ key: 'emptyTextEditorHintWithoutInlineChat', comment: [ 'Preserve double-square brackets and their order', 'language refers to a programming language' ] - }, '[[Select a language]] ({0}), or [[fill with template]] ({1}), or [[open a different editor]] ({2}) to get started.\nStart typing to dismiss or [[don\'t show]] this again.', keybindingLabels.at(0) ?? '', keybindingLabels.at(1) ?? '', keybindingLabels.at(2) ?? '')).replaceAll(' ()', ''); + }, '[[Select a language]] ({0}) to get started. Start typing to dismiss or [[don\'t show]] this again.', keybindingLabels.at(1) ?? '')).replaceAll(' ()', ''); const hintElement = renderFormattedText(hintMsg, { actionHandler: hintHandler, renderCodeSegments: false, @@ -296,8 +264,8 @@ class EmptyTextEditorHintContentWidget extends Disposable implements IContentWid hintElement.style.fontStyle = 'italic'; const ariaLabel = hasInlineChatProvider ? - localize('defaultHintAriaLabelWithInlineChat', 'Execute {0} to ask a question, execute {1} to select a language, or execute {2} to fill with template and get started. Start typing to dismiss.', ...keybindingLabels) : - localize('defaultHintAriaLabelWithoutInlineChat', 'Execute {0} to select a language, execute {1} to fill with template, or execute {2} to open a different editor and get started. Start typing to dismiss.', ...keybindingLabels); + localize('defaultHintAriaLabelWithInlineChat', 'Execute {0} to ask a question, execute {1} to select a language and get started. Start typing to dismiss.', ...keybindingLabels) : + localize('defaultHintAriaLabelWithoutInlineChat', 'Execute {0} to select a language and get started. Start typing to dismiss.', ...keybindingLabels); for (const anchor of hintElement.querySelectorAll('a')) { anchor.style.cursor = 'pointer'; } From fd173fc37af764dfa483adec515e7d737347eb41 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 4 Jul 2025 19:25:40 +0200 Subject: [PATCH 151/306] Fixes https://github.com/microsoft/vscode/issues/254153 (#254159) --- src/vs/workbench/api/common/extHostCommands.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index 0bda2e82e46..d914f21a29d 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -287,6 +287,10 @@ export class ExtHostCommands implements ExtHostCommandsShape { if (!command.extension) { return; } + if (id.startsWith('code.copilot.logStructured')) { + // This command is very active. See https://github.com/microsoft/vscode/issues/254153. + return; + } type ExtensionActionTelemetry = { extensionId: string; id: TelemetryTrustedValue; From d40925ecf5caf7acefccfc47ca509510626ec3c3 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 4 Jul 2025 19:28:02 +0200 Subject: [PATCH 152/306] Improves fetch log (#254156) --- src/vs/editor/common/languages.ts | 13 ++++++++++ .../browser/model/inlineCompletionsModel.ts | 8 ++++--- .../browser/model/inlineCompletionsSource.ts | 24 ++++++++++++++++--- src/vs/monaco.d.ts | 1 + .../api/browser/mainThreadLanguageFeatures.ts | 1 + 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 80995eee9b8..844fcf9886f 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -905,6 +905,8 @@ export interface InlineCompletionsProvider !!jumpedTo && jumpedTo === this._inlineCompletionItems.get()?.inlineEdit?.semanticId); - const providers = changeSummary.provider ? [changeSummary.provider] : this._languageFeaturesService.inlineCompletionsProvider.all(this.textModel); + const providers = changeSummary.provider + ? { providers: [changeSummary.provider], label: 'single:' + changeSummary.provider.providerId } + : { providers: this._languageFeaturesService.inlineCompletionsProvider.all(this.textModel), label: undefined }; const suppressedProviderGroupIds = this._suppressedInlineCompletionGroupIds.get(); - const availableProviders = providers.filter(provider => !(provider.groupId && suppressedProviderGroupIds.has(provider.groupId))); + const availableProviders = providers.providers.filter(provider => !(provider.groupId && suppressedProviderGroupIds.has(provider.groupId))); - return this._source.fetch(availableProviders, context, itemToPreserve?.identity, changeSummary.shouldDebounce, userJumpedToActiveCompletion, !!changeSummary.provider, requestInfo); + return this._source.fetch(availableProviders, providers.label, context, itemToPreserve?.identity, changeSummary.shouldDebounce, userJumpedToActiveCompletion, !!changeSummary.provider, requestInfo); }); public async trigger(tx?: ITransaction, options?: { onlyFetchInlineEdits?: boolean; noDelay?: boolean }): Promise { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index b1fa5014d95..767fecab227 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -98,7 +98,7 @@ export class InlineCompletionsSource extends Disposable { }); private _log(entry: - { sourceId: string; kind: 'start'; requestId: number; context: unknown } & IRecordableEditorLogEntry + { sourceId: string; kind: 'start'; requestId: number; context: unknown; provider: string | undefined } & IRecordableEditorLogEntry | { sourceId: string; kind: 'end'; error: unknown; durationMs: number; result: unknown; requestId: number; didAllProvidersReturn: boolean } & IRecordableLogEntry ) { if (this._loggingEnabled.get()) { @@ -110,7 +110,16 @@ export class InlineCompletionsSource extends Disposable { private readonly _loadingCount = observableValue(this, 0); public readonly loading = this._loadingCount.map(this, v => v > 0); - public fetch(providers: InlineCompletionsProvider[], context: InlineCompletionContextWithoutUuid, activeInlineCompletion: InlineSuggestionIdentity | undefined, withDebounce: boolean, userJumpedToActiveCompletion: IObservable, providerhasChangedCompletion: boolean, requestInfo: InlineSuggestRequestInfo): Promise { + public fetch( + providers: InlineCompletionsProvider[], + providersLabel: string | undefined, + context: InlineCompletionContextWithoutUuid, + activeInlineCompletion: InlineSuggestionIdentity | undefined, + withDebounce: boolean, + userJumpedToActiveCompletion: IObservable, + providerhasChangedCompletion: boolean, + requestInfo: InlineSuggestRequestInfo + ): Promise { const position = this._cursorPosition.get(); const request = new UpdateRequest(position, context, this._textModel.getVersionId(), new Set(providers)); @@ -150,7 +159,16 @@ export class InlineCompletionsSource extends Disposable { const requestId = InlineCompletionsSource._requestId++; if (this._loggingEnabled.get() || this._structuredFetchLogger.isEnabled.get()) { - this._log({ sourceId: 'InlineCompletions.fetch', kind: 'start', requestId, modelUri: this._textModel.uri, modelVersion: this._textModel.getVersionId(), context: { triggerKind: context.triggerKind }, time: Date.now() }); + this._log({ + sourceId: 'InlineCompletions.fetch', + kind: 'start', + requestId, + modelUri: this._textModel.uri, + modelVersion: this._textModel.getVersionId(), + context: { triggerKind: context.triggerKind, suggestInfo: context.selectedSuggestionInfo ? true : undefined }, + time: Date.now(), + provider: providersLabel, + }); } const startTime = new Date(); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 80dec01eee6..921d01dd9e4 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7512,6 +7512,7 @@ declare namespace monaco.languages { * Multiple providers can have the same group id. */ groupId?: InlineCompletionProviderGroupId; + providerId?: string; /** * Returns a list of preferred provider {@link groupId}s. * The current provider is only requested for completions if no provider with a preferred group id returned a result. diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 01cbab7d060..e7c975f3f90 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -685,6 +685,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread } }, groupId: groupId ?? extensionId, + providerId: new languages.VersionedExtensionId(extensionId, extensionVersion).toString(), yieldsToGroupIds: yieldsToExtensionIds, debounceDelayMs, displayName, From ef2abd908f1b3772520f877fbdaff98c361903fa Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 4 Jul 2025 19:28:32 +0200 Subject: [PATCH 153/306] Don't return the result list if operation got cancelled. (#254158) --- .../browser/model/provideInlineCompletions.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts index 56944ac1c37..f857484ea46 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts @@ -104,6 +104,9 @@ export function provideInlineCompletions( runWhenCancelled(cancellationTokenSource.token, () => { return list.removeRef(cancelReason); }); + if (cancellationTokenSource.token.isCancellationRequested) { + return undefined; // The list is disposed now, so we cannot return the items! + } for (const item of result.items) { data.push(toInlineSuggestData(item, list, defaultReplaceRange, model, languageConfigurationService, contextWithUuid, requestInfo)); From f52e13bc6b127586b7c5c93e6c17147914aab8e0 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 4 Jul 2025 19:12:56 +0000 Subject: [PATCH 154/306] Engineering - pull request actions should read from the cache (#254154) --- .github/workflows/pr-darwin-test.yml | 4 ++-- .github/workflows/pr-linux-test.yml | 4 ++-- .github/workflows/pr-win32-test.yml | 4 ++-- .github/workflows/pr.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr-darwin-test.yml b/.github/workflows/pr-darwin-test.yml index 0f536f4cb36..4c21f9c65b4 100644 --- a/.github/workflows/pr-darwin-test.yml +++ b/.github/workflows/pr-darwin-test.yml @@ -38,7 +38,7 @@ jobs: - name: Restore node_modules cache id: cache-node-modules - uses: actions/cache@v4 + uses: actions/cache/restore@v4 with: path: .build/node_modules_cache key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" @@ -91,7 +91,7 @@ jobs: - name: Restore built-in extensions cache id: cache-builtin-extensions - uses: actions/cache@v4 + uses: actions/cache/restore@v4 with: path: .build/builtInExtensions key: "builtin-extensions-${{ hashFiles('.build/builtindepshash') }}" diff --git a/.github/workflows/pr-linux-test.yml b/.github/workflows/pr-linux-test.yml index 6e75ada8b84..496ef7e77c1 100644 --- a/.github/workflows/pr-linux-test.yml +++ b/.github/workflows/pr-linux-test.yml @@ -55,7 +55,7 @@ jobs: - name: Restore node_modules cache id: cache-node-modules - uses: actions/cache@v4 + uses: actions/cache/restore@v4 with: path: .build/node_modules_cache key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" @@ -119,7 +119,7 @@ jobs: - name: Restore built-in extensions cache id: cache-builtin-extensions - uses: actions/cache@v4 + uses: actions/cache/restore@v4 with: path: .build/builtInExtensions key: "builtin-extensions-${{ hashFiles('.build/builtindepshash') }}" diff --git a/.github/workflows/pr-win32-test.yml b/.github/workflows/pr-win32-test.yml index 35b39311be5..30dc40961a3 100644 --- a/.github/workflows/pr-win32-test.yml +++ b/.github/workflows/pr-win32-test.yml @@ -40,7 +40,7 @@ jobs: node build/azure-pipelines/common/computeNodeModulesCacheKey.js win32 ${{ env.VSCODE_ARCH }} $(node -p process.arch) > .build/packagelockhash - name: Restore node_modules cache - uses: actions/cache@v4 + uses: actions/cache/restore@v4 id: node-modules-cache with: path: .build/node_modules_cache @@ -100,7 +100,7 @@ jobs: - name: Restore built-in extensions cache id: cache-builtin-extensions - uses: actions/cache@v4 + uses: actions/cache/restore@v4 with: path: .build/builtInExtensions key: "builtin-extensions-${{ hashFiles('.build/builtindepshash') }}" diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d1689793e0e..885e493e108 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -32,7 +32,7 @@ jobs: - name: Restore node_modules cache id: cache-node-modules - uses: actions/cache@v4 + uses: actions/cache/restore@v4 with: path: .build/node_modules_cache key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" From 1e610b289a7c00c0b3a986b523e7b19884594f32 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Fri, 4 Jul 2025 23:07:13 -0700 Subject: [PATCH 155/306] Focus input on chat widget before setting prompt (#254204) --- .../chat/browser/viewsWelcome/chatViewWelcomeController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index 5f501338086..603baeeb982 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -203,6 +203,7 @@ export class ChatViewWelcomePart extends Disposable { suggestedPrompt: prompt.prompt, }); + this.chatWidgetService.lastFocusedWidget?.focusInput(); this.chatWidgetService.lastFocusedWidget?.setInput(prompt.prompt); })); } From f62eaddb5f60c413d40604437fc2d2149465ddd5 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Fri, 4 Jul 2025 23:07:20 -0700 Subject: [PATCH 156/306] Move createInput out of welcomeViewContent creation. (#254205) --- .../contrib/chat/browser/chatWidget.ts | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 7cf409db576..863a9aabb5e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -672,6 +672,15 @@ export class ChatWidget extends Disposable implements IChatWidget { }; }); + + // reset the input in welcome view if it was rendered in experimental mode + if (this.container.classList.contains('experimental-welcome-view')) { + this.container.classList.remove('experimental-welcome-view'); + const renderFollowups = this.viewOptions.renderFollowups ?? false; + const renderStyle = this.viewOptions.renderStyle; + this.createInput(this.container, { renderFollowups, renderStyle }); + } + this.renderWelcomeViewContentIfNeeded(); this._onWillMaybeChangeHeight.fire(); @@ -715,20 +724,6 @@ export class ChatWidget extends Disposable implements IChatWidget { private renderWelcomeViewContentIfNeeded() { - // reset the input in welcome view if it was rendered in experimental mode - if (this.container.classList.contains('experimental-welcome-view')) { - this.container.classList.remove('experimental-welcome-view'); - // Preserve the current mode before recreating the input - const currentMode = this.input?.currentModeKind; - const renderFollowups = this.viewOptions.renderFollowups ?? false; - const renderStyle = this.viewOptions.renderStyle; - this.createInput(this.container, { renderFollowups, renderStyle }); - // Restore the mode after recreating the input - if (currentMode && this.input) { - this.input.setChatMode(currentMode, false); - } - } - if (this.viewOptions.renderStyle === 'compact' || this.viewOptions.renderStyle === 'minimal') { return; } @@ -747,8 +742,7 @@ export class ChatWidget extends Disposable implements IChatWidget { ); let welcomeContent: IChatViewWelcomeContent; - const enabled = false; - if (enabled && (startupExpValue === StartupExperimentGroup.MaximizedChat + if ((startupExpValue === StartupExperimentGroup.MaximizedChat || startupExpValue === StartupExperimentGroup.SplitEmptyEditorChat || startupExpValue === StartupExperimentGroup.SplitWelcomeChat || expIsActive) && this.contextKeyService.contextMatchesRules(chatSetupTriggerContext)) { From 1773a9fcc972da93f59b7d298632c0831ddbf240 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Sat, 5 Jul 2025 12:28:14 +0200 Subject: [PATCH 157/306] Add *.tsbuildinfo to .gitignore The `.tsbuildinfo` file is generated by TypeScript when using certain compiler options or running `tsc --build`. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b73ce578e7f..62394c60784 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,5 @@ vscode.db /cli/openssl product.overrides.json *.snap.actual +*.tsbuildinfo .vscode-test From c315e865bf38bbf9cde54dbb4c04d84c40fc809e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 6 Jul 2025 08:46:36 +0200 Subject: [PATCH 158/306] up `@playwright/test` to `1.53.2` (#254280) * up `@playwright/test` to `1.53.2` * enable tracing on CI always --- package-lock.json | 24 ++++++++++++------------ package.json | 2 +- test/smoke/src/main.ts | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 51c2dbe2b30..c72c61053d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "yazl": "^2.4.3" }, "devDependencies": { - "@playwright/test": "^1.52.0", + "@playwright/test": "^1.53.2", "@stylistic/eslint-plugin-ts": "^2.8.0", "@types/cookie": "^0.3.3", "@types/debug": "^4.1.5", @@ -1937,13 +1937,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", - "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "version": "1.53.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.2.tgz", + "integrity": "sha512-tEB2U5z74ebBeyfGNZ3Jfg29AnW+5HlWhvHtb/Mqco9pFdZU1ZLNdVb2UtB5CvmiilNr2ZfVH/qMmAROG/XTzw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.52.0" + "playwright": "1.53.2" }, "bin": { "playwright": "cli.js" @@ -13939,13 +13939,13 @@ } }, "node_modules/playwright": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", - "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "version": "1.53.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.2.tgz", + "integrity": "sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.52.0" + "playwright-core": "1.53.2" }, "bin": { "playwright": "cli.js" @@ -13971,9 +13971,9 @@ } }, "node_modules/playwright/node_modules/playwright-core": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", - "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "version": "1.53.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.2.tgz", + "integrity": "sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index b1a4469038c..0da56483be3 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "yazl": "^2.4.3" }, "devDependencies": { - "@playwright/test": "^1.52.0", + "@playwright/test": "^1.53.2", "@stylistic/eslint-plugin-ts": "^2.8.0", "@types/cookie": "^0.3.3", "@types/debug": "^4.1.5", diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index bdc5a3776e2..45bf283ce04 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -365,7 +365,7 @@ before(async function () { verbose: opts.verbose, remote: opts.remote, web: opts.web, - tracing: opts.tracing, + tracing: opts.tracing || process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE, headless: opts.headless, browser: opts.browser, extraArgs: (opts.electronArgs || '').split(' ').map(arg => arg.trim()).filter(arg => !!arg) From eb4b57f8438a77809b6838d208bb92541598c06f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 6 Jul 2025 09:21:38 +0200 Subject: [PATCH 159/306] layout - ensure panel/auxbar are focused if maximised after startup (#254294) * layout - ensure panel/auxbar are focused if maximised after startup * fix: ensure whenReadyPromise is completed before focusing the layout --- src/vs/workbench/browser/layout.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 2c3c925cb14..f9baf7254fe 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -1110,6 +1110,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Await for promises that we recorded to update // our ready and restored states properly. Promises.settled(layoutReadyPromises).finally(() => { + + // Focus the active maximized part in case we have + // not yet focused a specific element and panel + // or auxiliary bar are maximized. + if (getActiveElement() === mainWindow.document.body && (this.isPanelMaximized() || this.isAuxiliaryBarMaximized())) { + this.focus(); + } + this.whenReadyPromise.complete(); Promises.settled(layoutRestoredPromises).finally(() => { From 698b638444802ff62a78e5e4bb4933c04b769ca6 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Sun, 6 Jul 2025 10:34:00 +0000 Subject: [PATCH 160/306] Engineering - Add VSCODE_QUALITY global variable (#254290) Add VSCODE_QUALITY global variable --- .github/workflows/pr.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 885e493e108..ac8b8ad73a7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -12,6 +12,9 @@ concurrency: permissions: {} +env: + VSCODE_QUALITY: 'oss' + jobs: compile: name: Compile & Hygiene From 9b592e0c0ab8ec0077e368c506f6561bbddced45 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 6 Jul 2025 15:47:20 +0200 Subject: [PATCH 161/306] Implement `--transient` CLI option for stateless VS Code sessions (#254223) --- src/vs/code/node/cli.ts | 15 +++++++++++++++ src/vs/platform/environment/common/argv.ts | 1 + src/vs/platform/environment/node/argv.ts | 1 + 3 files changed, 17 insertions(+) diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 6d2e397a68d..f318aaaa953 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -242,6 +242,21 @@ export async function main(argv: string[]): Promise { }); } + // Handle --transient option + if (args['transient']) { + const tempParentDir = randomPath(tmpdir(), 'vscode'); + const tempUserDataDir = join(tempParentDir, 'data'); + const tempExtensionsDir = join(tempParentDir, 'extensions'); + + addArg(argv, '--user-data-dir', tempUserDataDir); + addArg(argv, '--extensions-dir', tempExtensionsDir); + addArg(argv, '--disable-updates'); + + if (args.verbose) { + console.log(`Warning: state is temporarily stored in: "${tempParentDir}"`); + } + } + const hasReadStdinArg = args._.some(arg => arg === '-') || args.chat?._.some(arg => arg === '-'); if (hasReadStdinArg) { // remove the "-" argument when we read from stdin diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 2ffbb9773bc..b097b84feed 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -107,6 +107,7 @@ export interface NativeParsedArgs { 'install-source'?: string; 'add-mcp'?: string[]; 'disable-updates'?: boolean; + 'transient'?: boolean; 'use-inmemory-secretstorage'?: boolean; 'password-store'?: string; 'disable-workspace-trust'?: boolean; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 342718df668..91cce390ae1 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -168,6 +168,7 @@ export const OPTIONS: OptionDescriptions> = { 'skip-welcome': { type: 'boolean' }, 'disable-telemetry': { type: 'boolean' }, 'disable-updates': { type: 'boolean' }, + 'transient': { type: 'boolean' }, 'use-inmemory-secretstorage': { type: 'boolean', deprecates: ['disable-keytar'] }, 'password-store': { type: 'string' }, 'disable-workspace-trust': { type: 'boolean' }, From 09723854ed2d6161446b45c5b0a2b749eecb28d9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 7 Jul 2025 07:46:30 +0200 Subject: [PATCH 162/306] cli - always print `--transient` info (#254383) --- src/vs/code/node/cli.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index f318aaaa953..d89f3cfe530 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -252,9 +252,7 @@ export async function main(argv: string[]): Promise { addArg(argv, '--extensions-dir', tempExtensionsDir); addArg(argv, '--disable-updates'); - if (args.verbose) { - console.log(`Warning: state is temporarily stored in: "${tempParentDir}"`); - } + console.log(`Warning: state is temporarily stored in: "${tempParentDir}"`); } const hasReadStdinArg = args._.some(arg => arg === '-') || args.chat?._.some(arg => arg === '-'); From aa65bacd06f0dc856c23845fa3e7c3b37accb599 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 7 Jul 2025 08:27:15 +0000 Subject: [PATCH 163/306] Engineering - use GITHUB_TOKEN in the "Compile & Hygiene" task (#254395) --- .github/workflows/pr.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ac8b8ad73a7..ec6f8a54ea5 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -83,6 +83,8 @@ jobs: - name: Compile & Hygiene run: npm exec -- npm-run-all -lp core-ci-pr extensions-ci-pr hygiene eslint valid-layers-check define-class-fields-check vscode-dts-compile-check tsec-compile-check + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} linux-cli-tests: name: Linux From c5e55875cb7c7c83350a3f169ca42155f987cea5 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 7 Jul 2025 12:48:12 +0200 Subject: [PATCH 164/306] default to marketplace latest api for all extension name requests (#254417) --- .../common/extensionGalleryService.ts | 36 +++++++------------ .../common/extensionManagement.ts | 1 - .../browser/extensionsWorkbenchService.ts | 2 +- 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index d23e8d441c6..1907be1803a 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -594,7 +594,7 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle const options = CancellationToken.isCancellationToken(arg1) ? {} : arg1 as IExtensionQueryOptions; const token = CancellationToken.isCancellationToken(arg1) ? arg1 : arg2 as CancellationToken; - const resourceApi = await this.getResourceApi(extensionGalleryManifest, !!options.updateCheck); + const resourceApi = await this.getResourceApi(extensionGalleryManifest); const result = resourceApi ? await this.getExtensionsUsingResourceApi(extensionInfos, options, resourceApi, extensionGalleryManifest, token) : await this.getExtensionsUsingQueryApi(extensionInfos, options, extensionGalleryManifest, token); @@ -626,35 +626,23 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle return result; } - private async getResourceApi(extensionGalleryManifest: IExtensionGalleryManifest, updateCheck: boolean): Promise<{ uri: string; fallback?: string } | undefined> { - const latestVersionResource = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, ExtensionGalleryResourceType.ExtensionLatestVersionUri); - if (!latestVersionResource) { - return undefined; - } - - if (this.productService.quality !== 'stable') { - return { - uri: latestVersionResource, - fallback: this.unpkgResourceApi - }; - } - - const value = updateCheck - ? await this.assignmentService?.getTreatment<'unpkg' | 'marketplace' | 'none'>('extensions.gallery.useResourceApi') ?? 'marketplace' - : await this.assignmentService?.getTreatment<'unpkg' | 'marketplace' | 'none'>('extensions.gallery.useLatestApi') ?? 'unpkg'; - - if (value === 'marketplace') { - return { - uri: latestVersionResource, - fallback: this.unpkgResourceApi - }; - } + private async getResourceApi(extensionGalleryManifest: IExtensionGalleryManifest): Promise<{ uri: string; fallback?: string } | undefined> { + const value = await this.assignmentService?.getTreatment<'unpkg' | 'marketplace'>('extensions.gallery.useResourceApi') ?? 'marketplace'; if (value === 'unpkg' && this.unpkgResourceApi) { return { uri: this.unpkgResourceApi }; } + const latestVersionResource = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, ExtensionGalleryResourceType.ExtensionLatestVersionUri); + if (latestVersionResource) { + return { + uri: latestVersionResource, + fallback: this.unpkgResourceApi + }; + } + return undefined; + } private async getExtensionsUsingQueryApi(extensionInfos: ReadonlyArray, options: IExtensionQueryOptions, extensionGalleryManifest: IExtensionGalleryManifest, token: CancellationToken): Promise { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 2f8d1e83b99..990349a24cf 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -383,7 +383,6 @@ export interface IExtensionQueryOptions { compatible?: boolean; queryAllVersions?: boolean; source?: string; - updateCheck?: boolean; } export interface IExtensionGalleryCapabilities { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 752dcc3196f..ffe36f413bb 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -1935,7 +1935,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension count: infos.length, }); this.logService.trace(`Checking updates for extensions`, infos.map(e => e.id).join(', ')); - const galleryExtensions = await this.galleryService.getExtensions(infos, { targetPlatform, compatible: true, productVersion: this.getProductVersion(), updateCheck: true }, CancellationToken.None); + const galleryExtensions = await this.galleryService.getExtensions(infos, { targetPlatform, compatible: true, productVersion: this.getProductVersion() }, CancellationToken.None); if (galleryExtensions.length) { await this.syncInstalledExtensionsWithGallery(galleryExtensions, infos); } From fd9e98cab8e5c2931052e0afec41b7e13cd6059f Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 7 Jul 2025 20:48:47 +1000 Subject: [PATCH 165/306] Update notebook troubleshooting tool to display EOL (#254415) --- .../notebook/browser/contrib/troubleshoot/layout.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts b/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts index 2748815fe1a..023929630ef 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts @@ -182,10 +182,13 @@ export class TroubleshootController extends Disposable implements INotebookEdito topLine.style.backgroundColor = 'rgba(255, 0, 0, 0.7)'; overlayContainer.appendChild(topLine); - const cellTop = this._notebookEditor.getAbsoluteTopOfElement(cell); - + const getLayoutInfo = () => { + const eol = cell.textModel?.getEOL() === '\n' ? 'LF' : 'CRLF'; + const scrollTop = this._notebookEditor.getAbsoluteTopOfElement(cell); + return `cell #${index} (handle: ${cell.handle}) | AbsoluteTopOfElement: ${scrollTop}px | EOL: ${eol}`; + }; const label = document.createElement('div'); - label.textContent = `cell #${index} (handle: ${cell.handle}) | AbsoluteTopOfElement: ${cellTop}px`; + label.textContent = getLayoutInfo(); label.style.position = 'absolute'; label.style.top = '0px'; label.style.right = '10px'; @@ -213,10 +216,8 @@ export class TroubleshootController extends Disposable implements INotebookEdito // Update overlay when layout changes const updateLayout = () => { - const scrollTop = this._notebookEditor.getAbsoluteTopOfElement(cell); - // Update label text - label.textContent = `cell #${index} (handle: ${cell.handle}) | AbsoluteTopOfElement: ${scrollTop}px`; + label.textContent = getLayoutInfo(); // Refresh the overlay position if (overlayId) { From 0f91086c54ffef1cd76c4fdb236fa85d5f2932e2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 7 Jul 2025 04:11:33 -0700 Subject: [PATCH 166/306] autoApprove warning Fixes #253039 --- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 41a4e58260e..d5fee751611 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -213,7 +213,7 @@ configurationRegistry.registerConfiguration({ }, 'chat.tools.autoApprove': { default: false, - description: nls.localize('chat.tools.autoApprove', "Controls whether tool use should be automatically approved."), + markdownDescription: nls.localize('chat.tools.autoApprove', "Controls whether tool use should be automatically approved.\n\nThis will allow _all_ tools to run automatically without user confirmation, overriding any tool-specific settings such as terminal auto-approval. Use with caution and be extra wary of possible sources of prompt injection!"), type: 'boolean', tags: ['experimental'], policy: { From ad0e0ef046a68174a28c09aa4574afe91a53eed7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 7 Jul 2025 04:26:11 -0700 Subject: [PATCH 167/306] Update src/vs/workbench/contrib/chat/browser/chat.contribution.ts Co-authored-by: Nick Trogh <1908215+ntrogh@users.noreply.github.com> --- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index d5fee751611..eaef9e9d6d7 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -213,7 +213,7 @@ configurationRegistry.registerConfiguration({ }, 'chat.tools.autoApprove': { default: false, - markdownDescription: nls.localize('chat.tools.autoApprove', "Controls whether tool use should be automatically approved.\n\nThis will allow _all_ tools to run automatically without user confirmation, overriding any tool-specific settings such as terminal auto-approval. Use with caution and be extra wary of possible sources of prompt injection!"), + markdownDescription: nls.localize('chat.tools.autoApprove', "Controls whether tool use should be automatically approved.\n\nAllows _all_ tools to run automatically without user confirmation, overriding any tool-specific settings such as terminal auto-approval.\n\nUse with caution: carefully review selected tools and be extra wary of possible sources of prompt injection!"), type: 'boolean', tags: ['experimental'], policy: { From 49050d59b1e4c9750601edf017ff3ec982db4860 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 7 Jul 2025 11:28:30 +0000 Subject: [PATCH 168/306] Switch to :not approach to prevent text selection on buttons in hovers Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com> --- src/vs/base/browser/ui/hover/hoverWidget.css | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/vs/base/browser/ui/hover/hoverWidget.css b/src/vs/base/browser/ui/hover/hoverWidget.css index ccaac1ed9b2..074176c15dd 100644 --- a/src/vs/base/browser/ui/hover/hoverWidget.css +++ b/src/vs/base/browser/ui/hover/hoverWidget.css @@ -7,13 +7,17 @@ cursor: default; position: absolute; overflow: hidden; - user-select: text; - -webkit-user-select: text; box-sizing: border-box; line-height: 1.5em; white-space: var(--vscode-hover-whiteSpace, normal); } +/* Enable text selection for all elements except button-like elements */ +.monaco-hover :not(.action-container):not(.action):not(button):not(.monaco-button):not(.monaco-text-button):not([role="button"]) { + user-select: text; + -webkit-user-select: text; +} + .monaco-hover.fade-in { animation: fadein 100ms linear; } @@ -207,14 +211,3 @@ opacity: 0.4; cursor: default; } - -/* Prevent text selection in all button-like elements within hovers */ -.monaco-hover .action-container, -.monaco-hover .action, -.monaco-hover button, -.monaco-hover .monaco-button, -.monaco-hover .monaco-text-button, -.monaco-hover [role="button"] { - -webkit-user-select: none; - user-select: none; -} From 0c779fa53e0ee2538da9e7c7039351851b341d2f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 7 Jul 2025 04:39:26 -0700 Subject: [PATCH 169/306] Update shell type when it changes --- src/vs/workbench/contrib/terminal/browser/terminalInstance.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 7fb424f6eab..be55daf2992 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -489,6 +489,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } })); + this._register(this.onDidChangeShellType(() => refreshShellIntegrationInfoStatus(this))); this._register(this.capabilities.onDidRemoveCapabilityType(capability => { capabilityListeners.get(capability)?.dispose(); })); From b026898d94e723f0e04b370252ffc2d4257ce737 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 7 Jul 2025 05:24:36 -0700 Subject: [PATCH 170/306] Revert "Switch to :not approach to prevent text selection on buttons in hovers" This reverts commit 49050d59b1e4c9750601edf017ff3ec982db4860. --- src/vs/base/browser/ui/hover/hoverWidget.css | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/vs/base/browser/ui/hover/hoverWidget.css b/src/vs/base/browser/ui/hover/hoverWidget.css index 074176c15dd..ccaac1ed9b2 100644 --- a/src/vs/base/browser/ui/hover/hoverWidget.css +++ b/src/vs/base/browser/ui/hover/hoverWidget.css @@ -7,17 +7,13 @@ cursor: default; position: absolute; overflow: hidden; + user-select: text; + -webkit-user-select: text; box-sizing: border-box; line-height: 1.5em; white-space: var(--vscode-hover-whiteSpace, normal); } -/* Enable text selection for all elements except button-like elements */ -.monaco-hover :not(.action-container):not(.action):not(button):not(.monaco-button):not(.monaco-text-button):not([role="button"]) { - user-select: text; - -webkit-user-select: text; -} - .monaco-hover.fade-in { animation: fadein 100ms linear; } @@ -211,3 +207,14 @@ opacity: 0.4; cursor: default; } + +/* Prevent text selection in all button-like elements within hovers */ +.monaco-hover .action-container, +.monaco-hover .action, +.monaco-hover button, +.monaco-hover .monaco-button, +.monaco-hover .monaco-text-button, +.monaco-hover [role="button"] { + -webkit-user-select: none; + user-select: none; +} From 69ca3cf7abc7d4effb127e5666143340385ff335 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 7 Jul 2025 05:40:49 -0700 Subject: [PATCH 171/306] Polish comment, remove useless test --- .../suggest/browser/terminalSuggestAddon.ts | 6 ++-- .../test/browser/terminalSuggestAddon.test.ts | 36 ------------------- 2 files changed, 3 insertions(+), 39 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 4635b259004..a132f371522 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -254,9 +254,9 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } // Wait for the shell type to initialize. This will wait a short period after launching to - // allow the shell type to be set if possible. This prevents user requests sometimes getting lost - // if requested shortly after the terminal is created. Completion providers can still work - // with undefined shell types (e.g., for pseudoterminal-based terminals). + // allow the shell type to be set if possible. This prevents user requests sometimes getting + // lost if requested shortly after the terminal is created. Completion providers can still + // work with undefined shell types such as Pseudoterminal-based extension terminals. await this._shellTypeInit; let doNotRequestExtensionCompletions = false; diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.test.ts index 41c644577e7..046f0f15800 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.test.ts @@ -33,39 +33,3 @@ suite('Terminal Suggest Addon - Inline Completion, Shell Type Support', () => { strictEqual(isInlineCompletionSupported(undefined), false); }); }); - -suite('Terminal Suggest Addon - Provider Filtering with Undefined Shell Type', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - /** - * Test function that simulates the provider filtering logic from TerminalCompletionService._collectCompletions - */ - function shouldProviderBeFiltered(provider: { shellTypes?: string[] }, shellType: string | undefined): boolean { - // This replicates the logic from terminalCompletionService.ts line 169 - if (provider.shellTypes && shellType && !provider.shellTypes.includes(shellType)) { - return true; // Provider should be filtered out - } - return false; // Provider should be included - } - - test('providers with no shellTypes restriction should work with undefined shellType', () => { - const provider = {}; // No shellTypes specified - strictEqual(shouldProviderBeFiltered(provider, undefined), false); - }); - - test('providers with shellTypes restriction should work with undefined shellType', () => { - const provider = { shellTypes: ['bash', 'zsh'] }; - strictEqual(shouldProviderBeFiltered(provider, undefined), false); - }); - - test('providers with shellTypes restriction should work with matching shellType', () => { - const provider = { shellTypes: ['bash', 'zsh'] }; - strictEqual(shouldProviderBeFiltered(provider, 'bash'), false); - strictEqual(shouldProviderBeFiltered(provider, 'zsh'), false); - }); - - test('providers with shellTypes restriction should be filtered out for non-matching shellType', () => { - const provider = { shellTypes: ['bash'] }; - strictEqual(shouldProviderBeFiltered(provider, 'powershell'), true); - }); -}); From 4d5d3155bd89023b2598ff096a2b4b11721bb6dc Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Mon, 7 Jul 2025 15:07:55 +0200 Subject: [PATCH 172/306] Fixes https://github.com/microsoft/vscode/issues/253216 (#253910) --- .../contrib/inlineCompletions/browser/controller/commands.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts index 4f203a95a52..00683c3398e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts @@ -195,6 +195,7 @@ export class AcceptInlineCompletion extends EditorAction { EditorContextKeys.tabMovesFocus.toNegated(), SuggestContext.Visible.toNegated(), EditorContextKeys.hoverFocused.toNegated(), + InlineCompletionContextKeys.hasSelection.toNegated(), InlineCompletionContextKeys.inlineSuggestionHasIndentationLessThanTabSize, ), From c02c34fb43279b1c9424eb86348685ed3ad6411c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 7 Jul 2025 16:32:59 +0200 Subject: [PATCH 173/306] themes - ensure default colors match default themes (#254222) --- .../themes/common/workbenchThemeService.ts | 298 +++++++++++++++--- 1 file changed, 259 insertions(+), 39 deletions(-) diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index 3806f1a2d6c..4ec328971cd 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -52,57 +52,277 @@ export enum ThemeSettingDefaults { } export const COLOR_THEME_DARK_INITIAL_COLORS = { - 'activityBar.activeBorder': '#0078d4', + 'activityBar.activeBorder': '#0078D4', 'activityBar.background': '#181818', - 'activityBar.border': '#2b2b2b', - 'activityBar.foreground': '#d7d7d7', + 'activityBar.border': '#2B2B2B', + 'activityBar.foreground': '#D7D7D7', 'activityBar.inactiveForeground': '#868686', - 'editorGroup.border': '#ffffff17', + 'activityBarBadge.background': '#0078D4', + 'activityBarBadge.foreground': '#FFFFFF', + 'badge.background': '#616161', + 'badge.foreground': '#F8F8F8', + 'button.background': '#0078D4', + 'button.border': '#FFFFFF12', + 'button.foreground': '#FFFFFF', + 'button.hoverBackground': '#026EC1', + 'button.secondaryBackground': '#313131', + 'button.secondaryForeground': '#CCCCCC', + 'button.secondaryHoverBackground': '#3C3C3C', + 'chat.slashCommandBackground': '#26477866', + 'chat.slashCommandForeground': '#85B6FF', + 'chat.editedFileForeground': '#E2C08D', + 'checkbox.background': '#313131', + 'checkbox.border': '#3C3C3C', + 'debugToolBar.background': '#181818', + 'descriptionForeground': '#9D9D9D', + 'dropdown.background': '#313131', + 'dropdown.border': '#3C3C3C', + 'dropdown.foreground': '#CCCCCC', + 'dropdown.listBackground': '#1F1F1F', + 'editor.background': '#1F1F1F', + 'editor.findMatchBackground': '#9E6A03', + 'editor.foreground': '#CCCCCC', + 'editorGroup.border': '#FFFFFF17', 'editorGroupHeader.tabsBackground': '#181818', - 'editorGroupHeader.tabsBorder': '#2b2b2b', + 'editorGroupHeader.tabsBorder': '#2B2B2B', + 'editorGutter.addedBackground': '#2EA043', + 'editorGutter.deletedBackground': '#F85149', + 'editorGutter.modifiedBackground': '#0078D4', + 'editorLineNumber.activeForeground': '#CCCCCC', + 'editorLineNumber.foreground': '#6E7681', + 'editorOverviewRuler.border': '#010409', + 'editorWidget.background': '#202020', + 'errorForeground': '#F85149', + 'focusBorder': '#0078D4', + 'foreground': '#CCCCCC', + 'icon.foreground': '#CCCCCC', + 'input.background': '#313131', + 'input.border': '#3C3C3C', + 'input.foreground': '#CCCCCC', + 'input.placeholderForeground': '#989898', + 'inputOption.activeBackground': '#2489DB82', + 'inputOption.activeBorder': '#2488DB', + 'keybindingLabel.foreground': '#CCCCCC', + 'menu.background': '#1F1F1F', + 'menu.selectionBackground': '#0078d4', + 'notificationCenterHeader.background': '#1F1F1F', + 'notificationCenterHeader.foreground': '#CCCCCC', + 'notifications.background': '#1F1F1F', + 'notifications.border': '#2B2B2B', + 'notifications.foreground': '#CCCCCC', + 'panel.background': '#181818', + 'panel.border': '#2B2B2B', + 'panelInput.border': '#2B2B2B', + 'panelTitle.activeBorder': '#0078D4', + 'panelTitle.activeForeground': '#CCCCCC', + 'panelTitle.inactiveForeground': '#9D9D9D', + 'peekViewEditor.background': '#1F1F1F', + 'peekViewEditor.matchHighlightBackground': '#BB800966', + 'peekViewResult.background': '#1F1F1F', + 'peekViewResult.matchHighlightBackground': '#BB800966', + 'pickerGroup.border': '#3C3C3C', + 'progressBar.background': '#0078D4', + 'quickInput.background': '#222222', + 'quickInput.foreground': '#CCCCCC', + 'settings.dropdownBackground': '#313131', + 'settings.dropdownBorder': '#3C3C3C', + 'settings.headerForeground': '#FFFFFF', + 'settings.modifiedItemIndicator': '#BB800966', + 'sideBar.background': '#181818', + 'sideBar.border': '#2B2B2B', + 'sideBar.foreground': '#CCCCCC', + 'sideBarSectionHeader.background': '#181818', + 'sideBarSectionHeader.border': '#2B2B2B', + 'sideBarSectionHeader.foreground': '#CCCCCC', + 'sideBarTitle.foreground': '#CCCCCC', 'statusBar.background': '#181818', - 'statusBar.border': '#2b2b2b', - 'statusBar.foreground': '#cccccc', - 'statusBar.noFolderBackground': '#1f1f1f', - 'tab.activeBackground': '#1f1f1f', - 'tab.activeBorder': '#1f1f1f', - 'tab.activeBorderTop': '#0078d4', - 'tab.activeForeground': '#ffffff', - 'tab.border': '#2b2b2b', + 'statusBar.border': '#2B2B2B', + 'statusBar.debuggingBackground': '#0078D4', + 'statusBar.debuggingForeground': '#FFFFFF', + 'statusBar.focusBorder': '#0078D4', + 'statusBar.foreground': '#CCCCCC', + 'statusBar.noFolderBackground': '#1F1F1F', + 'statusBarItem.focusBorder': '#0078D4', + 'statusBarItem.prominentBackground': '#6E768166', + 'statusBarItem.remoteBackground': '#0078D4', + 'statusBarItem.remoteForeground': '#FFFFFF', + 'tab.activeBackground': '#1F1F1F', + 'tab.activeBorder': '#1F1F1F', + 'tab.activeBorderTop': '#0078D4', + 'tab.activeForeground': '#FFFFFF', + 'tab.selectedBorderTop': '#6caddf', + 'tab.border': '#2B2B2B', + 'tab.hoverBackground': '#1F1F1F', + 'tab.inactiveBackground': '#181818', + 'tab.inactiveForeground': '#9D9D9D', + 'tab.unfocusedActiveBorder': '#1F1F1F', + 'tab.unfocusedActiveBorderTop': '#2B2B2B', + 'tab.unfocusedHoverBackground': '#1F1F1F', + 'terminal.foreground': '#CCCCCC', + 'terminal.tab.activeBorder': '#0078D4', + 'textBlockQuote.background': '#2B2B2B', + 'textBlockQuote.border': '#616161', + 'textCodeBlock.background': '#2B2B2B', + 'textLink.activeForeground': '#4daafc', 'textLink.foreground': '#4daafc', + 'textPreformat.foreground': '#D0D0D0', + 'textPreformat.background': '#3C3C3C', + 'textSeparator.foreground': '#21262D', 'titleBar.activeBackground': '#181818', - 'titleBar.activeForeground': '#cccccc', - 'titleBar.border': '#2b2b2b', - 'titleBar.inactiveBackground': '#1f1f1f', - 'titleBar.inactiveForeground': '#9d9d9d', - 'welcomePage.tileBackground': '#2b2b2b' + 'titleBar.activeForeground': '#CCCCCC', + 'titleBar.border': '#2B2B2B', + 'titleBar.inactiveBackground': '#1F1F1F', + 'titleBar.inactiveForeground': '#9D9D9D', + 'welcomePage.tileBackground': '#2B2B2B', + 'welcomePage.progress.foreground': '#0078D4', + 'widget.border': '#313131' }; export const COLOR_THEME_LIGHT_INITIAL_COLORS = { 'activityBar.activeBorder': '#005FB8', - 'activityBar.background': '#f8f8f8', - 'activityBar.border': '#e5e5e5', - 'activityBar.foreground': '#1f1f1f', + 'activityBar.background': '#F8F8F8', + 'activityBar.border': '#E5E5E5', + 'activityBar.foreground': '#1F1F1F', 'activityBar.inactiveForeground': '#616161', - 'editorGroup.border': '#e5e5e5', - 'editorGroupHeader.tabsBackground': '#f8f8f8', - 'editorGroupHeader.tabsBorder': '#e5e5e5', - 'statusBar.background': '#f8f8f8', - 'statusBar.border': '#e5e5e5', - 'statusBar.foreground': '#3b3b3b', - 'statusBar.noFolderBackground': '#f8f8f8', - 'tab.activeBackground': '#ffffff', - 'tab.activeBorder': '#f8f8f8', - 'tab.activeBorderTop': '#005fb8', - 'tab.activeForeground': '#3b3b3b', - 'tab.border': '#e5e5e5', - 'textLink.foreground': '#005fb8', - 'titleBar.activeBackground': '#f8f8f8', - 'titleBar.activeForeground': '#1e1e1e', + 'activityBarBadge.background': '#005FB8', + 'activityBarBadge.foreground': '#FFFFFF', + 'badge.background': '#CCCCCC', + 'badge.foreground': '#3B3B3B', + 'button.background': '#005FB8', + 'button.border': '#0000001a', + 'button.foreground': '#FFFFFF', + 'button.hoverBackground': '#0258A8', + 'button.secondaryBackground': '#E5E5E5', + 'button.secondaryForeground': '#3B3B3B', + 'button.secondaryHoverBackground': '#CCCCCC', + 'chat.slashCommandBackground': '#ADCEFF7A', + 'chat.slashCommandForeground': '#26569E', + 'chat.editedFileForeground': '#895503', + 'checkbox.background': '#F8F8F8', + 'checkbox.border': '#CECECE', + 'descriptionForeground': '#3B3B3B', + 'dropdown.background': '#FFFFFF', + 'dropdown.border': '#CECECE', + 'dropdown.foreground': '#3B3B3B', + 'dropdown.listBackground': '#FFFFFF', + 'editor.background': '#FFFFFF', + 'editor.foreground': '#3B3B3B', + 'editor.inactiveSelectionBackground': '#E5EBF1', + 'editor.selectionHighlightBackground': '#ADD6FF80', + 'editorGroup.border': '#E5E5E5', + 'editorGroupHeader.tabsBackground': '#F8F8F8', + 'editorGroupHeader.tabsBorder': '#E5E5E5', + 'editorGutter.addedBackground': '#2EA043', + 'editorGutter.deletedBackground': '#F85149', + 'editorGutter.modifiedBackground': '#005FB8', + 'editorIndentGuide.background1': '#D3D3D3', + 'editorLineNumber.activeForeground': '#171184', + 'editorLineNumber.foreground': '#6E7681', + 'editorOverviewRuler.border': '#E5E5E5', + 'editorSuggestWidget.background': '#F8F8F8', + 'editorWidget.background': '#F8F8F8', + 'errorForeground': '#F85149', + 'focusBorder': '#005FB8', + 'foreground': '#3B3B3B', + 'icon.foreground': '#3B3B3B', + 'input.background': '#FFFFFF', + 'input.border': '#CECECE', + 'input.foreground': '#3B3B3B', + 'input.placeholderForeground': '#767676', + 'inputOption.activeBackground': '#BED6ED', + 'inputOption.activeBorder': '#005FB8', + 'inputOption.activeForeground': '#000000', + 'keybindingLabel.foreground': '#3B3B3B', + 'list.activeSelectionBackground': '#E8E8E8', + 'list.activeSelectionForeground': '#000000', + 'list.activeSelectionIconForeground': '#000000', + 'list.hoverBackground': '#F2F2F2', + 'list.focusAndSelectionOutline': '#005FB8', + 'menu.border': '#CECECE', + 'menu.selectionBackground': '#005FB8', + 'menu.selectionForeground': '#ffffff', + 'notebook.cellBorderColor': '#E5E5E5', + 'notebook.selectedCellBackground': '#C8DDF150', + 'notificationCenterHeader.background': '#FFFFFF', + 'notificationCenterHeader.foreground': '#3B3B3B', + 'notifications.background': '#FFFFFF', + 'notifications.border': '#E5E5E5', + 'notifications.foreground': '#3B3B3B', + 'panel.background': '#F8F8F8', + 'panel.border': '#E5E5E5', + 'panelInput.border': '#E5E5E5', + 'panelTitle.activeBorder': '#005FB8', + 'panelTitle.activeForeground': '#3B3B3B', + 'panelTitle.inactiveForeground': '#3B3B3B', + 'peekViewEditor.matchHighlightBackground': '#BB800966', + 'peekViewResult.background': '#FFFFFF', + 'peekViewResult.matchHighlightBackground': '#BB800966', + 'pickerGroup.border': '#E5E5E5', + 'pickerGroup.foreground': '#8B949E', + 'ports.iconRunningProcessForeground': '#369432', + 'progressBar.background': '#005FB8', + 'quickInput.background': '#F8F8F8', + 'quickInput.foreground': '#3B3B3B', + 'searchEditor.textInputBorder': '#CECECE', + 'settings.dropdownBackground': '#FFFFFF', + 'settings.dropdownBorder': '#CECECE', + 'settings.headerForeground': '#1F1F1F', + 'settings.modifiedItemIndicator': '#BB800966', + 'settings.numberInputBorder': '#CECECE', + 'settings.textInputBorder': '#CECECE', + 'sideBar.background': '#F8F8F8', + 'sideBar.border': '#E5E5E5', + 'sideBar.foreground': '#3B3B3B', + 'sideBarSectionHeader.background': '#F8F8F8', + 'sideBarSectionHeader.border': '#E5E5E5', + 'sideBarSectionHeader.foreground': '#3B3B3B', + 'sideBarTitle.foreground': '#3B3B3B', + 'statusBar.background': '#F8F8F8', + 'statusBar.foreground': '#3B3B3B', + 'statusBar.border': '#E5E5E5', + 'statusBarItem.hoverBackground': '#B8B8B850', + 'statusBarItem.compactHoverBackground': '#CCCCCC', + 'statusBar.debuggingBackground': '#FD716C', + 'statusBar.debuggingForeground': '#000000', + 'statusBar.focusBorder': '#005FB8', + 'statusBar.noFolderBackground': '#F8F8F8', + 'statusBarItem.errorBackground': '#C72E0F', + 'statusBarItem.focusBorder': '#005FB8', + 'statusBarItem.prominentBackground': '#6E768166', + 'statusBarItem.remoteBackground': '#005FB8', + 'statusBarItem.remoteForeground': '#FFFFFF', + 'tab.activeBackground': '#FFFFFF', + 'tab.activeBorder': '#F8F8F8', + 'tab.activeBorderTop': '#005FB8', + 'tab.activeForeground': '#3B3B3B', + 'tab.selectedBorderTop': '#68a3da', + 'tab.border': '#E5E5E5', + 'tab.hoverBackground': '#FFFFFF', + 'tab.inactiveBackground': '#F8F8F8', + 'tab.inactiveForeground': '#868686', + 'tab.lastPinnedBorder': '#D4D4D4', + 'tab.unfocusedActiveBorder': '#F8F8F8', + 'tab.unfocusedActiveBorderTop': '#E5E5E5', + 'tab.unfocusedHoverBackground': '#F8F8F8', + 'terminalCursor.foreground': '#005FB8', + 'terminal.foreground': '#3B3B3B', + 'terminal.inactiveSelectionBackground': '#E5EBF1', + 'terminal.tab.activeBorder': '#005FB8', + 'textBlockQuote.background': '#F8F8F8', + 'textBlockQuote.border': '#E5E5E5', + 'textCodeBlock.background': '#F8F8F8', + 'textLink.activeForeground': '#005FB8', + 'textLink.foreground': '#005FB8', + 'textPreformat.foreground': '#3B3B3B', + 'textPreformat.background': '#0000001F', + 'textSeparator.foreground': '#21262D', + 'titleBar.activeBackground': '#F8F8F8', + 'titleBar.activeForeground': '#1E1E1E', 'titleBar.border': '#E5E5E5', - 'titleBar.inactiveBackground': '#f8f8f8', - 'titleBar.inactiveForeground': '#8b949e', - 'welcomePage.tileBackground': '#f3f3f3' + 'titleBar.inactiveBackground': '#F8F8F8', + 'titleBar.inactiveForeground': '#8B949E', + 'welcomePage.tileBackground': '#F3F3F3', + 'widget.border': '#E5E5E5' }; export interface IWorkbenchTheme { From e8feeeaaae6d2626d5c8736b75350fabe9ad9bd1 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 7 Jul 2025 10:36:01 -0400 Subject: [PATCH 174/306] get value of ctx keys (#254448) --- src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 3a66a88b82c..9330fbbb24c 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -88,6 +88,7 @@ import { IChatService } from '../../chat/common/chatService.js'; import { ChatAgentLocation, ChatModeKind } from '../../chat/common/constants.js'; import { CHAT_OPEN_ACTION_ID } from '../../chat/browser/actions/chatActions.js'; import { IChatAgentService } from '../../chat/common/chatAgents.js'; +import { getActiveElement } from '../../../../base/browser/dom.js'; const QUICKOPEN_HISTORY_LIMIT_CONFIG = 'task.quickOpen.history'; @@ -2933,7 +2934,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } private async _trust(): Promise { - if (ServerlessWebContext && !TaskExecutionSupportedContext) { + const context = this._contextKeyService.getContext(getActiveElement()); + if (ServerlessWebContext.getValue(this._contextKeyService) && !TaskExecutionSupportedContext?.evaluate(context)) { return false; } await this._workspaceTrustManagementService.workspaceTrustInitialized; From 3641b928b008e4853ec1d6e6a62ca7cabd6983fe Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 7 Jul 2025 16:41:17 +0200 Subject: [PATCH 175/306] Add notebook as an inline completion editor type (#254449) --- .../browser/model/inlineCompletionsModel.ts | 7 +++++-- .../browser/model/provideInlineCompletions.ts | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index 8128c79c665..c93523dabe4 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -128,13 +128,16 @@ export class InlineCompletionsModel extends Disposable { })); { // Determine editor type + const isNotebook = this.textModel.uri.scheme === 'vscode-notebook-cell'; const [diffEditor] = this._codeEditorService.listDiffEditors() .filter(d => d.getOriginalEditor().getId() === this._editor.getId() || d.getModifiedEditor().getId() === this._editor.getId()); - this.editorType = !!diffEditor ? InlineCompletionEditorType.DiffEditor : InlineCompletionEditorType.TextEditor; - this.isInDiffEditor = this.editorType === InlineCompletionEditorType.DiffEditor; + this.isInDiffEditor = !!diffEditor; + this.editorType = isNotebook ? InlineCompletionEditorType.Notebook + : this.isInDiffEditor ? InlineCompletionEditorType.DiffEditor + : InlineCompletionEditorType.TextEditor; } this._register(recomputeInitiallyAndOnChange(this._fetchInlineCompletionsPromise)); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts index f857484ea46..307a4e3762b 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts @@ -418,7 +418,8 @@ export interface IDisplayLocation { export enum InlineCompletionEditorType { TextEditor = 'textEditor', - DiffEditor = 'diffEditor' + DiffEditor = 'diffEditor', + Notebook = 'notebook', } /** From 55b9191fe3772ad17a2e236ba5771a20a5347b2b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 7 Jul 2025 17:40:56 +0200 Subject: [PATCH 176/306] ci - improve log message on failure (#254460) --- test/smoke/test/index.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/test/smoke/test/index.js b/test/smoke/test/index.js index b7c1277a6c6..ee1ac564720 100644 --- a/test/smoke/test/index.js +++ b/test/smoke/test/index.js @@ -46,13 +46,30 @@ mocha.run(failures => { const rootPath = join(__dirname, '..', '..', '..'); const logPath = join(rootPath, '.build', 'logs'); - if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) { + if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { console.log(` ################################################################### # # # Logs are attached as build artefact and can be downloaded # # from the build Summary page (Summary -> Related -> N published) # # # +# Please also scan through attached crash logs in case the # +# failure was caused by a native crash. # +# # +# Show playwright traces on: https://trace.playwright.dev/ # +# # +################################################################### + `); + } else if (process.env.GITHUB_WORKSPACE) { + console.log(` +################################################################### +# # +# Logs are attached as build artefact and can be downloaded # +# from the build Summary page (Summary -> Artifacts) # +# # +# Please also scan through attached crash logs in case the # +# failure was caused by a native crash. # +# # # Show playwright traces on: https://trace.playwright.dev/ # # # ################################################################### From 1a79d96de103f7698e05dd9d71b0b4ef8f0a8ef6 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:54:28 +0000 Subject: [PATCH 177/306] Fix tiles wrapping OOTB in chat welcome view (#254209) * Initial plan * Fix tiles wrapping OOTB in chat welcome view Change flex-wrap from wrap to nowrap in .chat-welcome-view-suggested-prompts to prevent tiles from wrapping unnecessarily out of the box. Co-authored-by: bhavyaus <25044782+bhavyaus@users.noreply.github.com> * Update chat welcome view to allow suggested prompts to wrap and add row gap --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bhavyaus <25044782+bhavyaus@users.noreply.github.com> Co-authored-by: bhavyaus --- src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css index b1bbaff2195..d6d06e188c0 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css @@ -131,6 +131,7 @@ div.chat-welcome-view { display: flex; flex-wrap: wrap; justify-content: center; + row-gap: 8px; margin-top: 4px; > .chat-welcome-view-suggested-prompt { From 716794e6389fd963b3d2635ca27dc85dc00ab317 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 7 Jul 2025 18:23:11 +0000 Subject: [PATCH 178/306] Engineering - more descriptive cache names (#254496) Engineering - mode descriptive cache names --- .github/workflows/pr-darwin-test.yml | 2 +- .github/workflows/pr-linux-test.yml | 2 +- .github/workflows/pr-node-modules.yml | 8 ++++---- .github/workflows/pr-win32-test.yml | 2 +- .github/workflows/pr.yml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr-darwin-test.yml b/.github/workflows/pr-darwin-test.yml index 4c21f9c65b4..36b355665e8 100644 --- a/.github/workflows/pr-darwin-test.yml +++ b/.github/workflows/pr-darwin-test.yml @@ -41,7 +41,7 @@ jobs: uses: actions/cache/restore@v4 with: path: .build/node_modules_cache - key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" + key: "node_modules-macos-${{ hashFiles('.build/packagelockhash') }}" - name: Extract node_modules cache if: steps.cache-node-modules.outputs.cache-hit == 'true' diff --git a/.github/workflows/pr-linux-test.yml b/.github/workflows/pr-linux-test.yml index 496ef7e77c1..5bfe74cf7d1 100644 --- a/.github/workflows/pr-linux-test.yml +++ b/.github/workflows/pr-linux-test.yml @@ -58,7 +58,7 @@ jobs: uses: actions/cache/restore@v4 with: path: .build/node_modules_cache - key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" + key: "node_modules-linux-${{ hashFiles('.build/packagelockhash') }}" - name: Extract node_modules cache if: steps.cache-node-modules.outputs.cache-hit == 'true' diff --git a/.github/workflows/pr-node-modules.yml b/.github/workflows/pr-node-modules.yml index 08bf54f22c2..eed798ec5f0 100644 --- a/.github/workflows/pr-node-modules.yml +++ b/.github/workflows/pr-node-modules.yml @@ -30,7 +30,7 @@ jobs: uses: actions/cache@v4 with: path: .build/node_modules_cache - key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" + key: "node_modules-compile-${{ hashFiles('.build/packagelockhash') }}" - name: Install build tools if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -106,7 +106,7 @@ jobs: uses: actions/cache@v4 with: path: .build/node_modules_cache - key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" + key: "node_modules-linux-${{ hashFiles('.build/packagelockhash') }}" - name: Install build dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -180,7 +180,7 @@ jobs: uses: actions/cache@v4 with: path: .build/node_modules_cache - key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" + key: "node_modules-macos-${{ hashFiles('.build/packagelockhash') }}" - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -246,7 +246,7 @@ jobs: id: node-modules-cache with: path: .build/node_modules_cache - key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" + key: "node_modules-windows-${{ hashFiles('.build/packagelockhash') }}" - name: Install dependencies if: steps.node-modules-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/pr-win32-test.yml b/.github/workflows/pr-win32-test.yml index 30dc40961a3..3781e01fbaf 100644 --- a/.github/workflows/pr-win32-test.yml +++ b/.github/workflows/pr-win32-test.yml @@ -44,7 +44,7 @@ jobs: id: node-modules-cache with: path: .build/node_modules_cache - key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" + key: "node_modules-windows-${{ hashFiles('.build/packagelockhash') }}" - name: Extract node_modules cache if: steps.node-modules-cache.outputs.cache-hit == 'true' diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ec6f8a54ea5..b43eff9f41d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -38,7 +38,7 @@ jobs: uses: actions/cache/restore@v4 with: path: .build/node_modules_cache - key: "node_modules-${{ hashFiles('.build/packagelockhash') }}" + key: "node_modules-compile-${{ hashFiles('.build/packagelockhash') }}" - name: Extract node_modules cache if: steps.cache-node-modules.outputs.cache-hit == 'true' From dadbc584d9b02c5964621c67eebefa871c051655 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 7 Jul 2025 11:25:52 -0700 Subject: [PATCH 179/306] chat: fix showing notebook cells is running out of panel chat (#254492) Fixes #254336 --- .../toolInvocationParts/chatToolConfirmationSubPart.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts index da36386ccaa..e002e7999e6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts @@ -251,11 +251,15 @@ export class ToolConfirmationSubPart extends BaseChatToolInvocationSubPart { const messageSeeMoreObserver = this._register(new ElementSizeObserver(elements.message, undefined)); const updateSeeMoreDisplayed = () => { const show = messageSeeMoreObserver.getHeight() > SHOW_MORE_MESSAGE_HEIGHT_TRIGGER; - elements.messageContainer.classList.toggle('can-see-more', show); + if (elements.messageContainer.classList.contains('can-see-more') !== show) { + elements.messageContainer.classList.toggle('can-see-more', show); + this._onDidChangeHeight.fire(); + } }; this._register(dom.addDisposableListener(elements.showMore, 'click', () => { elements.messageContainer.classList.toggle('can-see-more', false); + this._onDidChangeHeight.fire(); messageSeeMoreObserver.dispose(); })); From d1d4aa663f8f516884d3477a2276f7363b9d7a3f Mon Sep 17 00:00:00 2001 From: Elijah King Date: Mon, 7 Jul 2025 13:28:14 -0700 Subject: [PATCH 180/306] revised new welcome experience intro slide to use correct color tokens, updates to correct color per theme --- .../common/media/ai-powered-suggestions.svg | 46 +++++++++---------- .../common/media/customize-ai.svg | 11 ++--- .../common/media/multi-file-edits.svg | 4 +- 3 files changed, 28 insertions(+), 33 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/media/ai-powered-suggestions.svg b/src/vs/workbench/contrib/welcomeGettingStarted/common/media/ai-powered-suggestions.svg index bcb0653de01..03363851219 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/media/ai-powered-suggestions.svg +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/media/ai-powered-suggestions.svg @@ -21,32 +21,32 @@ - - + + - - + - + @@ -56,9 +56,9 @@ - - + @@ -74,13 +74,13 @@ - + - + @@ -99,8 +99,8 @@ - + @@ -114,8 +114,8 @@ - + @@ -126,14 +126,14 @@ - + + - diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/media/customize-ai.svg b/src/vs/workbench/contrib/welcomeGettingStarted/common/media/customize-ai.svg index 3594f6f9c6b..978e2594e8c 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/media/customize-ai.svg +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/media/customize-ai.svg @@ -3,10 +3,8 @@ - - + + - + diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/media/multi-file-edits.svg b/src/vs/workbench/contrib/welcomeGettingStarted/common/media/multi-file-edits.svg index 583a2c3607d..146196fdfb7 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/media/multi-file-edits.svg +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/media/multi-file-edits.svg @@ -323,8 +323,8 @@ - - + + From 7deabf42e78c70999de7f2a7acca2a0525253fda Mon Sep 17 00:00:00 2001 From: Aman Karmani Date: Mon, 7 Jul 2025 13:34:30 -0700 Subject: [PATCH 181/306] [engineering] add testSplit option to unit-test runner (#253049) helpful to run tests in parallel --- test/unit/electron/index.js | 4 +++- test/unit/electron/renderer.js | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/test/unit/electron/index.js b/test/unit/electron/index.js index 2072d9c2bde..bf89650d0aa 100644 --- a/test/unit/electron/index.js +++ b/test/unit/electron/index.js @@ -30,6 +30,7 @@ const minimist = require('minimist'); * grep: string; * run: string; * runGlob: string; + * testSplit: string; * dev: boolean; * reporter: string; * 'reporter-options': string; @@ -46,7 +47,7 @@ const minimist = require('minimist'); * }} */ const args = minimist(process.argv.slice(2), { - string: ['grep', 'run', 'runGlob', 'reporter', 'reporter-options', 'waitServer', 'timeout', 'crash-reporter-directory', 'tfs', 'coveragePath', 'coverageFormats'], + string: ['grep', 'run', 'runGlob', 'reporter', 'reporter-options', 'waitServer', 'timeout', 'crash-reporter-directory', 'tfs', 'coveragePath', 'coverageFormats', 'testSplit'], boolean: ['build', 'coverage', 'help', 'dev', 'per-test-coverage'], alias: { 'grep': ['g', 'f'], @@ -67,6 +68,7 @@ Options: --grep, -g, -f only run tests matching --run only run tests from --runGlob, --glob, --runGrep only run tests matching +--testSplit / split tests into parts and run the th part --build run with build output (out-build) --coverage generate coverage report --per-test-coverage generate a per-test V8 coverage report, only valid with the full-json-stream reporter diff --git a/test/unit/electron/renderer.js b/test/unit/electron/renderer.js index af451fbab8e..23f66cc2fe5 100644 --- a/test/unit/electron/renderer.js +++ b/test/unit/electron/renderer.js @@ -171,7 +171,14 @@ async function loadTestModules(opts) { const pattern = opts.runGlob || _tests_glob; const files = await globAsync(pattern, { cwd: loadFn._out }); - const modules = files.map(file => file.replace(/\.js$/, '')); + let modules = files.map(file => file.replace(/\.js$/, '')); + if (opts.testSplit) { + const [i, n] = opts.testSplit.split('/').map(Number); + const chunkSize = Math.floor(modules.length / n); + const start = (i - 1) * chunkSize; + const end = i === n ? modules.length : i * chunkSize; + modules = modules.slice(start, end); + } return loadModules(modules); } From 8f6286939c3f283cfbbe118d406442445cdae368 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 7 Jul 2025 14:52:31 -0700 Subject: [PATCH 182/306] mcp: fix only keeping 1 MCP server per config file (#254521) Fixes #254498 --- src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts index 0ba9db36d5b..46ad72e1724 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts @@ -216,7 +216,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ } else { server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), local, gallery, undefined); } - this._local = this._local.filter(e => e.name === local.name); + this._local = this._local.filter(e => e.name !== local.name); this._local.push(server); this._onChange.fire(server); return server; From ffdc173416377252095a5cb3cd84c92b007aea7a Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Mon, 7 Jul 2025 16:13:11 -0600 Subject: [PATCH 183/306] add non-markdown description back for `chat.tools.autoApprove` (#254529) add non-markdown description back for chat.tools.autoApprove --- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index eaef9e9d6d7..858099af013 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -213,7 +213,9 @@ configurationRegistry.registerConfiguration({ }, 'chat.tools.autoApprove': { default: false, - markdownDescription: nls.localize('chat.tools.autoApprove', "Controls whether tool use should be automatically approved.\n\nAllows _all_ tools to run automatically without user confirmation, overriding any tool-specific settings such as terminal auto-approval.\n\nUse with caution: carefully review selected tools and be extra wary of possible sources of prompt injection!"), + // Description is added in for policy parser. See https://github.com/microsoft/vscode/issues/254526 + description: nls.localize('chat.tools.autoApprove.description', "Controls whether tool use should be automatically approved. Allow all tools to run automatically without user confirmation, overriding any tool-specific settings such as terminal auto-approval. Use with caution: carefully review selected tools and be extra wary of possible sources of prompt injection!"), + markdownDescription: nls.localize('chat.tools.autoApprove.markdownDescription', "Controls whether tool use should be automatically approved.\n\nAllows _all_ tools to run automatically without user confirmation, overriding any tool-specific settings such as terminal auto-approval.\n\nUse with caution: carefully review selected tools and be extra wary of possible sources of prompt injection!"), type: 'boolean', tags: ['experimental'], policy: { From 0e1bbe8de0415ce5332260ab5c34224b89c1bd68 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 7 Jul 2025 15:21:35 -0700 Subject: [PATCH 184/306] Add "Show chats" to chat editor menu (#254530) For microsoft/vscode-copilot-release#10291 --- .../chat/browser/actions/chatActions.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 38257061335..3fd59de56a8 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -38,7 +38,7 @@ import product from '../../../../../platform/product/common/product.js'; import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { ToggleTitleBarConfigAction } from '../../../../browser/parts/titlebar/titlebarActions.js'; -import { IsCompactTitleBarContext } from '../../../../common/contextkeys.js'; +import { ActiveEditorContext, IsCompactTitleBarContext } from '../../../../common/contextkeys.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { IViewDescriptorService, ViewContainerLocation } from '../../../../common/views.js'; import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; @@ -348,12 +348,18 @@ export function registerChatActions() { super({ id: `workbench.action.chat.history`, title: localize2('chat.history.label', "Show Chats..."), - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.equals('view', ChatViewId), - group: 'navigation', - order: 2 - }, + menu: [ + { + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', ChatViewId), + group: 'navigation', + order: 2 + }, + { + id: MenuId.EditorTitle, + when: ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), + }, + ], category: CHAT_CATEGORY, icon: Codicon.history, f1: true, From c8441f22c6c4ea4b63a3f63295280487d919c914 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 8 Jul 2025 08:31:00 +1000 Subject: [PATCH 185/306] Live upadtes to Notebook Cell EOL in troubleshooting (#254525) --- .../notebook/browser/contrib/troubleshoot/layout.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts b/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts index 023929630ef..3c0b9f6941c 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts @@ -183,7 +183,7 @@ export class TroubleshootController extends Disposable implements INotebookEdito overlayContainer.appendChild(topLine); const getLayoutInfo = () => { - const eol = cell.textModel?.getEOL() === '\n' ? 'LF' : 'CRLF'; + const eol = cell.textBuffer.getEOL() === '\n' ? 'LF' : 'CRLF'; const scrollTop = this._notebookEditor.getAbsoluteTopOfElement(cell); return `cell #${index} (handle: ${cell.handle}) | AbsoluteTopOfElement: ${scrollTop}px | EOL: ${eol}`; }; @@ -230,7 +230,14 @@ export class TroubleshootController extends Disposable implements INotebookEdito this._localStore.add(cell.onDidChangeLayout((e) => { updateLayout(); })); - + this._localStore.add(cell.textBuffer.onDidChangeContent(() => { + updateLayout(); + })); + if (cell.textModel) { + this._localStore.add(cell.textModel.onDidChangeContent(() => { + updateLayout(); + })); + } this._localStore.add(this._notebookEditor.onDidChangeLayout(() => { updateLayout(); })); From b922e2790c77a072a358995e6f376fa61cb171db Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 7 Jul 2025 15:32:47 -0700 Subject: [PATCH 186/306] mcp: include correct scope when creating server install events (#254532) Refs #254520 --- .../mcp/common/mcpWorkbenchManagementService.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts b/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts index f70a1ba04c5..d4aef30266e 100644 --- a/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts +++ b/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts @@ -147,7 +147,7 @@ export class WorkbenchMcpManagementService extends Disposable implements IWorkbe })); this._register(this.workspaceMcpManagementService.onDidInstallMcpServers(async e => { - const { mcpServerInstallResult } = this.createInstallMcpServerResultsFromEvent(e); + const { mcpServerInstallResult } = this.createInstallMcpServerResultsFromEvent(e, LocalMcpServerScope.Workspace); this._onDidInstallMcpServersInCurrentProfile.fire(mcpServerInstallResult); this._onDidInstallMcpServersInCurrentProfile.fire(mcpServerInstallResult); })); @@ -163,7 +163,7 @@ export class WorkbenchMcpManagementService extends Disposable implements IWorkbe })); this._register(this.workspaceMcpManagementService.onDidUpdateMcpServers(e => { - const { mcpServerInstallResult } = this.createInstallMcpServerResultsFromEvent(e); + const { mcpServerInstallResult } = this.createInstallMcpServerResultsFromEvent(e, LocalMcpServerScope.Workspace); this._onDidUpdateMcpServersInCurrentProfile.fire(mcpServerInstallResult); this._onDidUpdateMcpServersInCurrentProfile.fire(mcpServerInstallResult); })); @@ -204,13 +204,13 @@ export class WorkbenchMcpManagementService extends Disposable implements IWorkbe })); } - private createInstallMcpServerResultsFromEvent(e: readonly InstallMcpServerResult[]) { + private createInstallMcpServerResultsFromEvent(e: readonly InstallMcpServerResult[], scope: LocalMcpServerScope) { const mcpServerInstallResult: IWorkbenchMcpServerInstallResult[] = []; const mcpServerInstallResultInCurrentProfile: IWorkbenchMcpServerInstallResult[] = []; for (const result of e) { const workbenchResult = { ...result, - local: result.local ? this.toWorkspaceMcpServer(result.local, LocalMcpServerScope.User) : undefined + local: result.local ? this.toWorkspaceMcpServer(result.local, scope) : undefined }; mcpServerInstallResult.push(workbenchResult); if (this.uriIdentityService.extUri.isEqual(result.mcpResource, this.userDataProfileService.currentProfile.mcpResource)) { @@ -222,7 +222,7 @@ export class WorkbenchMcpManagementService extends Disposable implements IWorkbe } private handleInstallMcpServerResultsFromEvent(e: readonly InstallMcpServerResult[], emitter: Emitter, currentProfileEmitter: Emitter): void { - const { mcpServerInstallResult, mcpServerInstallResultInCurrentProfile } = this.createInstallMcpServerResultsFromEvent(e); + const { mcpServerInstallResult, mcpServerInstallResultInCurrentProfile } = this.createInstallMcpServerResultsFromEvent(e, LocalMcpServerScope.User); emitter.fire(mcpServerInstallResult); if (mcpServerInstallResultInCurrentProfile.length) { currentProfileEmitter.fire(mcpServerInstallResultInCurrentProfile); From 2f3845b686d1a757af2c682400649a6e487a8c23 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 7 Jul 2025 16:07:05 -0700 Subject: [PATCH 187/306] Pick up latest TS for building VSCode --- 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 c72c61053d4..5c22dd835b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -150,7 +150,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^5.9.0-dev.20250613", + "typescript": "^5.9.0-dev.20250707", "typescript-eslint": "^8.8.0", "util": "^0.12.4", "webpack": "^5.94.0", @@ -17133,9 +17133,9 @@ "dev": true }, "node_modules/typescript": { - "version": "5.9.0-dev.20250613", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.0-dev.20250613.tgz", - "integrity": "sha512-5bzU//x0svUVjUCMlHRG7IassWFQ7/dofYXqSSbQpQ8a1Kh/GqIsvfc2CQIS+EpYrdoHaNLroxmlsLNsqLXaww==", + "version": "5.9.0-dev.20250707", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.0-dev.20250707.tgz", + "integrity": "sha512-aLNq2y90Rk+gkNN2ptDQOoA4IeCnpEDoMMkz0o4jZcVC7fEeYAwgJ3Z+c2G9//TCukY8IvXGu0Jbfp2FtvdcFQ==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 0da56483be3..e874629f586 100644 --- a/package.json +++ b/package.json @@ -209,7 +209,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^5.9.0-dev.20250613", + "typescript": "^5.9.0-dev.20250707", "typescript-eslint": "^8.8.0", "util": "^0.12.4", "webpack": "^5.94.0", From 2b3fe79f88285e7539717a5b94f52a94938ab608 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 7 Jul 2025 16:52:37 -0700 Subject: [PATCH 188/306] Revert service-worker js -> ts rewrite This is causing build issues by pulling in the webworker typings --- build/buildfile.js | 1 - src/tsconfig.json | 3 + .../{service-worker.ts => service-worker.js} | 181 ++++++++++++------ 3 files changed, 124 insertions(+), 61 deletions(-) rename src/vs/workbench/contrib/webview/browser/pre/{service-worker.ts => service-worker.js} (78%) diff --git a/build/buildfile.js b/build/buildfile.js index b5e8f6e7e6c..3acb1218b99 100644 --- a/build/buildfile.js +++ b/build/buildfile.js @@ -44,7 +44,6 @@ exports.code = [ createModuleDescription('vs/code/node/cliProcessMain'), createModuleDescription('vs/code/electron-utility/sharedProcess/sharedProcessMain'), createModuleDescription('vs/code/electron-browser/workbench/workbench'), - createModuleDescription('vs/workbench/contrib/webview/browser/pre/service-worker') ]; exports.codeWeb = createModuleDescription('vs/code/browser/workbench/workbench'); diff --git a/src/tsconfig.json b/src/tsconfig.json index 88d3daa0f29..bfda0ebafc1 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -31,5 +31,8 @@ "./vs/**/*.ts", "./vscode-dts/vscode.proposed.*.d.ts", "./vscode-dts/vscode.d.ts" + ], + "exclude": [ + "vs/workbench/contrib/webview/browser/pre/service-worker.js" ] } diff --git a/src/vs/workbench/contrib/webview/browser/pre/service-worker.ts b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js similarity index 78% rename from src/vs/workbench/contrib/webview/browser/pre/service-worker.ts rename to src/vs/workbench/contrib/webview/browser/pre/service-worker.js index 11c1acb9652..81cff0feb89 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/service-worker.ts +++ b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js @@ -2,9 +2,11 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +//@ts-check /// -const sw: ServiceWorkerGlobalScope = self as any as ServiceWorkerGlobalScope; +/** @type {ServiceWorkerGlobalScope} */ +const sw = /** @type {any} */ (self); const VERSION = 4; @@ -16,38 +18,54 @@ const searchParams = new URL(location.toString()).searchParams; const remoteAuthority = searchParams.get('remoteAuthority'); -let outerIframeMessagePort: MessagePort | undefined; +/** @type {MessagePort|undefined} */ +let outerIframeMessagePort; /** * Origin used for resources */ const resourceBaseAuthority = searchParams.get('vscode-resource-base-authority'); + +/** @type {number} */ const resolveTimeout = 30_000; -type RequestStoreResult = { - status: 'ok'; - value: T; -} | { - status: 'timeout'; -}; -interface RequestStoreEntry { - resolve: (x: RequestStoreResult) => void; - promise: Promise>; -} +/** + * @template T + * @typedef {{ status: 'ok', value: T } | { status: 'timeout' }} RequestStoreResult + */ -class RequestStore { - private map: Map> = new Map(); - private requestPool: number = 0; - create(): { requestId: number; promise: Promise> } { +/** + * @template T + * @typedef {{ resolve: (x: RequestStoreResult) => void, promise: Promise> }} RequestStoreEntry + */ + + +/** + * @template T + */ +class RequestStore { + constructor() { + /** @type {Map>} */ + this.map = new Map(); + /** @type {number} */ + this.requestPool = 0; + } + + /** + * @returns {{ requestId: number, promise: Promise> }} + */ + create() { const requestId = ++this.requestPool; - let resolve: (x: RequestStoreResult) => void; - const promise = new Promise>(r => resolve = r); + /** @type {(x: RequestStoreResult) => void} */ + let resolve; + const promise = new Promise(r => resolve = r); - const entry: RequestStoreEntry = { resolve: resolve!, promise }; + /** @type {RequestStoreEntry} */ + const entry = { resolve, promise }; this.map.set(requestId, entry); const dispose = () => { @@ -62,7 +80,12 @@ class RequestStore { return { requestId, promise }; } - resolve(requestId: number, result: T): boolean { + /** + * @param {number} requestId + * @param {T} result + * @returns {boolean} + */ + resolve(requestId, result) { const entry = this.map.get(requestId); if (!entry) { return false; @@ -76,12 +99,14 @@ class RequestStore { /** * Map of requested paths to responses. */ -const resourceRequestStore = new RequestStore(); +/** @type {RequestStore} */ +const resourceRequestStore = new RequestStore(); /** * Map of requested localhost origins to optional redirects. */ -const localhostRequestStore = new RequestStore(); +/** @type {RequestStore} */ +const localhostRequestStore = new RequestStore(); const unauthorized = () => new Response('Unauthorized', { status: 401, }); @@ -95,12 +120,13 @@ const methodNotAllowed = () => const requestTimeout = () => new Response('Request Timeout', { status: 408, }); -sw.addEventListener('message', async (event: ExtendableMessageEvent) => { +sw.addEventListener('message', async (event) => { if (!event.source) { return; } - const source = event.source as Client; + /** @type {Client} */ + const source = event.source; switch (event.data.channel) { case 'version': { outerIframeMessagePort = event.ports[0]; @@ -115,7 +141,8 @@ sw.addEventListener('message', async (event: ExtendableMessageEvent) => { return; } case 'did-load-resource': { - const response = event.data.data as ResourceResponse; + /** @type {ResourceResponse} */ + const response = event.data.data; if (!resourceRequestStore.resolve(response.id, response)) { console.log('Could not resolve unknown resource', response.path); } @@ -135,7 +162,7 @@ sw.addEventListener('message', async (event: ExtendableMessageEvent) => { } }); -sw.addEventListener('fetch', (event: FetchEvent) => { +sw.addEventListener('fetch', (event) => { const requestUrl = new URL(event.request.url); if (typeof resourceBaseAuthority === 'string' && requestUrl.protocol === 'https:' && requestUrl.hostname.endsWith('.' + resourceBaseAuthority)) { switch (event.request.method) { @@ -184,25 +211,32 @@ sw.addEventListener('fetch', (event: FetchEvent) => { } }); -sw.addEventListener('install', (event: ExtendableEvent) => { +sw.addEventListener('install', (event) => { event.waitUntil(sw.skipWaiting()); // Activate worker immediately }); -sw.addEventListener('activate', (event: ExtendableEvent) => { +sw.addEventListener('activate', (event) => { event.waitUntil(sw.clients.claim()); // Become available to all pages }); -interface ResourceRequestUrlComponents { - scheme: string; - authority: string; - path: string; - query: string; -} +/** + * @typedef {Object} ResourceRequestUrlComponents + * @property {string} scheme + * @property {string} authority + * @property {string} path + * @property {string} query + */ + +/** + * @param {FetchEvent} event + * @param {ResourceRequestUrlComponents} requestUrlComponents + * @returns {Promise} + */ async function processResourceRequest( - event: FetchEvent, - requestUrlComponents: ResourceRequestUrlComponents -): Promise { + event, + requestUrlComponents +) { let client = await sw.clients.get(event.clientId); if (!client) { client = await getWorkerClientForId(event.clientId); @@ -227,10 +261,12 @@ async function processResourceRequest( const shouldTryCaching = (event.request.method === 'GET'); - const resolveResourceEntry = ( - result: RequestStoreResult, - cachedResponse: Response | undefined - ): Response => { + /** + * @param {RequestStoreResult} result + * @param {Response|undefined} cachedResponse + * @returns {Response} + */ + const resolveResourceEntry = (result, cachedResponse) => { if (result.status === 'timeout') { return requestTimeout(); } @@ -252,7 +288,8 @@ async function processResourceRequest( return notFound(); } - const commonHeaders: Record = { + /** @type {Record} */ + const commonHeaders = { 'Access-Control-Allow-Origin': '*', }; @@ -287,7 +324,8 @@ async function processResourceRequest( } } - const headers: Record = { + /** @type {Record} */ + const headers = { ...commonHeaders, 'Content-Type': entry.mime, 'Content-Length': byteLength.toString(), @@ -312,7 +350,7 @@ async function processResourceRequest( headers['Cross-Origin-Opener-Policy'] = 'same-origin'; } - const response = new Response(entry.data as Uint8Array, { + const response = new Response(entry.data, { status: 200, headers }); @@ -325,7 +363,8 @@ async function processResourceRequest( return response.clone(); }; - let cached: Response | undefined; + /** @type {Response|undefined} */ + let cached; if (shouldTryCaching) { const cache = await caches.open(resourceCacheName); cached = await cache.match(event.request); @@ -366,10 +405,15 @@ async function processResourceRequest( return promise.then(entry => resolveResourceEntry(entry, cached)); } +/** + * @param {FetchEvent} event + * @param {URL} requestUrl + * @returns {Promise} + */ async function processLocalhostRequest( - event: FetchEvent, - requestUrl: URL -): Promise { + event, + requestUrl +) { const client = await sw.clients.get(event.clientId); if (!client) { // This is expected when requesting resources on other localhost ports @@ -390,9 +434,11 @@ async function processLocalhostRequest( const origin = requestUrl.origin; - const resolveRedirect = async ( - result: RequestStoreResult - ): Promise => { + /** + * @param {RequestStoreResult} result + * @returns {Promise} + */ + const resolveRedirect = async function (result) { if (result.status !== 'ok' || !result.value) { return fetch(event.request); } @@ -432,12 +478,20 @@ async function processLocalhostRequest( return promise.then(resolveRedirect); } -function getWebviewIdForClient(client: Client): string | null { +/** + * @param {Client} client + * @returns {string|null} + */ +function getWebviewIdForClient(client) { const requesterClientUrl = new URL(client.url); return requesterClientUrl.searchParams.get('id'); } -async function getOuterIframeClient(webviewId: string): Promise { +/** + * @param {string} webviewId + * @returns {Promise} + */ +async function getOuterIframeClient(webviewId) { const allClients = await sw.clients.matchAll({ includeUncontrolled: true }); return allClients.filter(client => { const clientUrl = new URL(client.url); @@ -446,7 +500,11 @@ async function getOuterIframeClient(webviewId: string): Promise { }); } -async function getWorkerClientForId(clientId: string): Promise { +/** + * @param {string} clientId + * @returns {Promise} + */ +async function getWorkerClientForId(clientId) { const allDedicatedWorkerClients = await sw.clients.matchAll({ type: 'worker' }); const allSharedWorkerClients = await sw.clients.matchAll({ type: 'sharedworker' }); const allWorkerClients = [...allDedicatedWorkerClients, ...allSharedWorkerClients]; @@ -455,9 +513,12 @@ async function getWorkerClientForId(clientId: string): Promise Date: Mon, 7 Jul 2025 18:33:12 -0600 Subject: [PATCH 189/306] fix #248581 (#254536) fix https://github.com/microsoft/vscode/issues/248581 --- src/vs/base/common/policy.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/vs/base/common/policy.ts b/src/vs/base/common/policy.ts index befbff12794..e814b6e8e12 100644 --- a/src/vs/base/common/policy.ts +++ b/src/vs/base/common/policy.ts @@ -28,10 +28,19 @@ export interface IPolicy { readonly previewFeature?: boolean; /** - * Default value for a 'previewFeature' policy. Default is `false`. - * Remarks: - * A default value is only relevant when previewFeature is `true`. - * In all other instances, a value is required when setting a policy. - */ + * The value that a preview feature will use when its corresponding policy is active. + * + * Only applicable when `previewFeature: true`. When a preview feature's policy is enabled, + * this value determines what value the feature receives. + * + * For example: + * - If `defaultValue: true`, the feature's setting is locked to `true` WHEN the policy is in effect. + * - If `defaultValue: 'foo'`, the feature's setting is locked to 'foo' WHEN the policy is in effect. + * + * If omitted, 'false' is the assumed value. + * + * Note: This is unrelated to VS Code settings and their default values. This specifically controls + * the value of a preview feature's setting when policy is overriding it. + */ readonly defaultValue?: string | number | boolean; } From 67ef9d7893208a4ac89dd026aff5f9af00324dbf Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 8 Jul 2025 09:09:08 +0200 Subject: [PATCH 190/306] layout - reduce flicker for startup editors (#254506) --- src/vs/workbench/browser/layout.ts | 8 -------- .../browser/startupPage.ts | 4 ---- .../workingCopy/common/workingCopyBackup.ts | 5 ----- .../common/workingCopyBackupService.ts | 17 ----------------- 4 files changed, 34 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index f9baf7254fe..ac154915b74 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -7,7 +7,6 @@ import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } import { Event, Emitter } from '../../base/common/event.js'; import { EventType, addDisposableListener, getClientArea, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize, getActiveDocument, getWindows, getActiveWindow, isActiveDocument, getWindow, getWindowId, getActiveElement, Dimension } from '../../base/browser/dom.js'; import { onDidChangeFullscreen, isFullscreen, isWCOEnabled } from '../../base/browser/browser.js'; -import { IWorkingCopyBackupService } from '../services/workingCopy/common/workingCopyBackup.js'; import { isWindows, isLinux, isMacintosh, isWeb, isIOS } from '../../base/common/platform.js'; import { EditorInputCapabilities, GroupIdentifier, isResourceEditorInput, IUntypedEditorInput, pathsToEditors } from '../common/editor.js'; import { SidebarPart } from './parts/sidebar/sidebarPart.js'; @@ -286,7 +285,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private titleService!: ITitleService; private viewDescriptorService!: IViewDescriptorService; private contextService!: IWorkspaceContextService; - private workingCopyBackupService!: IWorkingCopyBackupService; private notificationService!: INotificationService; private themeService!: IThemeService; private statusBarService!: IStatusbarService; @@ -314,7 +312,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.hostService = accessor.get(IHostService); this.contextService = accessor.get(IWorkspaceContextService); this.storageService = accessor.get(IStorageService); - this.workingCopyBackupService = accessor.get(IWorkingCopyBackupService); this.themeService = accessor.get(IThemeService); this.extensionService = accessor.get(IExtensionService); this.logService = accessor.get(ILogService); @@ -837,11 +834,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return []; // do not open any empty untitled file if we restored groups/editors from previous session } - const hasBackups = await this.workingCopyBackupService.hasBackups(); - if (hasBackups) { - return []; // do not open any empty untitled file if we have backups to restore - } - return [{ editor: { resource: undefined } // open empty untitled file }]; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts index e11dd8dc01d..9b5282c0f7d 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts @@ -12,7 +12,6 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { onUnexpectedError } from '../../../../base/common/errors.js'; import { IWorkspaceContextService, UNKNOWN_EMPTY_WINDOW_WORKSPACE, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IWorkingCopyBackupService } from '../../../services/workingCopy/common/workingCopyBackup.js'; import { ILifecycleService, LifecyclePhase, StartupKind } from '../../../services/lifecycle/common/lifecycle.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { IFileService } from '../../../../platform/files/common/files.js'; @@ -85,7 +84,6 @@ export class StartupPageRunnerContribution extends Disposable implements IWorkbe constructor( @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorService private readonly editorService: IEditorService, - @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, @IFileService private readonly fileService: IFileService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @@ -135,8 +133,6 @@ export class StartupPageRunnerContribution extends Disposable implements IWorkbe const enabled = isStartupPageEnabled(this.configurationService, this.contextService, this.environmentService, this.logService); if (enabled && this.lifecycleService.startupKind !== StartupKind.ReloadedWindow) { - const hasBackups = await this.workingCopyBackupService.hasBackups(); - if (hasBackups) { return; } // Open the welcome even if we opened a set of default editors if (!this.editorService.activeEditor || this.layoutService.openedDefaultEditors) { diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyBackup.ts b/src/vs/workbench/services/workingCopy/common/workingCopyBackup.ts index 36f2f555c73..31fe1078fd8 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyBackup.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyBackup.ts @@ -38,11 +38,6 @@ export interface IWorkingCopyBackupService { readonly _serviceBrand: undefined; - /** - * Finds out if there are any working copy backups stored. - */ - hasBackups(): Promise; - /** * Finds out if a working copy backup with the given identifier * and optional version exists. diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyBackupService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyBackupService.ts index 5bc0f5c326e..8ced7d7bbff 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyBackupService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyBackupService.ts @@ -152,10 +152,6 @@ export abstract class WorkingCopyBackupService extends Disposable implements IWo } } - hasBackups(): Promise { - return this.impl.hasBackups(); - } - hasBackupSync(identifier: IWorkingCopyIdentifier, versionId?: number, meta?: IWorkingCopyBackupMeta): boolean { return this.impl.hasBackupSync(identifier, versionId, meta); } @@ -227,15 +223,6 @@ class WorkingCopyBackupServiceImpl extends Disposable implements IWorkingCopyBac return this.model; } - async hasBackups(): Promise { - const model = await this.ready; - - // Ensure to await any pending backup operations - await this.joinBackups(); - - return model.count() > 0; - } - hasBackupSync(identifier: IWorkingCopyIdentifier, versionId?: number, meta?: IWorkingCopyBackupMeta): boolean { if (!this.model) { return false; @@ -545,10 +532,6 @@ export class InMemoryWorkingCopyBackupService extends Disposable implements IWor super(); } - async hasBackups(): Promise { - return this.backups.size > 0; - } - hasBackupSync(identifier: IWorkingCopyIdentifier, versionId?: number): boolean { const backupResource = this.toBackupResource(identifier); From b88394f67ff3e248f4ac8124934a264962eaddd4 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 8 Jul 2025 09:38:35 +0200 Subject: [PATCH 191/306] themes - define more default colors based on theme values (#254580) --- .../themes/common/workbenchThemeService.ts | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index 4ec328971cd..6ba25af1b33 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -52,6 +52,7 @@ export enum ThemeSettingDefaults { } export const COLOR_THEME_DARK_INITIAL_COLORS = { + 'actionBar.toggledBackground': '#383a49', 'activityBar.activeBorder': '#0078D4', 'activityBar.background': '#181818', 'activityBar.border': '#2B2B2B', @@ -82,12 +83,16 @@ export const COLOR_THEME_DARK_INITIAL_COLORS = { 'editor.background': '#1F1F1F', 'editor.findMatchBackground': '#9E6A03', 'editor.foreground': '#CCCCCC', + 'editor.inactiveSelectionBackground': '#3A3D41', + 'editor.selectionHighlightBackground': '#ADD6FF26', 'editorGroup.border': '#FFFFFF17', 'editorGroupHeader.tabsBackground': '#181818', 'editorGroupHeader.tabsBorder': '#2B2B2B', 'editorGutter.addedBackground': '#2EA043', 'editorGutter.deletedBackground': '#F85149', 'editorGutter.modifiedBackground': '#0078D4', + 'editorIndentGuide.activeBackground1': '#707070', + 'editorIndentGuide.background1': '#404040', 'editorLineNumber.activeForeground': '#CCCCCC', 'editorLineNumber.foreground': '#6E7681', 'editorOverviewRuler.border': '#010409', @@ -103,8 +108,13 @@ export const COLOR_THEME_DARK_INITIAL_COLORS = { 'inputOption.activeBackground': '#2489DB82', 'inputOption.activeBorder': '#2488DB', 'keybindingLabel.foreground': '#CCCCCC', + 'list.activeSelectionIconForeground': '#FFF', + 'list.dropBackground': '#383B3D', 'menu.background': '#1F1F1F', + 'menu.border': '#454545', + 'menu.foreground': '#CCCCCC', 'menu.selectionBackground': '#0078d4', + 'menu.separatorBackground': '#454545', 'notificationCenterHeader.background': '#1F1F1F', 'notificationCenterHeader.foreground': '#CCCCCC', 'notifications.background': '#1F1F1F', @@ -121,6 +131,7 @@ export const COLOR_THEME_DARK_INITIAL_COLORS = { 'peekViewResult.background': '#1F1F1F', 'peekViewResult.matchHighlightBackground': '#BB800966', 'pickerGroup.border': '#3C3C3C', + 'ports.iconRunningProcessForeground': '#369432', 'progressBar.background': '#0078D4', 'quickInput.background': '#222222', 'quickInput.foreground': '#CCCCCC', @@ -150,35 +161,40 @@ export const COLOR_THEME_DARK_INITIAL_COLORS = { 'tab.activeBorder': '#1F1F1F', 'tab.activeBorderTop': '#0078D4', 'tab.activeForeground': '#FFFFFF', - 'tab.selectedBorderTop': '#6caddf', 'tab.border': '#2B2B2B', 'tab.hoverBackground': '#1F1F1F', 'tab.inactiveBackground': '#181818', 'tab.inactiveForeground': '#9D9D9D', + 'tab.lastPinnedBorder': '#ccc3', + 'tab.selectedBackground': '#222222', + 'tab.selectedBorderTop': '#6caddf', + 'tab.selectedForeground': '#ffffffa0', 'tab.unfocusedActiveBorder': '#1F1F1F', 'tab.unfocusedActiveBorderTop': '#2B2B2B', 'tab.unfocusedHoverBackground': '#1F1F1F', 'terminal.foreground': '#CCCCCC', + 'terminal.inactiveSelectionBackground': '#3A3D41', 'terminal.tab.activeBorder': '#0078D4', 'textBlockQuote.background': '#2B2B2B', 'textBlockQuote.border': '#616161', 'textCodeBlock.background': '#2B2B2B', 'textLink.activeForeground': '#4daafc', 'textLink.foreground': '#4daafc', - 'textPreformat.foreground': '#D0D0D0', 'textPreformat.background': '#3C3C3C', + 'textPreformat.foreground': '#D0D0D0', 'textSeparator.foreground': '#21262D', 'titleBar.activeBackground': '#181818', 'titleBar.activeForeground': '#CCCCCC', 'titleBar.border': '#2B2B2B', 'titleBar.inactiveBackground': '#1F1F1F', 'titleBar.inactiveForeground': '#9D9D9D', - 'welcomePage.tileBackground': '#2B2B2B', 'welcomePage.progress.foreground': '#0078D4', + 'welcomePage.tileBackground': '#2B2B2B', 'widget.border': '#313131' }; export const COLOR_THEME_LIGHT_INITIAL_COLORS = { + 'actionBar.toggledBackground': '#dddddd', 'activityBar.activeBorder': '#005FB8', 'activityBar.background': '#F8F8F8', 'activityBar.border': '#E5E5E5', @@ -201,6 +217,7 @@ export const COLOR_THEME_LIGHT_INITIAL_COLORS = { 'checkbox.background': '#F8F8F8', 'checkbox.border': '#CECECE', 'descriptionForeground': '#3B3B3B', + 'diffEditor.unchangedRegionBackground': '#f8f8f8', 'dropdown.background': '#FFFFFF', 'dropdown.border': '#CECECE', 'dropdown.foreground': '#3B3B3B', @@ -215,6 +232,7 @@ export const COLOR_THEME_LIGHT_INITIAL_COLORS = { 'editorGutter.addedBackground': '#2EA043', 'editorGutter.deletedBackground': '#F85149', 'editorGutter.modifiedBackground': '#005FB8', + 'editorIndentGuide.activeBackground1': '#939393', 'editorIndentGuide.background1': '#D3D3D3', 'editorLineNumber.activeForeground': '#171184', 'editorLineNumber.foreground': '#6E7681', @@ -236,8 +254,8 @@ export const COLOR_THEME_LIGHT_INITIAL_COLORS = { 'list.activeSelectionBackground': '#E8E8E8', 'list.activeSelectionForeground': '#000000', 'list.activeSelectionIconForeground': '#000000', - 'list.hoverBackground': '#F2F2F2', 'list.focusAndSelectionOutline': '#005FB8', + 'list.hoverBackground': '#F2F2F2', 'menu.border': '#CECECE', 'menu.selectionBackground': '#005FB8', 'menu.selectionForeground': '#ffffff', @@ -278,16 +296,16 @@ export const COLOR_THEME_LIGHT_INITIAL_COLORS = { 'sideBarSectionHeader.foreground': '#3B3B3B', 'sideBarTitle.foreground': '#3B3B3B', 'statusBar.background': '#F8F8F8', - 'statusBar.foreground': '#3B3B3B', 'statusBar.border': '#E5E5E5', - 'statusBarItem.hoverBackground': '#B8B8B850', - 'statusBarItem.compactHoverBackground': '#CCCCCC', 'statusBar.debuggingBackground': '#FD716C', 'statusBar.debuggingForeground': '#000000', 'statusBar.focusBorder': '#005FB8', + 'statusBar.foreground': '#3B3B3B', 'statusBar.noFolderBackground': '#F8F8F8', + 'statusBarItem.compactHoverBackground': '#CCCCCC', 'statusBarItem.errorBackground': '#C72E0F', 'statusBarItem.focusBorder': '#005FB8', + 'statusBarItem.hoverBackground': '#B8B8B850', 'statusBarItem.prominentBackground': '#6E768166', 'statusBarItem.remoteBackground': '#005FB8', 'statusBarItem.remoteForeground': '#FFFFFF', @@ -295,26 +313,28 @@ export const COLOR_THEME_LIGHT_INITIAL_COLORS = { 'tab.activeBorder': '#F8F8F8', 'tab.activeBorderTop': '#005FB8', 'tab.activeForeground': '#3B3B3B', - 'tab.selectedBorderTop': '#68a3da', 'tab.border': '#E5E5E5', 'tab.hoverBackground': '#FFFFFF', 'tab.inactiveBackground': '#F8F8F8', 'tab.inactiveForeground': '#868686', 'tab.lastPinnedBorder': '#D4D4D4', + 'tab.selectedBackground': '#ffffffa5', + 'tab.selectedBorderTop': '#68a3da', + 'tab.selectedForeground': '#333333b3', 'tab.unfocusedActiveBorder': '#F8F8F8', 'tab.unfocusedActiveBorderTop': '#E5E5E5', 'tab.unfocusedHoverBackground': '#F8F8F8', - 'terminalCursor.foreground': '#005FB8', 'terminal.foreground': '#3B3B3B', 'terminal.inactiveSelectionBackground': '#E5EBF1', 'terminal.tab.activeBorder': '#005FB8', + 'terminalCursor.foreground': '#005FB8', 'textBlockQuote.background': '#F8F8F8', 'textBlockQuote.border': '#E5E5E5', 'textCodeBlock.background': '#F8F8F8', 'textLink.activeForeground': '#005FB8', 'textLink.foreground': '#005FB8', - 'textPreformat.foreground': '#3B3B3B', 'textPreformat.background': '#0000001F', + 'textPreformat.foreground': '#3B3B3B', 'textSeparator.foreground': '#21262D', 'titleBar.activeBackground': '#F8F8F8', 'titleBar.activeForeground': '#1E1E1E', From f158f622b2c111ce732c7bb59889661be3e5bde8 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 8 Jul 2025 17:39:20 +1000 Subject: [PATCH 192/306] Ensure EOL of cell documents is as expected (#254578) --- .../contrib/notebook/common/model/notebookCellTextModel.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 2b0d842ec93..d38ebca6d63 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -117,6 +117,7 @@ export class NotebookCellTextModel extends Disposable implements ICell { } this._textBuffer = this._register(createTextBuffer(this._source, this._defaultEOL).textBuffer); + this._textBuffer.setEOL(this._defaultEOL === model.DefaultEndOfLine.LF ? '\n' : '\r\n'); this._register(this._textBuffer.onDidChangeContent(() => { this._hash = null; From 80154acf961538196ce8e83c319261e2b9e61704 Mon Sep 17 00:00:00 2001 From: Eugene Strizhok Date: Tue, 8 Jul 2025 09:41:22 +0200 Subject: [PATCH 193/306] Correct capitalization of 'JetBrains' and 'ReSharper' in settings UI (#254472) Proper casing for JetBrains & ReSharper --- src/vs/workbench/contrib/preferences/common/preferences.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/preferences/common/preferences.ts b/src/vs/workbench/contrib/preferences/common/preferences.ts index a71b33eba08..69cb9266e08 100644 --- a/src/vs/workbench/contrib/preferences/common/preferences.ts +++ b/src/vs/workbench/contrib/preferences/common/preferences.ts @@ -224,6 +224,10 @@ knownTermMappings.set('powershell', 'PowerShell'); knownTermMappings.set('javascript', 'JavaScript'); knownTermMappings.set('typescript', 'TypeScript'); knownTermMappings.set('github', 'GitHub'); +knownTermMappings.set('jet brains', 'JetBrains'); +knownTermMappings.set('jetbrains', 'JetBrains'); +knownTermMappings.set('re sharper', 'ReSharper'); +knownTermMappings.set('resharper', 'ReSharper'); export function wordifyKey(key: string): string { key = key From bcd950d6be13fc909a0758d2ef1145f01ce98f1c Mon Sep 17 00:00:00 2001 From: Aman Karmani Date: Tue, 8 Jul 2025 00:41:53 -0700 Subject: [PATCH 194/306] [engineering] add label to packageTask (#253779) previously appeared as "(anonymous)" in the logs --- build/gulpfile.vscode.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index f819cd6a1c2..22090d74318 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -213,7 +213,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op const destination = path.join(path.dirname(root), destinationFolderName); platform = platform || process.platform; - return () => { + const task = () => { const electron = require('@vscode/gulp-electron'); const json = require('gulp-json-editor'); @@ -422,6 +422,8 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op return result.pipe(vfs.dest(destination)); }; + task.taskName = `package-${platform}-${arch}`; + return task; } function patchWin32DependenciesTask(destinationFolderName) { From 7fd9305b695af02e4a2b7eb0e71a8afaffbe51bf Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 8 Jul 2025 09:57:52 +0200 Subject: [PATCH 195/306] polish a bit (#254591) --- .../common/mcpWorkbenchManagementService.ts | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts b/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts index d4aef30266e..9bf2786d826 100644 --- a/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts +++ b/src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts @@ -124,8 +124,21 @@ export class WorkbenchMcpManagementService extends Disposable implements IWorkbe } })); - this._register(this.mcpManagementService.onDidInstallMcpServers(e => this.handleInstallMcpServerResultsFromEvent(e, this._onDidInstallMcpServers, this._onDidInstallMcpServersInCurrentProfile))); - this._register(this.mcpManagementService.onDidUpdateMcpServers(e => this.handleInstallMcpServerResultsFromEvent(e, this._onDidUpdateMcpServers, this._onDidUpdateMcpServersInCurrentProfile))); + this._register(this.mcpManagementService.onDidInstallMcpServers(e => { + const { mcpServerInstallResult, mcpServerInstallResultInCurrentProfile } = this.createInstallMcpServerResultsFromEvent(e, LocalMcpServerScope.User); + this._onDidInstallMcpServers.fire(mcpServerInstallResult); + if (mcpServerInstallResultInCurrentProfile.length) { + this._onDidInstallMcpServersInCurrentProfile.fire(mcpServerInstallResultInCurrentProfile); + } + })); + + this._register(this.mcpManagementService.onDidUpdateMcpServers(e => { + const { mcpServerInstallResult, mcpServerInstallResultInCurrentProfile } = this.createInstallMcpServerResultsFromEvent(e, LocalMcpServerScope.User); + this._onDidUpdateMcpServers.fire(mcpServerInstallResult); + if (mcpServerInstallResultInCurrentProfile.length) { + this._onDidUpdateMcpServersInCurrentProfile.fire(mcpServerInstallResultInCurrentProfile); + } + })); this._register(this.mcpManagementService.onUninstallMcpServer(e => { this._onUninstallMcpServer.fire(e); @@ -148,7 +161,7 @@ export class WorkbenchMcpManagementService extends Disposable implements IWorkbe this._register(this.workspaceMcpManagementService.onDidInstallMcpServers(async e => { const { mcpServerInstallResult } = this.createInstallMcpServerResultsFromEvent(e, LocalMcpServerScope.Workspace); - this._onDidInstallMcpServersInCurrentProfile.fire(mcpServerInstallResult); + this._onDidInstallMcpServers.fire(mcpServerInstallResult); this._onDidInstallMcpServersInCurrentProfile.fire(mcpServerInstallResult); })); @@ -164,7 +177,7 @@ export class WorkbenchMcpManagementService extends Disposable implements IWorkbe this._register(this.workspaceMcpManagementService.onDidUpdateMcpServers(e => { const { mcpServerInstallResult } = this.createInstallMcpServerResultsFromEvent(e, LocalMcpServerScope.Workspace); - this._onDidUpdateMcpServersInCurrentProfile.fire(mcpServerInstallResult); + this._onDidUpdateMcpServers.fire(mcpServerInstallResult); this._onDidUpdateMcpServersInCurrentProfile.fire(mcpServerInstallResult); })); @@ -204,7 +217,7 @@ export class WorkbenchMcpManagementService extends Disposable implements IWorkbe })); } - private createInstallMcpServerResultsFromEvent(e: readonly InstallMcpServerResult[], scope: LocalMcpServerScope) { + private createInstallMcpServerResultsFromEvent(e: readonly InstallMcpServerResult[], scope: LocalMcpServerScope): { mcpServerInstallResult: IWorkbenchMcpServerInstallResult[]; mcpServerInstallResultInCurrentProfile: IWorkbenchMcpServerInstallResult[] } { const mcpServerInstallResult: IWorkbenchMcpServerInstallResult[] = []; const mcpServerInstallResultInCurrentProfile: IWorkbenchMcpServerInstallResult[] = []; for (const result of e) { @@ -221,14 +234,6 @@ export class WorkbenchMcpManagementService extends Disposable implements IWorkbe return { mcpServerInstallResult, mcpServerInstallResultInCurrentProfile }; } - private handleInstallMcpServerResultsFromEvent(e: readonly InstallMcpServerResult[], emitter: Emitter, currentProfileEmitter: Emitter): void { - const { mcpServerInstallResult, mcpServerInstallResultInCurrentProfile } = this.createInstallMcpServerResultsFromEvent(e, LocalMcpServerScope.User); - emitter.fire(mcpServerInstallResult); - if (mcpServerInstallResultInCurrentProfile.length) { - currentProfileEmitter.fire(mcpServerInstallResultInCurrentProfile); - } - } - private async handleRemoteInstallMcpServerResultsFromEvent(e: readonly InstallMcpServerResult[], emitter: Emitter, currentProfileEmitter: Emitter): Promise { const mcpServerInstallResult: IWorkbenchMcpServerInstallResult[] = []; const mcpServerInstallResultInCurrentProfile: IWorkbenchMcpServerInstallResult[] = []; From c104dccddb7a0e8eebd29f0fe75302c5676cc15b Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 8 Jul 2025 10:08:08 +0200 Subject: [PATCH 196/306] fix #254589 (#254595) --- .../workbench/contrib/mcp/browser/mcpCommands.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index 120d0e34fcf..27b1e707779 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -731,12 +731,6 @@ export class ShowInstalledMcpServersCommand extends Action2 { category, precondition: HasInstalledMcpServersContext, f1: true, - menu: { - id: CHAT_CONFIG_MENU_ID, - when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)), - order: 14, - group: '0_level' - } }); } @@ -750,6 +744,16 @@ export class ShowInstalledMcpServersCommand extends Action2 { } } +MenuRegistry.appendMenuItem(CHAT_CONFIG_MENU_ID, { + command: { + id: McpCommandIds.ShowInstalled, + title: localize2('mcp.servers', "MCP Servers") + }, + when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)), + order: 14, + group: '0_level' +}); + abstract class OpenMcpResourceCommand extends Action2 { protected abstract getURI(accessor: ServicesAccessor): Promise; From 6332b5db18e9422913cec28ee554063aa3b1739c Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:05:22 +0200 Subject: [PATCH 197/306] Fix ghost text view data (#254601) fix ghosttext view data --- .../browser/view/ghostText/ghostTextView.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts index 3d0f6602e06..1110c710055 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts @@ -35,6 +35,7 @@ import { IMouseEvent, StandardMouseEvent } from '../../../../../../base/browser/ import { CodeEditorWidget } from '../../../../../browser/widget/codeEditor/codeEditorWidget.js'; import { TokenWithTextArray } from '../../../../../common/tokens/tokenWithTextArray.js'; import { InlineCompletionViewData } from '../inlineEdits/inlineEditsViewInterface.js'; +import { sum } from '../../../../../../base/common/arrays.js'; export interface IGhostTextWidgetModel { readonly targetTextModel: IObservable; @@ -125,16 +126,18 @@ export class GhostTextView extends Disposable { }; }); - const cursorColumn = this._editor.getSelection()?.getStartPosition().column; + const cursorColumn = this._editor.getSelection()?.getStartPosition().column!; + const disjointInlineTexts = inlineTextsWithTokens.filter(inline => inline.text !== ''); + const hasInsertionOnCurrentLine = disjointInlineTexts.length !== 0; const renderData: InlineCompletionViewData = { - cursorColumnDistance: cursorColumn !== undefined ? Math.abs((inlineTextsWithTokens.length > 0 ? inlineTextsWithTokens[0].column : 1) - cursorColumn) : -1, - cursorLineDistance: inlineTextsWithTokens.length > 0 ? 0 : additionalLines.findIndex(line => line.content !== ''), - lineCountOriginal: inlineTextsWithTokens.length > 0 ? 1 : 0, - lineCountModified: additionalLines.length + (inlineTextsWithTokens.length > 0 ? 1 : 0), + cursorColumnDistance: (hasInsertionOnCurrentLine ? disjointInlineTexts[0].column : 1) - cursorColumn, + cursorLineDistance: hasInsertionOnCurrentLine ? 0 : (additionalLines.findIndex(line => line.content !== '') + 1), + lineCountOriginal: hasInsertionOnCurrentLine ? 1 : 0, + lineCountModified: additionalLines.length + (hasInsertionOnCurrentLine ? 1 : 0), characterCountOriginal: 0, - characterCountModified: inlineTextsWithTokens.reduce((acc, inline) => acc + inline.text.length, 0) + tokenizedAdditionalLines.reduce((acc, line) => acc + line.content.getTextLength(), 0), - disjointReplacements: inlineTextsWithTokens.length + (additionalLines.length > 0 ? 1 : 0), - sameShapeReplacements: inlineTextsWithTokens.length > 1 && inlineTextsWithTokens.length === 0 ? inlineTextsWithTokens.every(inline => inline.text.length === inlineTextsWithTokens[0].text.length) : undefined, + characterCountModified: sum(disjointInlineTexts.map(inline => inline.text.length)) + sum(tokenizedAdditionalLines.map(line => line.content.getTextLength())), + disjointReplacements: disjointInlineTexts.length + (additionalLines.length > 0 ? 1 : 0), + sameShapeReplacements: disjointInlineTexts.length > 1 && tokenizedAdditionalLines.length === 0 ? disjointInlineTexts.every(inline => inline.text === disjointInlineTexts[0].text) : undefined, }; this._model.handleInlineCompletionShown.read(reader)?.(renderData); From 80662bd27ca7de271e51d6b90961e055dcd8b6e0 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 8 Jul 2025 11:13:33 +0200 Subject: [PATCH 198/306] fix #252726 (#254602) --- .../workbench/contrib/extensions/browser/extensionsViewer.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts index b2a54e1f873..282f5e496dd 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts @@ -265,8 +265,7 @@ class ExtensionRenderer implements IListRenderer, IExt public renderTemplate(container: HTMLElement): IExtensionTemplateData { container.classList.add('extension'); - const icon = dom.append(container, dom.$('img.icon')); - const iconWidget = this.instantiationService.createInstance(ExtensionIconWidget, icon); + const iconWidget = this.instantiationService.createInstance(ExtensionIconWidget, container); const details = dom.append(container, dom.$('.details')); const header = dom.append(details, dom.$('.header')); From 8bf15c45c1845f9ab4bc5ea0757dd87c67a12c60 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 8 Jul 2025 11:28:39 +0200 Subject: [PATCH 199/306] update distro (#254607) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e874629f586..766552720b6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.103.0", - "distro": "f79d65a2e4f2caf1099ed08b494763e63710c2bc", + "distro": "7fd50e9bdc1a124eb32a184ee4bc0d437cdae9f0", "author": { "name": "Microsoft Corporation" }, @@ -234,4 +234,4 @@ "optionalDependencies": { "windows-foreground-love": "0.5.0" } -} +} \ No newline at end of file From c7a34d136174a9393d632e3a5c76a89224877381 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Tue, 8 Jul 2025 12:21:41 +0200 Subject: [PATCH 200/306] refactor: extracting inlineDecoration types into separate file (#254611) --- .../renderStrategy/fullFileRenderStrategy.ts | 3 +- .../renderStrategy/viewportRenderStrategy.ts | 3 +- src/vs/editor/browser/gpu/viewGpuContext.ts | 2 +- .../browser/viewParts/viewLines/viewLine.ts | 2 +- .../diffEditorViewZones.ts | 2 +- .../diffEditorViewZones/renderLines.ts | 3 +- .../common/viewLayout/lineDecorations.ts | 2 +- .../common/viewLayout/viewLineRenderer.ts | 2 +- src/vs/editor/common/viewModel.ts | 35 +---------------- .../common/viewModel/inlineDecorations.ts | 39 +++++++++++++++++++ .../common/viewModel/modelLineProjection.ts | 3 +- .../common/viewModel/viewModelDecorations.ts | 3 +- .../editor/common/viewModel/viewModelImpl.ts | 3 +- .../browser/model/ghostText.ts | 2 +- .../browser/view/ghostText/ghostTextView.ts | 2 +- .../inlineEditsInsertionView.ts | 2 +- .../inlineEditsLineReplacementView.ts | 2 +- .../viewModel/viewModelDecorations.test.ts | 2 +- .../common/viewLayout/lineDecorations.test.ts | 2 +- .../viewLayout/viewLineRenderer.test.ts | 2 +- .../chatEditingCodeEditorIntegration.ts | 2 +- .../browser/inlineChatStrategies.ts | 2 +- .../inlineDiff/notebookCellDiffDecorator.ts | 2 +- 23 files changed, 67 insertions(+), 55 deletions(-) create mode 100644 src/vs/editor/common/viewModel/inlineDecorations.ts diff --git a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts index 6db8ab9e476..9588c78c49c 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts @@ -10,7 +10,7 @@ import { CursorColumns } from '../../../common/core/cursorColumns.js'; import type { IViewLineTokens } from '../../../common/tokens/lineTokens.js'; import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLineMappingChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewThemeChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../../common/viewEvents.js'; import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; -import type { InlineDecoration, ViewLineRenderingData } from '../../../common/viewModel.js'; +import type { ViewLineRenderingData } from '../../../common/viewModel.js'; import type { ViewContext } from '../../../common/viewModel/viewContext.js'; import type { ViewLineOptions } from '../../viewParts/viewLines/viewLineOptions.js'; import type { ITextureAtlasPageGlyph } from '../atlas/atlas.js'; @@ -22,6 +22,7 @@ import { quadVertices } from '../gpuUtils.js'; import { GlyphRasterizer } from '../raster/glyphRasterizer.js'; import { ViewGpuContext } from '../viewGpuContext.js'; import { BaseRenderStrategy } from './baseRenderStrategy.js'; +import { InlineDecoration } from '../../../common/viewModel/inlineDecorations.js'; const enum Constants { IndicesPerCell = 6, diff --git a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts index d051c007716..9dbfa48a53f 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts @@ -11,7 +11,8 @@ import { CursorColumns } from '../../../common/core/cursorColumns.js'; import type { IViewLineTokens } from '../../../common/tokens/lineTokens.js'; import { type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLineMappingChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewThemeChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../../common/viewEvents.js'; import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; -import type { InlineDecoration, ViewLineRenderingData } from '../../../common/viewModel.js'; +import type { ViewLineRenderingData } from '../../../common/viewModel.js'; +import { InlineDecoration } from '../../../common/viewModel/inlineDecorations.js'; import type { ViewContext } from '../../../common/viewModel/viewContext.js'; import type { ViewLineOptions } from '../../viewParts/viewLines/viewLineOptions.js'; import type { ITextureAtlasPageGlyph } from '../atlas/atlas.js'; diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 953e7c23f7a..4333d262ff6 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -22,8 +22,8 @@ import type { ViewContext } from '../../common/viewModel/viewContext.js'; import { DecorationCssRuleExtractor } from './css/decorationCssRuleExtractor.js'; import { Event } from '../../../base/common/event.js'; import { EditorOption, type IEditorOptions } from '../../common/config/editorOptions.js'; -import { InlineDecorationType } from '../../common/viewModel.js'; import { DecorationStyleCache } from './css/decorationStyleCache.js'; +import { InlineDecorationType } from '../../common/viewModel/inlineDecorations.js'; export class ViewGpuContext extends Disposable { /** diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts index 962c31a4bef..a715d6df138 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts @@ -13,13 +13,13 @@ import { FloatHorizontalRange, VisibleRanges } from '../../view/renderingContext import { LineDecoration } from '../../../common/viewLayout/lineDecorations.js'; import { CharacterMapping, ForeignElementType, RenderLineInput, renderViewLine, DomPosition, RenderWhitespace } from '../../../common/viewLayout/viewLineRenderer.js'; import { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; -import { InlineDecorationType } from '../../../common/viewModel.js'; import { isHighContrast } from '../../../../platform/theme/common/theme.js'; import { EditorFontLigatures } from '../../../common/config/editorOptions.js'; import { DomReadingContext } from './domReadingContext.js'; import type { ViewLineOptions } from './viewLineOptions.js'; import { ViewGpuContext } from '../../gpu/viewGpuContext.js'; import { OffsetRange } from '../../../common/core/ranges/offsetRange.js'; +import { InlineDecorationType } from '../../../common/viewModel/inlineDecorations.js'; const canUseFastRenderedViewLine = (function () { if (platform.isNative) { diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts index 0a2fd9eda46..cc9a23d23b3 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts @@ -26,11 +26,11 @@ import { Position } from '../../../../../common/core/position.js'; import { DetailedLineRangeMapping } from '../../../../../common/diff/rangeMapping.js'; import { ScrollType } from '../../../../../common/editorCommon.js'; import { BackgroundTokenizationState } from '../../../../../common/tokenizationTextModelPart.js'; -import { InlineDecoration, InlineDecorationType } from '../../../../../common/viewModel.js'; import { IClipboardService } from '../../../../../../platform/clipboard/common/clipboardService.js'; import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js'; import { DiffEditorOptions } from '../../diffEditorOptions.js'; import { Range } from '../../../../../common/core/range.js'; +import { InlineDecoration, InlineDecorationType } from '../../../../../common/viewModel/inlineDecorations.js'; /** * Ensures both editors have the same height by aligning unchanged lines. diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.ts index ac4aee78443..99afc414d65 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.ts @@ -13,7 +13,8 @@ import { ModelLineProjectionData } from '../../../../../common/modelLineProjecti import { IViewLineTokens, LineTokens } from '../../../../../common/tokens/lineTokens.js'; import { LineDecoration } from '../../../../../common/viewLayout/lineDecorations.js'; import { RenderLineInput, renderViewLine } from '../../../../../common/viewLayout/viewLineRenderer.js'; -import { InlineDecoration, ViewLineRenderingData } from '../../../../../common/viewModel.js'; +import { ViewLineRenderingData } from '../../../../../common/viewModel.js'; +import { InlineDecoration } from '../../../../../common/viewModel/inlineDecorations.js'; const ttPolicy = createTrustedTypesPolicy('diffEditorWidget', { createHTML: value => value }); diff --git a/src/vs/editor/common/viewLayout/lineDecorations.ts b/src/vs/editor/common/viewLayout/lineDecorations.ts index c607968187d..7641e62c859 100644 --- a/src/vs/editor/common/viewLayout/lineDecorations.ts +++ b/src/vs/editor/common/viewLayout/lineDecorations.ts @@ -5,8 +5,8 @@ import * as strings from '../../../base/common/strings.js'; import { Constants } from '../../../base/common/uint.js'; +import { InlineDecoration, InlineDecorationType } from '../viewModel/inlineDecorations.js'; import { LinePartMetadata } from './linePart.js'; -import { InlineDecoration, InlineDecorationType } from '../viewModel.js'; export class LineDecoration { _lineDecorationBrand: void = undefined; diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index 5911aa3dad1..1b1cf84e5f0 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -9,9 +9,9 @@ import * as strings from '../../../base/common/strings.js'; import { IViewLineTokens } from '../tokens/lineTokens.js'; import { StringBuilder } from '../core/stringBuilder.js'; import { LineDecoration, LineDecorationsNormalizer } from './lineDecorations.js'; -import { InlineDecorationType } from '../viewModel.js'; import { LinePart, LinePartMetadata } from './linePart.js'; import { OffsetRange } from '../core/ranges/offsetRange.js'; +import { InlineDecorationType } from '../viewModel/inlineDecorations.js'; export const enum RenderWhitespace { None = 0, diff --git a/src/vs/editor/common/viewModel.ts b/src/vs/editor/common/viewModel.ts index 3071cbf0ff3..67c0df001d3 100644 --- a/src/vs/editor/common/viewModel.ts +++ b/src/vs/editor/common/viewModel.ts @@ -18,6 +18,7 @@ import { BracketGuideOptions, IActiveIndentGuideInfo, IndentGuide } from './text import { IViewLineTokens } from './tokens/lineTokens.js'; import { ViewEventHandler } from './viewEventHandler.js'; import { VerticalRevealType } from './viewEvents.js'; +import { InlineDecoration, SingleLineInlineDecoration } from './viewModel/inlineDecorations.js'; export interface IViewModel extends ICursorSimpleModel { @@ -398,40 +399,6 @@ export class ViewLineRenderingData { } } -export const enum InlineDecorationType { - Regular = 0, - Before = 1, - After = 2, - RegularAffectingLetterSpacing = 3 -} - -export class InlineDecoration { - constructor( - public readonly range: Range, - public readonly inlineClassName: string, - public readonly type: InlineDecorationType - ) { - } -} - -export class SingleLineInlineDecoration { - constructor( - public readonly startOffset: number, - public readonly endOffset: number, - public readonly inlineClassName: string, - public readonly inlineClassNameAffectsLetterSpacing: boolean - ) { - } - - toInlineDecoration(lineNumber: number): InlineDecoration { - return new InlineDecoration( - new Range(lineNumber, this.startOffset + 1, lineNumber, this.endOffset + 1), - this.inlineClassName, - this.inlineClassNameAffectsLetterSpacing ? InlineDecorationType.RegularAffectingLetterSpacing : InlineDecorationType.Regular - ); - } -} - export class ViewModelDecoration { _viewModelDecorationBrand: void = undefined; diff --git a/src/vs/editor/common/viewModel/inlineDecorations.ts b/src/vs/editor/common/viewModel/inlineDecorations.ts new file mode 100644 index 00000000000..c33336342f0 --- /dev/null +++ b/src/vs/editor/common/viewModel/inlineDecorations.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../core/range.js'; + +export const enum InlineDecorationType { + Regular = 0, + Before = 1, + After = 2, + RegularAffectingLetterSpacing = 3 +} + +export class InlineDecoration { + constructor( + public readonly range: Range, + public readonly inlineClassName: string, + public readonly type: InlineDecorationType + ) { } +} + +export class SingleLineInlineDecoration { + constructor( + public readonly startOffset: number, + public readonly endOffset: number, + public readonly inlineClassName: string, + public readonly inlineClassNameAffectsLetterSpacing: boolean + ) { + } + + toInlineDecoration(lineNumber: number): InlineDecoration { + return new InlineDecoration( + new Range(lineNumber, this.startOffset + 1, lineNumber, this.endOffset + 1), + this.inlineClassName, + this.inlineClassNameAffectsLetterSpacing ? InlineDecorationType.RegularAffectingLetterSpacing : InlineDecorationType.Regular + ); + } +} diff --git a/src/vs/editor/common/viewModel/modelLineProjection.ts b/src/vs/editor/common/viewModel/modelLineProjection.ts index b01a1de2898..d98e4aa98cf 100644 --- a/src/vs/editor/common/viewModel/modelLineProjection.ts +++ b/src/vs/editor/common/viewModel/modelLineProjection.ts @@ -9,7 +9,8 @@ import { IRange } from '../core/range.js'; import { EndOfLinePreference, ITextModel, PositionAffinity } from '../model.js'; import { LineInjectedText } from '../textModelEvents.js'; import { InjectedText, ModelLineProjectionData } from '../modelLineProjectionData.js'; -import { SingleLineInlineDecoration, ViewLineData } from '../viewModel.js'; +import { ViewLineData } from '../viewModel.js'; +import { SingleLineInlineDecoration } from './inlineDecorations.js'; export interface IModelLineProjection { isVisible(): boolean; diff --git a/src/vs/editor/common/viewModel/viewModelDecorations.ts b/src/vs/editor/common/viewModel/viewModelDecorations.ts index beab19d6d79..8070ccbe541 100644 --- a/src/vs/editor/common/viewModel/viewModelDecorations.ts +++ b/src/vs/editor/common/viewModel/viewModelDecorations.ts @@ -9,9 +9,10 @@ import { Range } from '../core/range.js'; import { IEditorConfiguration } from '../config/editorConfiguration.js'; import { IModelDecoration, ITextModel, PositionAffinity } from '../model.js'; import { IViewModelLines } from './viewModelLines.js'; -import { ICoordinatesConverter, InlineDecoration, InlineDecorationType, ViewModelDecoration } from '../viewModel.js'; +import { ICoordinatesConverter, ViewModelDecoration } from '../viewModel.js'; import { filterFontDecorations, filterValidationDecorations } from '../config/editorOptions.js'; import { StandardTokenType } from '../encodedTokenAttributes.js'; +import { InlineDecoration, InlineDecorationType } from './inlineDecorations.js'; export interface IDecorationsViewportData { /** diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 7e65e7a1d45..712778b665d 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -34,7 +34,7 @@ import { ViewLayout } from '../viewLayout/viewLayout.js'; import { MinimapTokensColorTracker } from './minimapTokensColorTracker.js'; import { ILineBreaksComputer, ILineBreaksComputerFactory, InjectedText } from '../modelLineProjectionData.js'; import { ViewEventHandler } from '../viewEventHandler.js'; -import { ICoordinatesConverter, InlineDecoration, ILineHeightChangeAccessor, IViewModel, IWhitespaceChangeAccessor, MinimapLinesRenderingData, OverviewRulerDecorationsGroup, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from '../viewModel.js'; +import { ICoordinatesConverter, ILineHeightChangeAccessor, IViewModel, IWhitespaceChangeAccessor, MinimapLinesRenderingData, OverviewRulerDecorationsGroup, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from '../viewModel.js'; import { ViewModelDecorations } from './viewModelDecorations.js'; import { FocusChangedEvent, HiddenAreasChangedEvent, ModelContentChangedEvent, ModelDecorationsChangedEvent, ModelFontChangedEvent, ModelLanguageChangedEvent, ModelLanguageConfigurationChangedEvent, ModelLineHeightChangedEvent, ModelOptionsChangedEvent, ModelTokensChangedEvent, OutgoingViewModelEvent, ReadOnlyEditAttemptEvent, ScrollChangedEvent, ViewModelEventDispatcher, ViewModelEventsCollector, ViewZonesChangedEvent, WidgetFocusChangedEvent } from '../viewModelEventDispatcher.js'; import { IViewModelLines, ViewModelLinesFromModelAsIs, ViewModelLinesFromProjectedModel } from './viewModelLines.js'; @@ -42,6 +42,7 @@ import { IThemeService } from '../../../platform/theme/common/themeService.js'; import { GlyphMarginLanesModel } from './glyphLanesModel.js'; import { ICustomLineHeightData } from '../viewLayout/lineHeights.js'; import { TextModelEditReason } from '../textModelEditReason.js'; +import { InlineDecoration } from './inlineDecorations.js'; const USE_IDENTITY_LINES_COLLECTION = true; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts index 30f4e475890..4629b406532 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts @@ -9,9 +9,9 @@ import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; import { TextReplacement, TextEdit } from '../../../../common/core/edits/textEdit.js'; import { LineDecoration } from '../../../../common/viewLayout/lineDecorations.js'; -import { InlineDecoration } from '../../../../common/viewModel.js'; import { ColumnRange } from '../../../../common/core/ranges/columnRange.js'; import { assertFn, checkAdjacentItems } from '../../../../../base/common/assert.js'; +import { InlineDecoration } from '../../../../common/viewModel/inlineDecorations.js'; export class GhostText { constructor( diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts index 1110c710055..0d5a7f290c8 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts @@ -25,7 +25,6 @@ import { IModelDeltaDecoration, ITextModel, InjectedTextCursorStops, PositionAff import { LineTokens } from '../../../../../common/tokens/lineTokens.js'; import { LineDecoration } from '../../../../../common/viewLayout/lineDecorations.js'; import { RenderLineInput, renderViewLine } from '../../../../../common/viewLayout/viewLineRenderer.js'; -import { InlineDecorationType } from '../../../../../common/viewModel.js'; import { GhostText, GhostTextReplacement, IGhostTextLine } from '../../model/ghostText.js'; import { RangeSingleLine } from '../../../../../common/core/ranges/rangeSingleLine.js'; import { ColumnRange } from '../../../../../common/core/ranges/columnRange.js'; @@ -35,6 +34,7 @@ import { IMouseEvent, StandardMouseEvent } from '../../../../../../base/browser/ import { CodeEditorWidget } from '../../../../../browser/widget/codeEditor/codeEditorWidget.js'; import { TokenWithTextArray } from '../../../../../common/tokens/tokenWithTextArray.js'; import { InlineCompletionViewData } from '../inlineEdits/inlineEditsViewInterface.js'; +import { InlineDecorationType } from '../../../../../common/viewModel/inlineDecorations.js'; import { sum } from '../../../../../../base/common/arrays.js'; export interface IGhostTextWidgetModel { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts index 319d76cbddb..aec55db345a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts @@ -20,7 +20,7 @@ import { LineRange } from '../../../../../../common/core/ranges/lineRange.js'; import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js'; import { ILanguageService } from '../../../../../../common/languages/language.js'; import { LineTokens, TokenArray } from '../../../../../../common/tokens/lineTokens.js'; -import { InlineDecoration, InlineDecorationType } from '../../../../../../common/viewModel.js'; +import { InlineDecoration, InlineDecorationType } from '../../../../../../common/viewModel/inlineDecorations.js'; import { GhostText, GhostTextPart } from '../../../model/ghostText.js'; import { GhostTextView } from '../../ghostText/ghostTextView.js'; import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js'; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts index 2a40722dd23..7d9a54433c1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts @@ -23,7 +23,7 @@ import { LineRange } from '../../../../../../common/core/ranges/lineRange.js'; import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js'; import { ILanguageService } from '../../../../../../common/languages/language.js'; import { LineTokens, TokenArray } from '../../../../../../common/tokens/lineTokens.js'; -import { InlineDecoration, InlineDecorationType } from '../../../../../../common/viewModel.js'; +import { InlineDecoration, InlineDecorationType } from '../../../../../../common/viewModel/inlineDecorations.js'; import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js'; import { getEditorBlendedColor, getModifiedBorderColor, getOriginalBorderColor, modifiedChangedLineBackgroundColor, originalBackgroundColor } from '../theme.js'; import { getEditorValidOverlayRect, getPrefixTrim, mapOutFalsy, rectToProps } from '../utils/utils.js'; diff --git a/src/vs/editor/test/browser/viewModel/viewModelDecorations.test.ts b/src/vs/editor/test/browser/viewModel/viewModelDecorations.test.ts index bed88ad1260..055998d1694 100644 --- a/src/vs/editor/test/browser/viewModel/viewModelDecorations.test.ts +++ b/src/vs/editor/test/browser/viewModel/viewModelDecorations.test.ts @@ -7,8 +7,8 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { IEditorOptions } from '../../../common/config/editorOptions.js'; import { Range } from '../../../common/core/range.js'; -import { InlineDecoration, InlineDecorationType } from '../../../common/viewModel.js'; import { testViewModel } from './testViewModel.js'; +import { InlineDecoration, InlineDecorationType } from '../../../common/viewModel/inlineDecorations.js'; suite('ViewModelDecorations', () => { diff --git a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts index 59f87b95b43..efe951f3457 100644 --- a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts +++ b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { Range } from '../../../common/core/range.js'; import { DecorationSegment, LineDecoration, LineDecorationsNormalizer } from '../../../common/viewLayout/lineDecorations.js'; -import { InlineDecoration, InlineDecorationType } from '../../../common/viewModel.js'; +import { InlineDecoration, InlineDecorationType } from '../../../common/viewModel/inlineDecorations.js'; suite('Editor ViewLayout - ViewLineParts', () => { diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 1d2a6ab5002..a4f66b6211c 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -11,9 +11,9 @@ import { MetadataConsts } from '../../../common/encodedTokenAttributes.js'; import { IViewLineTokens } from '../../../common/tokens/lineTokens.js'; import { LineDecoration } from '../../../common/viewLayout/lineDecorations.js'; import { CharacterMapping, DomPosition, RenderLineInput, RenderLineOutput2, renderViewLine2 as renderViewLine } from '../../../common/viewLayout/viewLineRenderer.js'; -import { InlineDecorationType } from '../../../common/viewModel.js'; import { TestLineToken, TestLineTokens } from '../core/testLineToken.js'; import { OffsetRange } from '../../../common/core/ranges/offsetRange.js'; +import { InlineDecorationType } from '../../../common/viewModel/inlineDecorations.js'; function createViewLineTokens(viewLineTokens: TestLineToken[]): IViewLineTokens { return new TestLineTokens(viewLineTokens); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts index f8d12d6a5a4..b2f4fa3b9d4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts @@ -25,7 +25,6 @@ import { IDocumentDiff } from '../../../../../editor/common/diff/documentDiffPro import { DetailedLineRangeMapping } from '../../../../../editor/common/diff/rangeMapping.js'; import { IModelDeltaDecoration, ITextModel, MinimapPosition, OverviewRulerLane, TrackedRangeStickiness } from '../../../../../editor/common/model.js'; import { ModelDecorationOptions } from '../../../../../editor/common/model/textModel.js'; -import { InlineDecoration, InlineDecorationType } from '../../../../../editor/common/viewModel.js'; import { localize } from '../../../../../nls.js'; import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { MenuWorkbenchToolBar, HiddenItemStrategy } from '../../../../../platform/actions/browser/toolbar.js'; @@ -40,6 +39,7 @@ import { IModifiedFileEntry, IModifiedFileEntryChangeHunk, IModifiedFileEntryEdi import { isTextDiffEditorForEntry } from './chatEditing.js'; import { IEditorDecorationsCollection } from '../../../../../editor/common/editorCommon.js'; import { ChatAgentLocation } from '../../common/constants.js'; +import { InlineDecoration, InlineDecorationType } from '../../../../../editor/common/viewModel/inlineDecorations.js'; export interface IDocumentDiff2 extends IDocumentDiff { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index a2bcfa14a33..551435f3040 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -19,7 +19,6 @@ import { IEditorDecorationsCollection } from '../../../../editor/common/editorCo import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, IValidEditOperation, MinimapPosition, OverviewRulerLane, TrackedRangeStickiness } from '../../../../editor/common/model.js'; import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js'; import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; -import { InlineDecoration, InlineDecorationType } from '../../../../editor/common/viewModel.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { Progress } from '../../../../platform/progress/common/progress.js'; import { SaveReason } from '../../../common/editor.js'; @@ -41,6 +40,7 @@ import { Iterable } from '../../../../base/common/iterator.js'; import { ConflictActionsFactory, IContentWidgetAction } from '../../mergeEditor/browser/view/conflictActions.js'; import { observableValue } from '../../../../base/common/observable.js'; import { IMenuService, MenuItemAction } from '../../../../platform/actions/common/actions.js'; +import { InlineDecoration, InlineDecorationType } from '../../../../editor/common/viewModel/inlineDecorations.js'; export interface IEditObserver { start(): void; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookCellDiffDecorator.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookCellDiffDecorator.ts index 8da4d2c230d..5f3ae5f09cd 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookCellDiffDecorator.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookCellDiffDecorator.ts @@ -16,12 +16,12 @@ import { diffAddDecoration, diffWholeLineAddDecoration, diffDeleteDecoration } f import { IDocumentDiff } from '../../../../../../editor/common/diff/documentDiffProvider.js'; import { ITextModel, TrackedRangeStickiness, MinimapPosition, IModelDeltaDecoration, OverviewRulerLane } from '../../../../../../editor/common/model.js'; import { ModelDecorationOptions } from '../../../../../../editor/common/model/textModel.js'; -import { InlineDecoration, InlineDecorationType } from '../../../../../../editor/common/viewModel.js'; import { Range } from '../../../../../../editor/common/core/range.js'; import { NotebookCellTextModel } from '../../../common/model/notebookCellTextModel.js'; import { DetailedLineRangeMapping } from '../../../../../../editor/common/diff/rangeMapping.js'; import { minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../../../../scm/common/quickDiff.js'; import { INotebookOriginalCellModelFactory } from './notebookOriginalCellModelFactory.js'; +import { InlineDecoration, InlineDecorationType } from '../../../../../../editor/common/viewModel/inlineDecorations.js'; //TODO: allow client to set read-only - chateditsession should set read-only while making changes export class NotebookCellDiffDecorator extends DisposableStore { From a31b87c942bd79f0faa2fd0680d774daeb00f0b5 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 8 Jul 2025 12:34:58 +0200 Subject: [PATCH 201/306] fix #252206 (#254627) --- src/vs/workbench/services/views/browser/viewsService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/services/views/browser/viewsService.ts b/src/vs/workbench/services/views/browser/viewsService.ts index 9e4053c2535..0381326cbff 100644 --- a/src/vs/workbench/services/views/browser/viewsService.ts +++ b/src/vs/workbench/services/views/browser/viewsService.ts @@ -531,7 +531,7 @@ export class ViewsService extends Disposable implements IViewsService { layoutService.setPartHidden(true, getPartByLocation(viewLocation)); } } else { - viewsService.openView(viewDescriptor.id, !options?.preserveFocus); + await viewsService.openView(viewDescriptor.id, !options?.preserveFocus); } } })); From f8bb386a48f799f890d00f217a90f615c9b3866c Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:59:18 +0200 Subject: [PATCH 202/306] Update grammars (#254634) --- extensions/go/cgmanifest.json | 4 +- extensions/go/syntaxes/go.tmLanguage.json | 259 +++++++++++++----- extensions/julia/cgmanifest.json | 2 +- .../julia/syntaxes/julia.tmLanguage.json | 4 +- extensions/latex/cgmanifest.json | 4 +- extensions/latex/syntaxes/TeX.tmLanguage.json | 6 +- extensions/markdown-basics/cgmanifest.json | 2 +- .../syntaxes/markdown.tmLanguage.json | 4 +- extensions/r/cgmanifest.json | 4 +- extensions/r/syntaxes/r.tmLanguage.json | 5 +- extensions/sql/cgmanifest.json | 4 +- extensions/sql/syntaxes/sql.tmLanguage.json | 6 +- 12 files changed, 214 insertions(+), 90 deletions(-) diff --git a/extensions/go/cgmanifest.json b/extensions/go/cgmanifest.json index 2fbbe980d75..d41f8a2672d 100644 --- a/extensions/go/cgmanifest.json +++ b/extensions/go/cgmanifest.json @@ -6,12 +6,12 @@ "git": { "name": "go-syntax", "repositoryUrl": "https://github.com/worlpaker/go-syntax", - "commitHash": "0ce19cdf1cb5dab6aa99ccc933be9bd21e855ed1" + "commitHash": "8c70c078f56d237f72574ce49cc95839c4f8a741" } }, "license": "MIT", "description": "The file syntaxes/go.tmLanguage.json is from https://github.com/worlpaker/go-syntax, which in turn was derived from https://github.com/jeff-hykin/better-go-syntax.", - "version": "0.8.1" + "version": "0.8.4" } ], "version": 1 diff --git a/extensions/go/syntaxes/go.tmLanguage.json b/extensions/go/syntaxes/go.tmLanguage.json index 00472b67ddc..e83763a8eb5 100644 --- a/extensions/go/syntaxes/go.tmLanguage.json +++ b/extensions/go/syntaxes/go.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/worlpaker/go-syntax/commit/0ce19cdf1cb5dab6aa99ccc933be9bd21e855ed1", + "version": "https://github.com/worlpaker/go-syntax/commit/8c70c078f56d237f72574ce49cc95839c4f8a741", "name": "Go", "scopeName": "source.go", "patterns": [ @@ -34,7 +34,7 @@ "include": "#group-variables" }, { - "include": "#field_hover" + "include": "#hover" } ] }, @@ -115,7 +115,7 @@ "include": "#property_variables" }, { - "include": "#switch_select_case_variables" + "include": "#switch_variables" }, { "include": "#other_variables" @@ -1704,7 +1704,7 @@ }, "support_functions": { "comment": "Support Functions", - "match": "(?:(?:((?<=\\.)\\b\\w+)|(\\b\\w+))(\\[(?:(?:[\\w\\.\\*\\[\\]\\{\\}\"\\']+)(?:(?:\\,\\s*(?:[\\w\\.\\*\\[\\]\\{\\}]+))*))?\\])?(?=\\())", + "match": "(?:(?:((?<=\\.)\\b\\w+)|(\\b\\w+))(?\\[(?:[^\\[\\]]|\\g)*\\])?(?=\\())", "captures": { "1": { "name": "entity.name.function.support.go" @@ -1761,7 +1761,8 @@ "include": "#after_control_variables" }, { - "match": "(\\b[\\w\\.]+)(\\[(?:(?:[\\w\\.\\*\\[\\]\\{\\}]+)(?:(?:\\,\\s*(?:[\\w\\.\\*\\[\\]\\{\\}]+))*))?\\])?(?=\\{)(?\\[(?:[^\\[\\]]|\\g)*\\])?(?=\\{)", "captures": { "1": { "patterns": [ @@ -1807,7 +1808,7 @@ }, "type_assertion_inline": { "comment": "struct/interface types in-line (type assertion) | switch type keyword", - "match": "(?:(?<=\\.\\()(?:(\\btype\\b)|((?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?[\\w\\.\\[\\]\\*]+))(?=\\)))", + "match": "(?:(?<=\\.\\()(?:(\\btype\\b)|((?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:[\\[\\]\\*]+)?(?:[\\w\\.]+)(?:\\[(?:(?:[\\w\\.\\*\\[\\]\\{\\}]+)(?:(?:\\,\\s*(?:[\\w\\.\\*\\[\\]\\{\\}]+))*))?\\])?))(?=\\)))", "captures": { "1": { "name": "keyword.type.go" @@ -1815,7 +1816,31 @@ "2": { "patterns": [ { - "include": "#type-declarations" + "include": "#type-declarations-without-brackets" + }, + { + "match": "\\(", + "name": "punctuation.definition.begin.bracket.round.go" + }, + { + "match": "\\)", + "name": "punctuation.definition.end.bracket.round.go" + }, + { + "match": "\\[", + "name": "punctuation.definition.begin.bracket.square.go" + }, + { + "match": "\\]", + "name": "punctuation.definition.end.bracket.square.go" + }, + { + "match": "\\{", + "name": "punctuation.definition.begin.bracket.curly.go" + }, + { + "match": "\\}", + "name": "punctuation.definition.end.bracket.curly.go" }, { "match": "\\w+", @@ -1904,12 +1929,12 @@ }, { "comment": "one line with semicolon(;) without formatting gofmt - single type | property variables and types", - "match": "(?:(?<=\\{)((?:\\s*(?:(?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))?(?:(?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:[\\S]+)(?:\\;)?))+)\\s*(?=\\}))", + "match": "(?:(?<=\\{)((?:\\s*(?:(?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))?(?:(?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:[^\\s/]+)(?:\\;)?))+)\\s*(?=\\}))", "captures": { "1": { "patterns": [ { - "match": "(?:((?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))?((?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:[\\S]+)(?:\\;)?))", + "match": "(?:((?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))?((?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:[^\\s/]+)(?:\\;)?))", "captures": { "1": { "patterns": [ @@ -1958,7 +1983,7 @@ }, { "comment": "property variables and types", - "match": "(?:((?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))([^\\`\"\\/]+))", + "match": "(\\b\\w+(?:\\s*\\,\\s*\\b\\w+)*)\\s*([^\\`\"\\/]+)", "captures": { "1": { "patterns": [ @@ -1994,7 +2019,7 @@ "patterns": [ { "comment": "struct in struct types", - "begin": "(?:((?:\\w+(?:\\,\\s*\\w+)*)(?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:\\s+)(?:[\\[\\]\\*]+)?)(\\bstruct\\b)(?:\\s*)(\\{))", + "begin": "(?:((?:\\b\\w+(?:\\,\\s*\\b\\w+)*)(?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:\\s*)(?:[\\[\\]\\*]+)?)(\\bstruct\\b)(?:\\s*)(\\{))", "beginCaptures": { "1": { "patterns": [ @@ -2031,7 +2056,7 @@ }, { "comment": "interface in struct types", - "begin": "(?:((?:\\w+(?:\\,\\s*\\w+)*)(?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:\\s+)(?:[\\[\\]\\*]+)?)(\\binterface\\b)(?:\\s*)(\\{))", + "begin": "(?:((?:\\b\\w+(?:\\,\\s*\\b\\w+)*)(?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:\\s*)(?:[\\[\\]\\*]+)?)(\\binterface\\b)(?:\\s*)(\\{))", "beginCaptures": { "1": { "patterns": [ @@ -2068,7 +2093,7 @@ }, { "comment": "function in struct types", - "begin": "(?:((?:\\w+(?:\\,\\s*\\w+)*)(?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:\\s+)(?:[\\[\\]\\*]+)?)(\\bfunc\\b)(?:\\s*)(\\())", + "begin": "(?:((?:\\b\\w+(?:\\,\\s*\\b\\w+)*)(?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:\\s*)(?:[\\[\\]\\*]+)?)(\\bfunc\\b)(?:\\s*)(\\())", "beginCaptures": { "1": { "patterns": [ @@ -2390,7 +2415,7 @@ }, "after_control_variables": { "comment": "After control variables, to not highlight as a struct/interface (before formatting with gofmt)", - "match": "(?:(?<=\\brange\\b|\\bswitch\\b|\\;|\\bif\\b|\\bfor\\b|\\<|\\>|\\<\\=|\\>\\=|\\=\\=|\\!\\=|\\w(?:\\+|/|\\-|\\*|\\%)|\\w(?:\\+|/|\\-|\\*|\\%)\\=|\\|\\||\\&\\&)(?:\\s*)((?![\\[\\]]+)[[:alnum:]\\-\\_\\!\\.\\[\\]\\<\\>\\=\\*/\\+\\%\\:]+)(?:\\s*)(?=\\{))", + "match": "(?:(?<=\\brange\\b|\\;|\\bif\\b|\\bfor\\b|\\<|\\>|\\<\\=|\\>\\=|\\=\\=|\\!\\=|\\w(?:\\+|/|\\-|\\*|\\%)|\\w(?:\\+|/|\\-|\\*|\\%)\\=|\\|\\||\\&\\&)(?:\\s*)((?![\\[\\]]+)[[:alnum:]\\-\\_\\!\\.\\[\\]\\<\\>\\=\\*/\\+\\%\\:]+)(?:\\s*)(?=\\{))", "captures": { "1": { "patterns": [ @@ -2649,6 +2674,103 @@ } ] }, + "switch_variables": { + "comment": "variables after case control keyword in switch/select expression, to not scope them as property variables", + "patterns": [ + { + "comment": "single line", + "match": "(?:(?:^\\s*(\\bcase\\b))(?:\\s+)([\\s\\S]+(?:\\:)\\s*(?:/(?:/|\\*).*)?)$)", + "captures": { + "1": { + "name": "keyword.control.go" + }, + "2": { + "patterns": [ + { + "include": "#type-declarations" + }, + { + "include": "#support_functions" + }, + { + "include": "#variable_assignment" + }, + { + "match": "\\w+", + "name": "variable.other.go" + } + ] + } + } + }, + { + "comment": "multi lines", + "begin": "(?<=\\bswitch\\b)(?:\\s*)((?:[\\w\\.]+(?:\\s*(?:[\\:\\=\\!\\,\\+/\\-\\%\\<\\>\\|\\&]+)\\s*[\\w\\.]+)*\\s*(?:[\\:\\=\\!\\,\\+/\\-\\%\\<\\>\\|\\&]+))?(?:\\s*(?:[\\w\\.\\*\\(\\)\\[\\]\\+/\\-\\%\\<\\>\\|\\&]+)?\\s*(?:\\;\\s*(?:[\\w\\.\\*\\(\\)\\[\\]\\+/\\-\\%\\<\\>\\|\\&]+)\\s*)?))(\\{)", + "beginCaptures": { + "1": { + "patterns": [ + { + "include": "#support_functions" + }, + { + "include": "#type-declarations" + }, + { + "include": "#variable_assignment" + }, + { + "match": "\\w+", + "name": "variable.other.go" + } + ] + }, + "2": { + "name": "punctuation.definition.begin.bracket.curly.go" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.definition.end.bracket.curly.go" + } + }, + "patterns": [ + { + "begin": "\\bcase\\b", + "beginCaptures": { + "0": { + "name": "keyword.control.go" + } + }, + "end": "\\:", + "endCaptures": { + "0": { + "name": "punctuation.other.colon.go" + } + }, + "patterns": [ + { + "include": "#support_functions" + }, + { + "include": "#type-declarations" + }, + { + "include": "#variable_assignment" + }, + { + "match": "\\w+", + "name": "variable.other.go" + } + ] + }, + { + "include": "$self" + } + ] + } + ] + }, "var_assignment": { "comment": "variable assignment with var keyword", "patterns": [ @@ -2959,32 +3081,6 @@ } } }, - "switch_select_case_variables": { - "comment": "variables after case control keyword in switch/select expression, to not scope them as property variables", - "match": "(?:(?:^\\s*(\\bcase\\b))(?:\\s+)([\\s\\S]+(?:\\:)\\s*(?:/(?:/|\\*).*)?)$)", - "captures": { - "1": { - "name": "keyword.control.go" - }, - "2": { - "patterns": [ - { - "include": "#type-declarations" - }, - { - "include": "#support_functions" - }, - { - "include": "#variable_assignment" - }, - { - "match": "\\w+", - "name": "variable.other.go" - } - ] - } - } - }, "slice_index_variables": { "comment": "slice index and capacity variables, to not scope them as property variables", "match": "(?<=\\w\\[)((?:(?:\\b[\\w\\.\\*\\+/\\-\\%\\<\\>\\|\\&]+\\:)|(?:\\:\\b[\\w\\.\\*\\+/\\-\\%\\<\\>\\|\\&]+))(?:\\b[\\w\\.\\*\\+/\\-\\%\\<\\>\\|\\&]+)?(?:\\:\\b[\\w\\.\\*\\+/\\-\\%\\<\\>\\|\\&]+)?)(?=\\])", @@ -3077,40 +3173,65 @@ } } }, - "field_hover": { - "comment": "struct field property and types when hovering with the mouse", - "match": "(?:(?<=^\\bfield\\b)\\s+([\\w\\*\\.]+)\\s+([\\s\\S]+))", - "captures": { - "1": { - "patterns": [ - { - "include": "#type-declarations" + "hover": { + "comment": "hovering with the mouse", + "patterns": [ + { + "comment": "struct field property and types when hovering with the mouse", + "match": "(?:(?<=^\\bfield\\b)\\s+([\\w\\*\\.]+)\\s+([\\s\\S]+))", + "captures": { + "1": { + "patterns": [ + { + "include": "#type-declarations" + }, + { + "match": "\\w+", + "name": "variable.other.property.go" + } + ] }, - { - "match": "\\w+", - "name": "variable.other.property.go" + "2": { + "patterns": [ + { + "match": "\\binvalid\\b\\s+\\btype\\b", + "name": "invalid.field.go" + }, + { + "include": "#type-declarations-without-brackets" + }, + { + "include": "#parameter-variable-types" + }, + { + "match": "\\w+", + "name": "entity.name.type.go" + } + ] } - ] + } }, - "2": { - "patterns": [ - { - "match": "\\binvalid\\b\\s+\\btype\\b", - "name": "invalid.field.go" - }, - { - "include": "#type-declarations-without-brackets" - }, - { - "include": "#parameter-variable-types" - }, - { - "match": "\\w+", - "name": "entity.name.type.go" + { + "comment": "return types when hovering with the mouse", + "match": "(?:(?<=^\\breturns\\b)\\s+([\\s\\S]+))", + "captures": { + "1": { + "patterns": [ + { + "include": "#type-declarations-without-brackets" + }, + { + "include": "#parameter-variable-types" + }, + { + "match": "\\w+", + "name": "entity.name.type.go" + } + ] } - ] + } } - } + ] }, "other_variables": { "comment": "all other variables", diff --git a/extensions/julia/cgmanifest.json b/extensions/julia/cgmanifest.json index b15d7716c69..2d59c264d57 100644 --- a/extensions/julia/cgmanifest.json +++ b/extensions/julia/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "JuliaEditorSupport/atom-language-julia", "repositoryUrl": "https://github.com/JuliaEditorSupport/atom-language-julia", - "commitHash": "8eaad3e9560c223b00616c8a4610304b9b925d1c" + "commitHash": "111548fbd25d083ec131d2732a4f46953ea92a65" } }, "license": "MIT", diff --git a/extensions/julia/syntaxes/julia.tmLanguage.json b/extensions/julia/syntaxes/julia.tmLanguage.json index 0e19c8792f9..8f7298ea4ce 100644 --- a/extensions/julia/syntaxes/julia.tmLanguage.json +++ b/extensions/julia/syntaxes/julia.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/JuliaEditorSupport/atom-language-julia/commit/8eaad3e9560c223b00616c8a4610304b9b925d1c", + "version": "https://github.com/JuliaEditorSupport/atom-language-julia/commit/111548fbd25d083ec131d2732a4f46953ea92a65", "name": "Julia", "scopeName": "source.julia", "comment": "This grammar is used by Atom (Oniguruma), GitHub (PCRE), and VSCode (Oniguruma),\nso all regexps must be compatible with both engines.\n\nSpecs:\n- https://github.com/kkos/oniguruma/blob/master/doc/RE\n- https://www.pcre.org/current/doc/html/", @@ -337,7 +337,7 @@ "name": "keyword.control.as.julia" }, { - "match": "(@((?:\\.|[\\p{S}\\p{P}&&[^\\s@]]+)|(?:[[:alpha:]_\\p{Lu}\\p{Ll}\\p{Lt}\\p{Lm}\\p{Lo}\\p{Nl}\\p{Sc}⅀-⅄∿⊾⊿⊤⊥∂∅-∇∎∏∐∑∞∟∫-∳⋀-⋃◸-◿♯⟘⟙⟀⟁⦰-⦴⨀-⨆⨉-⨖⨛⨜𝛁𝛛𝛻𝜕𝜵𝝏𝝯𝞉𝞩𝟃ⁱ-⁾₁-₎∠-∢⦛-⦯℘℮゛-゜𝟎-𝟡]|[^\\P{So}←-⇿])(?:[[:word:]_!\\p{Lu}\\p{Ll}\\p{Lt}\\p{Lm}\\p{Lo}\\p{Nl}\\p{Sc}⅀-⅄∿⊾⊿⊤⊥∂∅-∇∎∏∐∑∞∟∫-∳⋀-⋃◸-◿♯⟘⟙⟀⟁⦰-⦴⨀-⨆⨉-⨖⨛⨜𝛁𝛛𝛻𝜕𝜵𝝏𝝯𝞉𝞩𝟃ⁱ-⁾₁-₎∠-∢⦛-⦯℘℮゛-゜𝟎-𝟡]|[^\\P{Mn}\u0001-¡]|[^\\P{Mc}\u0001-¡]|[^\\P{Nd}\u0001-¡]|[^\\P{Pc}\u0001-¡]|[^\\P{Sk}\u0001-¡]|[^\\P{Me}\u0001-¡]|[^\\P{No}\u0001-¡]|[′-‷⁗]|[^\\P{So}←-⇿])*))", + "match": "@(\\.|(?:[[:alpha:]_\\p{Lu}\\p{Ll}\\p{Lt}\\p{Lm}\\p{Lo}\\p{Nl}\\p{Sc}⅀-⅄∿⊾⊿⊤⊥∂∅-∇∎∏∐∑∞∟∫-∳⋀-⋃◸-◿♯⟘⟙⟀⟁⦰-⦴⨀-⨆⨉-⨖⨛⨜𝛁𝛛𝛻𝜕𝜵𝝏𝝯𝞉𝞩𝟃ⁱ-⁾₁-₎∠-∢⦛-⦯℘℮゛-゜𝟎-𝟡]|[^\\P{So}←-⇿])(?:[[:word:]_!\\p{Lu}\\p{Ll}\\p{Lt}\\p{Lm}\\p{Lo}\\p{Nl}\\p{Sc}⅀-⅄∿⊾⊿⊤⊥∂∅-∇∎∏∐∑∞∟∫-∳⋀-⋃◸-◿♯⟘⟙⟀⟁⦰-⦴⨀-⨆⨉-⨖⨛⨜𝛁𝛛𝛻𝜕𝜵𝝏𝝯𝞉𝞩𝟃ⁱ-⁾₁-₎∠-∢⦛-⦯℘℮゛-゜𝟎-𝟡]|[^\\P{Mn}\u0001-¡]|[^\\P{Mc}\u0001-¡]|[^\\P{Nd}\u0001-¡]|[^\\P{Pc}\u0001-¡]|[^\\P{Sk}\u0001-¡]|[^\\P{Me}\u0001-¡]|[^\\P{No}\u0001-¡]|[′-‷⁗]|[^\\P{So}←-⇿])*|[\\p{S}\\p{P}&&[^\\s@]]+)", "name": "support.function.macro.julia" } ] diff --git a/extensions/latex/cgmanifest.json b/extensions/latex/cgmanifest.json index fd381574f80..1e0ee670a79 100644 --- a/extensions/latex/cgmanifest.json +++ b/extensions/latex/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "jlelong/vscode-latex-basics", "repositoryUrl": "https://github.com/jlelong/vscode-latex-basics", - "commitHash": "eb0d146b16839076a61c3fdec85d6f80d9a94c8c" + "commitHash": "6bd99800f7b2cbd0e36cecb56fe1936da5affadb" } }, "license": "MIT", - "version": "1.13.0", + "version": "1.14.0", "description": "The files in syntaxes/ were originally part of https://github.com/James-Yu/LaTeX-Workshop. They have been extracted in the hope that they can useful outside of the LaTeX-Workshop extension.", "licenseDetail": [ "Copyright (c) vscode-latex-basics authors", diff --git a/extensions/latex/syntaxes/TeX.tmLanguage.json b/extensions/latex/syntaxes/TeX.tmLanguage.json index db2a62a2267..b31ccccb631 100644 --- a/extensions/latex/syntaxes/TeX.tmLanguage.json +++ b/extensions/latex/syntaxes/TeX.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jlelong/vscode-latex-basics/commit/b46aaf9bf4d265e63e262ded4bf9beffe19d35b2", + "version": "https://github.com/jlelong/vscode-latex-basics/commit/6bd99800f7b2cbd0e36cecb56fe1936da5affadb", "name": "TeX", "scopeName": "text.tex", "patterns": [ @@ -55,7 +55,7 @@ "name": "meta.catcode.tex" }, "iffalse-block": { - "begin": "(?<=^\\s*)((\\\\)iffalse)(?!\\s*[{}]\\s*\\\\fi)", + "begin": "(?<=^\\s*)((\\\\)iffalse)(?!\\s*[{}]\\s*\\\\fi\\b)", "beginCaptures": { "1": { "name": "keyword.control.tex" @@ -65,7 +65,7 @@ } }, "contentName": "comment.line.percentage.tex", - "end": "((\\\\)(?:else|fi))", + "end": "((\\\\)(?:else|fi)\\b)", "endCaptures": { "1": { "name": "keyword.control.tex" diff --git a/extensions/markdown-basics/cgmanifest.json b/extensions/markdown-basics/cgmanifest.json index 380b0c74ac6..82e47b637e1 100644 --- a/extensions/markdown-basics/cgmanifest.json +++ b/extensions/markdown-basics/cgmanifest.json @@ -33,7 +33,7 @@ "git": { "name": "microsoft/vscode-markdown-tm-grammar", "repositoryUrl": "https://github.com/microsoft/vscode-markdown-tm-grammar", - "commitHash": "7418dd20d76c72e82fadee2909e03239e9973b35" + "commitHash": "548ccb91ef58ba40ac745b400d889933ccd5eb4d" } }, "license": "MIT", diff --git a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json index 9761ca716ab..c6d5110bd02 100644 --- a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json +++ b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/7418dd20d76c72e82fadee2909e03239e9973b35", + "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/548ccb91ef58ba40ac745b400d889933ccd5eb4d", "name": "Markdown", "scopeName": "text.html.markdown", "patterns": [ @@ -3084,7 +3084,7 @@ "name": "punctuation.definition.strikethrough.markdown" } }, - "match": "(? Date: Tue, 8 Jul 2025 14:07:56 +0200 Subject: [PATCH 203/306] Fixes ARC tracking (#254445) * Fixes https://github.com/microsoft/vscode/issues/254419 (#254421) * Fixes https://github.com/microsoft/vscode-internalbacklog/issues/5608 (#254423) --- .../browser/editSourceTrackingFeature.ts | 6 ++++-- .../browser/editSourceTrackingImpl.ts | 14 +++++++++++--- .../browser/editTelemetry.contribution.ts | 8 +++++++- .../contrib/editTelemetry/browser/settings.ts | 1 + 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingFeature.ts b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingFeature.ts index 7d437dd8d5f..59e8d2245e8 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingFeature.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingFeature.ts @@ -24,13 +24,14 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js'; import { EditSource } from './documentWithAnnotatedEdits.js'; import { EditSourceTrackingImpl } from './editSourceTrackingImpl.js'; -import { EDIT_TELEMETRY_SHOW_DECORATIONS, EDIT_TELEMETRY_SHOW_STATUS_BAR } from './settings.js'; +import { EDIT_TELEMETRY_DETAILS_SETTING_ID, EDIT_TELEMETRY_SHOW_DECORATIONS, EDIT_TELEMETRY_SHOW_STATUS_BAR } from './settings.js'; import { VSCodeWorkspace } from './vscodeObservableWorkspace.js'; export class EditTrackingFeature extends Disposable { private readonly _editSourceTrackingShowDecorations; private readonly _editSourceTrackingShowStatusBar; + private readonly _editSourceDetailsEnabled; private readonly _showStateInMarkdownDoc = 'editTelemetry.showDebugDetails'; private readonly _toggleDecorations = 'editTelemetry.toggleDebugDecorations'; @@ -46,6 +47,7 @@ export class EditTrackingFeature extends Disposable { this._editSourceTrackingShowDecorations = makeSettable(observableConfigValue(EDIT_TELEMETRY_SHOW_DECORATIONS, false, this._configurationService)); this._editSourceTrackingShowStatusBar = observableConfigValue(EDIT_TELEMETRY_SHOW_STATUS_BAR, false, this._configurationService); + this._editSourceDetailsEnabled = observableConfigValue(EDIT_TELEMETRY_DETAILS_SETTING_ID, false, this._configurationService); const onDidAddGroupSignal = observableSignalFromEvent(this, this._editorGroupsService.onDidAddGroup); const onDidRemoveGroupSignal = observableSignalFromEvent(this, this._editorGroupsService.onDidRemoveGroup); @@ -72,7 +74,7 @@ export class EditTrackingFeature extends Disposable { const impl = this._register(this._instantiationService.createInstance(EditSourceTrackingImpl, this._workspace, (doc, reader) => { const map = visibleUris.read(reader); return map.get(doc.uri.toString()) !== undefined; - })); + }, this._editSourceDetailsEnabled)); this._register(autorun((reader) => { if (!this._editSourceTrackingShowDecorations.read(reader)) { diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts index edadf7750b5..333bf17d85f 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts @@ -26,17 +26,21 @@ export class EditSourceTrackingImpl extends Disposable { constructor( private readonly _workspace: ObservableWorkspace, private readonly _docIsVisible: (doc: IObservableDocument, reader: IReader) => boolean, + private readonly _statsEnabled: IObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); const scmBridge = this._instantiationService.createInstance(ScmBridge); - this.docsState = mapObservableArrayCached(this, this._workspace.documents, (doc, store) => { + const states = mapObservableArrayCached(this, this._workspace.documents, (doc, store) => { const docIsVisible = derived(reader => this._docIsVisible(doc, reader)); const wasEverVisible = derivedObservableWithCache(this, (reader, lastVal) => lastVal || docIsVisible.read(reader)); - return wasEverVisible.map(v => v ? [doc, store.add(this._instantiationService.createInstance(TrackedDocumentInfo, doc, docIsVisible, scmBridge))] as const : undefined); - }).recomputeInitiallyAndOnChange(this._store).map((entries, reader) => new Map(entries.map(e => e.read(reader)).filter(isDefined))); + return wasEverVisible.map(v => v ? [doc, store.add(this._instantiationService.createInstance(TrackedDocumentInfo, doc, docIsVisible, scmBridge, this._statsEnabled))] as const : undefined); + }); + + this.docsState = states.map((entries, reader) => new Map(entries.map(e => e.read(reader)).filter(isDefined))) + .recomputeInitiallyAndOnChange(this._store); } } @@ -78,6 +82,7 @@ class TrackedDocumentInfo extends Disposable { private readonly _doc: IObservableDocument, docIsVisible: IObservable, private readonly _scm: ScmBridge, + private readonly _statsEnabled: IObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ITelemetryService private readonly _telemetryService: ITelemetryService ) { @@ -95,6 +100,7 @@ class TrackedDocumentInfo extends Disposable { const longtermResetSignal = observableSignal('resetSignal'); this.longtermTracker = derived((reader) => { + if (!this._statsEnabled.read(reader)) { return undefined; } longtermResetSignal.read(reader); const t = reader.store.add(new DocumentEditSourceTracker(docWithJustReason, undefined)); @@ -134,6 +140,8 @@ class TrackedDocumentInfo extends Disposable { const resetSignal = observableSignal('resetSignal'); this.windowedTracker = derived((reader) => { + if (!this._statsEnabled.read(reader)) { return undefined; } + if (!docIsVisible.read(reader)) { return undefined; } diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts index 4e9310cb8e8..74f6d3865fe 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts @@ -7,7 +7,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { EditTelemetryService } from './editTelemetryService.js'; import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; import { localize } from '../../../../nls.js'; -import { EDIT_TELEMETRY_SETTING_ID, EDIT_TELEMETRY_SHOW_DECORATIONS, EDIT_TELEMETRY_SHOW_STATUS_BAR } from './settings.js'; +import { EDIT_TELEMETRY_DETAILS_SETTING_ID, EDIT_TELEMETRY_SETTING_ID, EDIT_TELEMETRY_SHOW_DECORATIONS, EDIT_TELEMETRY_SHOW_STATUS_BAR } from './settings.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; registerWorkbenchContribution2('EditTelemetryService', EditTelemetryService, WorkbenchPhase.AfterRestored); @@ -25,6 +25,12 @@ configurationRegistry.registerConfiguration({ default: true, tags: ['experimental'], }, + [EDIT_TELEMETRY_DETAILS_SETTING_ID]: { + markdownDescription: localize('telemetry.editStats.detailed.enabled', "Controls whether to enable telemetry for detailed edit statistics (only sends statistics if general telemetry is enabled)."), + type: 'boolean', + default: false, + tags: ['experimental'], + }, [EDIT_TELEMETRY_SHOW_STATUS_BAR]: { markdownDescription: localize('telemetry.editStats.showStatusBar', "Controls whether to show the status bar for edit telemetry."), type: 'boolean', diff --git a/src/vs/workbench/contrib/editTelemetry/browser/settings.ts b/src/vs/workbench/contrib/editTelemetry/browser/settings.ts index e337e5e734f..b87eb3e7ca4 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/settings.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/settings.ts @@ -4,5 +4,6 @@ *--------------------------------------------------------------------------------------------*/ export const EDIT_TELEMETRY_SETTING_ID = 'telemetry.editStats.enabled'; +export const EDIT_TELEMETRY_DETAILS_SETTING_ID = 'telemetry.editStats.details.enabled'; export const EDIT_TELEMETRY_SHOW_DECORATIONS = 'telemetry.editStats.showDecorations'; export const EDIT_TELEMETRY_SHOW_STATUS_BAR = 'telemetry.editStats.showStatusBar'; From 4398d9f33d6fa7e4fd351658e4c427c05b71db04 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Tue, 8 Jul 2025 14:21:27 +0200 Subject: [PATCH 204/306] refactor: extracting ViewModelDecoration code into a separate file (#254615) * exracting viewmodeldecoration code into a separate file * adding changes --- .../editor/browser/view/renderingContext.ts | 3 +- .../viewParts/decorations/decorations.ts | 2 +- .../browser/viewParts/minimap/minimap.ts | 3 +- .../viewLayout/viewLinesViewportData.ts | 3 +- .../common/viewModel/viewModelDecoration.ts | 79 +++++++++++++++++++ .../common/viewModel/viewModelDecorations.ts | 62 +-------------- .../browser/unicodeHighlighter.ts | 2 +- 7 files changed, 89 insertions(+), 65 deletions(-) create mode 100644 src/vs/editor/common/viewModel/viewModelDecoration.ts diff --git a/src/vs/editor/browser/view/renderingContext.ts b/src/vs/editor/browser/view/renderingContext.ts index 6fe17c1a9f7..fdb24034701 100644 --- a/src/vs/editor/browser/view/renderingContext.ts +++ b/src/vs/editor/browser/view/renderingContext.ts @@ -6,7 +6,8 @@ import { Position } from '../../common/core/position.js'; import { Range } from '../../common/core/range.js'; import { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; -import { IViewLayout, ViewModelDecoration } from '../../common/viewModel.js'; +import { IViewLayout } from '../../common/viewModel.js'; +import { ViewModelDecoration } from '../../common/viewModel/viewModelDecoration.js'; export interface IViewLines { linesVisibleRangesForRange(range: Range, includeNewLines: boolean): LineVisibleRanges[] | null; diff --git a/src/vs/editor/browser/viewParts/decorations/decorations.ts b/src/vs/editor/browser/viewParts/decorations/decorations.ts index 6ae2287ccfc..6fc2d3e0fec 100644 --- a/src/vs/editor/browser/viewParts/decorations/decorations.ts +++ b/src/vs/editor/browser/viewParts/decorations/decorations.ts @@ -9,8 +9,8 @@ import { HorizontalRange, RenderingContext } from '../../view/renderingContext.j import { EditorOption } from '../../../common/config/editorOptions.js'; import { Range } from '../../../common/core/range.js'; import * as viewEvents from '../../../common/viewEvents.js'; -import { ViewModelDecoration } from '../../../common/viewModel.js'; import { ViewContext } from '../../../common/viewModel/viewContext.js'; +import { ViewModelDecoration } from '../../../common/viewModel/viewModelDecoration.js'; export class DecorationsOverlay extends DynamicViewOverlay { diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index 63919280735..744fa07466e 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -26,7 +26,7 @@ import { RenderingContext, RestrictedRenderingContext } from '../../view/renderi import { ViewContext } from '../../../common/viewModel/viewContext.js'; import { EditorTheme } from '../../../common/editorTheme.js'; import * as viewEvents from '../../../common/viewEvents.js'; -import { ViewLineData, ViewModelDecoration } from '../../../common/viewModel.js'; +import { ViewLineData } from '../../../common/viewModel.js'; import { minimapSelection, minimapBackground, minimapForegroundOpacity, editorForeground } from '../../../../platform/theme/common/colorRegistry.js'; import { ModelDecorationMinimapOptions } from '../../../common/model/textModel.js'; import { Selection } from '../../../common/core/selection.js'; @@ -37,6 +37,7 @@ import { MinimapPosition, MinimapSectionHeaderStyle, TextModelResolvedOptions } import { createSingleCallFunction } from '../../../../base/common/functional.js'; import { LRUCache } from '../../../../base/common/map.js'; import { DEFAULT_FONT_FAMILY } from '../../../../base/browser/fonts.js'; +import { ViewModelDecoration } from '../../../common/viewModel/viewModelDecoration.js'; /** * The orthogonal distance to the slider at which dragging "resets". This implements "snapping" diff --git a/src/vs/editor/common/viewLayout/viewLinesViewportData.ts b/src/vs/editor/common/viewLayout/viewLinesViewportData.ts index 91eaceca811..5d6accce9de 100644 --- a/src/vs/editor/common/viewLayout/viewLinesViewportData.ts +++ b/src/vs/editor/common/viewLayout/viewLinesViewportData.ts @@ -5,7 +5,8 @@ import { Range } from '../core/range.js'; import { Selection } from '../core/selection.js'; -import { IPartialViewLinesViewportData, IViewModel, IViewWhitespaceViewportData, ViewLineRenderingData, ViewModelDecoration } from '../viewModel.js'; +import { IPartialViewLinesViewportData, IViewModel, IViewWhitespaceViewportData, ViewLineRenderingData } from '../viewModel.js'; +import { ViewModelDecoration } from '../viewModel/viewModelDecoration.js'; /** * Contains all data needed to render at a specific viewport. diff --git a/src/vs/editor/common/viewModel/viewModelDecoration.ts b/src/vs/editor/common/viewModel/viewModelDecoration.ts new file mode 100644 index 00000000000..9d1e7a9ec72 --- /dev/null +++ b/src/vs/editor/common/viewModel/viewModelDecoration.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IModelDecoration, IModelDecorationOptions, ITextModel } from '../model.js'; +import { Range } from '../core/range.js'; +import { StandardTokenType } from '../encodedTokenAttributes.js'; + +export class ViewModelDecoration { + _viewModelDecorationBrand: void = undefined; + + public readonly range: Range; + public readonly options: IModelDecorationOptions; + + constructor(range: Range, options: IModelDecorationOptions) { + this.range = range; + this.options = options; + } +} + +export function isModelDecorationVisible(model: ITextModel, decoration: IModelDecoration): boolean { + if (decoration.options.hideInCommentTokens && isModelDecorationInComment(model, decoration)) { + return false; + } + + if (decoration.options.hideInStringTokens && isModelDecorationInString(model, decoration)) { + return false; + } + + return true; +} + +export function isModelDecorationInComment(model: ITextModel, decoration: IModelDecoration): boolean { + return testTokensInRange( + model, + decoration.range, + (tokenType) => tokenType === StandardTokenType.Comment + ); +} + +export function isModelDecorationInString(model: ITextModel, decoration: IModelDecoration): boolean { + return testTokensInRange( + model, + decoration.range, + (tokenType) => tokenType === StandardTokenType.String + ); +} + +/** + * Calls the callback for every token that intersects the range. + * If the callback returns `false`, iteration stops and `false` is returned. + * Otherwise, `true` is returned. + */ +function testTokensInRange(model: ITextModel, range: Range, callback: (tokenType: StandardTokenType) => boolean): boolean { + for (let lineNumber = range.startLineNumber; lineNumber <= range.endLineNumber; lineNumber++) { + const lineTokens = model.tokenization.getLineTokens(lineNumber); + const isFirstLine = lineNumber === range.startLineNumber; + const isEndLine = lineNumber === range.endLineNumber; + + let tokenIdx = isFirstLine ? lineTokens.findTokenIndexAtOffset(range.startColumn - 1) : 0; + while (tokenIdx < lineTokens.getCount()) { + if (isEndLine) { + const startOffset = lineTokens.getStartOffset(tokenIdx); + if (startOffset > range.endColumn - 1) { + break; + } + } + + const callbackResult = callback(lineTokens.getStandardTokenType(tokenIdx)); + if (!callbackResult) { + return false; + } + tokenIdx++; + } + } + return true; +} + diff --git a/src/vs/editor/common/viewModel/viewModelDecorations.ts b/src/vs/editor/common/viewModel/viewModelDecorations.ts index 8070ccbe541..b5ee4ec63e9 100644 --- a/src/vs/editor/common/viewModel/viewModelDecorations.ts +++ b/src/vs/editor/common/viewModel/viewModelDecorations.ts @@ -9,9 +9,9 @@ import { Range } from '../core/range.js'; import { IEditorConfiguration } from '../config/editorConfiguration.js'; import { IModelDecoration, ITextModel, PositionAffinity } from '../model.js'; import { IViewModelLines } from './viewModelLines.js'; -import { ICoordinatesConverter, ViewModelDecoration } from '../viewModel.js'; +import { ICoordinatesConverter } from '../viewModel.js'; import { filterFontDecorations, filterValidationDecorations } from '../config/editorOptions.js'; -import { StandardTokenType } from '../encodedTokenAttributes.js'; +import { isModelDecorationVisible, ViewModelDecoration } from './viewModelDecoration.js'; import { InlineDecoration, InlineDecorationType } from './inlineDecorations.js'; export interface IDecorationsViewportData { @@ -187,61 +187,3 @@ export class ViewModelDecorations implements IDisposable { }; } } - -export function isModelDecorationVisible(model: ITextModel, decoration: IModelDecoration): boolean { - if (decoration.options.hideInCommentTokens && isModelDecorationInComment(model, decoration)) { - return false; - } - - if (decoration.options.hideInStringTokens && isModelDecorationInString(model, decoration)) { - return false; - } - - return true; -} - -export function isModelDecorationInComment(model: ITextModel, decoration: IModelDecoration): boolean { - return testTokensInRange( - model, - decoration.range, - (tokenType) => tokenType === StandardTokenType.Comment - ); -} - -export function isModelDecorationInString(model: ITextModel, decoration: IModelDecoration): boolean { - return testTokensInRange( - model, - decoration.range, - (tokenType) => tokenType === StandardTokenType.String - ); -} - -/** - * Calls the callback for every token that intersects the range. - * If the callback returns `false`, iteration stops and `false` is returned. - * Otherwise, `true` is returned. - */ -function testTokensInRange(model: ITextModel, range: Range, callback: (tokenType: StandardTokenType) => boolean): boolean { - for (let lineNumber = range.startLineNumber; lineNumber <= range.endLineNumber; lineNumber++) { - const lineTokens = model.tokenization.getLineTokens(lineNumber); - const isFirstLine = lineNumber === range.startLineNumber; - const isEndLine = lineNumber === range.endLineNumber; - - let tokenIdx = isFirstLine ? lineTokens.findTokenIndexAtOffset(range.startColumn - 1) : 0; - while (tokenIdx < lineTokens.getCount()) { - if (isEndLine) { - const startOffset = lineTokens.getStartOffset(tokenIdx); - if (startOffset > range.endColumn - 1) { - break; - } - } - - const callbackResult = callback(lineTokens.getStandardTokenType(tokenIdx)); - if (!callbackResult) { - return false; - } - tokenIdx++; - } - } - return true; -} diff --git a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts index b4dcc3a8116..cd2b8dead73 100644 --- a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts +++ b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts @@ -21,7 +21,6 @@ import { ModelDecorationOptions } from '../../../common/model/textModel.js'; import { UnicodeHighlighterOptions, UnicodeHighlighterReason, UnicodeHighlighterReasonKind, UnicodeTextModelHighlighter } from '../../../common/services/unicodeTextModelHighlighter.js'; import { IEditorWorkerService, IUnicodeHighlightsResult } from '../../../common/services/editorWorker.js'; import { ILanguageService } from '../../../common/languages/language.js'; -import { isModelDecorationInComment, isModelDecorationInString, isModelDecorationVisible } from '../../../common/viewModel/viewModelDecorations.js'; import { HoverAnchor, HoverAnchorType, HoverParticipantRegistry, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverParts } from '../../hover/browser/hoverTypes.js'; import { MarkdownHover, renderMarkdownHovers } from '../../hover/browser/markdownHoverParticipant.js'; import { BannerController } from './bannerController.js'; @@ -34,6 +33,7 @@ import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js' import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { safeIntl } from '../../../../base/common/date.js'; +import { isModelDecorationInComment, isModelDecorationInString, isModelDecorationVisible } from '../../../common/viewModel/viewModelDecoration.js'; export const warningIcon = registerIcon('extensions-warning-message', Codicon.warning, nls.localize('warningIcon', 'Icon shown with a warning message in the extensions editor.')); From 491bf7a849db928753841a6627aa081041d27ab4 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Tue, 8 Jul 2025 14:53:29 +0200 Subject: [PATCH 205/306] Stray console log (#254645) --- .../languageProviders/promptHeaderDiagnosticsProvider.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderDiagnosticsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderDiagnosticsProvider.ts index 256f6340b24..d11d6bab8d0 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderDiagnosticsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderDiagnosticsProvider.ts @@ -66,7 +66,6 @@ class PromptHeaderDiagnosticsProvider extends ProviderInstanceBase { if (!completed || token.isCancellationRequested) { return; } - console.log(`Prompt header diagnostics for ${this.model.uri.toString()}:`); const markers: IMarkerData[] = []; for (const diagnostic of header.diagnostics) { @@ -83,7 +82,6 @@ class PromptHeaderDiagnosticsProvider extends ProviderInstanceBase { } if (markers.length === 0) { - console.warn(`No markers for ${this.model.uri.toString()}`); this.markerService.remove(MARKERS_OWNER_ID, [this.model.uri]); return; } From 2084f6d0223b1b5e3d8d365c5cd8da38fb4bddfa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:21:54 +0000 Subject: [PATCH 206/306] Initial plan From 1c5aac43c2e7dc47146a81c1cc0201f77d541c9a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 8 Jul 2025 15:27:47 +0200 Subject: [PATCH 207/306] tests - close editors from debug API tests (#254646) --- .../vscode-api-tests/src/singlefolder-tests/debug.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts index cc2f2675297..60c2931418a 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts @@ -6,11 +6,14 @@ import * as assert from 'assert'; import { basename } from 'path'; import { commands, debug, Disposable, FunctionBreakpoint, window, workspace } from 'vscode'; -import { assertNoRpc, createRandomFile, disposeAll } from '../utils'; +import { assertNoRpc, closeAllEditors, createRandomFile, disposeAll } from '../utils'; suite('vscode API - debug', function () { - teardown(assertNoRpc); + teardown(async function () { + assertNoRpc(); + await closeAllEditors(); + }); test('breakpoints are available before accessing debug extension API', async () => { const file = await createRandomFile(undefined, undefined, '.js'); From 9448ca54054612df13aa399bcf427fc639cf5c9d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 8 Jul 2025 15:28:35 +0200 Subject: [PATCH 208/306] debt - harden disablement of experiments in code for tests (#254642) --- src/vs/platform/assignment/common/assignmentService.ts | 2 +- .../services/assignment/common/assignmentService.ts | 9 +++++++-- .../common/coreExperimentationService.ts | 8 ++++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/vs/platform/assignment/common/assignmentService.ts b/src/vs/platform/assignment/common/assignmentService.ts index 413bd60f7ff..d5ab03ae0b6 100644 --- a/src/vs/platform/assignment/common/assignmentService.ts +++ b/src/vs/platform/assignment/common/assignmentService.ts @@ -19,7 +19,7 @@ export abstract class BaseAssignmentService implements IAssignmentService { private overrideInitDelay: Promise; protected get experimentsEnabled(): boolean { - return true; + return !this.environmentService.disableExperiments; } constructor( diff --git a/src/vs/workbench/services/assignment/common/assignmentService.ts b/src/vs/workbench/services/assignment/common/assignmentService.ts index d18437e1bca..ebded17a67d 100644 --- a/src/vs/workbench/services/assignment/common/assignmentService.ts +++ b/src/vs/workbench/services/assignment/common/assignmentService.ts @@ -18,6 +18,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { BaseAssignmentService } from '../../../../platform/assignment/common/assignmentService.js'; import { workbenchConfigurationNodeBase } from '../../../common/configuration.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; export const IWorkbenchAssignmentService = createDecorator('WorkbenchAssignmentService'); @@ -86,7 +87,8 @@ export class WorkbenchAssignmentService extends BaseAssignmentService { @IStorageService storageService: IStorageService, @IConfigurationService configurationService: IConfigurationService, @IProductService productService: IProductService, - @IEnvironmentService environmentService: IEnvironmentService + @IEnvironmentService environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService ) { super( @@ -100,7 +102,10 @@ export class WorkbenchAssignmentService extends BaseAssignmentService { } protected override get experimentsEnabled(): boolean { - return !this.environmentService.disableExperiments && this.configurationService.getValue('workbench.enableExperiments') === true; + return !this.environmentService.disableExperiments && + !this.environmentService.extensionTestsLocationURI && + !this.workbenchEnvironmentService.enableSmokeTestDriver && + this.configurationService.getValue('workbench.enableExperiments') === true; } override async getTreatment(name: string): Promise { diff --git a/src/vs/workbench/services/coreExperimentation/common/coreExperimentationService.ts b/src/vs/workbench/services/coreExperimentation/common/coreExperimentationService.ts index 944e9bee9de..5ab4a26c6b3 100644 --- a/src/vs/workbench/services/coreExperimentation/common/coreExperimentationService.ts +++ b/src/vs/workbench/services/coreExperimentation/common/coreExperimentationService.ts @@ -89,8 +89,12 @@ export class CoreExperimentationService extends Disposable implements ICoreExper ) { super(); - if (environmentService.disableExperiments) { - return; // explicitly disabled + if ( + environmentService.disableExperiments || + environmentService.enableSmokeTestDriver || + environmentService.extensionTestsLocationURI + ) { + return; //not applicable in this environment } this.initializeExperiments(); From d1f33e87c353f510c4df60b37bc47a7376b51851 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:30:52 +0000 Subject: [PATCH 209/306] Add current working directory to shell integration tooltip Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com> --- src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts b/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts index 192c9878d0a..df9237cb08b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts @@ -95,6 +95,10 @@ export function refreshShellIntegrationInfoStatus(instance: ITerminalInstance) { if (instance.shellType) { detailedAdditions.push(`Shell type: \`${instance.shellType}\``); } + const cwd = instance.capabilities.get(TerminalCapability.CwdDetection)?.getCwd() || instance.cwd; + if (cwd) { + detailedAdditions.push(`Current working directory: \`${cwd}\``); + } const seenSequences = Array.from(instance.xterm.shellIntegration.seenSequences); if (seenSequences.length > 0) { detailedAdditions.push(`Seen sequences: ${seenSequences.map(e => `\`${e}\``).join(', ')}`); From 8ca89245c549bd0d3a46ef1298f5ae487db53d1f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 8 Jul 2025 07:24:30 -0700 Subject: [PATCH 210/306] Simplify terminal initial hint Fixes #254658 --- .../terminal.initialHint.contribution.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts index 4bef1ee66e5..996b5c18ccb 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts @@ -18,19 +18,18 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; -import { IProductService } from '../../../../../platform/product/common/productService.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { ITerminalCapabilityStore, TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { IChatAgent, IChatAgentService } from '../../../chat/common/chatAgents.js'; +import { ChatAgentLocation } from '../../../chat/common/constants.js'; import { IDetachedTerminalInstance, ITerminalContribution, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalService, IXtermTerminal } from '../../../terminal/browser/terminal.js'; import { registerTerminalContribution, type IDetachedCompatibleTerminalContributionContext, type ITerminalContributionContext } from '../../../terminal/browser/terminalExtensions.js'; import { TerminalInstance } from '../../../terminal/browser/terminalInstance.js'; import { TerminalInitialHintSettingId } from '../common/terminalInitialHintConfiguration.js'; import './media/terminalInitialHint.css'; import { TerminalChatCommandId } from './terminalChat.js'; -import { ChatAgentLocation } from '../../../chat/common/constants.js'; const $ = dom.$; @@ -215,12 +214,10 @@ class TerminalInitialHintWidget extends Disposable { constructor( private readonly _instance: ITerminalInstance, - @IChatAgentService private readonly _chatAgentService: IChatAgentService, @ICommandService private readonly _commandService: ICommandService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IContextMenuService private readonly _contextMenuService: IContextMenuService, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IProductService private readonly _productService: IProductService, @IStorageService private readonly _storageService: IStorageService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @ITerminalService private readonly _terminalService: ITerminalService, @@ -244,13 +241,7 @@ class TerminalInitialHintWidget extends Disposable { } private _getHintInlineChat(agents: IChatAgent[]) { - let providerName = (agents.length === 1 ? agents[0].fullName : undefined) ?? this._productService.nameShort; - const defaultAgent = this._chatAgentService.getDefaultAgent(ChatAgentLocation.Panel); - if (defaultAgent?.extensionId.value === agents[0].extensionId.value) { - providerName = defaultAgent.fullName ?? providerName; - } - - let ariaLabel = `Ask ${providerName} something or start typing to dismiss.`; + let ariaLabel = `Open chat.`; const handleClick = () => { this._storageService.store(Constants.InitialHintHideStorageKey, true, StorageScope.APPLICATION, StorageTarget.USER); @@ -285,7 +276,7 @@ class TerminalInitialHintWidget extends Disposable { const keybindingHintLabel = keybindingHint?.getLabel(); if (keybindingHint && keybindingHintLabel) { - const actionPart = localize('emptyHintText', 'Press {0} to ask {1} to do something. ', keybindingHintLabel, providerName); + const actionPart = localize('emptyHintText', 'Open chat {0}. ', keybindingHintLabel); const [before, after] = actionPart.split(keybindingHintLabel).map((fragment) => { const hintPart = $('a', undefined, fragment); @@ -316,7 +307,7 @@ class TerminalInitialHintWidget extends Disposable { comment: [ 'Preserve double-square brackets and their order', ] - }, '[[Ask {0} to do something]] or start typing to dismiss.', providerName); + }, '[[Open chat]] or start typing to dismiss.'); const rendered = renderFormattedText(hintMsg, { actionHandler: hintHandler }); hintElement.appendChild(rendered); } From f6c92424f911efc00d286d1ba90d5aae4cc0cc53 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 8 Jul 2025 07:42:55 -0700 Subject: [PATCH 211/306] Simplify cwd fetching --- src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts b/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts index df9237cb08b..ebd16411d7a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts @@ -95,7 +95,7 @@ export function refreshShellIntegrationInfoStatus(instance: ITerminalInstance) { if (instance.shellType) { detailedAdditions.push(`Shell type: \`${instance.shellType}\``); } - const cwd = instance.capabilities.get(TerminalCapability.CwdDetection)?.getCwd() || instance.cwd; + const cwd = instance.cwd; if (cwd) { detailedAdditions.push(`Current working directory: \`${cwd}\``); } From 8391270b79698f70b49acbeeef75b6b5bc83d31a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 8 Jul 2025 08:14:05 -0700 Subject: [PATCH 212/306] Push sticky scroll line up if it obscures cursor line Fixes #248001 --- .../stickyScroll/browser/terminalStickyScrollOverlay.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts index 0da38ee5994..3e94022f700 100644 --- a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts +++ b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts @@ -337,8 +337,9 @@ export class TerminalStickyScrollOverlay extends Disposable { // following command. let endMarkerOffset = 0; if (!isPartialCommand && command.endMarker && command.endMarker.line !== -1) { - if (buffer.viewportY + stickyScrollLineCount > command.endMarker.line) { - const diff = buffer.viewportY + stickyScrollLineCount - command.endMarker.line; + const lastLine = Math.min(command.endMarker.line, buffer.baseY + buffer.cursorY); + if (buffer.viewportY + stickyScrollLineCount > lastLine) { + const diff = buffer.viewportY + stickyScrollLineCount - lastLine; endMarkerOffset = diff * rowHeight; } } From d37ee81e3dea684c2a8b74c5bcafc2e4433d4a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20L=C3=A9cuyer?= <30299784+Jiogo18@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:18:12 -0400 Subject: [PATCH 213/306] Git - l10n discard changes dialogs (#254366) --- extensions/git/src/commands.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 152e78b161f..fc4193392b8 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -2247,8 +2247,8 @@ export class CommandCenter { const messageWarning = !discardUntrackedChangesToTrash ? resources.length === 1 - ? '\n\nThis is IRREVERSIBLE!\nThis file will be FOREVER LOST if you proceed.' - : '\n\nThis is IRREVERSIBLE!\nThese files will be FOREVER LOST if you proceed.' + ? '\n\n' + l10n.t('This is IRREVERSIBLE!\nThis file will be FOREVER LOST if you proceed.') + : '\n\n' + l10n.t('This is IRREVERSIBLE!\nThese files will be FOREVER LOST if you proceed.') : ''; const message = resources.length === 1 @@ -2258,11 +2258,11 @@ export class CommandCenter { const messageDetail = discardUntrackedChangesToTrash ? isWindows ? resources.length === 1 - ? 'You can restore this file from the Recycle Bin.' - : 'You can restore these files from the Recycle Bin.' + ? l10n.t('You can restore this file from the Recycle Bin.') + : l10n.t('You can restore these files from the Recycle Bin.') : resources.length === 1 - ? 'You can restore this file from the Trash.' - : 'You can restore these files from the Trash.' + ? l10n.t('You can restore this file from the Trash.') + : l10n.t('You can restore these files from the Trash.') : ''; const primaryAction = discardUntrackedChangesToTrash From bbf6eaec0b732b2b99b77b162fb117446b7ee9fa Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Tue, 8 Jul 2025 15:07:23 +0200 Subject: [PATCH 214/306] includes extension version --- src/vs/editor/common/languages.ts | 35 ++++++++++++++++++- .../common/standalone/standaloneEnums.ts | 2 +- src/vs/editor/common/textModelEditReason.ts | 32 +++++++++++++---- .../contrib/codeAction/browser/codeAction.ts | 2 +- .../browser/model/inlineCompletionsModel.ts | 6 ++-- .../suggest/browser/suggestController.ts | 4 +-- src/vs/monaco.d.ts | 1 - .../api/browser/mainThreadLanguageFeatures.ts | 2 +- .../browser/editSourceTrackingImpl.ts | 3 ++ 9 files changed, 70 insertions(+), 17 deletions(-) diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 844fcf9886f..6628ca9c2a9 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -905,7 +905,8 @@ export interface InlineCompletionsProvider wrapped lines get +2 indentation toward the parent. */ DeepIndent = 3 -} +} \ No newline at end of file diff --git a/src/vs/editor/common/textModelEditReason.ts b/src/vs/editor/common/textModelEditReason.ts index 9cd02aa1f4c..171a618debe 100644 --- a/src/vs/editor/common/textModelEditReason.ts +++ b/src/vs/editor/common/textModelEditReason.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ProviderId } from './languages.js'; + const privateSymbol = Symbol('TextModelEditReason'); export class TextModelEditReason { @@ -33,9 +35,14 @@ export class TextModelEditReason { * Converts the metadata to a key string. * Only includes properties/values that have `level` many `$` prefixes or less. */ - public toKey(level: number): string { + public toKey(level: number, filter: { [TKey in keyof ITextModelEditReasonMetadata]?: boolean } = {}): string { const metadata = this.metadata; const keys = Object.entries(metadata).filter(([key, value]) => { + const filterVal = (filter as Record)[key]; + if (filterVal !== undefined) { + return filterVal; + } + const prefixCount = (key.match(/\$/g) || []).length; return prefixCount <= level && value !== undefined && value !== null && value !== ''; }).map(([key, value]) => `${key}:${value}`); @@ -68,21 +75,21 @@ export const EditReasons = { } as const); }, - inlineCompletionAccept(data: { nes: boolean; requestUuid: string; extensionId: string }) { + inlineCompletionAccept(data: { nes: boolean; requestUuid: string; providerId?: ProviderId }) { return createEditReason({ source: 'inlineCompletionAccept', $nes: data.nes, - $extensionId: data.extensionId, + ...toProperties(data.providerId), $$requestUuid: data.requestUuid, } as const); }, - inlineCompletionPartialAccept(data: { nes: boolean; requestUuid: string; extensionId: string; type: 'word' | 'line' }) { + inlineCompletionPartialAccept(data: { nes: boolean; requestUuid: string; providerId?: ProviderId; type: 'word' | 'line' }) { return createEditReason({ source: 'inlineCompletionPartialAccept', type: data.type, $nes: data.nes, - $extensionId: data.extensionId, + ...toProperties(data.providerId), $$requestUuid: data.requestUuid, } as const); }, @@ -108,11 +115,22 @@ export const EditReasons = { eolChange: () => createEditReason({ source: 'eolChange' } as const), applyEdits: () => createEditReason({ source: 'applyEdits' } as const), snippet: () => createEditReason({ source: 'snippet' } as const), - suggest: (data: { extensionId: string | undefined }) => createEditReason({ source: 'suggest', $extensionId: data.extensionId } as const), + suggest: (data: { providerId: ProviderId | undefined }) => createEditReason({ source: 'suggest', ...toProperties(data.providerId) } as const), - codeAction: (data: { kind: string | undefined; extensionId: string | undefined }) => createEditReason({ source: 'codeAction', $kind: data.kind, $extensionId: data.extensionId } as const) + codeAction: (data: { kind: string | undefined; providerId: ProviderId | undefined }) => createEditReason({ source: 'codeAction', $kind: data.kind, ...toProperties(data.providerId) } as const) }; +function toProperties(version: ProviderId | undefined) { + if (!version) { + return {}; + } + return { + $extensionId: version.extensionId, + $extensionVersion: version.extensionVersion, + $providerId: version.providerId, + }; +} + type Values = T[keyof T]; type ITextModelEditReasonMetadata = Values<{ [TKey in keyof typeof EditReasons]: ReturnType['metadataT'] }>; diff --git a/src/vs/editor/contrib/codeAction/browser/codeAction.ts b/src/vs/editor/contrib/codeAction/browser/codeAction.ts index eaad94fdaf5..4680bee9c5b 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeAction.ts @@ -310,7 +310,7 @@ export async function applyCodeAction( code: 'undoredo.codeAction', respectAutoSaveConfig: codeActionReason !== ApplyCodeActionReason.OnSave, showPreview: options?.preview, - reason: EditReasons.codeAction({ kind: item.action.kind, extensionId: item.provider?.extensionId }), + reason: EditReasons.codeAction({ kind: item.action.kind, providerId: languages.ProviderId.fromExtensionId(item.provider?.extensionId) }), }); if (!result.isApplied) { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index c93523dabe4..ee8ef49336e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -392,7 +392,7 @@ export class InlineCompletionsModel extends Disposable { const userJumpedToActiveCompletion = this._jumpedToId.map(jumpedTo => !!jumpedTo && jumpedTo === this._inlineCompletionItems.get()?.inlineEdit?.semanticId); const providers = changeSummary.provider - ? { providers: [changeSummary.provider], label: 'single:' + changeSummary.provider.providerId } + ? { providers: [changeSummary.provider], label: 'single:' + changeSummary.provider.providerId?.toString() } : { providers: this._languageFeaturesService.inlineCompletionsProvider.all(this.textModel), label: undefined }; const suppressedProviderGroupIds = this._suppressedInlineCompletionGroupIds.get(); const availableProviders = providers.providers.filter(provider => !(provider.groupId && suppressedProviderGroupIds.has(provider.groupId))); @@ -776,14 +776,14 @@ export class InlineCompletionsModel extends Disposable { return EditReasons.inlineCompletionPartialAccept({ nes: completion.isInlineEdit, requestUuid: completion.requestUuid, - extensionId: completion.source.provider.groupId ?? 'unknown', + providerId: completion.source.provider.providerId, type, }); } else { return EditReasons.inlineCompletionAccept({ nes: completion.isInlineEdit, requestUuid: completion.requestUuid, - extensionId: completion.source.provider.groupId ?? 'unknown', + providerId: completion.source.provider.providerId, }); } } diff --git a/src/vs/editor/contrib/suggest/browser/suggestController.ts b/src/vs/editor/contrib/suggest/browser/suggestController.ts index d831d235ae8..a7f75606aa8 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestController.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestController.ts @@ -24,7 +24,7 @@ import { Range } from '../../../common/core/range.js'; import { IEditorContribution, ScrollType } from '../../../common/editorCommon.js'; import { EditorContextKeys } from '../../../common/editorContextKeys.js'; import { ITextModel, TrackedRangeStickiness } from '../../../common/model.js'; -import { CompletionItemInsertTextRule, CompletionItemProvider, CompletionTriggerKind } from '../../../common/languages.js'; +import { CompletionItemInsertTextRule, CompletionItemProvider, CompletionTriggerKind, ProviderId } from '../../../common/languages.js'; import { SnippetController2 } from '../../snippet/browser/snippetController2.js'; import { SnippetParser } from '../../snippet/browser/snippetParser.js'; import { ISuggestMemoryService } from './suggestMemory.js'; @@ -459,7 +459,7 @@ export class SuggestController implements IEditorContribution { adjustWhitespace: !(item.completion.insertTextRules! & CompletionItemInsertTextRule.KeepWhitespace), clipboardText: event.model.clipboardText, overtypingCapturer: this._overtypingCapturer.value, - reason: EditReasons.suggest({ extensionId: item.extensionId?.value }), + reason: EditReasons.suggest({ providerId: ProviderId.fromExtensionId(item.extensionId?.value) }), }); if (!(flags & InsertFlags.NoAfterUndoStop)) { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 921d01dd9e4..80dec01eee6 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7512,7 +7512,6 @@ declare namespace monaco.languages { * Multiple providers can have the same group id. */ groupId?: InlineCompletionProviderGroupId; - providerId?: string; /** * Returns a list of preferred provider {@link groupId}s. * The current provider is only requested for completions if no provider with a preferred group id returned a result. diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index e7c975f3f90..394cabe3f53 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -685,7 +685,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread } }, groupId: groupId ?? extensionId, - providerId: new languages.VersionedExtensionId(extensionId, extensionVersion).toString(), + providerId: new languages.ProviderId(extensionId, extensionVersion, groupId), yieldsToGroupIds: yieldsToExtensionIds, debounceDelayMs, displayName, diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts index 333bf17d85f..75790e7e589 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts @@ -344,6 +344,7 @@ class ArcTelemetrySender extends Disposable { res.telemetryService.publicLog2<{ extensionId: string; + extensionVersion: string; opportunityId: string; didBranchChange: number; timeDelayMs: number; @@ -354,6 +355,7 @@ class ArcTelemetrySender extends Disposable { comment: 'Reports the accepted and retained character count for an inline completion/edit.'; extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id (copilot or copilot-chat); which provided this inline completion.' }; + extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the extension.' }; opportunityId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Unique identifier for an opportunity to show an inline completion or NES.' }; didBranchChange: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Indicates if the branch changed in the meantime. If the branch changed (value is 1); this event should probably be ignored.' }; @@ -362,6 +364,7 @@ class ArcTelemetrySender extends Disposable { originalCharCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The original character count before any edits.' }; }>('editTelemetry.reportInlineEditArc', { extensionId: data.$extensionId ?? '', + extensionVersion: data.$extensionVersion ?? '', opportunityId: data.$$requestUuid ?? 'unknown', didBranchChange: res.didBranchChange ? 1 : 0, timeDelayMs: res.timeDelayMs, From 49e7cf8dbbaba0e983fb81bcb4e704697dcfdf70 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Tue, 8 Jul 2025 18:02:54 +0200 Subject: [PATCH 215/306] Adds sourceKeyWithoutExtId to editTelemetry.editSources.details --- src/vs/editor/common/textModelEditReason.ts | 4 +- .../browser/documentWithAnnotatedEdits.ts | 28 +++++++------ .../browser/editSourceTrackingImpl.ts | 41 +++++++++++++++---- .../editTelemetry/browser/editTracker.ts | 13 +++--- 4 files changed, 57 insertions(+), 29 deletions(-) diff --git a/src/vs/editor/common/textModelEditReason.ts b/src/vs/editor/common/textModelEditReason.ts index 171a618debe..7f3f5516d97 100644 --- a/src/vs/editor/common/textModelEditReason.ts +++ b/src/vs/editor/common/textModelEditReason.ts @@ -35,7 +35,7 @@ export class TextModelEditReason { * Converts the metadata to a key string. * Only includes properties/values that have `level` many `$` prefixes or less. */ - public toKey(level: number, filter: { [TKey in keyof ITextModelEditReasonMetadata]?: boolean } = {}): string { + public toKey(level: number, filter: { [TKey in ITextModelEditReasonMetadataKeys]?: boolean } = {}): string { const metadata = this.metadata; const keys = Object.entries(metadata).filter(([key, value]) => { const filterVal = (filter as Record)[key]; @@ -133,6 +133,8 @@ function toProperties(version: ProviderId | undefined) { type Values = T[keyof T]; type ITextModelEditReasonMetadata = Values<{ [TKey in keyof typeof EditReasons]: ReturnType['metadataT'] }>; +type ITextModelEditReasonMetadataKeys = Values<{ [TKey in keyof typeof EditReasons]: keyof ReturnType['metadataT'] }>; + function avoidPathRedaction(str: string | undefined): string | undefined { if (str === undefined) { diff --git a/src/vs/workbench/contrib/editTelemetry/browser/documentWithAnnotatedEdits.ts b/src/vs/workbench/contrib/editTelemetry/browser/documentWithAnnotatedEdits.ts index 08ddfb6a33c..88e1f926e1c 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/documentWithAnnotatedEdits.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/documentWithAnnotatedEdits.ts @@ -13,7 +13,7 @@ import { IEditorWorkerService } from '../../../../editor/common/services/editorW import { TextModelEditReason } from '../../../../editor/common/textModelEditReason.js'; import { IObservableDocument } from './observableWorkspace.js'; -export interface IDocumentWithAnnotatedEdits = EditSourceData> { +export interface IDocumentWithAnnotatedEdits = EditKeySourceData> { readonly value: IObservableWithChange }>; waitForQueue(): Promise; } @@ -22,8 +22,8 @@ export interface IDocumentWithAnnotatedEdits { - public readonly value: IObservableWithChange }>; +export class DocumentWithSourceAnnotatedEdits extends Disposable implements IDocumentWithAnnotatedEdits { + public readonly value: IObservableWithChange }>; constructor(private readonly _originalDoc: IObservableDocument) { super(); @@ -32,7 +32,7 @@ export class DocumentWithAnnotatedEdits extends Disposable implements IDocumentW this._register(runOnChange(this._originalDoc.value, (val, _prevVal, edits) => { const eComposed = AnnotatedStringEdit.compose(edits.map(e => { - const editSourceData = new EditReasonData(e.reason); + const editSourceData = new EditSourceData(e.reason); return e.mapData(() => editSourceData); })); @@ -46,9 +46,9 @@ export class DocumentWithAnnotatedEdits extends Disposable implements IDocumentW } /** - * Only joins touching edits if the source and the metadata is the same. + * Only joins touching edits if the source and the metadata is the same (e.g. requestUuids must be equal). */ -export class EditReasonData implements IEditData { +export class EditSourceData implements IEditData { public readonly source; public readonly key; @@ -59,31 +59,33 @@ export class EditReasonData implements IEditData { this.source = EditSourceBase.create(this.editReason); } - join(data: EditReasonData): EditReasonData | undefined { + join(data: EditSourceData): EditSourceData | undefined { if (this.editReason !== data.editReason) { return undefined; } return this; } - toEditSourceData(): EditSourceData { - return new EditSourceData(this.key, this.source); + toEditSourceData(): EditKeySourceData { + return new EditKeySourceData(this.key, this.source, this.editReason); } } -export class EditSourceData implements IEditData { +export class EditKeySourceData implements IEditData { constructor( public readonly key: string, public readonly source: EditSource, + public readonly representative: TextModelEditReason, ) { } - join(data: EditSourceData): EditSourceData | undefined { + join(data: EditKeySourceData): EditKeySourceData | undefined { if (this.key !== data.key) { return undefined; } if (this.source !== data.source) { return undefined; } + // The representatives could be different! (But equal modulo key) return this; } } @@ -197,7 +199,7 @@ class UnknownEditSource extends EditSourceBase { public getColor(): string { return '#ff000033'; } } -export class CombineStreamedChanges> extends Disposable implements IDocumentWithAnnotatedEdits { +export class CombineStreamedChanges> extends Disposable implements IDocumentWithAnnotatedEdits { private readonly _value: ISettableObservable }>; readonly value: IObservableWithChange }>; private readonly _runStore = this._register(new DisposableStore()); @@ -265,7 +267,7 @@ export class CombineStreamedChanges }[] }) { +function isChatEdit(next: { value: StringText; change: { edit: AnnotatedStringEdit }[] }) { return next.change.every(c => c.edit.replacements.every(e => { if (e.data.source.category === 'ai' && e.data.source.feature === 'chat') { return true; diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts index 75790e7e589..66ef9b4eda0 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts @@ -12,11 +12,12 @@ import { URI } from '../../../../base/common/uri.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { AnnotatedStringEdit, BaseStringEdit } from '../../../../editor/common/core/edits/stringEdit.js'; import { StringText } from '../../../../editor/common/core/text/abstractText.js'; +import { TextModelEditReason } from '../../../../editor/common/textModelEditReason.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { ISCMRepository, ISCMService } from '../../scm/common/scm.js'; import { ArcTracker } from './arcTracker.js'; -import { CombineStreamedChanges, DocumentWithAnnotatedEdits, EditReasonData, EditSource, EditSourceData, IDocumentWithAnnotatedEdits, MinimizeEditsProcessor } from './documentWithAnnotatedEdits.js'; +import { CombineStreamedChanges, DocumentWithSourceAnnotatedEdits, EditKeySourceData, EditSource, EditSourceData, IDocumentWithAnnotatedEdits, MinimizeEditsProcessor } from './documentWithAnnotatedEdits.js'; import { DocumentEditSourceTracker, TrackedEdit } from './editTracker.js'; import { ObservableWorkspace, IObservableDocument } from './observableWorkspace.js'; @@ -89,9 +90,9 @@ class TrackedDocumentInfo extends Disposable { super(); // Use the listener service and special events from core to annotate where an edit came from (is async) - let processedDoc: IDocumentWithAnnotatedEdits = this._store.add(new DocumentWithAnnotatedEdits(_doc)); + let processedDoc: IDocumentWithAnnotatedEdits = this._store.add(new DocumentWithSourceAnnotatedEdits(_doc)); // Combine streaming edits into one and make edit smaller - processedDoc = this._store.add(this._instantiationService.createInstance((CombineStreamedChanges), processedDoc)); + processedDoc = this._store.add(this._instantiationService.createInstance((CombineStreamedChanges), processedDoc)); // Remove common suffix and prefix from edits processedDoc = this._store.add(new MinimizeEditsProcessor(processedDoc)); @@ -223,6 +224,10 @@ class TrackedDocumentInfo extends Disposable { isTrackedByGit: isTrackedByGit ? 1 : 0, }); + const sourceKeyToRepresentative = new Map(); + for (const r of ranges) { + sourceKeyToRepresentative.set(r.sourceKey, r.sourceRepresentative); + } const sums = sumByCategory(ranges, r => r.range.length, r => r.sourceKey); const entries = Object.entries(sums).filter(([key, value]) => value !== undefined); @@ -233,9 +238,21 @@ class TrackedDocumentInfo extends Disposable { if (value === undefined) { continue; } + + + const repr = sourceKeyToRepresentative.get(key); + const cleanedKey = repr?.toKey(1, { $extensionId: false, $extensionVersion: false }); + + const metadata = repr?.metadata; + const extensionId = metadata && '$extensionId' in metadata ? metadata.$extensionId : undefined; + const extensionVersion = metadata && '$extensionVersion' in metadata ? metadata.$extensionVersion : undefined; + this._telemetryService.publicLog2<{ mode: string; - reasonKey: string; + sourceKey: string; + extensionId: string; + extensionVersion: string; + sourceKeyWithoutExtId: string; languageId: string; statsUuid: string; modifiedCount: number; @@ -244,16 +261,22 @@ class TrackedDocumentInfo extends Disposable { owner: 'hediet'; comment: 'Reports distribution of various edit kinds.'; - reasonKey: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The reason for the edit.' }; + sourceKey: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the edit.' }; mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'longterm or 5minWindow' }; languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' }; statsUuid: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The unique identifier for the telemetry event.' }; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id which provided this inline completion.' }; + extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the extension.' }; + sourceKeyWithoutExtId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the edit.' }; modifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of nes modified characters'; isMeasurement: true }; totalModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Total number of characters'; isMeasurement: true }; }>('editTelemetry.editSources.details', { mode, - reasonKey: key, + sourceKey: key, + extensionId: extensionId ?? '', + extensionVersion: extensionVersion ?? '', + sourceKeyWithoutExtId: cleanedKey ?? '', languageId: this._doc.languageId.get(), statsUuid: statsUuid, modifiedCount: value, @@ -312,8 +335,8 @@ function mapObservableDelta(obs: IObservableWithChange, store: DisposableStore): IDocumentWithAnnotatedEdits { - const docWithJustReason: IDocumentWithAnnotatedEdits = { +function createDocWithJustReason(docWithAnnotatedEdits: IDocumentWithAnnotatedEdits, store: DisposableStore): IDocumentWithAnnotatedEdits { + const docWithJustReason: IDocumentWithAnnotatedEdits = { value: mapObservableDelta(docWithAnnotatedEdits.value, edit => ({ edit: edit.edit.mapData(d => d.data.toEditSourceData()) }), store), waitForQueue: () => docWithAnnotatedEdits.waitForQueue(), }; @@ -322,7 +345,7 @@ function createDocWithJustReason(docWithAnnotatedEdits: IDocumentWithAnnotatedEd class ArcTelemetrySender extends Disposable { constructor( - docWithAnnotatedEdits: IDocumentWithAnnotatedEdits, + docWithAnnotatedEdits: IDocumentWithAnnotatedEdits, scmRepoBridge: ScmRepoBridge | undefined, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editTracker.ts b/src/vs/workbench/contrib/editTelemetry/browser/editTracker.ts index 6311aee4ee4..a7226cbd693 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editTracker.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editTracker.ts @@ -8,14 +8,15 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { observableSignal, runOnChange, IReader } from '../../../../base/common/observable.js'; import { AnnotatedStringEdit } from '../../../../editor/common/core/edits/stringEdit.js'; import { OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js'; -import { IDocumentWithAnnotatedEdits, EditSourceData, EditSource } from './documentWithAnnotatedEdits.js'; +import { TextModelEditReason } from '../../../../editor/common/textModelEditReason.js'; +import { IDocumentWithAnnotatedEdits, EditKeySourceData, EditSource } from './documentWithAnnotatedEdits.js'; /** * Tracks a single document. */ export class DocumentEditSourceTracker extends Disposable { - private _edits: AnnotatedStringEdit = AnnotatedStringEdit.empty; - private _pendingExternalEdits: AnnotatedStringEdit = AnnotatedStringEdit.empty; + private _edits: AnnotatedStringEdit = AnnotatedStringEdit.empty; + private _pendingExternalEdits: AnnotatedStringEdit = AnnotatedStringEdit.empty; private readonly _update = observableSignal(this); @@ -55,8 +56,7 @@ export class DocumentEditSourceTracker extends Disposable { const ranges = this._edits.getNewRanges(); return ranges.map((r, idx) => { const e = this._edits.replacements[idx]; - const reason = e.data.source; - const te = new TrackedEdit(e.replaceRange, r, reason, e.data.key); + const te = new TrackedEdit(e.replaceRange, r, e.data.key, e.data.source, e.data.representative); return te; }); } @@ -90,7 +90,8 @@ export class TrackedEdit { constructor( public readonly originalRange: OffsetRange, public readonly range: OffsetRange, - public readonly source: EditSource, public readonly sourceKey: string, + public readonly source: EditSource, + public readonly sourceRepresentative: TextModelEditReason, ) { } } From 8f023e48c8c7f4f8f4991898cc461a4ee17bf72a Mon Sep 17 00:00:00 2001 From: Jason Kuo Date: Tue, 8 Jul 2025 11:22:54 -0500 Subject: [PATCH 216/306] Use preserveFocus when focusing stack frames (#251964) * Use preserveFocus when focusing stack frames * Add preserveFocus in openEditor call for disassembly view --- src/vs/workbench/contrib/debug/common/debugModel.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 75062d56a1d..59aa5a5ff43 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -542,10 +542,10 @@ export class StackFrame implements IStackFrame { async openInEditor(editorService: IEditorService, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise { const threadStopReason = this.thread.stoppedDetails?.reason; if (this.instructionPointerReference && - (threadStopReason === 'instruction breakpoint' || - (threadStopReason === 'step' && this.thread.lastSteppingGranularity === 'instruction') || + ((threadStopReason === 'instruction breakpoint' && !preserveFocus) || + (threadStopReason === 'step' && this.thread.lastSteppingGranularity === 'instruction' && !preserveFocus) || editorService.activeEditor instanceof DisassemblyViewInput)) { - return editorService.openEditor(DisassemblyViewInput.instance, { pinned: true, revealIfOpened: true }); + return editorService.openEditor(DisassemblyViewInput.instance, { pinned: true, revealIfOpened: true, preserveFocus }); } if (this.source.available) { From ecd0405577ee1c80fa5ce5a804fdc73bc09b64cf Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Tue, 8 Jul 2025 09:49:57 -0700 Subject: [PATCH 217/306] feat: time the different search providers (#252700) --- .../preferences/browser/preferencesSearch.ts | 10 ++-- .../preferences/browser/settingsEditor2.ts | 54 ++++++++++++++++--- .../contrib/preferences/common/preferences.ts | 7 +++ 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index e1b7dda3e85..4a38b9bb227 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -20,7 +20,7 @@ import { IAiSettingsSearchService } from '../../../services/aiSettingsSearch/com import { IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IGroupFilter, ISearchResult, ISetting, ISettingMatch, ISettingMatcher, ISettingsEditorModel, ISettingsGroup, SettingKeyMatchTypes, SettingMatchType } from '../../../services/preferences/common/preferences.js'; import { nullRange } from '../../../services/preferences/common/preferencesModels.js'; -import { IAiSearchProvider, IPreferencesSearchService, IRemoteSearchProvider, ISearchProvider, IWorkbenchSettingsConfiguration } from '../common/preferences.js'; +import { EMBEDDINGS_ONLY_SEARCH_PROVIDER_NAME, EMBEDDINGS_SEARCH_PROVIDER_NAME, IAiSearchProvider, IPreferencesSearchService, IRemoteSearchProvider, ISearchProvider, IWorkbenchSettingsConfiguration, LLM_RANKED_SEARCH_PROVIDER_NAME, STRING_MATCH_SEARCH_PROVIDER_NAME, TF_IDF_SEARCH_PROVIDER_NAME } from '../common/preferences.js'; export interface IEndpointDetails { urlBase?: string; @@ -134,7 +134,7 @@ export class LocalSearchProvider implements ISearchProvider { const alwaysAllowedMatchTypes = SettingMatchType.DescriptionOrValueMatch | SettingMatchType.LanguageTagSettingMatch; const filteredMatches = filterMatches .filter(m => (m.matchType & topKeyMatchType) || (m.matchType & alwaysAllowedMatchTypes) || m.matchType === SettingMatchType.ExactMatch) - .map(m => ({ ...m, providerName: 'local' })); + .map(m => ({ ...m, providerName: STRING_MATCH_SEARCH_PROVIDER_NAME })); return Promise.resolve({ filterMatches: filteredMatches, exactMatch: filteredMatches.some(m => m.matchType === SettingMatchType.ExactMatch) @@ -441,7 +441,7 @@ class EmbeddingsSearchProvider implements IRemoteSearchProvider { return []; } - const providerName = this._excludeSelectionStep ? 'embeddingsOnly' : 'embeddingsFull'; + const providerName = this._excludeSelectionStep ? EMBEDDINGS_ONLY_SEARCH_PROVIDER_NAME : EMBEDDINGS_SEARCH_PROVIDER_NAME; for (const settingKey of settings) { if (filterMatches.length === EmbeddingsSearchProvider.EMBEDDINGS_SETTINGS_SEARCH_MAX_PICKS) { break; @@ -550,7 +550,7 @@ class TfIdfSearchProvider implements IRemoteSearchProvider { matchType: SettingMatchType.RemoteMatch, keyMatchScore: 0, score: info.score, - providerName: 'tfIdf' + providerName: TF_IDF_SEARCH_PROVIDER_NAME }); } @@ -639,7 +639,7 @@ class AiSearchProvider implements IAiSearchProvider { matchType: SettingMatchType.RemoteMatch, keyMatchScore: 0, score: 0, // the results are sorted upstream. - providerName: 'llmRanked' + providerName: LLM_RANKED_SEARCH_PROVIDER_NAME }); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 1662f480b83..55b8304732a 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -23,6 +23,7 @@ import { Iterable } from '../../../../base/common/iterator.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, dispose, type IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import * as platform from '../../../../base/common/platform.js'; +import { StopWatch } from '../../../../base/common/stopwatch.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; @@ -58,7 +59,7 @@ import { nullRange, Settings2EditorModel } from '../../../services/preferences/c import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js'; import { IUserDataSyncWorkbenchService } from '../../../services/userDataSync/common/userDataSync.js'; import { SuggestEnabledInput } from '../../codeEditor/browser/suggestEnabledInput/suggestEnabledInput.js'; -import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, ENABLE_LANGUAGE_FILTER, EXTENSION_FETCH_TIMEOUT_MS, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, getExperimentalExtensionToggleData, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SHOW_AI_RESULTS, SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS, WorkbenchSettingsEditorSettings, WORKSPACE_TRUST_SETTING_TAG } from '../common/preferences.js'; +import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, EMBEDDINGS_SEARCH_PROVIDER_NAME, ENABLE_LANGUAGE_FILTER, EXTENSION_FETCH_TIMEOUT_MS, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, FILTER_MODEL_SEARCH_PROVIDER_NAME, getExperimentalExtensionToggleData, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, LANGUAGE_SETTING_TAG, LLM_RANKED_SEARCH_PROVIDER_NAME, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SHOW_AI_RESULTS, SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS, STRING_MATCH_SEARCH_PROVIDER_NAME, TF_IDF_SEARCH_PROVIDER_NAME, WorkbenchSettingsEditorSettings, WORKSPACE_TRUST_SETTING_TAG } from '../common/preferences.js'; import { settingsHeaderBorder, settingsSashBorder, settingsTextInputBorder } from '../common/settingsEditorColorRegistry.js'; import './media/settingsEditor2.css'; import { preferencesAiResultsIcon, preferencesClearInputIcon, preferencesFilterIcon } from './preferencesIcons.js'; @@ -191,6 +192,8 @@ export class SettingsEditor2 extends EditorPane { private searchInProgress: CancellationTokenSource | null = null; private aiSearchPromise: CancelablePromise | null = null; + private stopWatch: StopWatch; + private showAiResultsAction: Action | null = null; private searchInputDelayer: Delayer; @@ -276,6 +279,7 @@ export class SettingsEditor2 extends EditorPane { this.settingRowFocused = CONTEXT_SETTINGS_ROW_FOCUS.bindTo(contextKeyService); this.scheduledRefreshes = new Map(); + this.stopWatch = new StopWatch(false); this.editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, SETTINGS_EDITOR_STATE_KEY); @@ -1778,7 +1782,7 @@ export class SettingsEditor2 extends EditorPane { matchType: SettingMatchType.None, keyMatchScore: 0, score: 0, - providerName: 'filterModel' + providerName: FILTER_MODEL_SEARCH_PROVIDER_NAME }); } } @@ -1857,7 +1861,7 @@ export class SettingsEditor2 extends EditorPane { private doLocalSearch(query: string, token: CancellationToken): Promise { const localSearchProvider = this.preferencesSearchService.getLocalSearchProvider(query); - return this.searchWithProvider(SearchResultIdx.Local, localSearchProvider, token); + return this.searchWithProvider(SearchResultIdx.Local, localSearchProvider, STRING_MATCH_SEARCH_PROVIDER_NAME, token); } private doRemoteSearch(query: string, token: CancellationToken): Promise { @@ -1865,7 +1869,7 @@ export class SettingsEditor2 extends EditorPane { if (!remoteSearchProvider) { return Promise.resolve(null); } - return this.searchWithProvider(SearchResultIdx.Remote, remoteSearchProvider, token); + return this.searchWithProvider(SearchResultIdx.Remote, remoteSearchProvider, TF_IDF_SEARCH_PROVIDER_NAME, token); } private async doAiSearch(query: string, token: CancellationToken): Promise { @@ -1874,7 +1878,7 @@ export class SettingsEditor2 extends EditorPane { return null; } - const embeddingsResults = await this.searchWithProvider(SearchResultIdx.Embeddings, aiSearchProvider, token); + const embeddingsResults = await this.searchWithProvider(SearchResultIdx.Embeddings, aiSearchProvider, EMBEDDINGS_SEARCH_PROVIDER_NAME, token); if (!embeddingsResults || token.isCancellationRequested) { return null; } @@ -1896,26 +1900,62 @@ export class SettingsEditor2 extends EditorPane { return null; } + this.stopWatch.reset(); const result = await aiSearchProvider.getLLMRankedResults(token); - if (!result || token.isCancellationRequested) { + this.stopWatch.stop(); + + if (token.isCancellationRequested) { return null; } + // Only log the elapsed time if there are actual results. + if (result && result.filterMatches.length > 0) { + const elapsed = this.stopWatch.elapsed(); + this.logSearchPerformance(LLM_RANKED_SEARCH_PROVIDER_NAME, elapsed); + } + this.searchResultModel!.setResult(SearchResultIdx.AiSelected, result); return result; } - private async searchWithProvider(type: SearchResultIdx, searchProvider: ISearchProvider, token: CancellationToken): Promise { + private async searchWithProvider(type: SearchResultIdx, searchProvider: ISearchProvider, providerName: string, token: CancellationToken): Promise { + this.stopWatch.reset(); const result = await this._searchPreferencesModel(this.defaultSettingsEditorModel, searchProvider, token); + this.stopWatch.stop(); + if (token.isCancellationRequested) { // Handle cancellation like this because cancellation is lost inside the search provider due to async/await return null; } + + // Only log the elapsed time if there are actual results. + if (result && result.filterMatches.length > 0) { + const elapsed = this.stopWatch.elapsed(); + this.logSearchPerformance(providerName, elapsed); + } + this.searchResultModel ??= this.instantiationService.createInstance(SearchResultModel, this.viewState, this.settingsOrderByTocIndex, this.workspaceTrustManagementService.isWorkspaceTrusted()); this.searchResultModel.setResult(type, result); return result; } + private logSearchPerformance(providerName: string, elapsed: number): void { + type SettingsEditorSearchPerformanceEvent = { + providerName: string | undefined; + elapsedMs: number; + }; + type SettingsEditorSearchPerformanceClassification = { + providerName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the search provider, if applicable.' }; + elapsedMs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The time taken to perform the search, in milliseconds.' }; + owner: 'rzhao271'; + comment: 'Event emitted when the Settings editor calls a search provider to search for a setting'; + }; + this.telemetryService.publicLog2('settingsEditor.searchPerformance', { + providerName, + elapsedMs: elapsed, + }); + } + private renderResultCountMessages(showAiResultsMessage: boolean) { if (!this.currentSettingsModel) { return; diff --git a/src/vs/workbench/contrib/preferences/common/preferences.ts b/src/vs/workbench/contrib/preferences/common/preferences.ts index 69cb9266e08..7c0dd724f00 100644 --- a/src/vs/workbench/contrib/preferences/common/preferences.ts +++ b/src/vs/workbench/contrib/preferences/common/preferences.ts @@ -110,6 +110,13 @@ export const ENABLE_LANGUAGE_FILTER = true; export const ENABLE_EXTENSION_TOGGLE_SETTINGS = true; export const EXTENSION_FETCH_TIMEOUT_MS = 1000; +export const STRING_MATCH_SEARCH_PROVIDER_NAME = 'local'; +export const TF_IDF_SEARCH_PROVIDER_NAME = 'tfIdf'; +export const FILTER_MODEL_SEARCH_PROVIDER_NAME = 'filterModel'; +export const EMBEDDINGS_ONLY_SEARCH_PROVIDER_NAME = 'embeddingsOnly'; +export const EMBEDDINGS_SEARCH_PROVIDER_NAME = 'embeddingsFull'; +export const LLM_RANKED_SEARCH_PROVIDER_NAME = 'llmRanked'; + export enum WorkbenchSettingsEditorSettings { ShowAISearchToggle = 'workbench.settings.showAISearchToggle', EnableNaturalLanguageSearch = 'workbench.settings.enableNaturalLanguageSearch', From 45bcb2d5fb8bedbaa536a51a3b3519cc7675effa Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Tue, 8 Jul 2025 09:52:18 -0700 Subject: [PATCH 218/306] infinite response bot command (#254688) --- .github/commands.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/commands.json b/.github/commands.json index d948a1bf493..2d10e053f86 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -596,7 +596,7 @@ "reason": "not_planned", "comment": "Please look at the following meta issue: https://github.com/microsoft/vscode/issues/253126. Please refer to that issue for updates and discussions. Feel free to open a new issue if you think this is a different problem." }, - { + { "type": "label", "name": "~chat-billing", "removeLabel": "~chat-billing", @@ -604,5 +604,14 @@ "action": "close", "reason": "not_planned", "comment": "Please look at the following meta issue: https://github.com/microsoft/vscode/issues/252230. Please refer to that issue for updates and discussions. Feel free to open a new issue if you think this is a different problem." + }, + { + "type": "label", + "name": "~chat-infinite-response-loop", + "removeLabel": "~chat-infinite-response-loop", + "addLabel":"chat-infinite-response-loop", + "action": "close", + "reason": "not_planned", + "comment": "Please look at the following meta issue: https://github.com/microsoft/vscode/issues/253134. Please refer to that issue for updates and discussions. Feel free to open a new issue if you think this is a different problem." } ] From 9362739ef42d78d990745630f37dafcaa360bd34 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 8 Jul 2025 12:52:36 -0400 Subject: [PATCH 219/306] add `terminal` to `taskExecution` (#254477) fix #234440 --- .../extensions/common/extensionsApiProposals.ts | 3 +++ src/vs/workbench/api/common/extHost.api.impl.ts | 3 +++ src/vs/workbench/api/common/extHostTask.ts | 17 ++++++++++++++++- .../vscode.proposed.taskExecutionTerminal.d.ts | 15 +++++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/vscode-dts/vscode.proposed.taskExecutionTerminal.d.ts diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 7436d76f268..4592b9f0b96 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -344,6 +344,9 @@ const _allApiProposals = { tabInputTextMerge: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts', }, + taskExecutionTerminal: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.taskExecutionTerminal.d.ts', + }, taskPresentationGroup: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.taskPresentationGroup.d.ts', }, diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 5e1572b7576..87fc94481a3 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1358,6 +1358,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostTask.taskExecutions; }, onDidStartTask: (listeners, thisArgs?, disposables?) => { + if (!isProposedApiEnabled(extension, 'taskExecutionTerminal')) { + thisArgs.terminal = undefined; + } return _asExtensionEvent(extHostTask.onDidStartTask)(listeners, thisArgs, disposables); }, onDidEndTask: (listeners, thisArgs?, disposables?) => { diff --git a/src/vs/workbench/api/common/extHostTask.ts b/src/vs/workbench/api/common/extHostTask.ts index bf0d6e41fe2..34fe6fc1a11 100644 --- a/src/vs/workbench/api/common/extHostTask.ts +++ b/src/vs/workbench/api/common/extHostTask.ts @@ -360,6 +360,7 @@ namespace TaskFilterDTO { class TaskExecutionImpl implements vscode.TaskExecution { readonly #tasks: ExtHostTaskBase; + private _terminal: vscode.Terminal | undefined; constructor(tasks: ExtHostTaskBase, readonly _id: string, private readonly _task: vscode.Task) { this.#tasks = tasks; @@ -378,6 +379,14 @@ class TaskExecutionImpl implements vscode.TaskExecution { public fireDidEndProcess(value: tasks.ITaskProcessEndedDTO): void { } + + public get terminal(): vscode.Terminal | undefined { + return this._terminal; + } + + public set terminal(term: vscode.Terminal | undefined) { + this._terminal = term; + } } export interface HandlerData { @@ -497,8 +506,14 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask } this._lastStartedTask = execution.id; + const taskExecution = await this.getTaskExecution(execution); + const terminal = this._terminalService.getTerminalById(terminalId)?.value; + if (taskExecution) { + taskExecution.terminal = terminal; + } + this._onDidExecuteTask.fire({ - execution: await this.getTaskExecution(execution) + execution: taskExecution }); } diff --git a/src/vscode-dts/vscode.proposed.taskExecutionTerminal.d.ts b/src/vscode-dts/vscode.proposed.taskExecutionTerminal.d.ts new file mode 100644 index 00000000000..a0884721146 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.taskExecutionTerminal.d.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// #234440 +declare module 'vscode' { + + export interface TaskExecution { + /** + * The terminal associated with this task execution, if any. + */ + readonly terminal?: Terminal; + } +} From 6ba2ae25b32ed6a9ffa014770af23fd9fe07c6e9 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Tue, 8 Jul 2025 19:26:26 +0200 Subject: [PATCH 220/306] Also report ARC for edits with multiple replacements --- .../browser/editSourceTrackingImpl.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts index 66ef9b4eda0..a6abbd1ead2 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts @@ -5,6 +5,7 @@ import { reverseOrder, compareBy, numberComparator, sumBy } from '../../../../base/common/arrays.js'; import { IntervalTimer, TimeoutTimer } from '../../../../base/common/async.js'; +import { onUnexpectedError } from '../../../../base/common/errors.js'; import { toDisposable, DisposableStore, Disposable } from '../../../../base/common/lifecycle.js'; import { mapObservableArrayCached, derived, IReader, IObservable, observableSignal, runOnChange, IObservableWithChange, observableValue, transaction, derivedObservableWithCache } from '../../../../base/common/observable.js'; import { isDefined } from '../../../../base/common/types.js'; @@ -353,18 +354,21 @@ class ArcTelemetrySender extends Disposable { this._register(runOnChange(docWithAnnotatedEdits.value, (_val, _prev, changes) => { const edit = AnnotatedStringEdit.compose(changes.map(c => c.edit)); - if (edit.replacements.length !== 1) { + + if (!edit.replacements.some(r => r.data.editReason.metadata.source === 'inlineCompletionAccept')) { return; } - const singleEdit = edit.replacements[0]; - const data = singleEdit.data.editReason.metadata; - if (data?.source !== 'inlineCompletionAccept') { + if (!edit.replacements.every(r => r.data.editReason.metadata.source === 'inlineCompletionAccept')) { + onUnexpectedError(new Error('ArcTelemetrySender: Not all edits are inline completion accept edits!')); return; } + if (edit.replacements[0].data.editReason.metadata.source !== 'inlineCompletionAccept') { + return; + } + const data = edit.replacements[0].data.editReason.metadata; const docWithJustReason = createDocWithJustReason(docWithAnnotatedEdits, this._store); - const reporter = this._instantiationService.createInstance(ArcTelemetryReporter, docWithJustReason, scmRepoBridge, singleEdit.toEdit(), res => { - + const reporter = this._instantiationService.createInstance(ArcTelemetryReporter, docWithJustReason, scmRepoBridge, edit, res => { res.telemetryService.publicLog2<{ extensionId: string; extensionVersion: string; @@ -437,6 +441,7 @@ export class ArcTelemetryReporter { this._initialBranchName = this._gitRepo?.headBranchNameObs.get(); // This aligns with github inline completions + this._report(0); // for debugging this._reportAfter(30 * 1000); this._reportAfter(120 * 1000); this._reportAfter(300 * 1000); From 2d948e9d255055ed08d6138d30ece4db216185f6 Mon Sep 17 00:00:00 2001 From: Yusuke Yamada Date: Wed, 9 Jul 2025 03:01:20 +0900 Subject: [PATCH 221/306] Fix invalid settings keys (#254609) --- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 858099af013..c68b718f8db 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -339,7 +339,7 @@ configurationRegistry.registerConfiguration({ } ], default: true, - markdownDescription: nls.localize('mpc.discovery.enabled', "Configures discovery of Model Context Protocol servers on the machine. It may be set to `true` or `false` to disable or enable all sources, and an mapping sources you wish to enable."), + markdownDescription: nls.localize('mcp.discovery.enabled', "Configures discovery of Model Context Protocol servers on the machine. It may be set to `true` or `false` to disable or enable all sources, and an mapping sources you wish to enable."), }, [mcpGalleryServiceUrlConfig]: { type: 'string', From 0ba52498bf2ad63724a638dd8526e4e8bcc831f7 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 8 Jul 2025 11:03:47 -0700 Subject: [PATCH 222/306] Pick up latest md katex plugin --- extensions/markdown-math/package-lock.json | 8 ++++---- extensions/markdown-math/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/markdown-math/package-lock.json b/extensions/markdown-math/package-lock.json index 73ae907e680..5cf113f9409 100644 --- a/extensions/markdown-math/package-lock.json +++ b/extensions/markdown-math/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@vscode/markdown-it-katex": "^1.1.1" + "@vscode/markdown-it-katex": "^1.1.2" }, "devDependencies": { "@types/markdown-it": "^0.0.0", @@ -32,9 +32,9 @@ "dev": true }, "node_modules/@vscode/markdown-it-katex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@vscode/markdown-it-katex/-/markdown-it-katex-1.1.1.tgz", - "integrity": "sha512-3KTlbsRBPJQLE2YmLL7K6nunTlU+W9T5+FjfNdWuIUKgxSS6HWLQHaO3L4MkJi7z7MpIPpY+g4N+cWNBPE/MSA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@vscode/markdown-it-katex/-/markdown-it-katex-1.1.2.tgz", + "integrity": "sha512-+4IIv5PgrmhKvW/3LpkpkGg257OViEhXkOOgCyj5KMsjsOfnRXkni8XAuuF9Ui5p3B8WnUovlDXAQNb8RJ/RaQ==", "license": "MIT", "dependencies": { "katex": "^0.16.4" diff --git a/extensions/markdown-math/package.json b/extensions/markdown-math/package.json index 6e599ae2a0e..27b5047527f 100644 --- a/extensions/markdown-math/package.json +++ b/extensions/markdown-math/package.json @@ -119,6 +119,6 @@ "url": "https://github.com/microsoft/vscode.git" }, "dependencies": { - "@vscode/markdown-it-katex": "^1.1.1" + "@vscode/markdown-it-katex": "^1.1.2" } } From 76d69433f51f0c92006883cc6ad7a68ee6cde7e2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:37:25 -0700 Subject: [PATCH 223/306] Revert "terminal: default 'terminal.integrated.inheritEnv' to false on Windows (#252325)" This reverts commit ae6080018a4b9bd5987ac1afd47532e84c07170e. --- .../terminal/common/terminalPlatformConfiguration.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts b/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts index e5763c0dce1..7d8d78ba4fe 100644 --- a/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts +++ b/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts @@ -5,7 +5,7 @@ import { getAllCodicons } from '../../../base/common/codicons.js'; import { IJSONSchema, IJSONSchemaMap } from '../../../base/common/jsonSchema.js'; -import { isWindows, OperatingSystem, Platform, PlatformToString } from '../../../base/common/platform.js'; +import { OperatingSystem, Platform, PlatformToString } from '../../../base/common/platform.js'; import { localize } from '../../../nls.js'; import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationRegistry } from '../../configuration/common/configurationRegistry.js'; import { Registry } from '../../registry/common/platform.js'; @@ -336,10 +336,9 @@ const terminalPlatformConfiguration: IConfigurationNode = { }, [TerminalSettingId.InheritEnv]: { scope: ConfigurationScope.APPLICATION, - description: localize('terminal.integrated.inheritEnv', "Whether new shells should inherit their environment from VS Code, which may source a login shell to ensure $PATH and other development variables are initialized."), + description: localize('terminal.integrated.inheritEnv', "Whether new shells should inherit their environment from VS Code, which may source a login shell to ensure $PATH and other development variables are initialized. This has no effect on Windows."), type: 'boolean', - // False by default on Windows to prevent powershell inheritance issues (#251446) - default: isWindows ? false : true, + default: true }, [TerminalSettingId.PersistentSessionScrollback]: { scope: ConfigurationScope.APPLICATION, From 86d326db8e92cf28bcfabdaf134432ae890370f4 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Tue, 8 Jul 2025 11:39:44 -0700 Subject: [PATCH 224/306] reset input in welcome view on context change for experimental mode (#254670) --- .../workbench/contrib/chat/browser/chatWidget.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index c0c7429f5dd..cb158a491e0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -461,6 +461,21 @@ export class ChatWidget extends Disposable implements IChatWidget { })); this._register(this.onDidChangeParsedInput(() => this.updateChatInputContext())); + + this._register(this.contextKeyService.onDidChangeContext(e => { + if (e.affectsSome(new Set([ + ChatContextKeys.Setup.installed.key, + ChatContextKeys.Entitlement.canSignUp.key + ]))) { + // reset the input in welcome view if it was rendered in experimental mode + if (this.container.classList.contains('experimental-welcome-view')) { + this.container.classList.remove('experimental-welcome-view'); + const renderFollowups = this.viewOptions.renderFollowups ?? false; + const renderStyle = this.viewOptions.renderStyle; + this.createInput(this.container, { renderFollowups, renderStyle }); + } + } + })); } private _lastSelectedAgent: IChatAgentData | undefined; From 983397225279176dbeecfaf0379ec285699e8f84 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 8 Jul 2025 14:50:49 -0400 Subject: [PATCH 225/306] only wrap for long lines if in screen reader mode (#254698) only wrap if in screen reader mode --- src/vs/editor/common/config/editorOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 291dc666573..6ca3a0b619d 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -2798,7 +2798,7 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption Date: Tue, 8 Jul 2025 11:55:50 -0700 Subject: [PATCH 226/306] Fix mode switching back to ask mode when extension is slow to load (#254539) * Fix mode switching back to ask mode when extension is slow to load Fix #254173 * fix test --- src/vs/workbench/contrib/chat/common/chatAgents.ts | 7 +++++-- .../workbench/contrib/chat/test/common/chatAgents.test.ts | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 9336fbe6ce5..dc74c196260 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -16,6 +16,7 @@ import { equalsIgnoreCase } from '../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; import { Command } from '../../../../editor/common/languages.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; @@ -27,7 +28,7 @@ import { ChatContextKeys } from './chatContextKeys.js'; import { IChatAgentEditedFileEvent, IChatProgressHistoryResponseContent, IChatRequestVariableData, ISerializableChatAgentData } from './chatModel.js'; import { IRawChatCommandContribution } from './chatParticipantContribTypes.js'; import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from './chatService.js'; -import { ChatAgentLocation, ChatModeKind } from './constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from './constants.js'; //#region agent service, commands etc @@ -238,6 +239,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService { constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(); this._hasDefaultAgent = ChatContextKeys.enabled.bindTo(this.contextKeyService); @@ -392,7 +394,8 @@ export class ChatAgentService extends Disposable implements IChatAgentService { } public get hasToolsAgent(): boolean { - return !!this._hasToolsAgent; + // The chat participant enablement is just based on this setting. Don't wait for the extension to be loaded. + return !!this.configurationService.getValue(ChatConfiguration.AgentEnabled); } getContributedDefaultAgent(location: ChatAgentLocation): IChatAgentData | undefined { diff --git a/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts b/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts index 41c00776359..5a2fa987cd7 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts @@ -9,6 +9,7 @@ import { ContextKeyExpression } from '../../../../../platform/contextkey/common/ import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { ChatAgentService, IChatAgentData, IChatAgentImplementation } from '../../common/chatAgents.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; const testAgentId = 'testAgent'; const testAgentData: IChatAgentData = { @@ -42,7 +43,7 @@ suite('ChatAgents', function () { let contextKeyService: TestingContextKeyService; setup(() => { contextKeyService = new TestingContextKeyService(); - chatAgentService = store.add(new ChatAgentService(contextKeyService)); + chatAgentService = store.add(new ChatAgentService(contextKeyService, new TestConfigurationService())); }); test('registerAgent', async () => { From 03cbd01f6117e389805f879f32bdfe34afb8892e Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 8 Jul 2025 15:08:09 -0400 Subject: [PATCH 227/306] fix terminal suggest bug related to aliases (#254482) fix #254457 --- extensions/terminal-suggest/src/fig/figInterface.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/terminal-suggest/src/fig/figInterface.ts b/extensions/terminal-suggest/src/fig/figInterface.ts index 46f634c7304..8fa8d2285bf 100644 --- a/extensions/terminal-suggest/src/fig/figInterface.ts +++ b/extensions/terminal-suggest/src/fig/figInterface.ts @@ -83,8 +83,8 @@ export async function getFigSuggestions( : availableCommands.filter(command => specLabel === (command.definitionCommand ?? (typeof command.label === 'string' ? command.label : command.label.label)))); if ( !(osIsWindows() - ? commandAndAliases.some(e => currentCommand.startsWith(removeAnyFileExtension((typeof e.label === 'string' ? e.label : e.label.label)))) - : commandAndAliases.some(e => currentCommand.startsWith(typeof e.label === 'string' ? e.label : e.label.label))) + ? commandAndAliases.some(e => currentCommand === (removeAnyFileExtension((typeof e.label === 'string' ? e.label : e.label.label)))) + : commandAndAliases.some(e => currentCommand === (typeof e.label === 'string' ? e.label : e.label.label))) ) { continue; } From 770c3c8e95a567a66aff68786a2e84d1772eb35b Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 8 Jul 2025 15:22:44 -0400 Subject: [PATCH 228/306] fix terminal suggest bug (#254717) fix #254713 --- .../suggest/browser/terminal.suggest.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index 9623d11ea6e..0f693c1f73b 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -432,7 +432,7 @@ registerActiveInstanceAction({ }, { primary: KeyCode.Enter, - when: ContextKeyExpr.or(ContextKeyExpr.notEquals(`config.${TerminalSuggestSettingId.SelectionMode}`, 'partial'), ContextKeyExpr.or(SimpleSuggestContext.FirstSuggestionFocused.toNegated(), SimpleSuggestContext.HasNavigated)), + when: ContextKeyExpr.and(SimpleSuggestContext.HasFocusedSuggestion, ContextKeyExpr.or(ContextKeyExpr.notEquals(`config.${TerminalSuggestSettingId.SelectionMode}`, 'partial'), ContextKeyExpr.or(SimpleSuggestContext.FirstSuggestionFocused.toNegated(), SimpleSuggestContext.HasNavigated))), weight: KeybindingWeight.WorkbenchContrib + 1 }], menu: { From a281c05b7e17bd6d7341dc4b68a100499117b54d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:27:43 -0700 Subject: [PATCH 229/306] Make before each step run with retries Fixes #253590 --- .../areas/terminal/terminal-shellIntegration.test.ts | 10 ++++++++-- .../src/areas/terminal/terminal-stickyScroll.test.ts | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts b/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts index b0d0db48bf3..7443c02b30a 100644 --- a/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts +++ b/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts @@ -94,14 +94,19 @@ export function setup(options?: { skipSuite: boolean }) { after(async function () { await settingsEditor.clearUserSettings(); }); - beforeEach(async function () { + + // Don't use beforeEach as that ignores the retry count, createEmptyTerminal has been + // flaky in the past + async function beforeEachSetup() { // Use the simplest profile to get as little process interaction as possible await terminal.createEmptyTerminal(); // Erase all content and reset cursor to top await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `${csi('2J')}${csi('H')}`); - }); + } + describe('VS Code sequences', () => { it('should handle the simple case', async () => { + await beforeEachSetup(); await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `${vsc('A')}Prompt> ${vsc('B')}exitcode 0`); await terminal.assertCommandDecorations({ placeholder: 1, success: 0, error: 0 }); await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `\\r\\n${vsc('C')}Success\\r\\n${vsc('D;0')}`); @@ -116,6 +121,7 @@ export function setup(options?: { skipSuite: boolean }) { }); describe('Final Term sequences', () => { it('should handle the simple case', async () => { + await beforeEachSetup(); await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `${ft('A')}Prompt> ${ft('B')}exitcode 0`); await terminal.assertCommandDecorations({ placeholder: 1, success: 0, error: 0 }); await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `\\r\\n${ft('C')}Success\\r\\n${ft('D;0')}`); diff --git a/test/smoke/src/areas/terminal/terminal-stickyScroll.test.ts b/test/smoke/src/areas/terminal/terminal-stickyScroll.test.ts index 495a3bf033f..67871470bee 100644 --- a/test/smoke/src/areas/terminal/terminal-stickyScroll.test.ts +++ b/test/smoke/src/areas/terminal/terminal-stickyScroll.test.ts @@ -50,12 +50,16 @@ export function setup(options?: { skipSuite: boolean }) { throw new Error(`Failed for command ${command}, exitcode ${exitCode}, text content ${element?.textContent}`); } - beforeEach(async () => { + // Don't use beforeEach as that ignores the retry count, createEmptyTerminal has been + // flaky in the past + async function beforeEachSetup() { // Create the simplest system profile to get as little process interaction as possible await terminal.createEmptyTerminal(); - }); + } it('should show sticky scroll when appropriate', async () => { + await beforeEachSetup(); + // Write prompt, fill viewport, finish command, print new prompt, verify sticky scroll await checkCommandAndOutput('sticky scroll 1', 0); @@ -64,6 +68,8 @@ export function setup(options?: { skipSuite: boolean }) { }); it('should support multi-line prompt', async () => { + await beforeEachSetup(); + // Standard multi-line prompt await checkCommandAndOutput('sticky scroll 1', 0, "Multi-line\\r\\nPrompt> ", 2); From bd9cc762113f4388d001943fb930408623d58295 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 8 Jul 2025 21:31:27 +0200 Subject: [PATCH 230/306] Refactor WorkbenchAssignmentService to remove unused IEnvironmentService dependency (#254709) --- .../assignment/common/assignmentService.ts | 13 +++++------- .../assignment/common/assignmentService.ts | 21 +++++++++---------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/vs/platform/assignment/common/assignmentService.ts b/src/vs/platform/assignment/common/assignmentService.ts index d5ab03ae0b6..383777c931e 100644 --- a/src/vs/platform/assignment/common/assignmentService.ts +++ b/src/vs/platform/assignment/common/assignmentService.ts @@ -4,34 +4,31 @@ *--------------------------------------------------------------------------------------------*/ import type { IExperimentationTelemetry, ExperimentationService as TASClient, IKeyValueStorage } from 'tas-client-umd'; -import { TelemetryLevel } from '../../telemetry/common/telemetry.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { IProductService } from '../../product/common/productService.js'; -import { getTelemetryLevel } from '../../telemetry/common/telemetryUtils.js'; import { AssignmentFilterProvider, ASSIGNMENT_REFETCH_INTERVAL, ASSIGNMENT_STORAGE_KEY, IAssignmentService, TargetPopulation } from './assignment.js'; import { importAMDNodeModule } from '../../../amdX.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; export abstract class BaseAssignmentService implements IAssignmentService { + _serviceBrand: undefined; + protected tasClient: Promise | undefined; + private networkInitialized = false; private overrideInitDelay: Promise; - protected get experimentsEnabled(): boolean { - return !this.environmentService.disableExperiments; - } - constructor( private readonly machineId: string, protected readonly configurationService: IConfigurationService, protected readonly productService: IProductService, protected readonly environmentService: IEnvironmentService, protected telemetry: IExperimentationTelemetry, + protected experimentsEnabled: boolean, private keyValueStorage?: IKeyValueStorage ) { - const isTesting = environmentService.extensionTestsLocationURI !== undefined; - if (!isTesting && productService.tasConfig && this.experimentsEnabled && getTelemetryLevel(this.configurationService) === TelemetryLevel.USAGE) { + if (productService.tasConfig && experimentsEnabled) { this.tasClient = this.setupTASClient(); } diff --git a/src/vs/workbench/services/assignment/common/assignmentService.ts b/src/vs/workbench/services/assignment/common/assignmentService.ts index ebded17a67d..c93a4852fd6 100644 --- a/src/vs/workbench/services/assignment/common/assignmentService.ts +++ b/src/vs/workbench/services/assignment/common/assignmentService.ts @@ -7,7 +7,7 @@ import { localize } from '../../../../nls.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import type { IKeyValueStorage, IExperimentationTelemetry } from 'tas-client-umd'; import { MementoObject, Memento } from '../../../common/memento.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryData } from '../../../../base/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; @@ -19,7 +19,7 @@ import { BaseAssignmentService } from '../../../../platform/assignment/common/as import { workbenchConfigurationNodeBase } from '../../../common/configuration.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js'; import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; -import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; +import { getTelemetryLevel } from '../../../../platform/telemetry/common/telemetryUtils.js'; export const IWorkbenchAssignmentService = createDecorator('WorkbenchAssignmentService'); @@ -82,14 +82,19 @@ class WorkbenchAssignmentServiceTelemetry implements IExperimentationTelemetry { } export class WorkbenchAssignmentService extends BaseAssignmentService { + constructor( @ITelemetryService private telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, @IConfigurationService configurationService: IConfigurationService, @IProductService productService: IProductService, - @IEnvironmentService environmentService: IEnvironmentService, - @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { + const experimentsEnabled = getTelemetryLevel(configurationService) === TelemetryLevel.USAGE && + !environmentService.disableExperiments && + !environmentService.extensionTestsLocationURI && + !environmentService.enableSmokeTestDriver && + configurationService.getValue('workbench.enableExperiments') === true; super( telemetryService.machineId, @@ -97,17 +102,11 @@ export class WorkbenchAssignmentService extends BaseAssignmentService { productService, environmentService, new WorkbenchAssignmentServiceTelemetry(telemetryService, productService), + experimentsEnabled, new MementoKeyValueStorage(new Memento('experiment.service.memento', storageService)) ); } - protected override get experimentsEnabled(): boolean { - return !this.environmentService.disableExperiments && - !this.environmentService.extensionTestsLocationURI && - !this.workbenchEnvironmentService.enableSmokeTestDriver && - this.configurationService.getValue('workbench.enableExperiments') === true; - } - override async getTreatment(name: string): Promise { const result = await super.getTreatment(name); type TASClientReadTreatmentData = { From d8367188a0da1bb74a82a97dc0c278f81a9f0946 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 8 Jul 2025 19:43:08 +0000 Subject: [PATCH 231/306] Engineering - use different token for topic branches (#254714) Engineering - use different token from topic branches --- .github/workflows/pr-darwin-test.yml | 6 +++--- .github/workflows/pr-linux-test.yml | 8 ++++---- .github/workflows/pr-node-modules.yml | 12 ++++++------ .github/workflows/pr-win32-test.yml | 6 +++--- .github/workflows/pr.yml | 4 ++-- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/pr-darwin-test.yml b/.github/workflows/pr-darwin-test.yml index 36b355665e8..e97f3bbb03a 100644 --- a/.github/workflows/pr-darwin-test.yml +++ b/.github/workflows/pr-darwin-test.yml @@ -68,7 +68,7 @@ jobs: VSCODE_ARCH: ${{ env.VSCODE_ARCH }} ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} # Avoid using dlopen to load Kerberos on macOS which can cause missing libraries # https://github.com/mongodb-js/kerberos/commit/04044d2814ad1d01e77f1ce87f26b03d86692cf2 # flipped the default to support legacy linux distros which shouldn't happen @@ -100,7 +100,7 @@ jobs: if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' run: node build/lib/builtInExtensions.js env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} - name: Transpile client and extensions run: npm run gulp transpile-client-esbuild transpile-extensions @@ -124,7 +124,7 @@ jobs: sleep 5 # optional: add a small delay between retries done env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} - name: 🧪 Run unit tests (Electron) if: ${{ inputs.electron_tests }} diff --git a/.github/workflows/pr-linux-test.yml b/.github/workflows/pr-linux-test.yml index 5bfe74cf7d1..b9c24a317d4 100644 --- a/.github/workflows/pr-linux-test.yml +++ b/.github/workflows/pr-linux-test.yml @@ -79,7 +79,7 @@ jobs: echo "Npm install failed $i, trying again..." done env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -101,7 +101,7 @@ jobs: VSCODE_ARCH: ${{ env.VSCODE_ARCH }} ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} - name: Create node_modules archive if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -128,7 +128,7 @@ jobs: if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' run: node build/lib/builtInExtensions.js env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} - name: Transpile client and extensions run: npm run gulp transpile-client-esbuild transpile-extensions @@ -152,7 +152,7 @@ jobs: sleep 5 # optional: add a small delay between retries done env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} - name: 🧪 Run unit tests (Electron) if: ${{ inputs.electron_tests }} diff --git a/.github/workflows/pr-node-modules.yml b/.github/workflows/pr-node-modules.yml index eed798ec5f0..fdc8a0901ec 100644 --- a/.github/workflows/pr-node-modules.yml +++ b/.github/workflows/pr-node-modules.yml @@ -52,7 +52,7 @@ jobs: env: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.VSCODE_OSS }} - name: Create node_modules archive if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -79,7 +79,7 @@ jobs: if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' run: node build/lib/builtInExtensions.js env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.VSCODE_OSS }} linux: name: Linux @@ -123,7 +123,7 @@ jobs: echo "Npm install failed $i, trying again..." done env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.VSCODE_OSS }} - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -145,7 +145,7 @@ jobs: VSCODE_ARCH: ${{ env.VSCODE_ARCH }} ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.VSCODE_OSS }} - name: Create node_modules archive if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -203,7 +203,7 @@ jobs: VSCODE_ARCH: ${{ env.VSCODE_ARCH }} ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.VSCODE_OSS }} # Avoid using dlopen to load Kerberos on macOS which can cause missing libraries # https://github.com/mongodb-js/kerberos/commit/04044d2814ad1d01e77f1ce87f26b03d86692cf2 # flipped the default to support legacy linux distros which shouldn't happen @@ -275,7 +275,7 @@ jobs: VSCODE_ARCH: ${{ env.VSCODE_ARCH }} ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.VSCODE_OSS }} - name: Create node_modules archive if: steps.node-modules-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/pr-win32-test.yml b/.github/workflows/pr-win32-test.yml index 3781e01fbaf..b0fed3bd32c 100644 --- a/.github/workflows/pr-win32-test.yml +++ b/.github/workflows/pr-win32-test.yml @@ -78,7 +78,7 @@ jobs: VSCODE_ARCH: ${{ env.VSCODE_ARCH }} ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} - name: Create node_modules archive if: steps.node-modules-cache.outputs.cache-hit != 'true' @@ -109,7 +109,7 @@ jobs: if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' run: node build/lib/builtInExtensions.js env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} - name: Transpile client and extensions shell: pwsh @@ -133,7 +133,7 @@ jobs: } } env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} - name: 🧪 Run unit tests (Electron) if: ${{ inputs.electron_tests }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b43eff9f41d..5e9449c5a5f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -64,7 +64,7 @@ jobs: env: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} - name: Create node_modules archive if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -84,7 +84,7 @@ jobs: - name: Compile & Hygiene run: npm exec -- npm-run-all -lp core-ci-pr extensions-ci-pr hygiene eslint valid-layers-check define-class-fields-check vscode-dts-compile-check tsec-compile-check env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} linux-cli-tests: name: Linux From d2a794a71916e6aedb93619948001cc887274a75 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 8 Jul 2025 13:09:17 -0700 Subject: [PATCH 232/306] Migrate off of `eslint-plugin-local` We can now do this natively --- .eslint-plugin-local/index.js | 2 + eslint.config.js | 2 +- package-lock.json | 269 ++++++++++++++++++++++++++++------ package.json | 5 +- 4 files changed, 231 insertions(+), 47 deletions(-) diff --git a/.eslint-plugin-local/index.js b/.eslint-plugin-local/index.js index 198cb8362dc..3646c8c4157 100644 --- a/.eslint-plugin-local/index.js +++ b/.eslint-plugin-local/index.js @@ -2,12 +2,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// @ts-check const glob = require('glob'); const path = require('path'); require('ts-node').register({ experimentalResolver: true, transpileOnly: true }); // Re-export all .ts files as rules +/** @type {Record} */ const rules = {}; glob.sync(`${__dirname}/*.ts`).forEach((file) => { rules[path.basename(file, '.ts')] = require(file); diff --git a/eslint.config.js b/eslint.config.js index dde24771a66..b1e83f734e8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -9,7 +9,7 @@ import tseslint from 'typescript-eslint'; import { fileURLToPath } from 'url'; import stylisticTs from '@stylistic/eslint-plugin-ts'; -import pluginLocal from 'eslint-plugin-local'; +import * as pluginLocal from './.eslint-plugin-local/index.js'; import pluginJsdoc from 'eslint-plugin-jsdoc'; import pluginHeader from 'eslint-plugin-header'; diff --git a/package-lock.json b/package-lock.json index 5c22dd835b9..09d8dc7afb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,7 +77,7 @@ "@types/winreg": "^1.2.30", "@types/yauzl": "^2.10.0", "@types/yazl": "^2.4.2", - "@typescript-eslint/utils": "^8.8.0", + "@typescript-eslint/utils": "^8.36.0", "@vscode/gulp-electron": "^1.37.1", "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.10.2", @@ -100,7 +100,6 @@ "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^50.3.1", - "eslint-plugin-local": "^6.0.0", "event-stream": "3.3.4", "fancy-log": "^1.3.3", "file-loader": "^6.2.0", @@ -905,16 +904,20 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } @@ -2047,18 +2050,6 @@ "node": ">=10" } }, - "node_modules/@thisismanta/pessimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@thisismanta/pessimist/-/pessimist-1.2.0.tgz", - "integrity": "sha512-rm8/zjNMuO9hPYhEMavVIIxmvawJJB8mthvbVXd74XUW7V/SbgmtDBQjICbCWKjluvA+gh+cqi7dv85/jexknA==", - "dev": true, - "dependencies": { - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/@tootallnate/once": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-3.0.0.tgz", @@ -2447,6 +2438,42 @@ "@types/node": "*" } }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz", + "integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.36.0", + "@typescript-eslint/types": "^8.36.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service/node_modules/@typescript-eslint/types": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz", + "integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.8.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz", @@ -2464,6 +2491,23 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz", + "integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/types": { "version": "8.8.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz", @@ -2531,15 +2575,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz", - "integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz", + "integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.8.0", - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/typescript-estree": "8.8.0" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.36.0", + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/typescript-estree": "8.36.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2549,7 +2594,139 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz", + "integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/visitor-keys": "8.36.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz", + "integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz", + "integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.36.0", + "@typescript-eslint/tsconfig-utils": "8.36.0", + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/visitor-keys": "8.36.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz", + "integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.36.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, "node_modules/@typescript-eslint/visitor-keys": { @@ -6629,23 +6806,6 @@ "spdx-license-ids": "^3.0.0" } }, - "node_modules/eslint-plugin-local": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-local/-/eslint-plugin-local-6.0.0.tgz", - "integrity": "sha512-pvy/pTTyanEKAqpYqy/SLfd4TdiAQ/yFO+GRXDGvGQa2vEUGtmlEjmWQXBDGSk790j4nrAB/7ipqPQY3nLduDg==", - "deprecated": "Since the coming of ESLint flat config file, you can specify local rules without the need of this package. For running ESLint rule unit tests, use eslint-rule-tester instead", - "dev": true, - "dependencies": { - "@thisismanta/pessimist": "^1.2.0", - "chalk": "^4.0.0" - }, - "bin": { - "eslint-plugin-local": "executable.js" - }, - "peerDependencies": { - "eslint": ">=9.0.0" - } - }, "node_modules/eslint-scope": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", @@ -17254,6 +17414,29 @@ } } }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz", + "integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.8.0", + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/typescript-estree": "8.8.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, "node_modules/typical": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", diff --git a/package.json b/package.json index 766552720b6..dd51bb0b769 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "@types/winreg": "^1.2.30", "@types/yauzl": "^2.10.0", "@types/yazl": "^2.4.2", - "@typescript-eslint/utils": "^8.8.0", + "@typescript-eslint/utils": "^8.36.0", "@vscode/gulp-electron": "^1.37.1", "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.10.2", @@ -159,7 +159,6 @@ "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^50.3.1", - "eslint-plugin-local": "^6.0.0", "event-stream": "3.3.4", "fancy-log": "^1.3.3", "file-loader": "^6.2.0", @@ -234,4 +233,4 @@ "optionalDependencies": { "windows-foreground-love": "0.5.0" } -} \ No newline at end of file +} From f0fff8b44be5a8b32a63ec050f8af892f6dd8e60 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 8 Jul 2025 14:17:10 -0700 Subject: [PATCH 233/306] Update sha --- src/vs/workbench/contrib/webview/browser/pre/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/webview/browser/pre/index.html b/src/vs/workbench/contrib/webview/browser/pre/index.html index b30d1d3c694..52a71b405fa 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index.html @@ -5,7 +5,7 @@ + content="default-src 'none'; script-src 'sha256-l9e7E37hgQqoIAqcFRgQ5/0NqMMW93mQmYAsUcDUyZ4=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> Date: Tue, 8 Jul 2025 19:17:14 -0400 Subject: [PATCH 234/306] use `onTerminalShellIntegration` for terminal suggest activation (#252329) --- extensions/terminal-suggest/package.json | 2 +- .../api/browser/mainThreadTerminalShellIntegration.ts | 9 ++++++++- .../services/extensions/common/extensionsRegistry.ts | 5 +++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/extensions/terminal-suggest/package.json b/extensions/terminal-suggest/package.json index 5ce429f4184..d689d6a652d 100644 --- a/extensions/terminal-suggest/package.json +++ b/extensions/terminal-suggest/package.json @@ -25,7 +25,7 @@ }, "main": "./out/terminalSuggestMain", "activationEvents": [ - "onTerminalCompletionsRequested" + "onTerminalShellIntegration:*" ], "repository": { "type": "git", diff --git a/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts b/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts index 492e2cfb793..8e40f843454 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts @@ -11,6 +11,7 @@ import { ITerminalService, type ITerminalInstance } from '../../contrib/terminal import { IWorkbenchEnvironmentService } from '../../services/environment/common/environmentService.js'; import { extHostNamedCustomer, type IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { TerminalShellExecutionCommandLineConfidence } from '../common/extHostTypes.js'; +import { IExtensionService } from '../../services/extensions/common/extensions.js'; @extHostNamedCustomer(MainContext.MainThreadTerminalShellIntegration) export class MainThreadTerminalShellIntegration extends Disposable implements MainThreadTerminalShellIntegrationShape { @@ -19,7 +20,8 @@ export class MainThreadTerminalShellIntegration extends Disposable implements Ma constructor( extHostContext: IExtHostContext, @ITerminalService private readonly _terminalService: ITerminalService, - @IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService, + @IExtensionService private readonly _extensionService: IExtensionService ) { super(); @@ -111,6 +113,11 @@ export class MainThreadTerminalShellIntegration extends Disposable implements Ma } private _enableShellIntegration(instance: ITerminalInstance): void { + this._extensionService.activateByEvent('onTerminalShellIntegration:*'); + this._register(instance.onDidChangeShellType(() => this._extensionService.activateByEvent(`onTerminalShellIntegration:${instance.shellType}`))); + if (instance.shellType) { + this._extensionService.activateByEvent(`onTerminalShellIntegration:${instance.shellType}`); + } this._proxy.$shellIntegrationChange(instance.instanceId); const cwdDetection = instance.capabilities.get(TerminalCapability.CwdDetection); if (cwdDetection) { diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index 859b9769123..95376a5a00f 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -403,6 +403,11 @@ export const schema: IJSONSchema = { body: 'onTerminalCompletionsRequested', description: nls.localize('vscode.extension.activationEvents.onTerminalCompletionsRequested', 'An activation event emitted when terminal completions are requested.'), }, + { + label: 'onTerminalShellIntegration', + body: 'onTerminalShellIntegration:${1:shellType}', + description: nls.localize('vscode.extension.activationEvents.onTerminalShellIntegration', 'An activation event emitted when terminal shell integration is activated for the given shell type.'), + }, { label: 'onMcpCollection', description: nls.localize('vscode.extension.activationEvents.onMcpCollection', 'An activation event emitted whenver a tool from the MCP server is requested.'), From aed497c521da4b9c600db8f26544c3eb432ec5a6 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 8 Jul 2025 19:17:35 -0400 Subject: [PATCH 235/306] add `onTerminal` activation event (#254444) --- .../contrib/terminal/browser/terminalService.ts | 12 ++++++++++-- .../services/extensions/common/extensionsRegistry.ts | 5 +++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 798bae0b1b1..6d03b954046 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -1017,10 +1017,18 @@ export class TerminalService extends Disposable implements ITerminalService { const location = await this.resolveLocation(options?.location) || this.defaultLocation; const parent = await this._getSplitParent(options?.location); this._terminalHasBeenCreated.set(true); + this._extensionService.activateByEvent('onTerminal:*'); + let instance; if (parent) { - return this._splitTerminal(shellLaunchConfig, location, parent); + instance = this._splitTerminal(shellLaunchConfig, location, parent); + } else { + instance = this._createTerminal(shellLaunchConfig, location, options); } - return this._createTerminal(shellLaunchConfig, location, options); + this._register(instance.onDidChangeShellType(() => this._extensionService.activateByEvent(`onTerminal:${instance.shellType}`))); + if (instance.shellType) { + this._extensionService.activateByEvent(`onTerminal:${instance.shellType}`); + } + return instance; } private async _getContributedProfile(shellLaunchConfig: IShellLaunchConfig, options?: ICreateTerminalOptions): Promise { diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index 95376a5a00f..451dbcd28f3 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -398,6 +398,11 @@ export const schema: IJSONSchema = { body: 'onLanguageModelTool:${1:toolId}', description: nls.localize('vscode.extension.activationEvents.onLanguageModelTool', 'An activation event emitted when the specified language model tool is invoked.'), }, + { + label: 'onTerminal', + body: 'onTerminal:{1:shellType}', + description: nls.localize('vscode.extension.activationEvents.onTerminal', 'An activation event emitted when a terminal of the given shell type is opened.'), + }, { label: 'onTerminalCompletionsRequested', body: 'onTerminalCompletionsRequested', From ea66f720013eb840da808551e841f0fe9bed1458 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 9 Jul 2025 10:40:19 +1000 Subject: [PATCH 236/306] Do not change EOL in Noetbook Cell documents (#254759) * Do not change EOL in Noetbook Cell documents * Remove unused ref --- .../contrib/notebook/browser/notebook.contribution.ts | 4 +--- .../contrib/notebook/common/model/notebookCellTextModel.ts | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index d1f2af83275..fbe016aa70d 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -10,7 +10,7 @@ import { extname, isEqual } from '../../../../base/common/resources.js'; import { assertType } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { toFormattedString } from '../../../../base/common/jsonFormatter.js'; -import { ITextModel, ITextBufferFactory, DefaultEndOfLine, ITextBuffer } from '../../../../editor/common/model.js'; +import { ITextModel, ITextBufferFactory, ITextBuffer } from '../../../../editor/common/model.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { ILanguageSelection, ILanguageService } from '../../../../editor/common/languages/language.js'; import { ITextModelContentProvider, ITextModelService } from '../../../../editor/common/services/resolverService.js'; @@ -398,8 +398,6 @@ class CellContentProvider implements ITextModelContentProvider { if (cell.uri.toString() === resource.toString()) { const bufferFactory: ITextBufferFactory = { create: (defaultEOL) => { - const newEOL = (defaultEOL === DefaultEndOfLine.CRLF ? '\r\n' : '\n'); - (cell.textBuffer as ITextBuffer).setEOL(newEOL); return { textBuffer: cell.textBuffer as ITextBuffer, disposable: Disposable.None }; }, getFirstLineText: (limit: number) => { diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index d38ebca6d63..2b0d842ec93 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -117,7 +117,6 @@ export class NotebookCellTextModel extends Disposable implements ICell { } this._textBuffer = this._register(createTextBuffer(this._source, this._defaultEOL).textBuffer); - this._textBuffer.setEOL(this._defaultEOL === model.DefaultEndOfLine.LF ? '\n' : '\r\n'); this._register(this._textBuffer.onDidChangeContent(() => { this._hash = null; From 105b3664c08a085b19797aa5ad919f6e9bdb58e6 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 8 Jul 2025 21:51:02 -0700 Subject: [PATCH 237/306] chat: refactor out chatEditingTimeline, tests, bugs (#254788) - Refactor out the ChatEditingTimeline into its own class. - Add some base unit tests for it. - Fix some off-by-1 errors and a logic error that caused diffs to not show on some edit pills. --- .../browser/chatEditing/chatEditingSession.ts | 465 +++-------------- .../chatEditing/chatEditingTimeline.ts | 461 +++++++++++++++++ .../test/browser/chatEditingTimeline.test.ts | 466 ++++++++++++++++++ 3 files changed, 994 insertions(+), 398 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTimeline.ts create mode 100644 src/vs/workbench/contrib/chat/test/browser/chatEditingTimeline.test.ts diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 2c68f6c901b..50c1b873f35 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -3,31 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equals as arraysEqual, binarySearch2 } from '../../../../../base/common/arrays.js'; -import { findLast } from '../../../../../base/common/arraysFind.js'; import { DeferredPromise, ITask, Sequencer, SequencerByKey, timeout } from '../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { Emitter } from '../../../../../base/common/event.js'; import { Iterable } from '../../../../../base/common/iterator.js'; -import { Disposable, DisposableStore, dispose } from '../../../../../base/common/lifecycle.js'; +import { Disposable, dispose } from '../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../base/common/map.js'; -import { autorun, derived, derivedOpts, IObservable, IReader, ITransaction, ObservablePromise, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { autorun, IObservable, IReader, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; import { isEqual } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { IBulkEditService } from '../../../../../editor/browser/services/bulkEditService.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; import { ITextModel } from '../../../../../editor/common/model.js'; -import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { localize } from '../../../../../nls.js'; import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { EditorActivation } from '../../../../../platform/editor/common/editor.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js'; import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; @@ -35,17 +30,15 @@ import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEdito import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js'; import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; import { INotebookService } from '../../../notebook/common/notebookService.js'; -import { ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IEditSessionEntryDiff, IModifiedEntryTelemetryInfo, IModifiedFileEntry, ISnapshotEntry, IStreamingEdits, ModifiedFileEntryState } from '../../common/chatEditingService.js'; -import { IChatRequestDisablement, IChatResponseModel } from '../../common/chatModel.js'; +import { ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IModifiedEntryTelemetryInfo, IModifiedFileEntry, ISnapshotEntry, IStreamingEdits, ModifiedFileEntryState } from '../../common/chatEditingService.js'; +import { IChatResponseModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; import { ChatEditingModifiedDocumentEntry } from './chatEditingModifiedDocumentEntry.js'; import { AbstractChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js'; import { ChatEditingModifiedNotebookEntry } from './chatEditingModifiedNotebookEntry.js'; import { ChatEditingSessionStorage, IChatEditingSessionSnapshot, IChatEditingSessionStop, StoredSessionState } from './chatEditingSessionStorage.js'; import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; -import { ChatEditingModifiedNotebookDiff } from './notebook/chatEditingModifiedNotebookDiff.js'; - -const POST_EDIT_STOP_ID = 'd19944f6-f46c-4e17-911b-79a8e843c7c0'; // randomly generated +import { ChatEditingTimeline } from './chatEditingTimeline.js'; class ThrottledSequencer extends Sequencer { @@ -81,19 +74,6 @@ class ThrottledSequencer extends Sequencer { } } -function getMaxHistoryIndex(history: readonly IChatEditingSessionSnapshot[]) { - const lastHistory = history.at(-1); - return lastHistory ? lastHistory.startIndex + lastHistory.stops.length : 0; -} - -function snapshotsEqualForDiff(a: ISnapshotEntry | undefined, b: ISnapshotEntry | undefined) { - if (!a || !b) { - return a === b; - } - - return isEqual(a.snapshotUri, b.snapshotUri) && a.current === b.current; -} - function getCurrentAndNextStop(requestId: string, stopId: string | undefined, history: readonly IChatEditingSessionSnapshot[]) { const snapshotIndex = history.findIndex(s => s.requestId === requestId); if (snapshotIndex === -1) { return undefined; } @@ -114,43 +94,9 @@ function getCurrentAndNextStop(requestId: string, stopId: string | undefined, hi return { current, next }; } -function getFirstAndLastStop(uri: URI, history: readonly IChatEditingSessionSnapshot[]): { current: ResourceMap; next: ResourceMap } | undefined { - let firstStopWithUri: IChatEditingSessionStop | undefined; - for (const snapshot of history) { - const stop = snapshot.stops.find(s => s.entries.has(uri)); - if (stop) { - firstStopWithUri = stop; - break; - } - } - - let lastStopWithUri: ResourceMap | undefined; - for (let i = history.length - 1; i >= 0; i--) { - const snapshot = history[i]; - if (snapshot.postEdit?.has(uri)) { - lastStopWithUri = snapshot.postEdit; - break; - } - - const stop = findLast(snapshot.stops, s => s.entries.has(uri)); - if (stop) { - lastStopWithUri = stop.entries; - break; - } - } - - if (!firstStopWithUri || !lastStopWithUri) { - return undefined; - } - - return { current: firstStopWithUri.entries, next: lastStopWithUri }; -} - export class ChatEditingSession extends Disposable implements IChatEditingSession { - private readonly _state = observableValue(this, ChatEditingSessionState.Initial); - private readonly _linearHistory = observableValue(this, []); - private readonly _linearHistoryIndex = observableValue(this, 0); + private readonly _timeline: ChatEditingTimeline; /** * Contains the contents of a file when the AI first began doing edits to it. @@ -169,27 +115,8 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return this._state; } - public readonly canUndo = derived((r) => { - if (this.state.read(r) !== ChatEditingSessionState.Idle) { - return false; - } - const linearHistoryIndex = this._linearHistoryIndex.read(r); - return linearHistoryIndex > 0; - }); - - public readonly canRedo = derived((r) => { - if (this.state.read(r) !== ChatEditingSessionState.Idle) { - return false; - } - const linearHistoryIndex = this._linearHistoryIndex.read(r); - return linearHistoryIndex < getMaxHistoryIndex(this._linearHistory.read(r)); - }); - - // public hiddenRequestIds = derived((r) => { - // const linearHistory = this._linearHistory.read(r); - // const linearHistoryIndex = this._linearHistoryIndex.read(r); - // return linearHistory.slice(linearHistoryIndex).map(s => s.requestId).filter((r): r is string => !!r); - // }); + public readonly canUndo: IObservable; + public readonly canRedo: IObservable; private readonly _onDidDispose = new Emitter(); get onDidDispose() { @@ -210,12 +137,14 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio @IEditorService private readonly _editorService: IEditorService, @IChatService private readonly _chatService: IChatService, @INotebookService private readonly _notebookService: INotebookService, - @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, - @IConfigurationService private readonly _configurationService: IConfigurationService, @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, ) { super(); - this._ignoreTrimWhitespaceObservable = observableConfigValue('diffEditor.ignoreTrimWhitespace', true, this._configurationService); + this._timeline = _instantiationService.createInstance(ChatEditingTimeline); + this.canRedo = this._timeline.canRedo.map((hasHistory, reader) => + hasHistory && this._state.read(reader) === ChatEditingSessionState.Idle); + this.canUndo = this._timeline.canUndo.map((hasHistory, reader) => + hasHistory && this._state.read(reader) === ChatEditingSessionState.Idle); } public async init(): Promise { @@ -224,11 +153,10 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio for (const [uri, content] of restoredSessionState.initialFileContents) { this._initialFileContents.set(uri, content); } - this._pendingSnapshot = restoredSessionState.pendingSnapshot; await this._restoreSnapshot(restoredSessionState.recentSnapshot, false); - transaction(async tx => { - this._linearHistory.set(restoredSessionState.linearHistory, tx); - this._linearHistoryIndex.set(restoredSessionState.linearHistoryIndex, tx); + transaction(tx => { + this._pendingSnapshot.set(restoredSessionState.pendingSnapshot, tx); + this._timeline.restoreFromState({ history: restoredSessionState.linearHistory, index: restoredSessionState.linearHistoryIndex }, tx); this._state.set(ChatEditingSessionState.Idle, tx); }); } else { @@ -259,222 +187,55 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio public storeState(): Promise { const storage = this._instantiationService.createInstance(ChatEditingSessionStorage, this.chatSessionId); + const timelineState = this._timeline.getStateForPersistence(); const state: StoredSessionState = { initialFileContents: this._initialFileContents, - pendingSnapshot: this._pendingSnapshot, + pendingSnapshot: this._pendingSnapshot.get(), recentSnapshot: this._createSnapshot(undefined, undefined), - linearHistoryIndex: this._linearHistoryIndex.get(), - linearHistory: this._linearHistory.get(), + linearHistoryIndex: timelineState.index, + linearHistory: timelineState.history, }; return storage.storeState(state); } - private _findSnapshot(requestId: string): IChatEditingSessionSnapshot | undefined { - return this._linearHistory.get().find(s => s.requestId === requestId); - } - - private _findEditStop(requestId: string, undoStop: string | undefined) { - const snapshot = this._findSnapshot(requestId); - if (!snapshot) { - return undefined; - } - const idx = snapshot.stops.findIndex(s => s.stopId === undoStop); - return idx === -1 ? undefined : { stop: snapshot.stops[idx], snapshot, historyIndex: snapshot.startIndex + idx }; - } - private _ensurePendingSnapshot() { - this._pendingSnapshot ??= this._createSnapshot(undefined, undefined); - } - - private _diffsBetweenStops = new Map>(); - private _fullDiffs = new Map>(); - - private readonly _ignoreTrimWhitespaceObservable: IObservable; - - /** - * Gets diff for text entries between stops. - * @param entriesContent Observable that observes either snapshot entry - * @param modelUrisObservable Observable that observes only the snapshot URIs. - */ - private _entryDiffBetweenTextStops( - entriesContent: IObservable<{ before: ISnapshotEntry; after: ISnapshotEntry } | undefined>, - modelUrisObservable: IObservable<[URI, URI] | undefined>, - ): IObservable | undefined> { - const modelRefsPromise = derived(this, (reader) => { - const modelUris = modelUrisObservable.read(reader); - if (!modelUris) { return undefined; } - - const store = reader.store.add(new DisposableStore()); - const promise = Promise.all(modelUris.map(u => this._textModelService.createModelReference(u))).then(refs => { - if (store.isDisposed) { - refs.forEach(r => r.dispose()); - } else { - refs.forEach(r => store.add(r)); - } - - return refs; - }); - - return new ObservablePromise(promise); - }); - - return derived((reader): ObservablePromise | undefined => { - const refs2 = modelRefsPromise.read(reader)?.promiseResult.read(reader); - const refs = refs2?.data; - if (!refs) { - return; - } - - const entries = entriesContent.read(reader); // trigger re-diffing when contents change - - if (entries?.before && ChatEditingModifiedNotebookEntry.canHandleSnapshot(entries.before)) { - const diffService = this._instantiationService.createInstance(ChatEditingModifiedNotebookDiff, entries.before, entries.after); - return new ObservablePromise(diffService.computeDiff()); - - } - const ignoreTrimWhitespace = this._ignoreTrimWhitespaceObservable.read(reader); - const promise = this._editorWorkerService.computeDiff( - refs[0].object.textEditorModel.uri, - refs[1].object.textEditorModel.uri, - { ignoreTrimWhitespace, computeMoves: false, maxComputationTimeMs: 3000 }, - 'advanced' - ).then((diff): IEditSessionEntryDiff => { - const entryDiff: IEditSessionEntryDiff = { - originalURI: refs[0].object.textEditorModel.uri, - modifiedURI: refs[1].object.textEditorModel.uri, - identical: !!diff?.identical, - quitEarly: !diff || diff.quitEarly, - added: 0, - removed: 0, - }; - if (diff) { - for (const change of diff.changes) { - entryDiff.removed += change.original.endLineNumberExclusive - change.original.startLineNumber; - entryDiff.added += change.modified.endLineNumberExclusive - change.modified.startLineNumber; - } - } - - return entryDiff; - }); - - return new ObservablePromise(promise); - }); - } - - private _createDiffBetweenStopsObservable(uri: URI, requestId: string | undefined, stopId: string | undefined): IObservable { - const entries = derivedOpts( - { - equalsFn: (a, b) => snapshotsEqualForDiff(a?.before, b?.before) && snapshotsEqualForDiff(a?.after, b?.after), - }, - reader => { - const stops = requestId ? - getCurrentAndNextStop(requestId, stopId, this._linearHistory.read(reader)) : - getFirstAndLastStop(uri, this._linearHistory.read(reader)); - if (!stops) { return undefined; } - const before = stops.current.get(uri); - const after = stops.next.get(uri); - if (!before || !after) { return undefined; } - return { before, after }; - }, - ); - - // Separate observable for model refs to avoid unnecessary disposal - const modelUrisObservable = derivedOpts<[URI, URI] | undefined>({ equalsFn: (a, b) => arraysEqual(a, b, isEqual) }, reader => { - const entriesValue = entries.read(reader); - if (!entriesValue) { return undefined; } - return [entriesValue.before.snapshotUri, entriesValue.after.snapshotUri]; - }); - - const diff = this._entryDiffBetweenTextStops(entries, modelUrisObservable); - - return derived(reader => { - return diff.read(reader)?.promiseResult.read(reader)?.data || undefined; - }); + const prev = this._pendingSnapshot.get(); + if (!prev) { + this._pendingSnapshot.set(this._createSnapshot(undefined, undefined), undefined); + } } public getEntryDiffBetweenStops(uri: URI, requestId: string | undefined, stopId: string | undefined) { - if (requestId) { - const key = `${uri}\0${requestId}\0${stopId}`; - let observable = this._diffsBetweenStops.get(key); - if (!observable) { - observable = this._createDiffBetweenStopsObservable(uri, requestId, stopId); - this._diffsBetweenStops.set(key, observable); - } - - return observable; - } else { - const key = uri.toString(); - let observable = this._fullDiffs.get(key); - if (!observable) { - observable = this._createDiffBetweenStopsObservable(uri, requestId, stopId); - this._fullDiffs.set(key, observable); - } - - return observable; - } + return this._timeline.getEntryDiffBetweenStops(uri, requestId, stopId); } public createSnapshot(requestId: string, undoStop: string | undefined, makeEmpty = undoStop !== undefined): void { - const snapshot = makeEmpty ? this._createEmptySnapshot(undoStop) : this._createSnapshot(requestId, undoStop); - - const linearHistoryPtr = this._linearHistoryIndex.get(); - const newLinearHistory: IChatEditingSessionSnapshot[] = []; - for (const entry of this._linearHistory.get()) { - if (entry.startIndex >= linearHistoryPtr) { - // all further entries are being dropped - break; - } else if (linearHistoryPtr - entry.startIndex < entry.stops.length) { - newLinearHistory.push({ requestId: entry.requestId, stops: entry.stops.slice(0, linearHistoryPtr - entry.startIndex), startIndex: entry.startIndex, postEdit: undefined }); - } else { - newLinearHistory.push(entry); - } - } - - const lastEntry = newLinearHistory.at(-1); - if (requestId && lastEntry?.requestId === requestId) { - // mirror over the saved postEdit modifications - if (lastEntry.postEdit && undoStop) { - const rebaseUri = (uri: URI) => URI.parse(uri.toString().replaceAll(POST_EDIT_STOP_ID, undoStop)); - for (const [uri, prev] of lastEntry.postEdit.entries()) { - snapshot.entries.set(uri, { ...prev, snapshotUri: rebaseUri(prev.snapshotUri), resource: rebaseUri(prev.resource) }); - } - } - - newLinearHistory[newLinearHistory.length - 1] = { ...lastEntry, stops: [...lastEntry.stops, snapshot], postEdit: undefined }; - } else { - newLinearHistory.push({ requestId, startIndex: lastEntry ? lastEntry.startIndex + lastEntry.stops.length : 0, stops: [snapshot], postEdit: undefined }); - } - - transaction((tx) => { - const last = newLinearHistory[newLinearHistory.length - 1]; - this._linearHistory.set(newLinearHistory, tx); - this._linearHistoryIndex.set(last.startIndex + last.stops.length, tx); - }); + this._timeline.pushSnapshot( + requestId, + undoStop, + makeEmpty ? ChatEditingTimeline.createEmptySnapshot(undoStop) : this._createSnapshot(requestId, undoStop), + ); } - private _createEmptySnapshot(undoStop: string | undefined): IChatEditingSessionStop { - return { - stopId: undoStop, - entries: new ResourceMap(), - }; - } - - private _createSnapshot(requestId: string | undefined, undoStop: string | undefined): IChatEditingSessionStop { + private _createSnapshot(requestId: string | undefined, stopId: string | undefined): IChatEditingSessionStop { const entries = new ResourceMap(); for (const entry of this._entriesObs.get()) { - entries.set(entry.modifiedURI, entry.createSnapshot(requestId, undoStop)); + entries.set(entry.modifiedURI, entry.createSnapshot(requestId, stopId)); } - - return { - stopId: undoStop, - entries, - }; + return { stopId, entries }; } public getSnapshot(requestId: string, undoStop: string | undefined, snapshotUri: URI): ISnapshotEntry | undefined { - const entries = undoStop === POST_EDIT_STOP_ID - ? this._findSnapshot(requestId)?.postEdit - : this._findEditStop(requestId, undoStop)?.stop.entries; + let entries: ResourceMap | undefined; + if (undoStop === ChatEditingTimeline.POST_EDIT_STOP_ID) { + // If postEdit, get from timeline state + const timelineState = this._timeline.getStateForPersistence(); + const snap = timelineState.history.find(s => s.requestId === requestId); + entries = snap?.postEdit; + } else { + const stopRef = this._timeline.getSnapshotForRestore(requestId, undoStop); + entries = stopRef?.stop.entries; + } return entries && [...entries.values()].find((e) => isEqual(e.snapshotUri, snapshotUri)); } @@ -488,29 +249,33 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } public getSnapshotUri(requestId: string, uri: URI, stopId: string | undefined): URI | undefined { - const stops = getCurrentAndNextStop(requestId, stopId, this._linearHistory.get()); + // This should be encapsulated in the timeline, but for now, fallback to legacy logic if needed. + // TODO: Move this logic into a timeline method if required by the design. + const timelineState = this._timeline.getStateForPersistence(); + const stops = getCurrentAndNextStop(requestId, stopId, timelineState.history); return stops?.next.get(uri)?.snapshotUri; } /** * A snapshot representing the state of the working set before a new request has been sent */ - private _pendingSnapshot: IChatEditingSessionStop | undefined; + private _pendingSnapshot = observableValue(this, undefined); + public async restoreSnapshot(requestId: string | undefined, stopId: string | undefined): Promise { if (requestId !== undefined) { - const stopRef = this._findEditStop(requestId, stopId); + const stopRef = this._timeline.getSnapshotForRestore(requestId, stopId); if (stopRef) { this._ensurePendingSnapshot(); - this._linearHistoryIndex.set(stopRef.historyIndex, undefined); await this._restoreSnapshot(stopRef.stop); + stopRef.apply(); this._updateRequestHiddenState(); } } else { - const pendingSnapshot = this._pendingSnapshot; + const pendingSnapshot = this._pendingSnapshot.get(); if (!pendingSnapshot) { return; // We don't have a pending snapshot that we can restore } - this._pendingSnapshot = undefined; + this._pendingSnapshot.set(undefined, undefined); await this._restoreSnapshot(pendingSnapshot, undefined); } } @@ -697,66 +462,34 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio }; } - private _getHistoryEntryByLinearIndex(index: number) { - const history = this._linearHistory.get(); - const searchedIndex = binarySearch2(history.length, (e) => history[e].startIndex - index); - const entry = history[searchedIndex < 0 ? (~searchedIndex) - 1 : searchedIndex]; - if (!entry || index - entry.startIndex >= entry.stops.length) { - return undefined; - } - - return { - entry, - stop: entry.stops[index - entry.startIndex] - }; - } - async undoInteraction(): Promise { - const newIndex = this._linearHistoryIndex.get() - 1; - const previousSnapshot = this._getHistoryEntryByLinearIndex(newIndex); - if (!previousSnapshot) { + const undo = this._timeline.getUndoSnapshot(); + if (!undo) { return; } - this._ensurePendingSnapshot(); - await this._restoreSnapshot(previousSnapshot.stop); - this._linearHistoryIndex.set(newIndex, undefined); + await this._restoreSnapshot(undo.stop); + undo.apply(); this._updateRequestHiddenState(); } async redoInteraction(): Promise { - const maxIndex = getMaxHistoryIndex(this._linearHistory.get()); - const newIndex = this._linearHistoryIndex.get() + 1; - if (newIndex > maxIndex) { - return; - } - - const nextSnapshot = newIndex === maxIndex ? this._pendingSnapshot : this._getHistoryEntryByLinearIndex(newIndex)?.stop; + const redo = this._timeline.getRedoSnapshot(); + const nextSnapshot = redo?.stop || this._pendingSnapshot.get(); if (!nextSnapshot) { return; } await this._restoreSnapshot(nextSnapshot); - this._linearHistoryIndex.set(newIndex, undefined); + if (redo) { + redo.apply(); + } else { + this._pendingSnapshot.set(undefined, undefined); + } this._updateRequestHiddenState(); } - private _updateRequestHiddenState() { - const history = this._linearHistory.get(); - const index = this._linearHistoryIndex.get(); - - const undoRequests: IChatRequestDisablement[] = []; - for (const entry of history) { - if (!entry.requestId) { - // ignored - } else if (entry.startIndex >= index) { - undoRequests.push({ requestId: entry.requestId }); - } else if (entry.startIndex + entry.stops.length > index) { - undoRequests.push({ requestId: entry.requestId, afterUndoStop: entry.stops[index - entry.startIndex].stopId }); - } - } - - this._chatService.getSession(this.chatSessionId)?.setDisabledRequests(undoRequests); + this._chatService.getSession(this.chatSessionId)?.setDisabledRequests(this._timeline.getRequestDisablement()); } private async _acceptStreamingEditsStart(responseModel: IChatResponseModel, undoStop: string | undefined, resource: URI) { @@ -764,74 +497,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio transaction((tx) => { this._state.set(ChatEditingSessionState.StreamingEdits, tx); entry.acceptStreamingEditsStart(responseModel, tx); - this.ensureEditInUndoStopMatches(responseModel.requestId, undoStop, entry, false, tx); + this._timeline.ensureEditInUndoStopMatches(responseModel.requestId, undoStop, entry, false, tx); }); } - /** - * Ensures the state of the file in the given snapshot matches the current - * state of the {@param entry}. This is used to handle concurrent file edits. - * - * Given the case of two different edits, we will place and undo stop right - * before we `textEditGroup` in the underlying markdown stream, but at the - * time those are added the edits haven't been made yet, so both files will - * simply have the unmodified state. - * - * This method is called after each edit, so after the first file finishes - * being edits, it will update its content in the second undo snapshot such - * that it can be undone successfully. - * - * We ensure that the same file is not concurrently edited via the - * {@link _streamingEditLocks}, avoiding race conditions. - * - * @param next If true, this will edit the snapshot _after_ the undo stop - */ - private ensureEditInUndoStopMatches(requestId: string, undoStop: string | undefined, entry: AbstractChatEditingModifiedFileEntry, next: boolean, tx: ITransaction | undefined) { - const history = this._linearHistory.get(); - const snapIndex = history.findIndex(s => s.requestId === requestId); - if (snapIndex === -1) { - return; - } - - const snap = history[snapIndex]; - let stopIndex = snap.stops.findIndex(s => s.stopId === undoStop); - if (stopIndex === -1) { - return; - } - - // special case: put the last change in the pendingSnapshot as needed - if (next) { - if (stopIndex === snap.stops.length - 1) { - const postEdit = new ResourceMap(snap.postEdit || this._createEmptySnapshot(undefined).entries); - if (!snap.postEdit || !entry.equalsSnapshot(postEdit.get(entry.modifiedURI))) { - postEdit.set(entry.modifiedURI, entry.createSnapshot(requestId, POST_EDIT_STOP_ID)); - const newHistory = history.slice(); - newHistory[snapIndex] = { ...snap, postEdit }; - this._linearHistory.set(newHistory, tx); - } - return; - } - stopIndex++; - } - - const stop = snap.stops[stopIndex]; - if (entry.equalsSnapshot(stop.entries.get(entry.modifiedURI))) { - return; - } - - const newMap = new ResourceMap(stop.entries); - newMap.set(entry.modifiedURI, entry.createSnapshot(requestId, stop.stopId)); - - const newStop = snap.stops.slice(); - newStop[stopIndex] = { ...stop, entries: newMap }; - - const newHistory = history.slice(); - newHistory[snapIndex] = { ...snap, stops: newStop }; - this._linearHistory.set(newHistory, tx); - } - private async _acceptEdits(resource: URI, textEdits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise { - this._fullDiffs.delete(resource.toString()); const entry = await this._getOrCreateModifiedFileEntry(resource, this._getTelemetryInfoForModel(responseModel)); await entry.acceptAgentEdits(resource, textEdits, isLastEdits, responseModel); } @@ -848,7 +518,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } private async _resolve(requestId: string, undoStop: string | undefined, resource: URI): Promise { - const hasOtherTasks = Iterable.some(this._streamingEditLocks.keys(), k => k !== resource.toString()); if (!hasOtherTasks) { this._state.set(ChatEditingSessionState.Idle, undefined); @@ -859,7 +528,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return; } - this.ensureEditInUndoStopMatches(requestId, undoStop, entry, /* next= */ true, undefined); + this._timeline.ensureEditInUndoStopMatches(requestId, undoStop, entry, /* next= */ true, undefined); return entry.acceptStreamingEditsEnd(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTimeline.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTimeline.ts new file mode 100644 index 00000000000..f5dfc91f294 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTimeline.ts @@ -0,0 +1,461 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + + +import { equals as arraysEqual, binarySearch2 } from '../../../../../base/common/arrays.js'; +import { findLast } from '../../../../../base/common/arraysFind.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../../base/common/map.js'; +import { derived, derivedOpts, IObservable, ITransaction, ObservablePromise, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { isEqual } from '../../../../../base/common/resources.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js'; +import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; +import { IEditSessionEntryDiff, ISnapshotEntry } from '../../common/chatEditingService.js'; +import { IChatRequestDisablement } from '../../common/chatModel.js'; +import { AbstractChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js'; +import { ChatEditingModifiedNotebookEntry } from './chatEditingModifiedNotebookEntry.js'; +import { IChatEditingSessionSnapshot, IChatEditingSessionStop } from './chatEditingSessionStorage.js'; +import { ChatEditingModifiedNotebookDiff } from './notebook/chatEditingModifiedNotebookDiff.js'; + +/** + * Timeline/undo-redo stack for ChatEditingSession. + */ +export class ChatEditingTimeline { + public static readonly POST_EDIT_STOP_ID = 'd19944f6-f46c-4e17-911b-79a8e843c7c0'; // randomly generated + public static createEmptySnapshot(undoStop: string | undefined): IChatEditingSessionStop { + return { + stopId: undoStop, + entries: new ResourceMap(), + }; + } + + private readonly _linearHistory = observableValue(this, []); + private readonly _linearHistoryIndex = observableValue(this, 0); + + private readonly _diffsBetweenStops = new Map>(); + private readonly _fullDiffs = new Map>(); + private readonly _ignoreTrimWhitespaceObservable: IObservable; + + public readonly canUndo: IObservable; + public readonly canRedo: IObservable; + + constructor( + @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService, + @ITextModelService private readonly _textModelService: ITextModelService, + ) { + this._ignoreTrimWhitespaceObservable = observableConfigValue('diffEditor.ignoreTrimWhitespace', true, configurationService); + + this.canUndo = derived(r => { + const linearHistoryIndex = this._linearHistoryIndex.read(r); + return linearHistoryIndex > 1; + }); + this.canRedo = derived(r => { + const linearHistoryIndex = this._linearHistoryIndex.read(r); + return linearHistoryIndex < getMaxHistoryIndex(this._linearHistory.read(r)); + }); + } + + /** + * Restore the timeline from a saved state (history array and index). + */ + public restoreFromState(state: { history: readonly IChatEditingSessionSnapshot[]; index: number }, tx: ITransaction): void { + this._linearHistory.set(state.history, tx); + this._linearHistoryIndex.set(state.index, tx); + } + + /** + * Get the snapshot and history index for restoring, given requestId and stopId. + * If requestId is undefined, returns undefined (pending snapshot is managed by session). + */ + public getSnapshotForRestore(requestId: string | undefined, stopId: string | undefined): { stop: IChatEditingSessionStop; historyIndex: number; apply(): void } | undefined { + if (requestId === undefined) { + return undefined; + } + const stopRef = this.findEditStop(requestId, stopId); + if (!stopRef) { + return undefined; + } + + return { stop: stopRef.stop, historyIndex: stopRef.historyIndex, apply: () => this._linearHistoryIndex.set(stopRef.historyIndex + 1, undefined) }; + } + + /** + * Ensures the state of the file in the given snapshot matches the current + * state of the {@param entry}. This is used to handle concurrent file edits. + * + * Given the case of two different edits, we will place and undo stop right + * before we `textEditGroup` in the underlying markdown stream, but at the + * time those are added the edits haven't been made yet, so both files will + * simply have the unmodified state. + * + * This method is called after each edit, so after the first file finishes + * being edits, it will update its content in the second undo snapshot such + * that it can be undone successfully. + * + * We ensure that the same file is not concurrently edited via the + * {@link _streamingEditLocks}, avoiding race conditions. + * + * @param next If true, this will edit the snapshot _after_ the undo stop + */ + public ensureEditInUndoStopMatches( + requestId: string, + undoStop: string | undefined, + entry: Pick, + next: boolean, + tx: ITransaction | undefined + ) { + const history = this._linearHistory.get(); + const snapIndex = history.findIndex((s) => s.requestId === requestId); + if (snapIndex === -1) { + return; + } + + const snap = history[snapIndex]; + let stopIndex = snap.stops.findIndex((s) => s.stopId === undoStop); + if (stopIndex === -1) { + return; + } + + if (next) { + if (stopIndex === snap.stops.length - 1) { + const postEdit = new ResourceMap(snap.postEdit || ChatEditingTimeline.createEmptySnapshot(undefined).entries); + if (!snap.postEdit || !entry.equalsSnapshot(postEdit.get(entry.modifiedURI) as ISnapshotEntry | undefined)) { + postEdit.set(entry.modifiedURI, entry.createSnapshot(requestId, ChatEditingTimeline.POST_EDIT_STOP_ID)); + const newHistory = history.slice(); + newHistory[snapIndex] = { ...snap, postEdit }; + this._linearHistory.set(newHistory, tx); + } + return; + } + stopIndex++; + } + + const stop = snap.stops[stopIndex]; + if (entry.equalsSnapshot(stop.entries.get(entry.modifiedURI))) { + return; + } + + const newMap = new ResourceMap(stop.entries); + newMap.set(entry.modifiedURI, entry.createSnapshot(requestId, stop.stopId)); + + const newStop = snap.stops.slice(); + newStop[stopIndex] = { ...stop, entries: newMap }; + + const newHistory = history.slice(); + newHistory[snapIndex] = { ...snap, stops: newStop }; + this._linearHistory.set(newHistory, tx); + } + + /** + * Get the undo snapshot (previous in history), or undefined if at start. + * If the timeline is at the end of the history, it will return the last stop + * pushed into the history. + */ + public getUndoSnapshot(): { stop: IChatEditingSessionStop; apply(): void } | undefined { + const idx = this._linearHistoryIndex.get() - 2; + const entry = this.getHistoryEntryByLinearIndex(idx); + if (entry) { + return { stop: entry.stop, apply: () => this._linearHistoryIndex.set(idx + 1, undefined) }; + } + return undefined; + } + + /** + * Get the redo snapshot (next in history), or undefined if at end. + */ + public getRedoSnapshot(): { stop: IChatEditingSessionStop; apply(): void } | undefined { + const idx = this._linearHistoryIndex.get(); + const entry = this.getHistoryEntryByLinearIndex(idx); + if (entry) { + return { stop: entry.stop, apply: () => this._linearHistoryIndex.set(idx + 1, undefined) }; + } + return undefined; + } + + /** + * Get the state for persistence (history and index). + */ + public getStateForPersistence(): { history: readonly IChatEditingSessionSnapshot[]; index: number } { + return { history: this._linearHistory.get(), index: this._linearHistoryIndex.get() }; + } + + private findSnapshot(requestId: string): IChatEditingSessionSnapshot | undefined { + return this._linearHistory.get().find((s) => s.requestId === requestId); + } + + private findEditStop(requestId: string, undoStop: string | undefined) { + const snapshot = this.findSnapshot(requestId); + if (!snapshot) { + return undefined; + } + const idx = snapshot.stops.findIndex((s) => s.stopId === undoStop); + return idx === -1 ? undefined : { stop: snapshot.stops[idx], snapshot, historyIndex: snapshot.startIndex + idx }; + } + + private getHistoryEntryByLinearIndex(index: number) { + const history = this._linearHistory.get(); + const searchedIndex = binarySearch2(history.length, (e) => history[e].startIndex - index); + const entry = history[searchedIndex < 0 ? (~searchedIndex) - 1 : searchedIndex]; + if (!entry || index - entry.startIndex >= entry.stops.length) { + return undefined; + } + return { + entry, + stop: entry.stops[index - entry.startIndex] + }; + } + + public pushSnapshot(requestId: string, undoStop: string | undefined, snapshot: IChatEditingSessionStop) { + const linearHistoryPtr = this._linearHistoryIndex.get(); + const newLinearHistory: IChatEditingSessionSnapshot[] = []; + for (const entry of this._linearHistory.get()) { + if (entry.startIndex >= linearHistoryPtr) { + break; + } else if (linearHistoryPtr - entry.startIndex < entry.stops.length) { + newLinearHistory.push({ requestId: entry.requestId, stops: entry.stops.slice(0, linearHistoryPtr - entry.startIndex), startIndex: entry.startIndex, postEdit: undefined }); + } else { + newLinearHistory.push(entry); + } + } + + const lastEntry = newLinearHistory.at(-1); + if (requestId && lastEntry?.requestId === requestId) { + if (lastEntry.postEdit && undoStop) { + const rebaseUri = (uri: URI) => uri.with({ query: uri.query.replace(ChatEditingTimeline.POST_EDIT_STOP_ID, undoStop) }); + for (const [uri, prev] of lastEntry.postEdit.entries()) { + snapshot.entries.set(uri, { ...prev, snapshotUri: rebaseUri(prev.snapshotUri), resource: rebaseUri(prev.resource) }); + } + } + newLinearHistory[newLinearHistory.length - 1] = { ...lastEntry, stops: [...lastEntry.stops, snapshot], postEdit: undefined }; + } else { + newLinearHistory.push({ requestId, startIndex: lastEntry ? lastEntry.startIndex + lastEntry.stops.length : 0, stops: [snapshot], postEdit: undefined }); + } + + transaction((tx) => { + const last = newLinearHistory[newLinearHistory.length - 1]; + this._linearHistory.set(newLinearHistory, tx); + this._linearHistoryIndex.set(last.startIndex + last.stops.length, tx); + }); + } + + /** + * Gets chat disablement entries for the current timeline state. + */ + public getRequestDisablement() { + const history = this._linearHistory.get(); + const index = this._linearHistoryIndex.get(); + const undoRequests: IChatRequestDisablement[] = []; + for (const entry of history) { + if (!entry.requestId) { + // ignored + } else if (entry.startIndex >= index) { + undoRequests.push({ requestId: entry.requestId }); + } else if (entry.startIndex + entry.stops.length > index) { + undoRequests.push({ requestId: entry.requestId, afterUndoStop: entry.stops[index - entry.startIndex].stopId }); + } + } + return undoRequests; + } + + /** + * Gets diff for text entries between stops. + * @param entriesContent Observable that observes either snapshot entry + * @param modelUrisObservable Observable that observes only the snapshot URIs. + */ + private _entryDiffBetweenTextStops( + entriesContent: IObservable<{ before: ISnapshotEntry; after: ISnapshotEntry } | undefined>, + modelUrisObservable: IObservable<[URI, URI] | undefined>, + ): IObservable | undefined> { + const modelRefsPromise = derived(this, (reader) => { + const modelUris = modelUrisObservable.read(reader); + if (!modelUris) { return undefined; } + + const store = reader.store.add(new DisposableStore()); + const promise = Promise.all(modelUris.map(u => this._textModelService.createModelReference(u))).then(refs => { + if (store.isDisposed) { + refs.forEach(r => r.dispose()); + } else { + refs.forEach(r => store.add(r)); + } + + return refs; + }); + + return new ObservablePromise(promise); + }); + + return derived((reader): ObservablePromise | undefined => { + const refs2 = modelRefsPromise.read(reader)?.promiseResult.read(reader); + const refs = refs2?.data; + if (!refs) { + return; + } + + const entries = entriesContent.read(reader); // trigger re-diffing when contents change + + if (entries?.before && ChatEditingModifiedNotebookEntry.canHandleSnapshot(entries.before)) { + const diffService = this._instantiationService.createInstance(ChatEditingModifiedNotebookDiff, entries.before, entries.after); + return new ObservablePromise(diffService.computeDiff()); + + } + const ignoreTrimWhitespace = this._ignoreTrimWhitespaceObservable.read(reader); + const promise = this._editorWorkerService.computeDiff( + refs[0].object.textEditorModel.uri, + refs[1].object.textEditorModel.uri, + { ignoreTrimWhitespace, computeMoves: false, maxComputationTimeMs: 3000 }, + 'advanced' + ).then((diff): IEditSessionEntryDiff => { + const entryDiff: IEditSessionEntryDiff = { + originalURI: refs[0].object.textEditorModel.uri, + modifiedURI: refs[1].object.textEditorModel.uri, + identical: !!diff?.identical, + quitEarly: !diff || diff.quitEarly, + added: 0, + removed: 0, + }; + if (diff) { + for (const change of diff.changes) { + entryDiff.removed += change.original.endLineNumberExclusive - change.original.startLineNumber; + entryDiff.added += change.modified.endLineNumberExclusive - change.modified.startLineNumber; + } + } + + return entryDiff; + }); + + return new ObservablePromise(promise); + }); + } + + private _createDiffBetweenStopsObservable(uri: URI, requestId: string | undefined, stopId: string | undefined): IObservable { + const entries = derivedOpts( + { + equalsFn: (a, b) => snapshotsEqualForDiff(a?.before, b?.before) && snapshotsEqualForDiff(a?.after, b?.after), + }, + reader => { + const stops = requestId ? + getCurrentAndNextStop(requestId, stopId, this._linearHistory.read(reader)) : + getFirstAndLastStop(uri, this._linearHistory.read(reader)); + if (!stops) { return undefined; } + const before = stops.current.get(uri); + const after = stops.next.get(uri); + if (!before || !after) { return undefined; } + return { before, after }; + }, + ); + + // Separate observable for model refs to avoid unnecessary disposal + const modelUrisObservable = derivedOpts<[URI, URI] | undefined>({ equalsFn: (a, b) => arraysEqual(a, b, isEqual) }, reader => { + const entriesValue = entries.read(reader); + if (!entriesValue) { return undefined; } + return [entriesValue.before.snapshotUri, entriesValue.after.snapshotUri]; + }); + + const diff = this._entryDiffBetweenTextStops(entries, modelUrisObservable); + + return derived(reader => { + return diff.read(reader)?.promiseResult.read(reader)?.data || undefined; + }); + } + + public getEntryDiffBetweenStops(uri: URI, requestId: string | undefined, stopId: string | undefined) { + if (requestId) { + const key = `${uri}\0${requestId}\0${stopId}`; + let observable = this._diffsBetweenStops.get(key); + if (!observable) { + observable = this._createDiffBetweenStopsObservable(uri, requestId, stopId); + this._diffsBetweenStops.set(key, observable); + } + + return observable; + } else { + const key = uri.toString(); + let observable = this._fullDiffs.get(key); + if (!observable) { + observable = this._createDiffBetweenStopsObservable(uri, requestId, stopId); + this._fullDiffs.set(key, observable); + } + + return observable; + } + } +} + +function getMaxHistoryIndex(history: readonly IChatEditingSessionSnapshot[]) { + const lastHistory = history.at(-1); + return lastHistory ? lastHistory.startIndex + lastHistory.stops.length : 0; +} + +function snapshotsEqualForDiff(a: ISnapshotEntry | undefined, b: ISnapshotEntry | undefined) { + if (!a || !b) { + return a === b; + } + + return isEqual(a.snapshotUri, b.snapshotUri) && a.current === b.current; +} + +function getCurrentAndNextStop(requestId: string, stopId: string | undefined, history: readonly IChatEditingSessionSnapshot[]) { + const snapshotIndex = history.findIndex(s => s.requestId === requestId); + if (snapshotIndex === -1) { return undefined; } + const snapshot = history[snapshotIndex]; + const stopIndex = snapshot.stops.findIndex(s => s.stopId === stopId); + if (stopIndex === -1) { return undefined; } + + const currentStop = snapshot.stops[stopIndex]; + const current = currentStop.entries; + const nextStop = stopIndex < snapshot.stops.length - 1 + ? snapshot.stops[stopIndex + 1] + : undefined; + const next = nextStop?.entries || snapshot.postEdit; + + + if (!next) { + return undefined; + } + + return { current, currentStopId: currentStop.stopId, next, nextStopId: nextStop?.stopId || ChatEditingTimeline.POST_EDIT_STOP_ID }; +} + +function getFirstAndLastStop(uri: URI, history: readonly IChatEditingSessionSnapshot[]) { + let firstStopWithUri: IChatEditingSessionStop | undefined; + for (const snapshot of history) { + const stop = snapshot.stops.find(s => s.entries.has(uri)); + if (stop) { + firstStopWithUri = stop; + break; + } + } + + let lastStopWithUri: ResourceMap | undefined; + let lastStopWithUriId: string | undefined; + for (let i = history.length - 1; i >= 0; i--) { + const snapshot = history[i]; + if (snapshot.postEdit?.has(uri)) { + lastStopWithUri = snapshot.postEdit; + lastStopWithUriId = ChatEditingTimeline.POST_EDIT_STOP_ID; + break; + } + + const stop = findLast(snapshot.stops, s => s.entries.has(uri)); + if (stop) { + lastStopWithUri = stop.entries; + lastStopWithUriId = stop.stopId; + break; + } + } + + if (!firstStopWithUri || !lastStopWithUri) { + return undefined; + } + + return { current: firstStopWithUri.entries, currentStopId: firstStopWithUri.stopId, next: lastStopWithUri, nextStopId: lastStopWithUriId! }; +} diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditingTimeline.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditingTimeline.test.ts new file mode 100644 index 00000000000..c5d5b851e03 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/chatEditingTimeline.test.ts @@ -0,0 +1,466 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { ChatEditingTimeline } from '../../browser/chatEditing/chatEditingTimeline.js'; +import { IChatEditingSessionStop } from '../../browser/chatEditing/chatEditingSessionStorage.js'; +import { transaction } from '../../../../../base/common/observable.js'; + +suite('ChatEditingTimeline', () => { + const ds = ensureNoDisposablesAreLeakedInTestSuite(); + let timeline: ChatEditingTimeline; + + setup(() => { + const instaService = workbenchInstantiationService(undefined, ds); + timeline = instaService.createInstance(ChatEditingTimeline); + }); + + suite('undo/redo', () => { + test('undo/redo with empty history', () => { + assert.strictEqual(timeline.getUndoSnapshot(), undefined); + assert.strictEqual(timeline.getRedoSnapshot(), undefined); + assert.strictEqual(timeline.canRedo.get(), false); + assert.strictEqual(timeline.canUndo.get(), false); + }); + }); + + function createSnapshot(stopId = 'stop', entries?: any): IChatEditingSessionStop { + return { + stopId, + entries: entries || new Map(), + }; + } + + suite('Basic functionality', () => { + test('pushSnapshot and undo/redo navigation', () => { + // Push two snapshots + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req1', 'stop2', createSnapshot('stop2')); + + // After two pushes, canUndo should be true, canRedo false + assert.strictEqual(timeline.canUndo.get(), true); + assert.strictEqual(timeline.canRedo.get(), false); + + // Undo should move back to stop1 + const undoSnap = timeline.getUndoSnapshot(); + assert.ok(undoSnap); + assert.strictEqual(undoSnap.stop.stopId, 'stop1'); + undoSnap.apply(); + assert.strictEqual(timeline.canUndo.get(), false); + assert.strictEqual(timeline.canRedo.get(), true); + + // Redo should move forward to stop2 + const redoSnap = timeline.getRedoSnapshot(); + assert.ok(redoSnap); + assert.strictEqual(redoSnap.stop.stopId, 'stop2'); + redoSnap.apply(); + assert.strictEqual(timeline.canUndo.get(), true); + assert.strictEqual(timeline.canRedo.get(), false); + }); + + test('restoreFromState restores history and index', () => { + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req1', 'stop2', createSnapshot('stop2')); + const state = timeline.getStateForPersistence(); + + // Move back + timeline.getUndoSnapshot()?.apply(); + + // Restore state + transaction(tx => timeline.restoreFromState(state, tx)); + assert.strictEqual(timeline.canUndo.get(), true); + assert.strictEqual(timeline.canRedo.get(), false); + }); + + test('getSnapshotForRestore returns correct snapshot', () => { + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req1', 'stop2', createSnapshot('stop2')); + + const snap = timeline.getSnapshotForRestore('req1', 'stop1'); + assert.ok(snap); + assert.strictEqual(snap.stop.stopId, 'stop1'); + snap.apply(); + + assert.strictEqual(timeline.canRedo.get(), true); + assert.strictEqual(timeline.canUndo.get(), false); + + const snap2 = timeline.getSnapshotForRestore('req1', 'stop2'); + assert.ok(snap2); + assert.strictEqual(snap2.stop.stopId, 'stop2'); + snap2.apply(); + + assert.strictEqual(timeline.canRedo.get(), false); + assert.strictEqual(timeline.canUndo.get(), true); + }); + + test('getRequestDisablement returns correct requests', () => { + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2')); + + // Move back to first + timeline.getUndoSnapshot()?.apply(); + + const disables = timeline.getRequestDisablement(); + assert.ok(Array.isArray(disables)); + assert.ok(disables.some(d => d.requestId === 'req2')); + }); + }); + + suite('Multiple requests', () => { + test('handles multiple requests with separate snapshots', () => { + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2')); + timeline.pushSnapshot('req3', 'stop3', createSnapshot('stop3')); + + assert.strictEqual(timeline.canUndo.get(), true); + assert.strictEqual(timeline.canRedo.get(), false); + + // Undo should go back through requests + let undoSnap = timeline.getUndoSnapshot(); + assert.ok(undoSnap); + assert.strictEqual(undoSnap.stop.stopId, 'stop2'); + undoSnap.apply(); + + undoSnap = timeline.getUndoSnapshot(); + assert.ok(undoSnap); + assert.strictEqual(undoSnap.stop.stopId, 'stop1'); + }); + + test('handles same request with multiple stops', () => { + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req1', 'stop2', createSnapshot('stop2')); + timeline.pushSnapshot('req1', 'stop3', createSnapshot('stop3')); + + const state = timeline.getStateForPersistence(); + assert.strictEqual(state.history.length, 1); + assert.strictEqual(state.history[0].stops.length, 3); + assert.strictEqual(state.history[0].requestId, 'req1'); + }); + + test('mixed requests and stops', () => { + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req1', 'stop2', createSnapshot('stop2')); + timeline.pushSnapshot('req2', 'stop3', createSnapshot('stop3')); + timeline.pushSnapshot('req2', 'stop4', createSnapshot('stop4')); + + const state = timeline.getStateForPersistence(); + assert.strictEqual(state.history.length, 2); + assert.strictEqual(state.history[0].stops.length, 2); + assert.strictEqual(state.history[1].stops.length, 2); + }); + }); + + suite('Edge cases', () => { + test('getSnapshotForRestore with non-existent request', () => { + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + + const snap = timeline.getSnapshotForRestore('nonexistent', 'stop1'); + assert.strictEqual(snap, undefined); + }); + + test('getSnapshotForRestore with non-existent stop', () => { + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + + const snap = timeline.getSnapshotForRestore('req1', 'nonexistent'); + assert.strictEqual(snap, undefined); + }); + }); + + suite('History manipulation', () => { + test('pushing snapshots after undo truncates future history', () => { + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req1', 'stop2', createSnapshot('stop2')); + timeline.pushSnapshot('req1', 'stop3', createSnapshot('stop3')); + + // Undo twice + timeline.getUndoSnapshot()?.apply(); + timeline.getUndoSnapshot()?.apply(); + + // Push new snapshot - should truncate stop3 + timeline.pushSnapshot('req1', 'new_stop', createSnapshot('new_stop')); + + const state = timeline.getStateForPersistence(); + assert.strictEqual(state.history[0].stops.length, 2); // stop1 + new_stop + assert.strictEqual(state.history[0].stops[1].stopId, 'new_stop'); + }); + + test('branching from middle of history creates new branch', () => { + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2')); + timeline.pushSnapshot('req3', 'stop3', createSnapshot('stop3')); + + // Undo to middle + timeline.getUndoSnapshot()?.apply(); + + // Push new request + timeline.pushSnapshot('req4', 'stop4', createSnapshot('stop4')); + + const state = timeline.getStateForPersistence(); + assert.strictEqual(state.history.length, 3); // req1, req2, req4 + assert.strictEqual(state.history[2].requestId, 'req4'); + }); + }); + + suite('State persistence', () => { + test('getStateForPersistence returns complete state', () => { + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2')); + + const state = timeline.getStateForPersistence(); + assert.ok(state.history); + assert.ok(typeof state.index === 'number'); + assert.strictEqual(state.history.length, 2); + assert.strictEqual(state.index, 2); + }); + + test('restoreFromState handles empty history', () => { + const emptyState = { history: [], index: 0 }; + + transaction(tx => timeline.restoreFromState(emptyState, tx)); + + assert.strictEqual(timeline.canUndo.get(), false); + assert.strictEqual(timeline.canRedo.get(), false); + }); + + test('restoreFromState with complex history', () => { + // Create complex state + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req1', 'stop2', createSnapshot('stop2')); + timeline.pushSnapshot('req2', 'stop3', createSnapshot('stop3')); + + const originalState = timeline.getStateForPersistence(); + + // Create new timeline and restore + const instaService = workbenchInstantiationService(undefined, ds); + const newTimeline = instaService.createInstance(ChatEditingTimeline); + transaction(tx => newTimeline.restoreFromState(originalState, tx)); + + const restoredState = newTimeline.getStateForPersistence(); + assert.deepStrictEqual(restoredState.index, originalState.index); + assert.strictEqual(restoredState.history.length, originalState.history.length); + }); + }); + + suite('Request disablement', () => { + test('getRequestDisablement at various positions', () => { + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2')); + timeline.pushSnapshot('req3', 'stop3', createSnapshot('stop3')); + + // At end - no disabled requests + let disables = timeline.getRequestDisablement(); + assert.strictEqual(disables.length, 0); + + // Move back one + timeline.getUndoSnapshot()?.apply(); + disables = timeline.getRequestDisablement(); + assert.strictEqual(disables.length, 1); + assert.strictEqual(disables[0].requestId, 'req3'); + + // Move back to beginning + timeline.getUndoSnapshot()?.apply(); + timeline.getUndoSnapshot()?.apply(); + disables = timeline.getRequestDisablement(); + assert.strictEqual(disables.length, 2); + }); + + test('getRequestDisablement with mixed request/stop structure', () => { + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req1', 'stop2', createSnapshot('stop2')); + timeline.pushSnapshot('req2', 'stop3', createSnapshot('stop3')); + + // Move to middle of req1 + timeline.getUndoSnapshot()?.apply(); + timeline.getUndoSnapshot()?.apply(); + + const disables = timeline.getRequestDisablement(); + assert.strictEqual(disables.length, 2); + + // Should have partial disable for req1 and full disable for req2 + const req1Disable = disables.find(d => d.requestId === 'req1'); + const req2Disable = disables.find(d => d.requestId === 'req2'); + + assert.ok(req1Disable); + assert.ok(req2Disable); + assert.ok(req1Disable.afterUndoStop); + assert.strictEqual(req2Disable.afterUndoStop, undefined); + }); + }); + + suite('Boundary conditions', () => { + test('undo/redo at boundaries', () => { + // Empty timeline + assert.strictEqual(timeline.getUndoSnapshot(), undefined); + assert.strictEqual(timeline.getRedoSnapshot(), undefined); + + // Single snapshot + timeline.pushSnapshot('req1', 'stop2', createSnapshot('stop2')); + timeline.pushSnapshot('req1', 'stop2', createSnapshot('stop2')); + assert.ok(timeline.getUndoSnapshot()); + assert.strictEqual(timeline.getRedoSnapshot(), undefined); + + // At beginning after undo + timeline.getUndoSnapshot()?.apply(); + assert.strictEqual(timeline.getUndoSnapshot(), undefined); + assert.ok(timeline.getRedoSnapshot()); + }); + + test('multiple undos and redos', () => { + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2')); + timeline.pushSnapshot('req3', 'stop3', createSnapshot('stop3')); + + // Undo all + const stops: string[] = []; + let undoSnap = timeline.getUndoSnapshot(); + while (undoSnap) { + stops.push(undoSnap.stop.stopId!); + undoSnap.apply(); + undoSnap = timeline.getUndoSnapshot(); + } + assert.deepStrictEqual(stops, ['stop2', 'stop1']); + + // Redo all + const redoStops: string[] = []; + let redoSnap = timeline.getRedoSnapshot(); + while (redoSnap) { + redoStops.push(redoSnap.stop.stopId!); + redoSnap.apply(); + redoSnap = timeline.getRedoSnapshot(); + } + assert.deepStrictEqual(redoStops, ['stop2', 'stop3']); + }); + }); + + suite('Static methods', () => { + test('createEmptySnapshot creates valid snapshot', () => { + const snapshot = ChatEditingTimeline.createEmptySnapshot('test-stop'); + assert.strictEqual(snapshot.stopId, 'test-stop'); + assert.ok(snapshot.entries); + assert.strictEqual(snapshot.entries.size, 0); + }); + + test('createEmptySnapshot with undefined stopId', () => { + const snapshot = ChatEditingTimeline.createEmptySnapshot(undefined); + assert.strictEqual(snapshot.stopId, undefined); + assert.ok(snapshot.entries); + }); + + test('POST_EDIT_STOP_ID is consistent', () => { + assert.strictEqual(typeof ChatEditingTimeline.POST_EDIT_STOP_ID, 'string'); + assert.ok(ChatEditingTimeline.POST_EDIT_STOP_ID.length > 0); + }); + }); + + suite('Observable behavior', () => { + test('canUndo observable updates correctly', () => { + assert.strictEqual(timeline.canUndo.get(), false); + + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req1', 'stop2', createSnapshot('stop2')); + assert.strictEqual(timeline.canUndo.get(), true); + + timeline.getUndoSnapshot()?.apply(); + assert.strictEqual(timeline.canUndo.get(), false); + }); + + test('canRedo observable updates correctly', () => { + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req1', 'stop2', createSnapshot('stop2')); + assert.strictEqual(timeline.canRedo.get(), false); + + timeline.getUndoSnapshot()?.apply(); + assert.strictEqual(timeline.canRedo.get(), true); + + timeline.getRedoSnapshot()?.apply(); + assert.strictEqual(timeline.canRedo.get(), false); + }); + }); + + suite('Complex scenarios', () => { + test('interleaved requests and undos', () => { + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2')); + + // Undo req2 + timeline.getUndoSnapshot()?.apply(); + + // Add req3 (should branch from req1) + timeline.pushSnapshot('req3', 'stop3', createSnapshot('stop3')); + + const state = timeline.getStateForPersistence(); + assert.strictEqual(state.history.length, 2); // req1, req3 + assert.strictEqual(state.history[1].requestId, 'req3'); + }); + + test('large number of snapshots', () => { + // Push 100 snapshots + for (let i = 1; i <= 100; i++) { + timeline.pushSnapshot(`req${i}`, `stop${i}`, createSnapshot(`stop${i}`)); + } + + assert.strictEqual(timeline.canUndo.get(), true); + assert.strictEqual(timeline.canRedo.get(), false); + + const state = timeline.getStateForPersistence(); + assert.strictEqual(state.history.length, 100); + assert.strictEqual(state.index, 100); + }); + + test('alternating single and multi-stop requests', () => { + // Single stop request + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + + // Multi-stop request + timeline.pushSnapshot('req2', 'stop2a', createSnapshot('stop2a')); + timeline.pushSnapshot('req2', 'stop2b', createSnapshot('stop2b')); + timeline.pushSnapshot('req2', 'stop2c', createSnapshot('stop2c')); + + // Single stop request + timeline.pushSnapshot('req3', 'stop3', createSnapshot('stop3')); + + const state = timeline.getStateForPersistence(); + assert.strictEqual(state.history.length, 3); + assert.strictEqual(state.history[0].stops.length, 1); + assert.strictEqual(state.history[1].stops.length, 3); + assert.strictEqual(state.history[2].stops.length, 1); + }); + }); + + suite('Error resilience', () => { + test('handles invalid apply calls gracefully', () => { + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req1', 'stop2', createSnapshot('stop2')); + + const undoSnap = timeline.getUndoSnapshot(); + assert.ok(undoSnap); + + // Apply twice - second should be safe + undoSnap.apply(); + undoSnap.apply(); // Should not throw + + assert.strictEqual(timeline.canUndo.get(), false); + }); + + test('getSnapshotForRestore with malformed stopId', () => { + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + + const snap = timeline.getSnapshotForRestore('req1', ''); + assert.strictEqual(snap, undefined); + }); + + test('handles restoration edge cases', () => { + const emptyState = { history: [], index: 0 }; + transaction(tx => timeline.restoreFromState(emptyState, tx)); + + // Should be safe to call methods on empty timeline + assert.strictEqual(timeline.getUndoSnapshot(), undefined); + assert.strictEqual(timeline.getRedoSnapshot(), undefined); + assert.deepStrictEqual(timeline.getRequestDisablement(), []); + }); + }); +}); From a40c65ef41c661b7c25c5266bc808ec5054bbd9c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 9 Jul 2025 08:01:16 +0200 Subject: [PATCH 238/306] debt - global view container toolbar misses telemetry source (#254793) --- src/vs/workbench/browser/parts/compositePart.ts | 2 +- src/vs/workbench/browser/parts/paneCompositePart.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 099641bd5e3..60d91880a83 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -89,7 +89,7 @@ export abstract class CompositePart extends Part { protected readonly registry: CompositeRegistry, private readonly activeCompositeSettingsKey: string, private readonly defaultCompositeId: string, - private readonly nameForTelemetry: string, + protected readonly nameForTelemetry: string, private readonly compositeCSSClass: string, private readonly titleForegroundColor: string | undefined, private readonly titleBorderColor: string | undefined, diff --git a/src/vs/workbench/browser/parts/paneCompositePart.ts b/src/vs/workbench/browser/parts/paneCompositePart.ts index 8cb89b2e576..9622c739865 100644 --- a/src/vs/workbench/browser/parts/paneCompositePart.ts +++ b/src/vs/workbench/browser/parts/paneCompositePart.ts @@ -365,7 +365,8 @@ export abstract class AbstractPaneCompositePart extends CompositePart Date: Wed, 9 Jul 2025 09:35:42 +0200 Subject: [PATCH 239/306] debt - lift assignment service into its only use (#254796) --- .../assignment/common/assignmentService.ts | 104 ------------- .../assignment/common/assignmentService.ts | 146 ++++++++++++++---- 2 files changed, 116 insertions(+), 134 deletions(-) delete mode 100644 src/vs/platform/assignment/common/assignmentService.ts diff --git a/src/vs/platform/assignment/common/assignmentService.ts b/src/vs/platform/assignment/common/assignmentService.ts deleted file mode 100644 index 383777c931e..00000000000 --- a/src/vs/platform/assignment/common/assignmentService.ts +++ /dev/null @@ -1,104 +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 type { IExperimentationTelemetry, ExperimentationService as TASClient, IKeyValueStorage } from 'tas-client-umd'; -import { IConfigurationService } from '../../configuration/common/configuration.js'; -import { IProductService } from '../../product/common/productService.js'; -import { AssignmentFilterProvider, ASSIGNMENT_REFETCH_INTERVAL, ASSIGNMENT_STORAGE_KEY, IAssignmentService, TargetPopulation } from './assignment.js'; -import { importAMDNodeModule } from '../../../amdX.js'; -import { IEnvironmentService } from '../../environment/common/environment.js'; - -export abstract class BaseAssignmentService implements IAssignmentService { - - _serviceBrand: undefined; - - protected tasClient: Promise | undefined; - - private networkInitialized = false; - private overrideInitDelay: Promise; - - constructor( - private readonly machineId: string, - protected readonly configurationService: IConfigurationService, - protected readonly productService: IProductService, - protected readonly environmentService: IEnvironmentService, - protected telemetry: IExperimentationTelemetry, - protected experimentsEnabled: boolean, - private keyValueStorage?: IKeyValueStorage - ) { - if (productService.tasConfig && experimentsEnabled) { - this.tasClient = this.setupTASClient(); - } - - // For development purposes, configure the delay until tas local tas treatment ovverrides are available - const overrideDelaySetting = this.configurationService.getValue('experiments.overrideDelay'); - const overrideDelay = typeof overrideDelaySetting === 'number' ? overrideDelaySetting : 0; - this.overrideInitDelay = new Promise(resolve => setTimeout(resolve, overrideDelay)); - } - - async getTreatment(name: string): Promise { - // For development purposes, allow overriding tas assignments to test variants locally. - await this.overrideInitDelay; - const override = this.configurationService.getValue('experiments.override.' + name); - if (override !== undefined) { - return override; - } - - if (!this.tasClient) { - return undefined; - } - - if (!this.experimentsEnabled) { - return undefined; - } - - let result: T | undefined; - const client = await this.tasClient; - - // The TAS client is initialized but we need to check if the initial fetch has completed yet - // If it is complete, return a cached value for the treatment - // If not, use the async call with `checkCache: true`. This will allow the module to return a cached value if it is present. - // Otherwise it will await the initial fetch to return the most up to date value. - if (this.networkInitialized) { - result = client.getTreatmentVariable('vscode', name); - } else { - result = await client.getTreatmentVariableAsync('vscode', name, true); - } - - result = client.getTreatmentVariable('vscode', name); - return result; - } - - private async setupTASClient(): Promise { - - const targetPopulation = this.productService.quality === 'stable' ? - TargetPopulation.Public : (this.productService.quality === 'exploration' ? - TargetPopulation.Exploration : TargetPopulation.Insiders); - - const filterProvider = new AssignmentFilterProvider( - this.productService.version, - this.productService.nameLong, - this.machineId, - targetPopulation - ); - - const tasConfig = this.productService.tasConfig!; - const tasClient = new (await importAMDNodeModule('tas-client-umd', 'lib/tas-client-umd.js')).ExperimentationService({ - filterProviders: [filterProvider], - telemetry: this.telemetry, - storageKey: ASSIGNMENT_STORAGE_KEY, - keyValueStorage: this.keyValueStorage, - assignmentContextTelemetryPropertyName: tasConfig.assignmentContextTelemetryPropertyName, - telemetryEventName: tasConfig.telemetryEventName, - endpoint: tasConfig.endpoint, - refetchInterval: ASSIGNMENT_REFETCH_INTERVAL, - }); - - await tasClient.initializePromise; - tasClient.initialFetch.then(() => this.networkInitialized = true); - - return tasClient; - } -} diff --git a/src/vs/workbench/services/assignment/common/assignmentService.ts b/src/vs/workbench/services/assignment/common/assignmentService.ts index c93a4852fd6..addfe055c6c 100644 --- a/src/vs/workbench/services/assignment/common/assignmentService.ts +++ b/src/vs/workbench/services/assignment/common/assignmentService.ts @@ -5,7 +5,7 @@ import { localize } from '../../../../nls.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import type { IKeyValueStorage, IExperimentationTelemetry } from 'tas-client-umd'; +import type { IKeyValueStorage, IExperimentationTelemetry, ExperimentationService as TASClient } from 'tas-client-umd'; import { MementoObject, Memento } from '../../../common/memento.js'; import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; @@ -13,28 +13,32 @@ import { ITelemetryData } from '../../../../base/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; -import { IAssignmentService } from '../../../../platform/assignment/common/assignment.js'; +import { ASSIGNMENT_REFETCH_INTERVAL, ASSIGNMENT_STORAGE_KEY, AssignmentFilterProvider, IAssignmentService, TargetPopulation } from '../../../../platform/assignment/common/assignment.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; -import { BaseAssignmentService } from '../../../../platform/assignment/common/assignmentService.js'; import { workbenchConfigurationNodeBase } from '../../../common/configuration.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js'; import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; import { getTelemetryLevel } from '../../../../platform/telemetry/common/telemetryUtils.js'; +import { importAMDNodeModule } from '../../../../amdX.js'; +import { timeout } from '../../../../base/common/async.js'; -export const IWorkbenchAssignmentService = createDecorator('WorkbenchAssignmentService'); +export const IWorkbenchAssignmentService = createDecorator('assignmentService'); export interface IWorkbenchAssignmentService extends IAssignmentService { getCurrentExperiments(): Promise; } class MementoKeyValueStorage implements IKeyValueStorage { - private mementoObj: MementoObject; - constructor(private memento: Memento) { + + private readonly mementoObj: MementoObject; + + constructor(private readonly memento: Memento) { this.mementoObj = memento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE); } async getValue(key: string, defaultValue?: T | undefined): Promise { const value = await this.mementoObj[key]; + return value || defaultValue; } @@ -45,16 +49,17 @@ class MementoKeyValueStorage implements IKeyValueStorage { } class WorkbenchAssignmentServiceTelemetry implements IExperimentationTelemetry { - private _lastAssignmentContext: string | undefined; - constructor( - private telemetryService: ITelemetryService, - private productService: IProductService - ) { } + private _lastAssignmentContext: string | undefined; get assignmentContext(): string[] | undefined { return this._lastAssignmentContext?.split(';'); } + constructor( + private readonly telemetryService: ITelemetryService, + private readonly productService: IProductService + ) { } + // __GDPR__COMMON__ "abexp.assignmentcontext" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } setSharedProperty(name: string, value: string): void { if (name === this.productService.tasConfig?.assignmentContextTelemetryPropertyName) { @@ -81,34 +86,49 @@ class WorkbenchAssignmentServiceTelemetry implements IExperimentationTelemetry { } } -export class WorkbenchAssignmentService extends BaseAssignmentService { +export class WorkbenchAssignmentService implements IAssignmentService { + + declare readonly _serviceBrand: undefined; + + private readonly tasClient: Promise | undefined; + + private networkInitialized = false; + private readonly overrideInitDelay: Promise; + + private readonly telemetry: WorkbenchAssignmentServiceTelemetry; + private readonly keyValueStorage: IKeyValueStorage; + + private readonly experimentsEnabled: boolean; constructor( - @ITelemetryService private telemetryService: ITelemetryService, + @ITelemetryService private readonly telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, - @IConfigurationService configurationService: IConfigurationService, - @IProductService productService: IProductService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IProductService private readonly productService: IProductService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { - const experimentsEnabled = getTelemetryLevel(configurationService) === TelemetryLevel.USAGE && + this.experimentsEnabled = getTelemetryLevel(configurationService) === TelemetryLevel.USAGE && !environmentService.disableExperiments && !environmentService.extensionTestsLocationURI && !environmentService.enableSmokeTestDriver && configurationService.getValue('workbench.enableExperiments') === true; - super( - telemetryService.machineId, - configurationService, - productService, - environmentService, - new WorkbenchAssignmentServiceTelemetry(telemetryService, productService), - experimentsEnabled, - new MementoKeyValueStorage(new Memento('experiment.service.memento', storageService)) - ); + if (productService.tasConfig && this.experimentsEnabled) { + this.tasClient = this.setupTASClient(); + } + + this.telemetry = new WorkbenchAssignmentServiceTelemetry(telemetryService, productService); + this.keyValueStorage = new MementoKeyValueStorage(new Memento('experiment.service.memento', storageService)); + + // For development purposes, configure the delay until tas local tas treatment ovverrides are available + const overrideDelaySetting = configurationService.getValue('experiments.overrideDelay'); + const overrideDelay = typeof overrideDelaySetting === 'number' ? overrideDelaySetting : 0; + this.overrideInitDelay = timeout(overrideDelay); } - override async getTreatment(name: string): Promise { - const result = await super.getTreatment(name); + async getTreatment(name: string): Promise { + const result = await this.doGetTreatment(name); + type TASClientReadTreatmentData = { treatmentName: string; treatmentValue: string; @@ -121,12 +141,77 @@ export class WorkbenchAssignmentService extends BaseAssignmentService { treatmentName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The name of the treatment that was read' }; }; - this.telemetryService.publicLog2('tasClientReadTreatmentComplete', - { treatmentName: name, treatmentValue: JSON.stringify(result) }); + this.telemetryService.publicLog2('tasClientReadTreatmentComplete', { + treatmentName: name, + treatmentValue: JSON.stringify(result) + }); return result; } + private async doGetTreatment(name: string): Promise { + await this.overrideInitDelay; // For development purposes, allow overriding tas assignments to test variants locally. + + const override = this.configurationService.getValue(`experiments.override.${name}`); + if (override !== undefined) { + return override; + } + + if (!this.tasClient) { + return undefined; + } + + if (!this.experimentsEnabled) { + return undefined; + } + + let result: T | undefined; + const client = await this.tasClient; + + // The TAS client is initialized but we need to check if the initial fetch has completed yet + // If it is complete, return a cached value for the treatment + // If not, use the async call with `checkCache: true`. This will allow the module to return a cached value if it is present. + // Otherwise it will await the initial fetch to return the most up to date value. + if (this.networkInitialized) { + result = client.getTreatmentVariable('vscode', name); + } else { + result = await client.getTreatmentVariableAsync('vscode', name, true); + } + + result = client.getTreatmentVariable('vscode', name); + return result; + } + + private async setupTASClient(): Promise { + const targetPopulation = this.productService.quality === 'stable' ? + TargetPopulation.Public : (this.productService.quality === 'exploration' ? + TargetPopulation.Exploration : TargetPopulation.Insiders); + + const filterProvider = new AssignmentFilterProvider( + this.productService.version, + this.productService.nameLong, + this.telemetryService.machineId, + targetPopulation + ); + + const tasConfig = this.productService.tasConfig!; + const tasClient = new (await importAMDNodeModule('tas-client-umd', 'lib/tas-client-umd.js')).ExperimentationService({ + filterProviders: [filterProvider], + telemetry: this.telemetry, + storageKey: ASSIGNMENT_STORAGE_KEY, + keyValueStorage: this.keyValueStorage, + assignmentContextTelemetryPropertyName: tasConfig.assignmentContextTelemetryPropertyName, + telemetryEventName: tasConfig.telemetryEventName, + endpoint: tasConfig.endpoint, + refetchInterval: ASSIGNMENT_REFETCH_INTERVAL, + }); + + await tasClient.initializePromise; + tasClient.initialFetch.then(() => this.networkInitialized = true); + + return tasClient; + } + async getCurrentExperiments(): Promise { if (!this.tasClient) { return undefined; @@ -138,11 +223,12 @@ export class WorkbenchAssignmentService extends BaseAssignmentService { await this.tasClient; - return (this.telemetry as WorkbenchAssignmentServiceTelemetry)?.assignmentContext; + return this.telemetry.assignmentContext; } } registerSingleton(IWorkbenchAssignmentService, WorkbenchAssignmentService, InstantiationType.Delayed); + const registry = Registry.as(ConfigurationExtensions.Configuration); registry.registerConfiguration({ ...workbenchConfigurationNodeBase, From 69a7f10fc3a48fd57298192be59d5a232359ef69 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 9 Jul 2025 09:36:20 +0200 Subject: [PATCH 240/306] fix: make progressBar and progressIndicator optional, add null checks for viewWelcomeController (#254799) --- src/vs/workbench/browser/parts/views/viewPane.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts index b14236832cf..7016f737c62 100644 --- a/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -342,8 +342,8 @@ export abstract class ViewPane extends Pane implements IView { readonly menuActions: ViewMenuActions; - private progressBar!: ProgressBar; - private progressIndicator!: IProgressIndicator; + private progressBar?: ProgressBar; + private progressIndicator?: IProgressIndicator; private toolbar?: WorkbenchToolBar; private readonly showActions: ViewPaneShowActions; @@ -355,7 +355,7 @@ export abstract class ViewPane extends Pane implements IView { private iconContainer?: HTMLElement; private iconContainerHover?: IManagedHover; protected twistiesContainer?: HTMLElement; - private viewWelcomeController!: ViewWelcomeController; + private viewWelcomeController?: ViewWelcomeController; private readonly headerActionViewItems: DisposableMap = this._register(new DisposableMap()); @@ -625,7 +625,7 @@ export abstract class ViewPane extends Pane implements IView { } protected layoutBody(height: number, width: number): void { - this.viewWelcomeController.layout(height, width); + this.viewWelcomeController?.layout(height, width); } onDidScrollRoot() { @@ -634,7 +634,6 @@ export abstract class ViewPane extends Pane implements IView { getProgressIndicator() { if (this.progressBar === undefined) { - // Progress bar this.progressBar = this._register(new ProgressBar(this.element, defaultProgressBarStyles)); this.progressBar.hide(); } @@ -660,7 +659,7 @@ export abstract class ViewPane extends Pane implements IView { } focus(): void { - if (this.viewWelcomeController.enabled) { + if (this.viewWelcomeController?.enabled) { this.viewWelcomeController.focus(); } else if (this.element) { this.element.focus(); From adb41290c8b832444272a9a488afb3a04290e674 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 9 Jul 2025 10:41:30 +0200 Subject: [PATCH 241/306] refactor: remove redundant actionRunner override in ChatEditorOverlayWidget (#254797) --- .../chat/browser/chatEditing/chatEditingEditorOverlay.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts index ce1a59573c5..6afb9cce2b5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts @@ -275,10 +275,6 @@ class ChatEditorOverlayWidget extends Disposable { }); } - override get actionRunner(): IActionRunner { - return super.actionRunner; - } - protected override getTooltip(): string | undefined { const value = super.getTooltip(); if (!value) { From e3710cb6b808c1f2b968f77a490fb9617935acd5 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 9 Jul 2025 10:43:25 +0200 Subject: [PATCH 242/306] fix - update provider selection in chat setup (#254819) --- src/vs/workbench/contrib/chat/browser/chatSetup.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 76c2c513091..8177d64bde8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -1255,7 +1255,7 @@ class ChatSetupController extends Disposable { this.setStep(ChatSetupStep.SigningIn); const result = await this.signIn({ useAlternateProvider: options.useAlternateProvider }); if (!result.session) { - const provider = options.useAlternateProvider ? defaultChat.alternativeProviderId : options.useEnterpriseProvider ? defaultChat.enterpriseProviderId : defaultChat.providerName; + const provider = options.useAlternateProvider ? defaultChat.alternativeProviderId : options.useEnterpriseProvider ? defaultChat.enterpriseProviderId : defaultChat.providerId; this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider }); return undefined; // treat as cancelled because signing in already triggers an error dialog } @@ -1304,7 +1304,7 @@ class ChatSetupController extends Disposable { const wasRunning = this.context.state.installed && !this.context.state.disabled; let signUpResult: boolean | { errorCode: number } | undefined = undefined; - const provider = options.useAlternateProvider ? defaultChat.alternativeProviderId : options.useEnterpriseProvider ? defaultChat.enterpriseProviderId : defaultChat.providerName; + const provider = options.useAlternateProvider ? defaultChat.alternativeProviderId : options.useEnterpriseProvider ? defaultChat.enterpriseProviderId : defaultChat.providerId; try { From 28738da36190fbb725d63bda84d82d854e33a641 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 9 Jul 2025 11:24:35 +0200 Subject: [PATCH 243/306] managed hover handles native hover implementation (#254825) * managed hover should handle native hover impl and not widgets * :lipstick: --- .../browser/ui/actionbar/actionViewItems.ts | 15 +++---- src/vs/base/browser/ui/button/button.ts | 4 +- .../ui/highlightedlabel/highlightedLabel.ts | 15 +++---- src/vs/base/browser/ui/iconLabel/iconLabel.ts | 22 ++--------- src/vs/base/browser/ui/toggle/toggle.ts | 14 ++----- .../services/hoverService/hoverService.ts | 39 ++++++++++++++++++- src/vs/platform/opener/browser/link.ts | 4 +- 7 files changed, 55 insertions(+), 58 deletions(-) diff --git a/src/vs/base/browser/ui/actionbar/actionViewItems.ts b/src/vs/base/browser/ui/actionbar/actionViewItems.ts index 84987a24288..86b3fdcb28b 100644 --- a/src/vs/base/browser/ui/actionbar/actionViewItems.ts +++ b/src/vs/base/browser/ui/actionbar/actionViewItems.ts @@ -228,16 +228,11 @@ export class BaseActionViewItem extends Disposable implements IActionViewItem { const title = this.getTooltip() ?? ''; this.updateAriaLabel(); - if (this.options.hoverDelegate?.showNativeHover) { - /* While custom hover is not inside custom hover */ - this.element.title = title; - } else { - if (!this.customHover && title !== '') { - const hoverDelegate = this.options.hoverDelegate ?? getDefaultHoverDelegate('element'); - this.customHover = this._store.add(getBaseLayerHoverDelegate().setupManagedHover(hoverDelegate, this.element, title)); - } else if (this.customHover) { - this.customHover.update(title); - } + if (!this.customHover && title !== '') { + const hoverDelegate = this.options.hoverDelegate ?? getDefaultHoverDelegate('element'); + this.customHover = this._store.add(getBaseLayerHoverDelegate().setupManagedHover(hoverDelegate, this.element, title)); + } else if (this.customHover) { + this.customHover.update(title); } } diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index f8add4a6aab..b6f4840005e 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -333,9 +333,7 @@ export class Button extends Disposable implements IButton { } setTitle(title: string) { - if (this.options.hoverDelegate?.showNativeHover) { - this._element.title = title; - } else if (!this._hover && title !== '') { + if (!this._hover && title !== '') { this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('element'), this._element, title)); } else if (this._hover) { this._hover.update(title); diff --git a/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts b/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts index 203caa79c61..0c6cb1f1fa7 100644 --- a/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts +++ b/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts @@ -135,16 +135,11 @@ export class HighlightedLabel extends Disposable { dom.reset(this.domNode, ...children); - if (this.options?.hoverDelegate?.showNativeHover) { - /* While custom hover is not inside custom hover */ - this.domNode.title = this.title; - } else { - if (!this.customHover && this.title !== '') { - const hoverDelegate = this.options?.hoverDelegate ?? getDefaultHoverDelegate('mouse'); - this.customHover = this._register(getBaseLayerHoverDelegate().setupManagedHover(hoverDelegate, this.domNode, this.title)); - } else if (this.customHover) { - this.customHover.update(this.title); - } + if (!this.customHover && this.title !== '') { + const hoverDelegate = this.options?.hoverDelegate ?? getDefaultHoverDelegate('mouse'); + this.customHover = this._register(getBaseLayerHoverDelegate().setupManagedHover(hoverDelegate, this.domNode, this.title)); + } else if (this.customHover) { + this.customHover.update(this.title); } this.didEverRender = true; diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index 986b3792068..4d6d6431283 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -15,8 +15,6 @@ import { Range } from '../../../common/range.js'; import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js'; import type { IManagedHoverTooltipMarkdownString } from '../hover/hover.js'; import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js'; -import { isString } from '../../../common/types.js'; -import { stripIcons } from '../../../common/iconLabels.js'; import { URI } from '../../../common/uri.js'; export interface IIconLabelCreationOptions { @@ -222,23 +220,9 @@ export class IconLabel extends Disposable { hoverTarget = this.creationOptions.hoverTargetOverride; } - if (this.hoverDelegate.showNativeHover) { - function setupNativeHover(htmlElement: HTMLElement, tooltip: string | IManagedHoverTooltipMarkdownString | undefined): void { - if (isString(tooltip)) { - // Icons don't render in the native hover so we strip them out - htmlElement.title = stripIcons(tooltip); - } else if (tooltip?.markdownNotSupportedFallback) { - htmlElement.title = tooltip.markdownNotSupportedFallback; - } else { - htmlElement.removeAttribute('title'); - } - } - setupNativeHover(hoverTarget, tooltip); - } else { - const hoverDisposable = getBaseLayerHoverDelegate().setupManagedHover(this.hoverDelegate, hoverTarget, tooltip); - if (hoverDisposable) { - this.customHovers.set(htmlElement, hoverDisposable); - } + const hoverDisposable = getBaseLayerHoverDelegate().setupManagedHover(this.hoverDelegate, hoverTarget, tooltip); + if (hoverDisposable) { + this.customHovers.set(htmlElement, hoverDisposable); } } diff --git a/src/vs/base/browser/ui/toggle/toggle.ts b/src/vs/base/browser/ui/toggle/toggle.ts index 287c44c94d1..5e0aabe51c8 100644 --- a/src/vs/base/browser/ui/toggle/toggle.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -132,7 +132,7 @@ export class Toggle extends Widget { readonly domNode: HTMLElement; private _checked: boolean; - private _hover?: IManagedHover; + private _hover: IManagedHover; constructor(opts: IToggleOpts) { super(); @@ -153,11 +153,7 @@ export class Toggle extends Widget { } this.domNode = document.createElement('div'); - if (this._opts.hoverDelegate?.showNativeHover) { - this.domNode.title = this._opts.title; - } else { - this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(opts.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this.domNode, this._opts.title)); - } + this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(opts.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this.domNode, this._opts.title)); this.domNode.classList.add(...classes); if (!this._opts.notFocusable) { this.domNode.tabIndex = 0; @@ -249,11 +245,7 @@ export class Toggle extends Widget { } setTitle(newTitle: string): void { - if (this._hover) { - this._hover.update(newTitle); - } else { - this.domNode.title = newTitle; - } + this._hover.update(newTitle); this.domNode.setAttribute('aria-label', newTitle); } diff --git a/src/vs/editor/browser/services/hoverService/hoverService.ts b/src/vs/editor/browser/services/hoverService/hoverService.ts index 523378c4c79..676e4b31545 100644 --- a/src/vs/editor/browser/services/hoverService/hoverService.ts +++ b/src/vs/editor/browser/services/hoverService/hoverService.ts @@ -20,16 +20,17 @@ import { IAccessibilityService } from '../../../../platform/accessibility/common import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js'; import { mainWindow } from '../../../../base/browser/window.js'; import { ContextViewHandler } from '../../../../platform/contextview/browser/contextViewService.js'; -import type { IHoverLifecycleOptions, IHoverOptions, IHoverWidget, IManagedHover, IManagedHoverContentOrFactory, IManagedHoverOptions } from '../../../../base/browser/ui/hover/hover.js'; +import { isManagedHoverTooltipMarkdownString, type IHoverLifecycleOptions, type IHoverOptions, type IHoverWidget, type IManagedHover, type IManagedHoverContentOrFactory, type IManagedHoverOptions } from '../../../../base/browser/ui/hover/hover.js'; import type { IHoverDelegate, IHoverDelegateTarget } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { ManagedHoverWidget } from './updatableHoverWidget.js'; import { timeout, TimeoutTimer } from '../../../../base/common/async.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { isNumber } from '../../../../base/common/types.js'; +import { isNumber, isString } from '../../../../base/common/types.js'; import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { EditorContextKeys } from '../../../common/editorContextKeys.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; +import { stripIcons } from '../../../../base/common/iconLabels.js'; export class HoverService extends Disposable implements IHoverService { declare readonly _serviceBrand: undefined; @@ -368,6 +369,10 @@ export class HoverService extends Disposable implements IHoverService { // TODO: Investigate performance of this function. There seems to be a lot of content created // and thrown away on start up setupManagedHover(hoverDelegate: IHoverDelegate, targetElement: HTMLElement, content: IManagedHoverContentOrFactory, options?: IManagedHoverOptions | undefined): IManagedHover { + if (hoverDelegate.showNativeHover) { + return setupNativeHover(targetElement, content); + } + targetElement.setAttribute('custom-hover', 'true'); if (targetElement.title !== '') { @@ -520,6 +525,36 @@ function getHoverIdFromContent(content: string | HTMLElement | IMarkdownString): return content.value; } +function getStringContent(contentOrFactory: IManagedHoverContentOrFactory): string | undefined { + const content = typeof contentOrFactory === 'function' ? contentOrFactory() : contentOrFactory; + if (isString(content)) { + // Icons don't render in the native hover so we strip them out + return stripIcons(content); + } + if (isManagedHoverTooltipMarkdownString(content)) { + return content.markdownNotSupportedFallback; + } + return undefined; +} + +function setupNativeHover(targetElement: HTMLElement, content: IManagedHoverContentOrFactory): IManagedHover { + function updateTitle(title: string | undefined) { + if (title) { + targetElement.setAttribute('title', title); + } else { + targetElement.removeAttribute('title'); + } + } + + updateTitle(getStringContent(content)); + return { + update: (content) => updateTitle(getStringContent(content)), + show: () => { }, + hide: () => { }, + dispose: () => updateTitle(undefined), + }; +} + class HoverContextViewDelegate implements IDelegate { // Render over all other context views diff --git a/src/vs/platform/opener/browser/link.ts b/src/vs/platform/opener/browser/link.ts index ff830dc05f2..848727fea0b 100644 --- a/src/vs/platform/opener/browser/link.ts +++ b/src/vs/platform/opener/browser/link.ts @@ -128,9 +128,7 @@ export class Link extends Disposable { } private setTooltip(title: string | undefined): void { - if (this.hoverDelegate.showNativeHover) { - this.el.title = title ?? ''; - } else if (!this.hover && title) { + if (!this.hover && title) { this.hover = this._register(this._hoverService.setupManagedHover(this.hoverDelegate, this.el, title)); } else if (this.hover) { this.hover.update(title); From 0f51f4269fa373831d1235e3756c2cd3545e7e3d Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 9 Jul 2025 11:37:48 +0200 Subject: [PATCH 244/306] Log typing speed (#253656) * log typing speed * include character count as reference for reliable speed computation and include more characters if not many in recent session window * call it typing interval --- src/vs/editor/common/languages.ts | 2 + .../browser/model/inlineCompletionsModel.ts | 7 + .../browser/model/provideInlineCompletions.ts | 4 + .../browser/model/typingSpeed.ts | 229 ++++++++++++++++++ src/vs/monaco.d.ts | 2 + .../api/browser/mainThreadLanguageFeatures.ts | 6 + 6 files changed, 250 insertions(+) create mode 100644 src/vs/editor/contrib/inlineCompletions/browser/model/typingSpeed.ts diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 6628ca9c2a9..54e9a802648 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -1002,6 +1002,8 @@ export type LifetimeSummary = { characterCountModified?: number; disjointReplacements?: number; sameShapeReplacements?: boolean; + typingInterval: number; + typingIntervalCharacterCount: number; }; export interface CodeAction { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index ee8ef49336e..e206d0376c3 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -47,6 +47,7 @@ import { TextModelEditReason, EditReasons } from '../../../../common/textModelEd import { ICodeEditorService } from '../../../../browser/services/codeEditorService.js'; import { InlineCompletionViewData, InlineCompletionViewKind } from '../view/inlineEdits/inlineEditsViewInterface.js'; import { IInlineCompletionsService } from '../../../../browser/services/inlineCompletionsService.js'; +import { TypingInterval } from './typingSpeed.js'; export class InlineCompletionsModel extends Disposable { private readonly _source; @@ -84,6 +85,8 @@ export class InlineCompletionsModel extends Disposable { private readonly _editorObs; + private readonly _typing: TypingInterval; + private readonly _suggestPreviewEnabled; private readonly _suggestPreviewMode; private readonly _inlineSuggestMode; @@ -120,6 +123,7 @@ export class InlineCompletionsModel extends Disposable { this._inlineEditsEnabled = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !!v.edits.enabled); this._inlineEditsShowCollapsedEnabled = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.showCollapsed); this._triggerCommandOnProviderChange = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.experimental.triggerCommandOnProviderChange); + this._typing = this._register(new TypingInterval(this.textModel)); this._register(this._inlineCompletionsService.onDidChangeIsSnoozing((isSnoozing) => { if (isSnoozing) { @@ -360,11 +364,14 @@ export class InlineCompletionsModel extends Disposable { reason += reason.length > 0 ? `:${changeSummary.changeReason}` : changeSummary.changeReason; } + const typingInterval = this._typing.getTypingInterval(); const requestInfo: InlineSuggestRequestInfo = { editorType: this.editorType, startTime: Date.now(), languageId: this.textModel.getLanguageId(), reason, + typingInterval: typingInterval.averageInterval, + typingIntervalCharacterCount: typingInterval.characterCount, }; let context: InlineCompletionContextWithoutUuid = { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts index 307a4e3762b..64642022e7d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts @@ -245,6 +245,8 @@ export type InlineSuggestRequestInfo = { editorType: InlineCompletionEditorType; languageId: string; reason: string; + typingInterval: number; + typingIntervalCharacterCount: number; }; export type InlineSuggestViewData = { @@ -351,6 +353,8 @@ export class InlineSuggestData { requestReason: this._requestInfo.reason, viewKind: this._viewData.viewKind, error: this._viewData.error, + typingInterval: this._requestInfo.typingInterval, + typingIntervalCharacterCount: this._requestInfo.typingIntervalCharacterCount, ...this._viewData.renderData, }; this.source.provider.handleEndOfLifetime(this.source.inlineSuggestions, this.sourceInlineCompletion, reason, summary); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/typingSpeed.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/typingSpeed.ts new file mode 100644 index 00000000000..9e42d5db345 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/typingSpeed.ts @@ -0,0 +1,229 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { sum } from '../../../../../base/common/arrays.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { ITextModel } from '../../../../common/model.js'; +import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js'; + +interface TypingSession { + startTime: number; + endTime: number; + characterCount: number; // Effective character count for typing interval calculation +} + +interface TypingIntervalResult { + averageInterval: number; // Average milliseconds between keystrokes + characterCount: number; // Number of characters involved in the computation +} + +/** + * Tracks typing speed as average milliseconds between keystrokes. + * Higher values indicate slower typing. + */ +export class TypingInterval extends Disposable { + + private readonly _typingSessions: TypingSession[] = []; + private _currentSession: TypingSession | null = null; + private _lastChangeTime = 0; + private _cachedTypingIntervalResult: TypingIntervalResult | null = null; + private _cacheInvalidated = true; + + // Configuration constants + private static readonly MAX_SESSION_GAP_MS = 3_000; // 3 seconds max gap between keystrokes in a session + private static readonly MIN_SESSION_DURATION_MS = 1_000; // Minimum session duration to consider + private static readonly SESSION_HISTORY_LIMIT = 50; // Keep last 50 sessions for calculation + private static readonly TYPING_SPEED_WINDOW_MS = 300_000; // 5 minutes window for speed calculation + private static readonly MIN_CHARS_FOR_RELIABLE_SPEED = 20; // Minimum characters needed for reliable speed calculation + + /** + * Gets the current typing interval as average milliseconds between keystrokes + * and the number of characters involved in the computation. + * Higher interval values indicate slower typing. + * Returns { interval: 0, characterCount: 0 } if no typing data is available. + */ + public getTypingInterval(): TypingIntervalResult { + if (this._cacheInvalidated || this._cachedTypingIntervalResult === null) { + this._cachedTypingIntervalResult = this._calculateTypingInterval(); + this._cacheInvalidated = false; + } + return this._cachedTypingIntervalResult; + } + + constructor(private readonly _textModel: ITextModel) { + super(); + + this._register(this._textModel.onDidChangeContent(e => this._updateTypingSpeed(e))); + } + + private _updateTypingSpeed(change: IModelContentChangedEvent): void { + const now = Date.now(); + const characterCount = this._calculateEffectiveCharacterCount(change); + + // If too much time has passed since last change, start a new session + if (this._currentSession && (now - this._lastChangeTime) > TypingInterval.MAX_SESSION_GAP_MS) { + this._finalizeCurrentSession(); + } + + // Start new session if none exists + if (!this._currentSession) { + this._currentSession = { + startTime: now, + endTime: now, + characterCount: 0 + }; + } + + // Update current session + this._currentSession.endTime = now; + this._currentSession.characterCount += characterCount; + + this._lastChangeTime = now; + this._cacheInvalidated = true; + } + + private _calculateEffectiveCharacterCount(change: IModelContentChangedEvent): number { + const actualCharCount = this._getActualCharacterCount(change); + + // If this is actual user typing, count all characters + if (this._isUserTyping(change)) { + return actualCharCount; + } + + // For all other actions (paste, suggestions, etc.), count as 1 regardless of size + return actualCharCount > 0 ? 1 : 0; + } + + private _getActualCharacterCount(change: IModelContentChangedEvent): number { + let totalChars = 0; + for (const c of change.changes) { + // Count characters added or removed (use the larger of the two) + totalChars += Math.max(c.text.length, c.rangeLength); + } + return totalChars; + } + + private _isUserTyping(change: IModelContentChangedEvent): boolean { + // If no detailed reasons, assume user typing + if (!change.detailedReasons || change.detailedReasons.length === 0) { + return true; + } + + // Check if any of the reasons indicate actual user typing + for (const reason of change.detailedReasons) { + if (this._isUserTypingReason(reason)) { + return true; + } + } + + return false; + } + + private _isUserTypingReason(reason: any): boolean { + // Handle undo/redo - not considered user typing + if (reason.metadata.isUndoing || reason.metadata.isRedoing) { + return false; + } + + // Handle different source types + switch (reason.metadata.source) { + case 'cursor': { + // Direct user input via cursor + const kind = reason.metadata.kind; + return kind === 'type' || kind === 'compositionType' || kind === 'compositionEnd'; + } + + default: + // All other sources (paste, suggestions, code actions, etc.) are not user typing + return false; + } + } + + private _finalizeCurrentSession(): void { + if (!this._currentSession) { + return; + } + + const sessionDuration = this._currentSession.endTime - this._currentSession.startTime; + + // Only keep sessions that meet minimum duration and have actual content + if (sessionDuration >= TypingInterval.MIN_SESSION_DURATION_MS && this._currentSession.characterCount > 0) { + this._typingSessions.push(this._currentSession); + + // Limit session history + if (this._typingSessions.length > TypingInterval.SESSION_HISTORY_LIMIT) { + this._typingSessions.shift(); + } + } + + this._currentSession = null; + } + + private _calculateTypingInterval(): TypingIntervalResult { + // Finalize current session for calculation + if (this._currentSession) { + const tempSession = { ...this._currentSession }; + const sessionDuration = tempSession.endTime - tempSession.startTime; + if (sessionDuration >= TypingInterval.MIN_SESSION_DURATION_MS && tempSession.characterCount > 0) { + const allSessions = [...this._typingSessions, tempSession]; + return this._calculateSpeedFromSessions(allSessions); + } + } + + return this._calculateSpeedFromSessions(this._typingSessions); + } + + private _calculateSpeedFromSessions(sessions: TypingSession[]): TypingIntervalResult { + if (sessions.length === 0) { + return { averageInterval: 0, characterCount: 0 }; + } + + // Sort sessions by recency (most recent first) to ensure we get the most recent sessions + const sortedSessions = [...sessions].sort((a, b) => b.endTime - a.endTime); + + // First, try the standard window + const cutoffTime = Date.now() - TypingInterval.TYPING_SPEED_WINDOW_MS; + const recentSessions = sortedSessions.filter(session => session.endTime > cutoffTime); + const olderSessions = sortedSessions.splice(recentSessions.length); + + let totalChars = sum(recentSessions.map(session => session.characterCount)); + + // If we don't have enough characters in the standard window, expand to include older sessions + for (let i = 0; i < olderSessions.length && totalChars < TypingInterval.MIN_CHARS_FOR_RELIABLE_SPEED; i++) { + recentSessions.push(olderSessions[i]); + totalChars += olderSessions[i].characterCount; + } + + const totalTime = sum(recentSessions.map(session => session.endTime - session.startTime)); + if (totalTime === 0 || totalChars <= 1) { + return { averageInterval: 0, characterCount: totalChars }; + } + + // Calculate average milliseconds between keystrokes + const keystrokeIntervals = Math.max(1, totalChars - 1); + const avgMsBetweenKeystrokes = totalTime / keystrokeIntervals; + + return { + averageInterval: Math.round(avgMsBetweenKeystrokes), + characterCount: totalChars + }; + } + + /** + * Reset all typing speed data + */ + public reset(): void { + this._typingSessions.length = 0; + this._currentSession = null; + this._lastChangeTime = 0; + this._cachedTypingIntervalResult = null; + this._cacheInvalidated = true; + } + + public override dispose(): void { + this._finalizeCurrentSession(); + super.dispose(); + } +} diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 80dec01eee6..c75078e1976 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7562,6 +7562,8 @@ declare namespace monaco.languages { characterCountModified?: number; disjointReplacements?: number; sameShapeReplacements?: boolean; + typingInterval: number; + typingIntervalCharacterCount: number; }; export interface CodeAction { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 394cabe3f53..cacae156aa7 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -657,6 +657,8 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread viewKind: lifetimeSummary.viewKind, requestReason: lifetimeSummary.requestReason, error: lifetimeSummary.error, + typingInterval: lifetimeSummary.typingInterval, + typingIntervalCharacterCount: lifetimeSummary.typingIntervalCharacterCount, languageId: lifetimeSummary.languageId, cursorColumnDistance: lifetimeSummary.cursorColumnDistance, cursorLineDistance: lifetimeSummary.cursorLineDistance, @@ -1309,6 +1311,8 @@ type InlineCompletionEndOfLifeEvent = { requestReason: string; languageId: string; error: string | undefined; + typingInterval: number; + typingIntervalCharacterCount: number; superseded: boolean; editorType: string; viewKind: string | undefined; @@ -1337,6 +1341,8 @@ type InlineCompletionsEndOfLifeClassification = { languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language ID of the document where the inline completion was shown' }; requestReason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The reason for the inline completion request' }; error: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The error message if the inline completion failed' }; + typingInterval: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The average typing interval of the user at the moment the inline completion was requested' }; + typingIntervalCharacterCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The character count involved in the typing interval calculation' }; superseded: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the inline completion was superseded by another one' }; editorType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of the editor where the inline completion was shown' }; viewKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The kind of the view where the inline completion was shown' }; From 8bd52a173e3578dd5a71bffa11d520111b0a294b Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:33:37 +0200 Subject: [PATCH 245/306] Redirect to vscode.dev/redirect and then vscode://.... (#254841) So that the browser remembers who triggered the redirect. Fixes https://github.com/microsoft/vscode/issues/254810 --- extensions/github-authentication/media/index.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/github-authentication/media/index.html b/extensions/github-authentication/media/index.html index 16712a2e5f7..385aa8991f1 100644 --- a/extensions/github-authentication/media/index.html +++ b/extensions/github-authentication/media/index.html @@ -47,15 +47,17 @@ document.querySelector('.error-message > .detail').textContent = error; document.querySelector('body').classList.add('error'); } else if (redirectUri) { + // Wrap the redirect URI so that the browser remembers who triggered the redirect + const wrappedRedirectUri = `https://vscode.dev/redirect?url=${encodeURIComponent(redirectUri)}`; // Set up the fallback link const fallbackLink = document.getElementById('fallback-link'); if (fallbackLink) { - fallbackLink.href = redirectUri; + fallbackLink.href = wrappedRedirectUri; } // Redirect after a delay setTimeout(() => { - window.location = redirectUri; + window.location = wrappedRedirectUri; }, 1000); } From fe3803eb3a4069445cda6712b1d455862b49b63b Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:44:43 +0200 Subject: [PATCH 246/306] Report snooze duration for inline completions (#254848) report snooze duration --- .../services/inlineCompletionsService.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/browser/services/inlineCompletionsService.ts b/src/vs/editor/browser/services/inlineCompletionsService.ts index f060fbddfb2..33bb81c2e51 100644 --- a/src/vs/editor/browser/services/inlineCompletionsService.ts +++ b/src/vs/editor/browser/services/inlineCompletionsService.ts @@ -14,6 +14,7 @@ import { InstantiationType, registerSingleton } from '../../../platform/instanti import { createDecorator, ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js'; import { IQuickInputService, IQuickPickItem } from '../../../platform/quickinput/common/quickInput.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js'; export const IInlineCompletionsService = createDecorator('IInlineCompletionsService'); @@ -69,7 +70,10 @@ export class InlineCompletionsService extends Disposable implements IInlineCompl private _timer: WindowIntervalTimer; - constructor(@IContextKeyService private _contextKeyService: IContextKeyService) { + constructor( + @IContextKeyService private _contextKeyService: IContextKeyService, + @ITelemetryService private _telemetryService: ITelemetryService, + ) { super(); this._timer = this._register(new WindowIntervalTimer()); @@ -92,6 +96,8 @@ export class InlineCompletionsService extends Disposable implements IInlineCompl } const wasSnoozing = this.isSnoozing(); + const timeLeft = this.snoozeTimeLeft; + this._snoozeTimeEnd = Date.now() + durationMs; if (!wasSnoozing) { @@ -108,6 +114,8 @@ export class InlineCompletionsService extends Disposable implements IInlineCompl }, this.snoozeTimeLeft + 1, ); + + this._reportSnooze(durationMs - timeLeft, durationMs); } isSnoozing(): boolean { @@ -116,11 +124,28 @@ export class InlineCompletionsService extends Disposable implements IInlineCompl cancelSnooze(): void { if (this.isSnoozing()) { + this._reportSnooze(-this.snoozeTimeLeft, 0); this._snoozeTimeEnd = undefined; this._timer.cancel(); this._onDidChangeIsSnoozing.fire(false); } } + + private _reportSnooze(deltaMs: number, totalMs: number): void { + const deltaSeconds = Math.round(deltaMs / 1000); + const totalSeconds = Math.round(totalMs / 1000); + type WorkspaceStatsClassification = { + owner: 'benibenj'; + comment: 'Snooze duration for inline completions'; + deltaSeconds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The duration by which the snooze has changed, in seconds.' }; + totalSeconds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The total duration for which inline completions are snoozed, in seconds.' }; + }; + type WorkspaceStatsEvent = { + deltaSeconds: number; + totalSeconds: number; + }; + this._telemetryService.publicLog2('inlineCompletions.snooze', { deltaSeconds, totalSeconds }); + } } registerSingleton(IInlineCompletionsService, InlineCompletionsService, InstantiationType.Delayed); From 1dc489a51970ff7805404540a45e9a303e07497d Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 9 Jul 2025 13:59:32 +0200 Subject: [PATCH 247/306] Allow also minimum and maximum on editor float options (#254856) allowing also minimum and maximum on editor float options --- src/vs/editor/common/config/editorOptions.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 6ca3a0b619d..88a8b184032 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -1256,6 +1256,9 @@ export function clampedFloat(value: any, defaultValue: T, mini class EditorFloatOption extends SimpleEditorOption { + public readonly minimum: number | undefined; + public readonly maximum: number | undefined; + public static clamp(n: number, min: number, max: number): number { if (n < min) { return min; @@ -1279,13 +1282,17 @@ class EditorFloatOption extends SimpleEditorOption number; - constructor(id: K, name: PossibleKeyName, defaultValue: number, validationFn: (value: number) => number, schema?: IConfigurationPropertySchema) { + constructor(id: K, name: PossibleKeyName, defaultValue: number, validationFn: (value: number) => number, schema?: IConfigurationPropertySchema, minimum?: number, maximum?: number) { if (typeof schema !== 'undefined') { schema.type = 'number'; schema.default = defaultValue; + schema.minimum = minimum; + schema.maximum = maximum; } super(id, name, defaultValue, schema); this.validationFn = validationFn; + this.minimum = minimum; + this.maximum = maximum; } public override validate(input: any): number { @@ -3203,7 +3210,9 @@ class EditorLineHeight extends EditorFloatOption { EditorOption.lineHeight, 'lineHeight', EDITOR_FONT_DEFAULTS.lineHeight, x => EditorFloatOption.clamp(x, 0, 150), - { markdownDescription: nls.localize('lineHeight', "Controls the line height. \n - Use 0 to automatically compute the line height from the font size.\n - Values between 0 and 8 will be used as a multiplier with the font size.\n - Values greater than or equal to 8 will be used as effective values.") } + { markdownDescription: nls.localize('lineHeight', "Controls the line height. \n - Use 0 to automatically compute the line height from the font size.\n - Values between 0 and 8 will be used as a multiplier with the font size.\n - Values greater than or equal to 8 will be used as effective values.") }, + 0, + 150 ); } From b3f848b4911cf61f3fe8dbccfc8c1910b01d1667 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 9 Jul 2025 14:28:55 +0200 Subject: [PATCH 248/306] add mcp servers feature contribution (#254862) --- .../contrib/mcp/common/mcpConfiguration.ts | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts index 97b463f6b7d..48c3e2eecf7 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts @@ -3,11 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { IJSONSchema, IJSONSchemaMap } from '../../../../base/common/jsonSchema.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; import { localize } from '../../../../nls.js'; -import { IMcpCollectionContribution } from '../../../../platform/extensions/common/extensions.js'; +import { IExtensionManifest, IMcpCollectionContribution } from '../../../../platform/extensions/common/extensions.js'; +import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; import { mcpSchemaId } from '../../../services/configuration/common/configuration.js'; import { inputsSchema } from '../../../services/configurationResolver/common/configurationResolverSchema.js'; +import { Extensions, IExtensionFeaturesRegistry, IExtensionFeatureTableRenderer, IRenderedData, IRowData, ITableData } from '../../../services/extensionManagement/common/extensionFeatures.js'; import { IExtensionPointDescriptor } from '../../../services/extensions/common/extensionsRegistry.js'; const mcpActivationEventPrefix = 'onMcpCollection:'; @@ -239,3 +244,42 @@ export const mcpContributionPoint: IExtensionPointDescriptor 0; + } + + render(manifest: IExtensionManifest): IRenderedData { + const mcpServerDefinitionProviders = manifest.contributes?.mcpServerDefinitionProviders ?? []; + const headers = [localize('id', "ID"), localize('name', "Name")]; + const rows: IRowData[][] = mcpServerDefinitionProviders + .map(mcpServerDefinitionProvider => { + return [ + new MarkdownString().appendMarkdown(`\`${mcpServerDefinitionProvider.id}\``), + mcpServerDefinitionProvider.label + ]; + }); + + return { + data: { + headers, + rows + }, + dispose: () => { } + }; + } +} + +Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: mcpConfigurationSection, + label: localize('mcpServerDefinitionProviders', "MCP Servers"), + access: { + canToggle: false + }, + renderer: new SyncDescriptor(McpServerDefinitionsProviderRenderer), +}); + From 87113bc25fa4b174439b0efa089de473e8146714 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 9 Jul 2025 14:55:15 +0200 Subject: [PATCH 249/306] add button to browse mcp servers in welcome view (#254863) --- .../contrib/mcp/browser/mcpServersView.ts | 15 +++++++++++++-- .../contrib/mcp/browser/media/mcpServersView.css | 8 +++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts index f54a3bd4327..ad5a7ec18b7 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts @@ -41,6 +41,8 @@ import { VIEW_CONTAINER } from '../../extensions/browser/extensions.contribution import { renderMarkdown } from '../../../../base/browser/markdownRenderer.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; +import { Button } from '../../../../base/browser/ui/button/button.js'; +import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; export interface McpServerListViewOptions { showWelcomeOnEmpty?: boolean; @@ -207,9 +209,8 @@ export class McpServersListView extends ViewPane { title.textContent = localize('mcp.welcome.title', "MCP Servers"); const description = dom.append(welcomeContent, dom.$('.mcp-welcome-description')); - const browseUrl = this.productService.quality === 'insider' ? 'https://code.visualstudio.com/insider/mcp' : 'https://code.visualstudio.com/mcp'; const markdownResult = this._register(renderMarkdown(new MarkdownString( - localize('mcp.welcome.descriptionWithLink', "Extend agent mode by installing [MCP servers]({0}) to bring extra tools for connecting to databases, invoking APIs and performing specialized tasks.", browseUrl), + localize('mcp.welcome.descriptionWithLink', "Extend agent mode by installing MCP servers to bring extra tools for connecting to databases, invoking APIs and performing specialized tasks."), { isTrusted: true } ), { actionHandler: { @@ -220,6 +221,16 @@ export class McpServersListView extends ViewPane { } })); description.appendChild(markdownResult.element); + + // Browse button + const buttonContainer = dom.append(welcomeContent, dom.$('.mcp-welcome-button-container')); + const button = this._register(new Button(buttonContainer, { + title: localize('mcp.welcome.browseButton', "Browse MCP Servers"), + ...defaultButtonStyles + })); + button.label = localize('mcp.welcome.browseButton', "Browse MCP Servers"); + + this._register(button.onDidClick(() => this.openerService.open(URI.parse(this.productService.quality === 'insider' ? 'https://code.visualstudio.com/insider/mcp' : 'https://code.visualstudio.com/mcp')))); } private async query(query: string): Promise { diff --git a/src/vs/workbench/contrib/mcp/browser/media/mcpServersView.css b/src/vs/workbench/contrib/mcp/browser/media/mcpServersView.css index bab1e75db0e..547e8f20582 100644 --- a/src/vs/workbench/contrib/mcp/browser/media/mcpServersView.css +++ b/src/vs/workbench/contrib/mcp/browser/media/mcpServersView.css @@ -36,12 +36,18 @@ .mcp-welcome-description { max-width: 350px; padding: 0 20px; - margin-top: 26px; + margin-top: 16px; a { color: var(--vscode-textLink-foreground); } } + .mcp-welcome-button-container { + margin-top: 16px; + max-width: 320px; + width: 100%; + } + } } From 08b4bcb3078eb5b1d17f0e86844a3586ba68e2f4 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 9 Jul 2025 14:59:22 +0200 Subject: [PATCH 250/306] Ignore mouse events when context menu is shown (#254864) ignore mouse events when context menu is shown --- .../contextmenu/browser/contextmenu.ts | 11 ------- .../hover/browser/contentHoverController.ts | 29 +++++++++++++++++++ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts index 756e14ca80f..d7447d18bd3 100644 --- a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts @@ -196,14 +196,6 @@ export class ContextMenuController implements IEditorContribution { return; } - // Disable hover - const oldHoverSetting = this._editor.getOption(EditorOption.hover); - this._editor.updateOptions({ - hover: { - enabled: false - } - }); - let anchor: IMouseEvent | IAnchor | null = event; if (!anchor) { // Ensure selection is visible @@ -251,9 +243,6 @@ export class ContextMenuController implements IEditorContribution { onHide: (wasCancelled: boolean) => { this._contextMenuIsBeingShownCount--; - this._editor.updateOptions({ - hover: oldHoverSetting - }); } }); } diff --git a/src/vs/editor/contrib/hover/browser/contentHoverController.ts b/src/vs/editor/contrib/hover/browser/contentHoverController.ts index c4c3bfd82ab..0cf939159b7 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverController.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverController.ts @@ -23,6 +23,7 @@ import './hover.css'; import { Emitter } from '../../../../base/common/event.js'; import { isOnColorDecorator } from '../../colorPicker/browser/hoverColorPicker/hoverColorPicker.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; // sticky hover widget which doesn't disappear on focus out and such const _sticky = false @@ -54,8 +55,11 @@ export class ContentHoverController extends Disposable implements IEditorContrib private _hoverSettings!: IHoverSettings; private _isMouseDown: boolean = false; + private _ignoreMouseEvents: boolean = false; + constructor( private readonly _editor: ICodeEditor, + @IContextMenuService _contextMenuService: IContextMenuService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IKeybindingService private readonly _keybindingService: IKeybindingService ) { @@ -67,6 +71,13 @@ export class ContentHoverController extends Disposable implements IEditorContrib } }, 0 )); + this._register(_contextMenuService.onDidShowContextMenu(() => { + this.hideContentHover(); + this._ignoreMouseEvents = true; + })); + this._register(_contextMenuService.onDidHideContextMenu(() => { + this._ignoreMouseEvents = false; + })); this._hookListeners(); this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { if (e.hasChanged(EditorOption.hover)) { @@ -115,12 +126,18 @@ export class ContentHoverController extends Disposable implements IEditorContrib } private _onEditorScrollChanged(e: IScrollEvent): void { + if (this._ignoreMouseEvents) { + return; + } if (e.scrollTopChanged || e.scrollLeftChanged) { this.hideContentHover(); } } private _onEditorMouseDown(mouseEvent: IEditorMouseEvent): void { + if (this._ignoreMouseEvents) { + return; + } this._isMouseDown = true; const shouldKeepHoverWidgetVisible = this._shouldKeepHoverWidgetVisible(mouseEvent); if (shouldKeepHoverWidgetVisible) { @@ -141,10 +158,16 @@ export class ContentHoverController extends Disposable implements IEditorContrib } private _onEditorMouseUp(): void { + if (this._ignoreMouseEvents) { + return; + } this._isMouseDown = false; } private _onEditorMouseLeave(mouseEvent: IPartialEditorMouseEvent): void { + if (this._ignoreMouseEvents) { + return; + } if (this.shouldKeepOpenOnEditorMouseMoveOrLeave) { return; } @@ -198,6 +221,9 @@ export class ContentHoverController extends Disposable implements IEditorContrib } private _onEditorMouseMove(mouseEvent: IEditorMouseEvent): void { + if (this._ignoreMouseEvents) { + return; + } this._mouseMoveEvent = mouseEvent; const shouldKeepCurrentHover = this._shouldKeepCurrentHover(mouseEvent); if (shouldKeepCurrentHover) { @@ -236,6 +262,9 @@ export class ContentHoverController extends Disposable implements IEditorContrib } private _onKeyDown(e: IKeyboardEvent): void { + if (this._ignoreMouseEvents) { + return; + } if (!this._contentWidget) { return; } From 97c9f5c08ed96abfbe8b1917a9a00073a5377302 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 9 Jul 2025 15:05:48 +0200 Subject: [PATCH 251/306] refactoring : extracting coordinatesConverter code to a separate file (#254616) * refactoring * resolving merge conflict * adding change --- .../browser/view/viewUserInputEvents.ts | 2 +- src/vs/editor/common/coordinatesConverter.ts | 105 ++++++++++++++++++ src/vs/editor/common/cursor/cursor.ts | 2 +- src/vs/editor/common/cursor/cursorContext.ts | 2 +- src/vs/editor/common/viewModel.ts | 25 +---- .../common/viewModel/viewModelDecorations.ts | 2 +- .../editor/common/viewModel/viewModelImpl.ts | 3 +- .../editor/common/viewModel/viewModelLines.ts | 79 +------------ .../multicursor/notebookMulticursor.ts | 2 +- 9 files changed, 117 insertions(+), 105 deletions(-) create mode 100644 src/vs/editor/common/coordinatesConverter.ts diff --git a/src/vs/editor/browser/view/viewUserInputEvents.ts b/src/vs/editor/browser/view/viewUserInputEvents.ts index 8a8bf0aa8ff..817af6a30fb 100644 --- a/src/vs/editor/browser/view/viewUserInputEvents.ts +++ b/src/vs/editor/browser/view/viewUserInputEvents.ts @@ -5,9 +5,9 @@ import { IKeyboardEvent } from '../../../base/browser/keyboardEvent.js'; import { IEditorMouseEvent, IMouseTarget, IMouseTargetViewZoneData, IPartialEditorMouseEvent, MouseTargetType } from '../editorBrowser.js'; -import { ICoordinatesConverter } from '../../common/viewModel.js'; import { IMouseWheelEvent } from '../../../base/browser/mouseEvent.js'; import { Position } from '../../common/core/position.js'; +import { ICoordinatesConverter } from '../../common/coordinatesConverter.js'; export interface EventCallback { (event: T): void; diff --git a/src/vs/editor/common/coordinatesConverter.ts b/src/vs/editor/common/coordinatesConverter.ts new file mode 100644 index 00000000000..dd7cd41995d --- /dev/null +++ b/src/vs/editor/common/coordinatesConverter.ts @@ -0,0 +1,105 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Position } from './core/position.js'; +import { Range } from './core/range.js'; +import { ITextModel, PositionAffinity } from './model.js'; + +export interface ICoordinatesConverter { + // View -> Model conversion and related methods + convertViewPositionToModelPosition(viewPosition: Position): Position; + convertViewRangeToModelRange(viewRange: Range): Range; + validateViewPosition(viewPosition: Position, expectedModelPosition: Position): Position; + validateViewRange(viewRange: Range, expectedModelRange: Range): Range; + + // Model -> View conversion and related methods + /** + * @param allowZeroLineNumber Should it return 0 when there are hidden lines at the top and the position is in the hidden area? + * @param belowHiddenRanges When the model position is in a hidden area, should it return the first view position after or before? + */ + convertModelPositionToViewPosition(modelPosition: Position, affinity?: PositionAffinity, allowZeroLineNumber?: boolean, belowHiddenRanges?: boolean): Position; + /** + * @param affinity Only has an effect if the range is empty. + */ + convertModelRangeToViewRange(modelRange: Range, affinity?: PositionAffinity): Range; + modelPositionIsVisible(modelPosition: Position): boolean; + getModelLineViewLineCount(modelLineNumber: number): number; + getViewLineNumberOfModelPosition(modelLineNumber: number, modelColumn: number): number; +} + +export class IdentityCoordinatesConverter implements ICoordinatesConverter { + + private readonly _model: ITextModel; + + constructor(model: ITextModel) { + this._model = model; + } + + private _validPosition(pos: Position): Position { + return this._model.validatePosition(pos); + } + + private _validRange(range: Range): Range { + return this._model.validateRange(range); + } + + // View -> Model conversion and related methods + + public convertViewPositionToModelPosition(viewPosition: Position): Position { + return this._validPosition(viewPosition); + } + + public convertViewRangeToModelRange(viewRange: Range): Range { + return this._validRange(viewRange); + } + + public validateViewPosition(_viewPosition: Position, expectedModelPosition: Position): Position { + return this._validPosition(expectedModelPosition); + } + + public validateViewRange(_viewRange: Range, expectedModelRange: Range): Range { + return this._validRange(expectedModelRange); + } + + // Model -> View conversion and related methods + + public convertModelPositionToViewPosition(modelPosition: Position): Position { + return this._validPosition(modelPosition); + } + + public convertModelRangeToViewRange(modelRange: Range): Range { + return this._validRange(modelRange); + } + + public modelPositionIsVisible(modelPosition: Position): boolean { + const lineCount = this._model.getLineCount(); + if (modelPosition.lineNumber < 1 || modelPosition.lineNumber > lineCount) { + // invalid arguments + return false; + } + return true; + } + + public modelRangeIsVisible(modelRange: Range): boolean { + const lineCount = this._model.getLineCount(); + if (modelRange.startLineNumber < 1 || modelRange.startLineNumber > lineCount) { + // invalid arguments + return false; + } + if (modelRange.endLineNumber < 1 || modelRange.endLineNumber > lineCount) { + // invalid arguments + return false; + } + return true; + } + + public getModelLineViewLineCount(modelLineNumber: number): number { + return 1; + } + + public getViewLineNumberOfModelPosition(modelLineNumber: number, modelColumn: number): number { + return modelLineNumber; + } +} diff --git a/src/vs/editor/common/cursor/cursor.ts b/src/vs/editor/common/cursor/cursor.ts index b413a448457..0078457a348 100644 --- a/src/vs/editor/common/cursor/cursor.ts +++ b/src/vs/editor/common/cursor/cursor.ts @@ -20,9 +20,9 @@ import { ITextModel, TrackedRangeStickiness, IModelDeltaDecoration, ICursorState import { RawContentChangedType, ModelInjectedTextChangedEvent, InternalModelContentChangeEvent } from '../textModelEvents.js'; import { VerticalRevealType, ViewCursorStateChangedEvent, ViewRevealRangeRequestEvent } from '../viewEvents.js'; import { dispose, Disposable } from '../../../base/common/lifecycle.js'; -import { ICoordinatesConverter } from '../viewModel.js'; import { CursorStateChangedEvent, ViewModelEventsCollector } from '../viewModelEventDispatcher.js'; import { TextModelEditReason, EditReasons } from '../textModelEditReason.js'; +import { ICoordinatesConverter } from '../coordinatesConverter.js'; export class CursorsController extends Disposable { diff --git a/src/vs/editor/common/cursor/cursorContext.ts b/src/vs/editor/common/cursor/cursorContext.ts index 30c25a6d626..01e77928fcf 100644 --- a/src/vs/editor/common/cursor/cursorContext.ts +++ b/src/vs/editor/common/cursor/cursorContext.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { ITextModel } from '../model.js'; -import { ICoordinatesConverter } from '../viewModel.js'; import { CursorConfiguration, ICursorSimpleModel } from '../cursorCommon.js'; +import { ICoordinatesConverter } from '../coordinatesConverter.js'; export class CursorContext { _cursorContextBrand: void = undefined; diff --git a/src/vs/editor/common/viewModel.ts b/src/vs/editor/common/viewModel.ts index 67c0df001d3..d17eb6b6152 100644 --- a/src/vs/editor/common/viewModel.ts +++ b/src/vs/editor/common/viewModel.ts @@ -6,13 +6,14 @@ import * as arrays from '../../base/common/arrays.js'; import { IScrollPosition, Scrollable } from '../../base/common/scrollable.js'; import * as strings from '../../base/common/strings.js'; +import { ICoordinatesConverter } from './coordinatesConverter.js'; import { IPosition, Position } from './core/position.js'; import { Range } from './core/range.js'; import { CursorConfiguration, CursorState, EditOperationType, IColumnSelectData, ICursorSimpleModel, PartialCursorState } from './cursorCommon.js'; import { CursorChangeReason } from './cursorEvents.js'; import { INewScrollPosition, ScrollType } from './editorCommon.js'; import { EditorTheme } from './editorTheme.js'; -import { EndOfLinePreference, IGlyphMarginLanesModel, IModelDecorationOptions, ITextModel, PositionAffinity } from './model.js'; +import { EndOfLinePreference, IGlyphMarginLanesModel, IModelDecorationOptions, ITextModel } from './model.js'; import { ILineBreaksComputer, InjectedText } from './modelLineProjectionData.js'; import { BracketGuideOptions, IActiveIndentGuideInfo, IndentGuide } from './textModelGuides.js'; import { IViewLineTokens } from './tokens/lineTokens.js'; @@ -223,28 +224,6 @@ export class Viewport { } } -export interface ICoordinatesConverter { - // View -> Model conversion and related methods - convertViewPositionToModelPosition(viewPosition: Position): Position; - convertViewRangeToModelRange(viewRange: Range): Range; - validateViewPosition(viewPosition: Position, expectedModelPosition: Position): Position; - validateViewRange(viewRange: Range, expectedModelRange: Range): Range; - - // Model -> View conversion and related methods - /** - * @param allowZeroLineNumber Should it return 0 when there are hidden lines at the top and the position is in the hidden area? - * @param belowHiddenRanges When the model position is in a hidden area, should it return the first view position after or before? - */ - convertModelPositionToViewPosition(modelPosition: Position, affinity?: PositionAffinity, allowZeroLineNumber?: boolean, belowHiddenRanges?: boolean): Position; - /** - * @param affinity Only has an effect if the range is empty. - */ - convertModelRangeToViewRange(modelRange: Range, affinity?: PositionAffinity): Range; - modelPositionIsVisible(modelPosition: Position): boolean; - getModelLineViewLineCount(modelLineNumber: number): number; - getViewLineNumberOfModelPosition(modelLineNumber: number, modelColumn: number): number; -} - export class MinimapLinesRenderingData { public readonly tabSize: number; public readonly data: Array; diff --git a/src/vs/editor/common/viewModel/viewModelDecorations.ts b/src/vs/editor/common/viewModel/viewModelDecorations.ts index b5ee4ec63e9..3a1da29ed75 100644 --- a/src/vs/editor/common/viewModel/viewModelDecorations.ts +++ b/src/vs/editor/common/viewModel/viewModelDecorations.ts @@ -9,10 +9,10 @@ import { Range } from '../core/range.js'; import { IEditorConfiguration } from '../config/editorConfiguration.js'; import { IModelDecoration, ITextModel, PositionAffinity } from '../model.js'; import { IViewModelLines } from './viewModelLines.js'; -import { ICoordinatesConverter } from '../viewModel.js'; import { filterFontDecorations, filterValidationDecorations } from '../config/editorOptions.js'; import { isModelDecorationVisible, ViewModelDecoration } from './viewModelDecoration.js'; import { InlineDecoration, InlineDecorationType } from './inlineDecorations.js'; +import { ICoordinatesConverter } from '../coordinatesConverter.js'; export interface IDecorationsViewportData { /** diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 712778b665d..dd4f4602edb 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -34,7 +34,7 @@ import { ViewLayout } from '../viewLayout/viewLayout.js'; import { MinimapTokensColorTracker } from './minimapTokensColorTracker.js'; import { ILineBreaksComputer, ILineBreaksComputerFactory, InjectedText } from '../modelLineProjectionData.js'; import { ViewEventHandler } from '../viewEventHandler.js'; -import { ICoordinatesConverter, ILineHeightChangeAccessor, IViewModel, IWhitespaceChangeAccessor, MinimapLinesRenderingData, OverviewRulerDecorationsGroup, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from '../viewModel.js'; +import { ILineHeightChangeAccessor, IViewModel, IWhitespaceChangeAccessor, MinimapLinesRenderingData, OverviewRulerDecorationsGroup, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from '../viewModel.js'; import { ViewModelDecorations } from './viewModelDecorations.js'; import { FocusChangedEvent, HiddenAreasChangedEvent, ModelContentChangedEvent, ModelDecorationsChangedEvent, ModelFontChangedEvent, ModelLanguageChangedEvent, ModelLanguageConfigurationChangedEvent, ModelLineHeightChangedEvent, ModelOptionsChangedEvent, ModelTokensChangedEvent, OutgoingViewModelEvent, ReadOnlyEditAttemptEvent, ScrollChangedEvent, ViewModelEventDispatcher, ViewModelEventsCollector, ViewZonesChangedEvent, WidgetFocusChangedEvent } from '../viewModelEventDispatcher.js'; import { IViewModelLines, ViewModelLinesFromModelAsIs, ViewModelLinesFromProjectedModel } from './viewModelLines.js'; @@ -43,6 +43,7 @@ import { GlyphMarginLanesModel } from './glyphLanesModel.js'; import { ICustomLineHeightData } from '../viewLayout/lineHeights.js'; import { TextModelEditReason } from '../textModelEditReason.js'; import { InlineDecoration } from './inlineDecorations.js'; +import { ICoordinatesConverter } from '../coordinatesConverter.js'; const USE_IDENTITY_LINES_COLLECTION = true; diff --git a/src/vs/editor/common/viewModel/viewModelLines.ts b/src/vs/editor/common/viewModel/viewModelLines.ts index d66abd71aad..74eef52bafa 100644 --- a/src/vs/editor/common/viewModel/viewModelLines.ts +++ b/src/vs/editor/common/viewModel/viewModelLines.ts @@ -17,7 +17,8 @@ import * as viewEvents from '../viewEvents.js'; import { createModelLineProjection, IModelLineProjection } from './modelLineProjection.js'; import { ILineBreaksComputer, ModelLineProjectionData, InjectedText, ILineBreaksComputerFactory } from '../modelLineProjectionData.js'; import { ConstantTimePrefixSumComputer } from '../model/prefixSumComputer.js'; -import { ICoordinatesConverter, ViewLineData } from '../viewModel.js'; +import { ViewLineData } from '../viewModel.js'; +import { ICoordinatesConverter, IdentityCoordinatesConverter } from '../coordinatesConverter.js'; export interface IViewModelLines extends IDisposable { createCoordinatesConverter(): ICoordinatesConverter; @@ -1120,7 +1121,7 @@ export class ViewModelLinesFromModelAsIs implements IViewModelLines { } public createCoordinatesConverter(): ICoordinatesConverter { - return new IdentityCoordinatesConverter(this); + return new IdentityCoordinatesConverter(this.model); } public getHiddenAreas(): Range[] { @@ -1255,77 +1256,3 @@ export class ViewModelLinesFromModelAsIs implements IViewModelLines { return null; } } - -class IdentityCoordinatesConverter implements ICoordinatesConverter { - private readonly _lines: ViewModelLinesFromModelAsIs; - - constructor(lines: ViewModelLinesFromModelAsIs) { - this._lines = lines; - } - - private _validPosition(pos: Position): Position { - return this._lines.model.validatePosition(pos); - } - - private _validRange(range: Range): Range { - return this._lines.model.validateRange(range); - } - - // View -> Model conversion and related methods - - public convertViewPositionToModelPosition(viewPosition: Position): Position { - return this._validPosition(viewPosition); - } - - public convertViewRangeToModelRange(viewRange: Range): Range { - return this._validRange(viewRange); - } - - public validateViewPosition(_viewPosition: Position, expectedModelPosition: Position): Position { - return this._validPosition(expectedModelPosition); - } - - public validateViewRange(_viewRange: Range, expectedModelRange: Range): Range { - return this._validRange(expectedModelRange); - } - - // Model -> View conversion and related methods - - public convertModelPositionToViewPosition(modelPosition: Position): Position { - return this._validPosition(modelPosition); - } - - public convertModelRangeToViewRange(modelRange: Range): Range { - return this._validRange(modelRange); - } - - public modelPositionIsVisible(modelPosition: Position): boolean { - const lineCount = this._lines.model.getLineCount(); - if (modelPosition.lineNumber < 1 || modelPosition.lineNumber > lineCount) { - // invalid arguments - return false; - } - return true; - } - - public modelRangeIsVisible(modelRange: Range): boolean { - const lineCount = this._lines.model.getLineCount(); - if (modelRange.startLineNumber < 1 || modelRange.startLineNumber > lineCount) { - // invalid arguments - return false; - } - if (modelRange.endLineNumber < 1 || modelRange.endLineNumber > lineCount) { - // invalid arguments - return false; - } - return true; - } - - public getModelLineViewLineCount(modelLineNumber: number): number { - return 1; - } - - public getViewLineNumberOfModelPosition(modelLineNumber: number, modelColumn: number): number { - return modelLineNumber; - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts b/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts index 571248d12e4..237326159ff 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts @@ -29,7 +29,6 @@ import { ILanguageConfigurationService } from '../../../../../../editor/common/l import { IModelDeltaDecoration, ITextModel, PositionAffinity } from '../../../../../../editor/common/model.js'; import { indentOfLine } from '../../../../../../editor/common/model/textModel.js'; import { ITextModelService } from '../../../../../../editor/common/services/resolverService.js'; -import { ICoordinatesConverter } from '../../../../../../editor/common/viewModel.js'; import { ViewModelEventsCollector } from '../../../../../../editor/common/viewModelEventDispatcher.js'; import { IAccessibilityService } from '../../../../../../platform/accessibility/common/accessibility.js'; import { MenuId, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; @@ -48,6 +47,7 @@ import { CellEditorOptions } from '../../view/cellParts/cellEditorOptions.js'; import { NotebookFindContrib } from '../find/notebookFindWidget.js'; import { NotebookTextModel } from '../../../common/model/notebookTextModel.js'; import { NotebookCellTextModel } from '../../../common/model/notebookCellTextModel.js'; +import { ICoordinatesConverter } from '../../../../../../editor/common/coordinatesConverter.js'; const NOTEBOOK_ADD_FIND_MATCH_TO_SELECTION_ID = 'notebook.addFindMatchToSelection'; const NOTEBOOK_SELECT_ALL_FIND_MATCHES_ID = 'notebook.selectAllFindMatches'; From a1fc17e0323c90e1a03957d47befe6d7215022e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:09:06 +0000 Subject: [PATCH 252/306] Initial plan From edb912556b9ed4b05151ced02338e2926b6b59f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:20:12 +0000 Subject: [PATCH 253/306] Fix terminal suggest providers default enablement logic Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com> --- .../browser/terminalCompletionService.ts | 2 +- .../common/terminalSuggestConfiguration.ts | 1 + ...erminalCompletionService.providers.test.ts | 177 ++++++++++++++++++ 3 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.providers.test.ts diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index eccaa26b1ad..e7db8aee3cb 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -156,7 +156,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo const providerConfig: { [key: string]: boolean } = this._configurationService.getValue(TerminalSuggestSettingId.Providers); providers = providers.filter(p => { const providerId = p.id; - return providerId && providerId in providerConfig && providerConfig[providerId] !== false; + return providerId && (!(providerId in providerConfig) || providerConfig[providerId] !== false); }); if (!providers.length) { diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts index 3434d973693..711e31090be 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts @@ -57,6 +57,7 @@ export interface ITerminalSuggestConfiguration { providers: { 'terminal-suggest': boolean; 'pwsh-shell-integration': boolean; + [key: string]: boolean; }; showStatusBar: boolean; cdPath: 'off' | 'relative' | 'absolute'; diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.providers.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.providers.test.ts new file mode 100644 index 00000000000..37255b314d2 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.providers.test.ts @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; +import { TerminalCapabilityStore } from '../../../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js'; +import { TerminalShellType } from '../../../../../../platform/terminal/common/terminal.js'; +import { ITerminalCompletionProvider, TerminalCompletionService } from '../../browser/terminalCompletionService.js'; +import { ITerminalCompletion, TerminalCompletionItemKind } from '../../browser/terminalCompletionItem.js'; +import { TerminalSuggestSettingId } from '../../common/terminalSuggestConfiguration.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; + +suite('TerminalCompletionService - Provider Configuration', () => { + let instantiationService: TestInstantiationService; + let configurationService: TestConfigurationService; + let terminalCompletionService: TerminalCompletionService; + let capabilities: TerminalCapabilityStore; + const disposables: IDisposable[] = []; + + ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => { + instantiationService = new TestInstantiationService(); + configurationService = new TestConfigurationService(); + instantiationService.stub(IConfigurationService, configurationService); + instantiationService.stub(ILogService, new NullLogService()); + + terminalCompletionService = instantiationService.createInstance(TerminalCompletionService); + capabilities = new TerminalCapabilityStore(); + }); + + teardown(() => { + disposables.forEach(d => d.dispose()); + disposables.length = 0; + terminalCompletionService.dispose(); + }); + + // Mock provider for testing + function createMockProvider(id: string): ITerminalCompletionProvider { + return { + id, + provideCompletions: async () => [{ + label: `completion-from-${id}`, + kind: TerminalCompletionItemKind.Method, + replacementIndex: 0, + replacementLength: 0 + }] + }; + } + + test('should enable providers by default when no configuration exists', async () => { + // Register two providers - one in default config, one not + const defaultProvider = createMockProvider('terminal-suggest'); + const newProvider = createMockProvider('new-extension-provider'); + + disposables.push(terminalCompletionService.registerTerminalCompletionProvider('test-ext', 'terminal-suggest', defaultProvider)); + disposables.push(terminalCompletionService.registerTerminalCompletionProvider('test-ext', 'new-extension-provider', newProvider)); + + // Set empty configuration (no provider keys) + configurationService.setUserConfiguration(TerminalSuggestSettingId.Providers, {}); + + const result = await terminalCompletionService.provideCompletions( + 'test', + 4, + false, + TerminalShellType.Bash, + capabilities, + CancellationToken.None + ); + + // Both providers should be enabled since they're not explicitly disabled + assert.ok(result, 'Should have completions'); + assert.strictEqual(result.length, 2, 'Should have completions from both providers'); + + const labels = result.map(c => c.label); + assert.ok(labels.includes('completion-from-terminal-suggest'), 'Should include completion from default provider'); + assert.ok(labels.includes('completion-from-new-extension-provider'), 'Should include completion from new provider'); + }); + + test('should disable providers when explicitly set to false', async () => { + const provider1 = createMockProvider('provider1'); + const provider2 = createMockProvider('provider2'); + + disposables.push(terminalCompletionService.registerTerminalCompletionProvider('test-ext', 'provider1', provider1)); + disposables.push(terminalCompletionService.registerTerminalCompletionProvider('test-ext', 'provider2', provider2)); + + // Disable provider1, leave provider2 unconfigured + configurationService.setUserConfiguration(TerminalSuggestSettingId.Providers, { + 'provider1': false + }); + + const result = await terminalCompletionService.provideCompletions( + 'test', + 4, + false, + TerminalShellType.Bash, + capabilities, + CancellationToken.None + ); + + // Only provider2 should be enabled + assert.ok(result, 'Should have completions'); + assert.strictEqual(result.length, 1, 'Should have completions from only one provider'); + assert.strictEqual(result[0].label, 'completion-from-provider2', 'Should only include completion from enabled provider'); + }); + + test('should enable providers when explicitly set to true', async () => { + const provider1 = createMockProvider('provider1'); + const provider2 = createMockProvider('provider2'); + + disposables.push(terminalCompletionService.registerTerminalCompletionProvider('test-ext', 'provider1', provider1)); + disposables.push(terminalCompletionService.registerTerminalCompletionProvider('test-ext', 'provider2', provider2)); + + // Explicitly enable provider1, leave provider2 unconfigured + configurationService.setUserConfiguration(TerminalSuggestSettingId.Providers, { + 'provider1': true + }); + + const result = await terminalCompletionService.provideCompletions( + 'test', + 4, + false, + TerminalShellType.Bash, + capabilities, + CancellationToken.None + ); + + // Both providers should be enabled + assert.ok(result, 'Should have completions'); + assert.strictEqual(result.length, 2, 'Should have completions from both providers'); + + const labels = result.map(c => c.label); + assert.ok(labels.includes('completion-from-provider1'), 'Should include completion from explicitly enabled provider'); + assert.ok(labels.includes('completion-from-provider2'), 'Should include completion from default enabled provider'); + }); + + test('should handle mixed configuration correctly', async () => { + const provider1 = createMockProvider('provider1'); + const provider2 = createMockProvider('provider2'); + const provider3 = createMockProvider('provider3'); + + disposables.push(terminalCompletionService.registerTerminalCompletionProvider('test-ext', 'provider1', provider1)); + disposables.push(terminalCompletionService.registerTerminalCompletionProvider('test-ext', 'provider2', provider2)); + disposables.push(terminalCompletionService.registerTerminalCompletionProvider('test-ext', 'provider3', provider3)); + + // Mixed configuration: enable provider1, disable provider2, leave provider3 unconfigured + configurationService.setUserConfiguration(TerminalSuggestSettingId.Providers, { + 'provider1': true, + 'provider2': false + }); + + const result = await terminalCompletionService.provideCompletions( + 'test', + 4, + false, + TerminalShellType.Bash, + capabilities, + CancellationToken.None + ); + + // provider1 and provider3 should be enabled, provider2 should be disabled + assert.ok(result, 'Should have completions'); + assert.strictEqual(result.length, 2, 'Should have completions from two providers'); + + const labels = result.map(c => c.label); + assert.ok(labels.includes('completion-from-provider1'), 'Should include completion from explicitly enabled provider'); + assert.ok(labels.includes('completion-from-provider3'), 'Should include completion from default enabled provider'); + assert.ok(!labels.includes('completion-from-provider2'), 'Should not include completion from disabled provider'); + }); +}); \ No newline at end of file From c8ce8e255ec109fe29cd742ff840a3788abaecad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Jul 2025 14:12:30 +0000 Subject: [PATCH 254/306] Extract provider filtering logic into protected method and move tests to main test file Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com> --- .../browser/terminalCompletionService.ts | 14 +- ...erminalCompletionService.providers.test.ts | 177 ------------------ .../browser/terminalCompletionService.test.ts | 101 ++++++++++ 3 files changed, 110 insertions(+), 182 deletions(-) delete mode 100644 src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.providers.test.ts diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index e7db8aee3cb..06531857053 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -153,11 +153,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return this._collectCompletions(providers, shellType, promptValue, cursorPosition, allowFallbackCompletions, capabilities, token, explicitlyInvoked); } - const providerConfig: { [key: string]: boolean } = this._configurationService.getValue(TerminalSuggestSettingId.Providers); - providers = providers.filter(p => { - const providerId = p.id; - return providerId && (!(providerId in providerConfig) || providerConfig[providerId] !== false); - }); + providers = this._getEnabledProviders(providers); if (!providers.length) { return; @@ -166,6 +162,14 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return this._collectCompletions(providers, shellType, promptValue, cursorPosition, allowFallbackCompletions, capabilities, token, explicitlyInvoked); } + protected _getEnabledProviders(providers: ITerminalCompletionProvider[]): ITerminalCompletionProvider[] { + const providerConfig: { [key: string]: boolean } = this._configurationService.getValue(TerminalSuggestSettingId.Providers); + return providers.filter(p => { + const providerId = p.id; + return providerId && (!(providerId in providerConfig) || providerConfig[providerId] !== false); + }); + } + private async _collectCompletions(providers: ITerminalCompletionProvider[], shellType: TerminalShellType | undefined, promptValue: string, cursorPosition: number, allowFallbackCompletions: boolean, capabilities: ITerminalCapabilityStore, token: CancellationToken, explicitlyInvoked?: boolean): Promise { const completionPromises = providers.map(async provider => { if (provider.shellTypes && shellType && !provider.shellTypes.includes(shellType)) { diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.providers.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.providers.test.ts deleted file mode 100644 index 37255b314d2..00000000000 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.providers.test.ts +++ /dev/null @@ -1,177 +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 assert from 'assert'; -import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { IDisposable } from '../../../../../../base/common/lifecycle.js'; -import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; -import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; -import { TerminalCapabilityStore } from '../../../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js'; -import { TerminalShellType } from '../../../../../../platform/terminal/common/terminal.js'; -import { ITerminalCompletionProvider, TerminalCompletionService } from '../../browser/terminalCompletionService.js'; -import { ITerminalCompletion, TerminalCompletionItemKind } from '../../browser/terminalCompletionItem.js'; -import { TerminalSuggestSettingId } from '../../common/terminalSuggestConfiguration.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; - -suite('TerminalCompletionService - Provider Configuration', () => { - let instantiationService: TestInstantiationService; - let configurationService: TestConfigurationService; - let terminalCompletionService: TerminalCompletionService; - let capabilities: TerminalCapabilityStore; - const disposables: IDisposable[] = []; - - ensureNoDisposablesAreLeakedInTestSuite(); - - setup(() => { - instantiationService = new TestInstantiationService(); - configurationService = new TestConfigurationService(); - instantiationService.stub(IConfigurationService, configurationService); - instantiationService.stub(ILogService, new NullLogService()); - - terminalCompletionService = instantiationService.createInstance(TerminalCompletionService); - capabilities = new TerminalCapabilityStore(); - }); - - teardown(() => { - disposables.forEach(d => d.dispose()); - disposables.length = 0; - terminalCompletionService.dispose(); - }); - - // Mock provider for testing - function createMockProvider(id: string): ITerminalCompletionProvider { - return { - id, - provideCompletions: async () => [{ - label: `completion-from-${id}`, - kind: TerminalCompletionItemKind.Method, - replacementIndex: 0, - replacementLength: 0 - }] - }; - } - - test('should enable providers by default when no configuration exists', async () => { - // Register two providers - one in default config, one not - const defaultProvider = createMockProvider('terminal-suggest'); - const newProvider = createMockProvider('new-extension-provider'); - - disposables.push(terminalCompletionService.registerTerminalCompletionProvider('test-ext', 'terminal-suggest', defaultProvider)); - disposables.push(terminalCompletionService.registerTerminalCompletionProvider('test-ext', 'new-extension-provider', newProvider)); - - // Set empty configuration (no provider keys) - configurationService.setUserConfiguration(TerminalSuggestSettingId.Providers, {}); - - const result = await terminalCompletionService.provideCompletions( - 'test', - 4, - false, - TerminalShellType.Bash, - capabilities, - CancellationToken.None - ); - - // Both providers should be enabled since they're not explicitly disabled - assert.ok(result, 'Should have completions'); - assert.strictEqual(result.length, 2, 'Should have completions from both providers'); - - const labels = result.map(c => c.label); - assert.ok(labels.includes('completion-from-terminal-suggest'), 'Should include completion from default provider'); - assert.ok(labels.includes('completion-from-new-extension-provider'), 'Should include completion from new provider'); - }); - - test('should disable providers when explicitly set to false', async () => { - const provider1 = createMockProvider('provider1'); - const provider2 = createMockProvider('provider2'); - - disposables.push(terminalCompletionService.registerTerminalCompletionProvider('test-ext', 'provider1', provider1)); - disposables.push(terminalCompletionService.registerTerminalCompletionProvider('test-ext', 'provider2', provider2)); - - // Disable provider1, leave provider2 unconfigured - configurationService.setUserConfiguration(TerminalSuggestSettingId.Providers, { - 'provider1': false - }); - - const result = await terminalCompletionService.provideCompletions( - 'test', - 4, - false, - TerminalShellType.Bash, - capabilities, - CancellationToken.None - ); - - // Only provider2 should be enabled - assert.ok(result, 'Should have completions'); - assert.strictEqual(result.length, 1, 'Should have completions from only one provider'); - assert.strictEqual(result[0].label, 'completion-from-provider2', 'Should only include completion from enabled provider'); - }); - - test('should enable providers when explicitly set to true', async () => { - const provider1 = createMockProvider('provider1'); - const provider2 = createMockProvider('provider2'); - - disposables.push(terminalCompletionService.registerTerminalCompletionProvider('test-ext', 'provider1', provider1)); - disposables.push(terminalCompletionService.registerTerminalCompletionProvider('test-ext', 'provider2', provider2)); - - // Explicitly enable provider1, leave provider2 unconfigured - configurationService.setUserConfiguration(TerminalSuggestSettingId.Providers, { - 'provider1': true - }); - - const result = await terminalCompletionService.provideCompletions( - 'test', - 4, - false, - TerminalShellType.Bash, - capabilities, - CancellationToken.None - ); - - // Both providers should be enabled - assert.ok(result, 'Should have completions'); - assert.strictEqual(result.length, 2, 'Should have completions from both providers'); - - const labels = result.map(c => c.label); - assert.ok(labels.includes('completion-from-provider1'), 'Should include completion from explicitly enabled provider'); - assert.ok(labels.includes('completion-from-provider2'), 'Should include completion from default enabled provider'); - }); - - test('should handle mixed configuration correctly', async () => { - const provider1 = createMockProvider('provider1'); - const provider2 = createMockProvider('provider2'); - const provider3 = createMockProvider('provider3'); - - disposables.push(terminalCompletionService.registerTerminalCompletionProvider('test-ext', 'provider1', provider1)); - disposables.push(terminalCompletionService.registerTerminalCompletionProvider('test-ext', 'provider2', provider2)); - disposables.push(terminalCompletionService.registerTerminalCompletionProvider('test-ext', 'provider3', provider3)); - - // Mixed configuration: enable provider1, disable provider2, leave provider3 unconfigured - configurationService.setUserConfiguration(TerminalSuggestSettingId.Providers, { - 'provider1': true, - 'provider2': false - }); - - const result = await terminalCompletionService.provideCompletions( - 'test', - 4, - false, - TerminalShellType.Bash, - capabilities, - CancellationToken.None - ); - - // provider1 and provider3 should be enabled, provider2 should be disabled - assert.ok(result, 'Should have completions'); - assert.strictEqual(result.length, 2, 'Should have completions from two providers'); - - const labels = result.map(c => c.label); - assert.ok(labels.includes('completion-from-provider1'), 'Should include completion from explicitly enabled provider'); - assert.ok(labels.includes('completion-from-provider3'), 'Should include completion from default enabled provider'); - assert.ok(!labels.includes('completion-from-provider2'), 'Should not include completion from disabled provider'); - }); -}); \ No newline at end of file diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index 950ff80d949..401f4aa3b35 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -770,4 +770,105 @@ suite('TerminalCompletionService', () => { }); }); + + suite('Provider Configuration', () => { + // Test class that extends TerminalCompletionService to access protected methods + class TestTerminalCompletionService extends TerminalCompletionService { + public _getEnabledProviders(providers: ITerminalCompletionProvider[]): ITerminalCompletionProvider[] { + return super._getEnabledProviders(providers); + } + } + + let testTerminalCompletionService: TestTerminalCompletionService; + + setup(() => { + testTerminalCompletionService = store.add(instantiationService.createInstance(TestTerminalCompletionService)); + }); + + // Mock provider for testing + function createMockProvider(id: string): ITerminalCompletionProvider { + return { + id, + provideCompletions: async () => [{ + label: `completion-from-${id}`, + kind: TerminalCompletionItemKind.Method, + replacementIndex: 0, + replacementLength: 0 + }] + }; + } + + test('should enable providers by default when no configuration exists', () => { + const defaultProvider = createMockProvider('terminal-suggest'); + const newProvider = createMockProvider('new-extension-provider'); + const providers = [defaultProvider, newProvider]; + + // Set empty configuration (no provider keys) + configurationService.setUserConfiguration(TerminalSuggestSettingId.Providers, {}); + + const result = testTerminalCompletionService._getEnabledProviders(providers); + + // Both providers should be enabled since they're not explicitly disabled + assert.strictEqual(result.length, 2, 'Should enable both providers by default'); + assert.ok(result.includes(defaultProvider), 'Should include default provider'); + assert.ok(result.includes(newProvider), 'Should include new provider'); + }); + + test('should disable providers when explicitly set to false', () => { + const provider1 = createMockProvider('provider1'); + const provider2 = createMockProvider('provider2'); + const providers = [provider1, provider2]; + + // Disable provider1, leave provider2 unconfigured + configurationService.setUserConfiguration(TerminalSuggestSettingId.Providers, { + 'provider1': false + }); + + const result = testTerminalCompletionService._getEnabledProviders(providers); + + // Only provider2 should be enabled + assert.strictEqual(result.length, 1, 'Should enable only one provider'); + assert.ok(result.includes(provider2), 'Should include unconfigured provider'); + assert.ok(!result.includes(provider1), 'Should not include disabled provider'); + }); + + test('should enable providers when explicitly set to true', () => { + const provider1 = createMockProvider('provider1'); + const provider2 = createMockProvider('provider2'); + const providers = [provider1, provider2]; + + // Explicitly enable provider1, leave provider2 unconfigured + configurationService.setUserConfiguration(TerminalSuggestSettingId.Providers, { + 'provider1': true + }); + + const result = testTerminalCompletionService._getEnabledProviders(providers); + + // Both providers should be enabled + assert.strictEqual(result.length, 2, 'Should enable both providers'); + assert.ok(result.includes(provider1), 'Should include explicitly enabled provider'); + assert.ok(result.includes(provider2), 'Should include unconfigured provider'); + }); + + test('should handle mixed configuration correctly', () => { + const provider1 = createMockProvider('provider1'); + const provider2 = createMockProvider('provider2'); + const provider3 = createMockProvider('provider3'); + const providers = [provider1, provider2, provider3]; + + // Mixed configuration: enable provider1, disable provider2, leave provider3 unconfigured + configurationService.setUserConfiguration(TerminalSuggestSettingId.Providers, { + 'provider1': true, + 'provider2': false + }); + + const result = testTerminalCompletionService._getEnabledProviders(providers); + + // provider1 and provider3 should be enabled, provider2 should be disabled + assert.strictEqual(result.length, 2, 'Should enable two providers'); + assert.ok(result.includes(provider1), 'Should include explicitly enabled provider'); + assert.ok(result.includes(provider3), 'Should include unconfigured provider'); + assert.ok(!result.includes(provider2), 'Should not include disabled provider'); + }); + }); }); From f3ed1cdbb0d69c79b723f3f478bc2699eb284e0b Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 9 Jul 2025 10:18:07 -0400 Subject: [PATCH 255/306] set pwsh provider off by default (#254876) fix #254867 --- .../suggest/common/terminalSuggestConfiguration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts index 3434d973693..ca440aaae60 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts @@ -78,7 +78,7 @@ export const terminalSuggestConfiguration: IStringDictionary Date: Wed, 9 Jul 2025 16:20:06 +0200 Subject: [PATCH 256/306] ChatEditingSessionStorage: improve saving a session: compute files hashes async, encode content only once (#254882) --- .../chatEditing/chatEditingSessionStorage.ts | 64 ++++++++++--------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts index d755618c797..e7780b7186f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { StringSHA1 } from '../../../../../base/common/hash.js'; +import { hashAsync } from '../../../../../base/common/hash.js'; import { ResourceMap } from '../../../../../base/common/map.js'; import { joinPath } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -147,36 +147,48 @@ export class ChatEditingSessionStorage { } } - const fileContents = new Map(); - const addFileContent = (content: string): string => { - const shaComputer = new StringSHA1(); - shaComputer.update(content); - const sha = shaComputer.digest().substring(0, 7); - fileContents.set(sha, content); - return sha; + const contentWritePromises = new Map>(); + + // saves a file content under a path containing a hash of the content. + // Returns the hash to represent the content. + const writeContent = async (content: string): Promise => { + const buffer = VSBuffer.fromString(content); + const hash = (await hashAsync(buffer)).substring(0, 7); + if (!existingContents.has(hash)) { + await this._fileService.writeFile(joinPath(contentsFolder, hash), buffer); + } + return hash; }; - const serializeResourceMap = (resourceMap: ResourceMap, serialize: (value: T) => any): ResourceMapDTO => { - return Array.from(resourceMap.entries()).map(([resourceURI, value]) => [resourceURI.toString(), serialize(value)]); + const addFileContent = async (content: string): Promise => { + let storedContentHash = contentWritePromises.get(content); + if (!storedContentHash) { + storedContentHash = writeContent(content); + contentWritePromises.set(content, storedContentHash); + } + return storedContentHash; }; - const serializeChatEditingSessionStop = (stop: IChatEditingSessionStop): IChatEditingSessionStopDTO => { + const serializeResourceMap = async (resourceMap: ResourceMap, serialize: (value: T) => Promise): Promise> => { + return await Promise.all(Array.from(resourceMap.entries()).map(async ([resourceURI, value]) => [resourceURI.toString(), await serialize(value)])); + }; + const serializeChatEditingSessionStop = async (stop: IChatEditingSessionStop): Promise => { return { stopId: stop.stopId, - entries: Array.from(stop.entries.values()).map(serializeSnapshotEntry) + entries: await Promise.all(Array.from(stop.entries.values()).map(serializeSnapshotEntry)) }; }; - const serializeChatEditingSessionSnapshot = (snapshot: IChatEditingSessionSnapshot): IChatEditingSessionSnapshotDTO2 => { + const serializeChatEditingSessionSnapshot = async (snapshot: IChatEditingSessionSnapshot): Promise => { return { requestId: snapshot.requestId, - stops: snapshot.stops.map(serializeChatEditingSessionStop), - postEdit: snapshot.postEdit ? Array.from(snapshot.postEdit.values()).map(serializeSnapshotEntry) : undefined + stops: await Promise.all(snapshot.stops.map(serializeChatEditingSessionStop)), + postEdit: snapshot.postEdit ? await Promise.all(Array.from(snapshot.postEdit.values()).map(serializeSnapshotEntry)) : undefined }; }; - const serializeSnapshotEntry = (entry: ISnapshotEntry): ISnapshotEntryDTO => { + const serializeSnapshotEntry = async (entry: ISnapshotEntry): Promise => { return { resource: entry.resource.toString(), languageId: entry.languageId, - originalHash: addFileContent(entry.original), - currentHash: addFileContent(entry.current), + originalHash: await addFileContent(entry.original), + currentHash: await addFileContent(entry.current), state: entry.state, snapshotUri: entry.snapshotUri.toString(), telemetryInfo: { requestId: entry.telemetryInfo.requestId, agentId: entry.telemetryInfo.agentId, command: entry.telemetryInfo.command } @@ -187,20 +199,14 @@ export class ChatEditingSessionStorage { const data: IChatEditingSessionDTO = { version: STORAGE_VERSION, sessionId: this.chatSessionId, - linearHistory: state.linearHistory.map(serializeChatEditingSessionSnapshot), + linearHistory: await Promise.all(state.linearHistory.map(serializeChatEditingSessionSnapshot)), linearHistoryIndex: state.linearHistoryIndex, - initialFileContents: serializeResourceMap(state.initialFileContents, value => addFileContent(value)), - pendingSnapshot: state.pendingSnapshot ? serializeChatEditingSessionStop(state.pendingSnapshot) : undefined, - recentSnapshot: serializeChatEditingSessionStop(state.recentSnapshot), + initialFileContents: await serializeResourceMap(state.initialFileContents, value => addFileContent(value)), + pendingSnapshot: state.pendingSnapshot ? await serializeChatEditingSessionStop(state.pendingSnapshot) : undefined, + recentSnapshot: await serializeChatEditingSessionStop(state.recentSnapshot), }; - this._logService.debug(`chatEditingSession: Storing editing session at ${storageFolder.toString()}: ${fileContents.size} files`); - - for (const [hash, content] of fileContents) { - if (!existingContents.has(hash)) { - await this._fileService.writeFile(joinPath(contentsFolder, hash), VSBuffer.fromString(content)); - } - } + this._logService.debug(`chatEditingSession: Storing editing session at ${storageFolder.toString()}: ${contentWritePromises.size} files`); await this._fileService.writeFile(joinPath(storageFolder, STORAGE_STATE_FILE), VSBuffer.fromString(JSON.stringify(data))); } catch (e) { From 95fbc18ca02ac20f0133d0e18ef39163419dd371 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 9 Jul 2025 16:51:41 +0200 Subject: [PATCH 257/306] Provide Maximized Chat View as an option for startup editor on workspace open. (fix microsoft/vscode-internalbacklog#5610) (#254874) * Provide Maximized Chat View as an option for startup editor on workspace open. (fix microsoft/vscode-internalbacklog#5610) * refactor - move initialization of isNew property --- .../electron-main/themeMainServiceImpl.ts | 2 + src/vs/workbench/browser/layout.ts | 83 +++++++++++++------ .../browser/workbench.contribution.ts | 6 +- .../chat/browser/actions/chatActions.ts | 2 +- 4 files changed, 64 insertions(+), 29 deletions(-) diff --git a/src/vs/platform/theme/electron-main/themeMainServiceImpl.ts b/src/vs/platform/theme/electron-main/themeMainServiceImpl.ts index 6d3402bba05..50b07e60807 100644 --- a/src/vs/platform/theme/electron-main/themeMainServiceImpl.ts +++ b/src/vs/platform/theme/electron-main/themeMainServiceImpl.ts @@ -345,6 +345,8 @@ export class ThemeMainService extends Disposable implements IThemeMainService { } else { if (auxiliaryBarDefaultVisibility === 'visible' || auxiliaryBarDefaultVisibility === 'visibleInWorkspace') { auxiliaryBarWidth = override.layoutInfo.auxiliaryBarWidth || partSplash.layoutInfo.auxiliaryBarWidth || ThemeMainService.DEFAULT_BAR_WIDTH; + } else if (auxiliaryBarDefaultVisibility === 'maximized' || auxiliaryBarDefaultVisibility === 'maximizedInWorkspace') { + auxiliaryBarWidth = Number.MAX_SAFE_INTEGER; // marker for a maximised auxiliary bar } else { auxiliaryBarWidth = 0; } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index ac154915b74..c01c75c25d3 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -419,7 +419,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } // Theme changes - this._register(this.themeService.onDidColorThemeChange(() => this.updateWindowsBorder())); + this._register(this.themeService.onDidColorThemeChange(() => this.updateWindowBorder())); // Window active / focus changes this._register(this.hostService.onDidChangeFocus(focused => this.onWindowFocusChanged(focused))); @@ -508,7 +508,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Propagate to grid this.workbenchGrid.setViewVisible(this.titleBarPartView, shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled)); - this.updateWindowsBorder(true); + this.updateWindowBorder(true); } } @@ -518,7 +518,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.state.runtime.activeContainerId = activeContainerId; // Indicate active window border - this.updateWindowsBorder(); + this.updateWindowBorder(); this._onDidChangeActiveContainer.fire(); } @@ -527,7 +527,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private onWindowFocusChanged(hasFocus: boolean): void { if (this.state.runtime.hasFocus !== hasFocus) { this.state.runtime.hasFocus = hasFocus; - this.updateWindowsBorder(); + this.updateWindowBorder(); } } @@ -582,7 +582,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.adjustPartPositions(position, panelAlignment, panelPosition); } - private updateWindowsBorder(skipLayout = false) { + private updateWindowBorder(skipLayout = false) { if ( isWeb || isWindows || // not working well with zooming (border often not visible) @@ -743,7 +743,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } // Window border - this.updateWindowsBorder(true); + this.updateWindowBorder(true); } private getDefaultLayoutViews(environmentService: IBrowserWorkbenchEnvironmentService, storageService: IStorageService): string[] | undefined { @@ -2377,7 +2377,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.state.runtime.maximized.delete(targetWindowId); } - this.updateWindowsBorder(); + this.updateWindowBorder(); this._onDidChangeWindowMaximized.fire({ windowId: targetWindowId, maximized }); } @@ -2800,6 +2800,12 @@ class LayoutStateModel extends Disposable { private readonly stateCache = new Map(); + private readonly isNew: { + [StorageScope.WORKSPACE]: boolean; + [StorageScope.PROFILE]: boolean; + [StorageScope.APPLICATION]: boolean; + }; + constructor( private readonly storageService: IStorageService, private readonly configurationService: IConfigurationService, @@ -2810,6 +2816,12 @@ class LayoutStateModel extends Disposable { ) { super(); + this.isNew = { + [StorageScope.WORKSPACE]: this.storageService.isNew(StorageScope.WORKSPACE), + [StorageScope.PROFILE]: this.storageService.isNew(StorageScope.PROFILE), + [StorageScope.APPLICATION]: this.storageService.isNew(StorageScope.APPLICATION) + }; + this._register(this.configurationService.onDidChangeConfiguration(configurationChange => this.updateStateFromLegacySettings(configurationChange))); } @@ -2894,9 +2906,11 @@ class LayoutStateModel extends Disposable { } switch (this.configurationService.getValue(WorkbenchLayoutSettings.AUXILIARYBAR_DEFAULT_VISIBILITY)) { + case 'maximized': case 'visible': return false; case 'visibleInWorkspace': + case 'maximizedInWorkspace': return workbenchState === WorkbenchState.EMPTY; default: return true; @@ -2945,7 +2959,7 @@ class LayoutStateModel extends Disposable { } }); - // With experimental treatment for new users + // Auxiliary bar: With experimental treatment for new users if ( this.storageService.isNew(StorageScope.APPLICATION) && this.contextService.getWorkbenchState() === WorkbenchState.EMPTY && @@ -2956,20 +2970,7 @@ class LayoutStateModel extends Disposable { ) ) { if (experiment.value.experimentGroup === StartupExperimentGroup.MaximizedChat) { - this.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_LAST_NON_MAXIMIZED_VISIBILITY, { - sideBarVisible: !this.getRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN), - panelVisible: !this.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN), - editorVisible: !this.getRuntimeValue(LayoutStateKeys.EDITOR_HIDDEN), - auxiliaryBarVisible: !this.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN) - }); - - this.setRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN, true); - this.setRuntimeValue(LayoutStateKeys.PANEL_HIDDEN, true); - this.setRuntimeValue(LayoutStateKeys.EDITOR_HIDDEN, true); - this.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN, false); - - this.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_LAST_NON_MAXIMIZED_SIZE, this.getInitializationValue(LayoutStateKeys.AUXILIARYBAR_SIZE)); - this.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_WAS_LAST_MAXIMIZED, true); + this.applyAuxiliaryBarMaximizedOverride(); } else if ( experiment.value.experimentGroup === StartupExperimentGroup.SplitEmptyEditorChat || experiment.value.experimentGroup === StartupExperimentGroup.SplitWelcomeChat @@ -2980,6 +2981,17 @@ class LayoutStateModel extends Disposable { } } + // Auxiliary bar: Based on setting for new workspaces + else if (this.isNew[StorageScope.WORKSPACE]) { + const defaultAuxiliaryBarVisibility = this.configurationService.getValue(WorkbenchLayoutSettings.AUXILIARYBAR_DEFAULT_VISIBILITY); + if ( + defaultAuxiliaryBarVisibility === 'maximized' || + (defaultAuxiliaryBarVisibility === 'maximizedInWorkspace' && this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY) + ) { + this.applyAuxiliaryBarMaximizedOverride(); + } + } + // Both editor and panel should not be hidden on startup unless auxiliary bar is maximized if ( this.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN) && @@ -2990,6 +3002,23 @@ class LayoutStateModel extends Disposable { } } + private applyAuxiliaryBarMaximizedOverride(): void { + this.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_LAST_NON_MAXIMIZED_VISIBILITY, { + sideBarVisible: !this.getRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN), + panelVisible: !this.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN), + editorVisible: !this.getRuntimeValue(LayoutStateKeys.EDITOR_HIDDEN), + auxiliaryBarVisible: !this.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN) + }); + + this.setRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN, true); + this.setRuntimeValue(LayoutStateKeys.PANEL_HIDDEN, true); + this.setRuntimeValue(LayoutStateKeys.EDITOR_HIDDEN, true); + this.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN, false); + + this.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_LAST_NON_MAXIMIZED_SIZE, this.getInitializationValue(LayoutStateKeys.AUXILIARYBAR_SIZE)); + this.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_WAS_LAST_MAXIMIZED, true); + } + save(workspace: boolean, global: boolean): void { let key: keyof typeof LayoutStateKeys; @@ -3071,13 +3100,15 @@ class LayoutStateModel extends Disposable { } private loadKeyFromStorage(key: WorkbenchLayoutStateKey): T | undefined { - let value: any = this.storageService.get(`${LayoutStateModel.STORAGE_PREFIX}${key.name}`, key.scope); + const value = this.storageService.get(`${LayoutStateModel.STORAGE_PREFIX}${key.name}`, key.scope); if (value !== undefined) { + this.isNew[key.scope] = false; // remember that we had previous state for this scope + switch (typeof key.defaultValue) { - case 'boolean': value = value === 'true'; break; - case 'number': value = parseInt(value); break; - case 'object': value = JSON.parse(value); break; + case 'boolean': return (value === 'true') as T; + case 'number': return parseInt(value) as T; + case 'object': return JSON.parse(value) as T; } } diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 3a756fb1ae8..7298b6962d0 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -536,14 +536,16 @@ const registry = Registry.as(ConfigurationExtensions.Con }, 'workbench.secondarySideBar.defaultVisibility': { 'type': 'string', - 'enum': ['hidden', 'visibleInWorkspace', 'visible'], + 'enum': ['hidden', 'visibleInWorkspace', 'visible', 'maximizedInWorkspace', 'maximized'], 'default': 'hidden', 'tags': ['onExp'], 'description': localize('secondarySideBarDefaultVisibility', "Controls the default visibility of the secondary side bar in workspaces or empty windows opened for the first time."), 'enumDescriptions': [ localize('workbench.secondarySideBar.defaultVisibility.hidden', "The secondary side bar is hidden by default."), localize('workbench.secondarySideBar.defaultVisibility.visibleInWorkspace', "The secondary side bar is visible by default if a workspace is opened."), - localize('workbench.secondarySideBar.defaultVisibility.visible', "The secondary side bar is visible by default.") + localize('workbench.secondarySideBar.defaultVisibility.visible', "The secondary side bar is visible by default."), + localize('workbench.secondarySideBar.defaultVisibility.maximizedInWorkspace', "The secondary side bar is visible and maximized by default if a workspace is opened."), + localize('workbench.secondarySideBar.defaultVisibility.maximized', "The secondary side bar is visible and maximized by default.") ] }, 'workbench.secondarySideBar.showLabels': { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 3fd59de56a8..2ce2ace4d58 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -1131,7 +1131,7 @@ registerAction2(class ToggleDefaultVisibilityAction extends Action2 { async run(accessor: ServicesAccessor) { const configurationService = accessor.get(IConfigurationService); - const currentValue = configurationService.getValue<'hidden' | 'visibleInWorkspace' | 'visible'>('workbench.secondarySideBar.defaultVisibility'); + const currentValue = configurationService.getValue<'hidden' | unknown>('workbench.secondarySideBar.defaultVisibility'); configurationService.updateValue('workbench.secondarySideBar.defaultVisibility', currentValue !== 'hidden' ? 'hidden' : 'visible'); } }); From 5fea2c1a606e66add10820dda3fa94afebc088fe Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 9 Jul 2025 07:54:40 -0700 Subject: [PATCH 258/306] Remove broken test This test never worked Fixes #234656 --- .../chat/test/browser/terminalInitialHint.test.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts b/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts index cc6faaada5b..1616d3317c2 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts @@ -94,14 +94,4 @@ suite('Terminal Initial Hint Addon', () => { strictEqual(eventCount, 1); }); }); - suite('Input', () => { - test('hint is not shown when there has been input', () => { - onDidChangeAgentsEmitter.fire(agent); - xterm.writeln('data'); - setTimeout(() => { - xterm.focus(); - strictEqual(eventCount, 0); - }, 50); - }); - }); }); From 34d2ec438c64532ce1a35a21d3a423e9cfe9050c Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 9 Jul 2025 18:28:31 +0200 Subject: [PATCH 259/306] Pylance mcp tools can not be deselected (#254917) --- .../contrib/chat/browser/actions/chatToolPicker.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts b/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts index cf3f1b4cdcc..9143400255a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts @@ -6,7 +6,6 @@ import { assertNever } from '../../../../../base/common/assert.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { diffSets } from '../../../../../base/common/collections.js'; import { Event } from '../../../../../base/common/event.js'; -import { Iterable } from '../../../../../base/common/iterator.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { assertType } from '../../../../../base/common/types.js'; @@ -391,10 +390,11 @@ export async function showToolsPicker( if (item.source.type === 'mcp') { mcpToolSets.add(item); - if (Iterable.every(item.getTools(), tool => result.get(tool))) { + const toolsInSet = Array.from(item.getTools()); + if (toolsInSet.length && toolsInSet.every(tool => result.get(tool))) { // ALL tools from the MCP tool set are here, replace them with just the toolset // but only when computing the final result - for (const tool of item.getTools()) { + for (const tool of toolsInSet) { result.delete(tool); } result.set(item, true); From ba25f9cd4cec7c9712f37c1f3cedfe5a7c7c9958 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Jul 2025 18:50:18 +0200 Subject: [PATCH 260/306] Fix PowerShell terminal link detection for paths with spaces (#254504) --- .../links/browser/terminalLocalLinkDetector.ts | 4 ++-- .../links/test/browser/terminalLocalLinkDetector.test.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts index 3cfea79b814..e7b27e43556 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts @@ -52,8 +52,8 @@ const fallbackMatchers: RegExp[] = [ // C:\foo/bar baz:339: error ... // C:\foo/bar baz:339:12: error ... [#178584, Clang] /^(?(?.+):(?\d+)(?::(?\d+))?) ?:/, - // Cmd prompt - /^(?(?.+))>/, + // PowerShell and cmd prompt + /^(?:PS\s+)?(?(?[^>]+))>/, // The whole line is the path /^ *(?(?.+))/ ]; diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts index 04d0dfa7415..32d6d384bb8 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts @@ -158,6 +158,8 @@ const supportedFallbackLinkFormats: LinkFormatInfo[] = [ { urlFormat: '{0}:{1}:{2} :', line: '5', column: '3', linkCellEndOffset: -2 }, { urlFormat: '{0}:{1}:', line: '5', linkCellEndOffset: -1 }, { urlFormat: '{0}:{1}:{2}:', line: '5', column: '3', linkCellEndOffset: -1 }, + // PowerShell prompt + { urlFormat: 'PS {0}>', linkCellStartOffset: 3, linkCellEndOffset: -1 }, // Cmd prompt { urlFormat: '{0}>', linkCellEndOffset: -1 }, // The whole line is the path From 7693191f993c5775e79ecc961accf06d1b0d200e Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 9 Jul 2025 09:54:41 -0700 Subject: [PATCH 261/306] chore: remove vsce-sign results from baseline (#254923) --- .config/guardian/.gdnbaselines | 47 +--------------------------------- 1 file changed, 1 insertion(+), 46 deletions(-) diff --git a/.config/guardian/.gdnbaselines b/.config/guardian/.gdnbaselines index 063d926b6ba..e8f9f8db2f7 100644 --- a/.config/guardian/.gdnbaselines +++ b/.config/guardian/.gdnbaselines @@ -296,21 +296,6 @@ "expirationDate": "2025-11-19 21:48:17Z", "justification": "This error is baselined with an expiration date of 180 days from 2025-06-02 21:48:17Z" }, - "216e2ac9cb596796224b47799f656570a01fa0d9b5f935608b47d15ab613c8e8": { - "signature": "216e2ac9cb596796224b47799f656570a01fa0d9b5f935608b47d15ab613c8e8", - "alternativeSignatures": [ - "07746898f43afab7cc50931b33154c2d9e1a35f82a649dbe8aecf785b3d5a813" - ], - "target": "file:///D:/a/_work/1/vscode-server-win32-x64/node_modules/@vscode/vsce-sign/bin/vsce-sign.exe", - "memberOf": [ - "default" - ], - "tool": "binskim", - "ruleId": "BA2008", - "createdDate": "2025-06-02 21:46:49Z", - "expirationDate": "2025-11-19 21:48:17Z", - "justification": "This error is baselined with an expiration date of 180 days from 2025-06-02 21:48:17Z" - }, "1d4a48ebc63e3b652146bc16309b2d960a7168d299c7ac94cf794347c06265ef": { "signature": "1d4a48ebc63e3b652146bc16309b2d960a7168d299c7ac94cf794347c06265ef", "alternativeSignatures": [ @@ -326,21 +311,6 @@ "expirationDate": "2025-11-19 21:48:17Z", "justification": "This error is baselined with an expiration date of 180 days from 2025-06-02 21:48:17Z" }, - "77797a3e44634bb2994bd13ccc95ff4575bba474585dbd2cf3068a1c16bc0624": { - "signature": "77797a3e44634bb2994bd13ccc95ff4575bba474585dbd2cf3068a1c16bc0624", - "alternativeSignatures": [ - "4a6cb67bd4b401e9669c13a2162660aaefc0a94a4122e5b50c198414db545672" - ], - "target": "file:///D:/a/_work/1/vscode-server-win32-x64-web/node_modules/@vscode/vsce-sign/bin/vsce-sign.exe", - "memberOf": [ - "default" - ], - "tool": "binskim", - "ruleId": "BA2008", - "createdDate": "2025-06-02 21:46:49Z", - "expirationDate": "2025-11-19 21:48:17Z", - "justification": "This error is baselined with an expiration date of 180 days from 2025-06-02 21:48:17Z" - }, "21b8091cf937b1be55c7a300483182fec206bc0cd8e2666727b29c8c200aa101": { "signature": "21b8091cf937b1be55c7a300483182fec206bc0cd8e2666727b29c8c200aa101", "alternativeSignatures": [ @@ -416,21 +386,6 @@ "expirationDate": "2025-11-19 21:48:17Z", "justification": "This error is baselined with an expiration date of 180 days from 2025-06-02 21:48:17Z" }, - "30418bcc5269eaeb2832a2404465784431d4e72a2af332320c2b1db4768902ad": { - "signature": "30418bcc5269eaeb2832a2404465784431d4e72a2af332320c2b1db4768902ad", - "alternativeSignatures": [ - "b7b9eb974d7d3a4ae14df8695ca5a62592c8c9d20b7eda70a6535d50cbda3e7f" - ], - "target": "file:///D:/a/_work/1/VSCode-win32-x64/resources/app/node_modules/@vscode/vsce-sign/bin/vsce-sign.exe", - "memberOf": [ - "default" - ], - "tool": "binskim", - "ruleId": "BA2008", - "createdDate": "2025-06-02 21:46:49Z", - "expirationDate": "2025-11-19 21:48:17Z", - "justification": "This error is baselined with an expiration date of 180 days from 2025-06-02 21:48:17Z" - }, "d23a7cc83e649f9a9c5831255cb7569d363799adb5490ff7e299685ea7cf5000": { "signature": "d23a7cc83e649f9a9c5831255cb7569d363799adb5490ff7e299685ea7cf5000", "alternativeSignatures": [ @@ -462,4 +417,4 @@ "justification": "This error is baselined with an expiration date of 180 days from 2025-06-02 21:48:17Z" } } -} \ No newline at end of file +} From b84fb5b4177671bb30ae7cbb143cf7c126cda3f3 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Wed, 9 Jul 2025 09:59:12 -0700 Subject: [PATCH 262/306] only allow model/mode setting when not in input mode (#254927) --- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index cb158a491e0..dee43656492 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1130,15 +1130,15 @@ export class ChatWidget extends Disposable implements IChatWidget { ChatContextKeys.currentlyEditing.bindTo(editedRequest.contextKeyService).set(false); } - this.inputPart.setChatMode(this.inlineInputPart.currentModeKind); - const currentModel = this.inlineInputPart.selectedLanguageModel; - if (currentModel) { - this.inputPart.switchModel(currentModel.metadata); - } - const isInput = this.configurationService.getValue('chat.editRequests') === 'input'; if (!isInput) { + this.inputPart.setChatMode(this.input.currentModeKind); + const currentModel = this.input.selectedLanguageModel; + if (currentModel) { + this.inputPart.switchModel(currentModel.metadata); + } + this.inputPart?.toggleChatInputOverlay(false); try { if (editedRequest?.rowContainer && editedRequest.rowContainer.contains(this.inputContainer)) { From e52bb47e4148510862e790f7d0c01dfda7b1f0a2 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 9 Jul 2025 19:02:53 +0200 Subject: [PATCH 263/306] fix #254926 (#254928) --- src/vs/workbench/api/browser/mainThreadMcp.ts | 2 ++ .../chat/browser/actions/chatToolPicker.ts | 18 +++++++++++++----- .../common/discovery/extensionMcpDiscovery.ts | 3 ++- .../discovery/installedMcpServersDiscovery.ts | 8 ++++---- .../workbench/contrib/mcp/common/mcpTypes.ts | 2 ++ 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadMcp.ts b/src/vs/workbench/api/browser/mainThreadMcp.ts index 985f83c6fdb..dbd50bc28ab 100644 --- a/src/vs/workbench/api/browser/mainThreadMcp.ts +++ b/src/vs/workbench/api/browser/mainThreadMcp.ts @@ -13,6 +13,7 @@ import Severity from '../../../base/common/severity.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import * as nls from '../../../nls.js'; import { IDialogService, IPromptButton } from '../../../platform/dialogs/common/dialogs.js'; +import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js'; import { LogLevel } from '../../../platform/log/common/log.js'; import { IMcpMessageTransport, IMcpRegistry } from '../../contrib/mcp/common/mcpRegistryTypes.js'; import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; @@ -91,6 +92,7 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { const serverDefinitions = observableValue('mcpServers', servers); const handle = this._mcpRegistry.registerCollection({ ...collection, + source: new ExtensionIdentifier(collection.extensionId), resolveServerLanch: collection.canResolveLaunch ? (async def => { const r = await this._proxy.$resolveMcpLaunch(collection.id, def.label); return r ? McpServerLaunch.fromSerialized(r) : undefined; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts b/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts index 9143400255a..1b3f8d46fcc 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts @@ -12,13 +12,14 @@ import { assertType } from '../../../../../base/common/types.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { localize } from '../../../../../nls.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; -import { IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js'; +import { ExtensionEditorTab, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js'; import { McpCommandIds } from '../../../mcp/common/mcpCommandIds.js'; import { IMcpRegistry } from '../../../mcp/common/mcpRegistryTypes.js'; -import { IMcpServer, IMcpService, McpConnectionState } from '../../../mcp/common/mcpTypes.js'; +import { IMcpServer, IMcpService, IMcpWorkbenchService, McpConnectionState, McpServerEditorTab } from '../../../mcp/common/mcpTypes.js'; import { ILanguageModelToolsService, IToolData, ToolDataSource, ToolSet } from '../../common/languageModelToolsService.js'; import { ConfigureToolSets } from '../tools/toolSetsContribution.js'; @@ -59,8 +60,9 @@ export async function showToolsPicker( const mcpService = accessor.get(IMcpService); const mcpRegistry = accessor.get(IMcpRegistry); const commandService = accessor.get(ICommandService); - const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); const editorService = accessor.get(IEditorService); + const mcpWorkbenchService = accessor.get(IMcpWorkbenchService); const toolsService = accessor.get(ILanguageModelToolsService); const mcpServerByTool = new Map(); @@ -88,7 +90,7 @@ export async function showToolsPicker( const addMcpPick: CallbackPick = { type: 'item', label: localize('addServer', "Add MCP Server..."), iconClass: ThemeIcon.asClassName(Codicon.add), pickable: false, run: () => commandService.executeCommand(McpCommandIds.AddConfiguration) }; const configureToolSetsPick: CallbackPick = { type: 'item', label: localize('configToolSet', "Configure Tool Sets..."), iconClass: ThemeIcon.asClassName(Codicon.gear), pickable: false, run: () => commandService.executeCommand(ConfigureToolSets.ID) }; - const addExpPick: CallbackPick = { type: 'item', label: localize('addExtension', "Install Extension..."), iconClass: ThemeIcon.asClassName(Codicon.add), pickable: false, run: () => extensionWorkbenchService.openSearch('@tag:language-model-tools') }; + const addExpPick: CallbackPick = { type: 'item', label: localize('addExtension', "Install Extension..."), iconClass: ThemeIcon.asClassName(Codicon.add), pickable: false, run: () => extensionsWorkbenchService.openSearch('@tag:language-model-tools') }; const addPick: CallbackPick = { type: 'item', label: localize('addAny', "Add More Tools..."), iconClass: ThemeIcon.asClassName(Codicon.add), pickable: false, run: async () => { const pick = await quickPickService.pick( @@ -143,7 +145,13 @@ export async function showToolsPicker( toolBuckets.set(key, bucket); const collection = mcpRegistry.collections.get().find(c => c.id === mcpServer.collection.id); - if (collection?.presentation?.origin) { + if (collection?.source) { + buttons.push({ + iconClass: ThemeIcon.asClassName(Codicon.settingsGear), + tooltip: localize('configMcpCol', "Configure {0}", collection.label), + action: () => collection.source ? collection.source instanceof ExtensionIdentifier ? extensionsWorkbenchService.open(collection.source.value, { tab: ExtensionEditorTab.Features, feature: 'mcp' }) : mcpWorkbenchService.open(collection.source, { tab: McpServerEditorTab.Configuration }) : undefined + }); + } else if (collection?.presentation?.origin) { buttons.push({ iconClass: ThemeIcon.asClassName(Codicon.settingsGear), tooltip: localize('configMcpCol', "Configure {0}", collection.label), diff --git a/src/vs/workbench/contrib/mcp/common/discovery/extensionMcpDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/extensionMcpDiscovery.ts index 74cd6b26cf0..9232a0a2e11 100644 --- a/src/vs/workbench/contrib/mcp/common/discovery/extensionMcpDiscovery.ts +++ b/src/vs/workbench/contrib/mcp/common/discovery/extensionMcpDiscovery.ts @@ -92,7 +92,8 @@ export class ExtensionMcpDiscovery extends Disposable implements IMcpDiscovery { isCached: !!serverDefs, load: () => this._activateExtensionServers(coll.id), removed: () => extensionCollections.deleteAndDispose(id), - } + }, + source: collections.description.identifier }); extensionCollections.set(id, dispo); diff --git a/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts index ccfca206ce2..24089082107 100644 --- a/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts +++ b/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts @@ -9,7 +9,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js'; import { StorageScope } from '../../../../../platform/storage/common/storage.js'; import { IMcpRegistry } from '../mcpRegistryTypes.js'; -import { McpServerDefinition, McpServerTransportType, IMcpWorkbenchService, IMcpConfigPath } from '../mcpTypes.js'; +import { McpServerDefinition, McpServerTransportType, IMcpWorkbenchService, IMcpConfigPath, IWorkbenchMcpServer } from '../mcpTypes.js'; import { IMcpDiscovery } from './mcpDiscovery.js'; import { mcpConfigurationSection } from '../mcpConfiguration.js'; import { posix as pathPosix, win32 as pathWin32, sep as pathSep } from '../../../../../base/common/path.js'; @@ -58,7 +58,7 @@ export class InstalledMcpServersDiscovery extends Disposable implements IMcpDisc private async sync(): Promise { try { const remoteEnv = await this.remoteAgentService.getEnvironment(); - const collections = new Map(); + const collections = new Map(); const mcpConfigPathInfos = new ResourceMap } | undefined>>(); for (const server of this.mcpWorkbenchService.local) { if (!server.local) { @@ -81,7 +81,7 @@ export class InstalledMcpServersDiscovery extends Disposable implements IMcpDisc let definitions = collections.get(collectionId); if (!definitions) { - definitions = [mcpConfigPath, []]; + definitions = [mcpConfigPath, [], server]; collections.set(collectionId, definitions); } @@ -135,7 +135,7 @@ export class InstalledMcpServersDiscovery extends Disposable implements IMcpDisc for (const [id, [mcpConfigPath, serverDefinitions]] of collections) { this.collectionDisposables.deleteAndDispose(id); this.collectionDisposables.set(id, this.mcpRegistry.registerCollection({ - id: id, + id, label: mcpConfigPath?.label ?? '', presentation: { order: serverDefinitions[0]?.presentation?.order, diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 97756f2bced..eb7c88dd470 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -71,6 +71,8 @@ export interface McpCollectionDefinition { removed?(): void; }; + readonly source?: IWorkbenchMcpServer | ExtensionIdentifier; + readonly presentation?: { /** Sort order of the collection. */ readonly order?: number; From 0900bb815af61cd017096f264eedba165342a17a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 9 Jul 2025 19:12:22 +0200 Subject: [PATCH 264/306] Agent Mode pauses when VS Code loses focus (fix #252384) (#254827) * Agent Mode pauses when VS Code loses focus (fix #252384) * add logging * refactor - rename throttling parameter to allowed * refactor - extend `IChatModel` with `IDisposable` --- src/vs/platform/native/common/native.ts | 2 ++ .../electron-main/nativeHostMainService.ts | 8 ++++++ .../contrib/chat/common/chatModel.ts | 2 +- .../contrib/chat/common/chatService.ts | 2 ++ .../contrib/chat/common/chatServiceImpl.ts | 13 ++++++++-- .../electron-browser/chat.contribution.ts | 25 +++++++++++++++++++ .../chat/test/common/chatService.test.ts | 2 ++ .../chat/test/common/mockChatService.ts | 2 ++ .../electron-browser/workbenchTestServices.ts | 3 ++- 9 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 858ade15e78..67e82e14f1c 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -130,6 +130,8 @@ export interface ICommonNativeHostService { saveWindowSplash(splash: IPartsSplash): Promise; + setBackgroundThrottling(allowed: boolean): Promise; + /** * Make the window focused. * @param options specify the specific window to focus and the focus mode. diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index f3d2fbc9a57..caed0fa6dc5 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -371,6 +371,14 @@ export class NativeHostMainService extends Disposable implements INativeHostMain this.themeMainService.saveWindowSplash(windowId, window?.openedWorkspace, splash); } + async setBackgroundThrottling(windowId: number | undefined, allowed: boolean): Promise { + const window = this.codeWindowById(windowId); + + this.logService.trace(`Setting background throttling for window ${windowId} to '${allowed}'`); + + window?.win?.webContents?.setBackgroundThrottling(allowed); + } + //#endregion diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 5556dfdc80e..7f6ae8d59a8 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -901,7 +901,7 @@ export interface IChatRequestDisablement { afterUndoStop?: string; } -export interface IChatModel { +export interface IChatModel extends IDisposable { readonly onDidDispose: Event; readonly onDidChange: Event; readonly sessionId: string; diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index f537edeed16..fbaaeae7670 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -574,6 +574,8 @@ export interface IChatService { activateDefaultAgent(location: ChatAgentLocation): Promise; readonly edits2Enabled: boolean; + + readonly requestInProgressObs: IObservable; } export const KEYWORD_ACTIVIATION_SETTING_ID = 'accessibility.voice.keywordActivation'; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 3690cdf64b3..df2842c459e 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -13,6 +13,7 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { Disposable, DisposableMap, IDisposable } from '../../../../base/common/lifecycle.js'; import { revive } from '../../../../base/common/marshalling.js'; +import { derived, IObservable, ObservableMap } from '../../../../base/common/observable.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; import { URI } from '../../../../base/common/uri.js'; import { isLocation } from '../../../../editor/common/languages.js'; @@ -109,7 +110,7 @@ class CancellableRequest implements IDisposable { export class ChatService extends Disposable implements IChatService { declare _serviceBrand: undefined; - private readonly _sessionModels = this._register(new DisposableMap()); + private readonly _sessionModels = new ObservableMap(); private readonly _pendingRequests = this._register(new DisposableMap()); private _persistedSessions: ISerializableChatsData; @@ -134,6 +135,8 @@ export class ChatService extends Disposable implements IChatService { private readonly _chatServiceTelemetry: ChatServiceTelemetry; private readonly _chatSessionStore: ChatSessionStore; + readonly requestInProgressObs: IObservable; + @memoize private get useFileStorage(): boolean { return this.configurationService.getValue(ChatConfiguration.UseFileStorage); @@ -196,6 +199,11 @@ export class ChatService extends Disposable implements IChatService { } this._register(storageService.onWillSaveState(() => this.saveState())); + + this.requestInProgressObs = derived(reader => { + const models = this._sessionModels.observable.read(reader).values(); + return Array.from(models).some(model => model.requestInProgressObs.read(reader)); + }); } isEnabled(location: ChatAgentLocation): boolean { @@ -1125,7 +1133,8 @@ export class ChatService extends Disposable implements IChatService { } } - this._sessionModels.deleteAndDispose(sessionId); + this._sessionModels.delete(sessionId); + model.dispose(); this._pendingRequests.get(sessionId)?.cancel(); this._pendingRequests.deleteAndDispose(sessionId); this._onDidDisposeSession.fire({ sessionId, reason: 'cleared' }); diff --git a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts index e9434bfd546..d3b943b273c 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts @@ -27,6 +27,9 @@ import { IWorkbenchLayoutService } from '../../../services/layout/browser/layout import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { ViewContainerLocation } from '../../../common/views.js'; +import { INativeHostService } from '../../../../platform/native/common/native.js'; +import { IChatService } from '../common/chatService.js'; +import { autorun } from '../../../../base/common/observable.js'; class NativeBuiltinToolsContribution extends Disposable implements IWorkbenchContribution { @@ -106,6 +109,27 @@ class ChatCommandLineHandler extends Disposable { } } +class ChatSuspendThrottlingHandler extends Disposable { + + static readonly ID = 'workbench.contrib.chatSuspendThrottlingHandler'; + + constructor( + @INativeHostService nativeHostService: INativeHostService, + @IChatService chatService: IChatService + ) { + super(); + + this._register(autorun(reader => { + const running = chatService.requestInProgressObs.read(reader); + + // When a chat request is in progress, we must ensure that background + // throttling is not applied so that the chat session can continue + // even when the window is not in focus. + nativeHostService.setBackgroundThrottling(!running); + })); + } +} + registerAction2(StartVoiceChatAction); registerAction2(InstallSpeechProviderForVoiceChatAction); @@ -126,3 +150,4 @@ registerChatDeveloperActions(); registerWorkbenchContribution2(KeywordActivationContribution.ID, KeywordActivationContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(NativeBuiltinToolsContribution.ID, NativeBuiltinToolsContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatCommandLineHandler.ID, ChatCommandLineHandler, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2(ChatSuspendThrottlingHandler.ID, ChatSuspendThrottlingHandler, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index 21ef20cdc00..0d011f6e027 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -287,6 +287,7 @@ suite('ChatService', () => { assert(chatModel2); await assertSnapshot(toSnapshotExportData(chatModel2)); + chatModel2.dispose(); }); test('can deserialize with response', async () => { @@ -315,6 +316,7 @@ suite('ChatService', () => { assert(chatModel2); await assertSnapshot(toSnapshotExportData(chatModel2)); + chatModel2.dispose(); }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index b3dbb032861..99e32b76c0f 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -5,6 +5,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Event } from '../../../../../base/common/event.js'; +import { observableValue } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, ISerializableChatData } from '../../common/chatModel.js'; import { IParsedChatRequest } from '../../common/chatParserTypes.js'; @@ -12,6 +13,7 @@ import { IChatCompleteResponse, IChatDetail, IChatProviderInfo, IChatSendRequest import { ChatAgentLocation } from '../../common/constants.js'; export class MockChatService implements IChatService { + requestInProgressObs = observableValue('name', false); edits2Enabled: boolean = false; _serviceBrand: undefined; transferredSessionData: IChatTransferredSessionData | undefined; diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 84d5af6fe3f..d71604c3c3f 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -104,11 +104,12 @@ export class TestNativeHostService implements INativeHostService { async isWindowAlwaysOnTop(options?: INativeHostOptions): Promise { return false; } async toggleWindowAlwaysOnTop(options?: INativeHostOptions): Promise { } async setWindowAlwaysOnTop(alwaysOnTop: boolean, options?: INativeHostOptions): Promise { } - getCursorScreenPoint(): Promise<{ readonly point: IPoint; readonly display: IRectangle }> { throw new Error('Method not implemented.'); } + async getCursorScreenPoint(): Promise<{ readonly point: IPoint; readonly display: IRectangle }> { throw new Error('Method not implemented.'); } async positionWindow(position: IRectangle, options?: INativeHostOptions): Promise { } async updateWindowControls(options: { height?: number; backgroundColor?: string; foregroundColor?: string }): Promise { } async setMinimumSize(width: number | undefined, height: number | undefined): Promise { } async saveWindowSplash(value: IPartsSplash): Promise { } + async setBackgroundThrottling(throttling: boolean): Promise { } async focusWindow(options?: INativeHostOptions): Promise { } async showMessageBox(options: Electron.MessageBoxOptions): Promise { throw new Error('Method not implemented.'); } async showSaveDialog(options: Electron.SaveDialogOptions): Promise { throw new Error('Method not implemented.'); } From e3d6829b41d9964043213e9406facd0516960298 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Wed, 9 Jul 2025 10:56:03 -0700 Subject: [PATCH 265/306] new implicit context flow (#254768) * new implicit flow * new implicit flow * some logic cleanup: * gate implicit context stuff behind setting * cleanup * hygiene weewoowewoo --- .../attachments/implicitContextAttachment.ts | 72 ++++++++++++++++--- .../contrib/chat/browser/chat.contribution.ts | 6 ++ .../contrib/chat/browser/chatInputPart.ts | 38 ++++++++-- .../browser/contrib/chatImplicitContext.ts | 24 ++++--- .../contrib/chat/browser/media/chat.css | 11 +++ 5 files changed, 129 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts index 1694d4242bc..b9829ffa9a0 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts @@ -4,10 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../../base/browser/dom.js'; +import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js'; import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; import { Button } from '../../../../../base/browser/ui/button/button.js'; import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { Codicon } from '../../../../../base/common/codicons.js'; +import { KeyCode } from '../../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; import { basename, dirname } from '../../../../../base/common/resources.js'; @@ -17,6 +19,7 @@ import { IModelService } from '../../../../../editor/common/services/model.js'; import { localize } from '../../../../../nls.js'; import { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { FileKind, IFileService } from '../../../../../platform/files/common/files.js'; @@ -25,6 +28,8 @@ import { ILabelService } from '../../../../../platform/label/common/label.js'; import { ResourceLabels } from '../../../../browser/labels.js'; import { ResourceContextKey } from '../../../../common/contextkeys.js'; import { IChatRequestImplicitVariableEntry } from '../../common/chatVariableEntries.js'; +import { IChatWidgetService } from '../chat.js'; +import { ChatAttachmentModel } from '../chatAttachmentModel.js'; export class ImplicitContextAttachmentWidget extends Disposable { public readonly domNode: HTMLElement; @@ -34,6 +39,7 @@ export class ImplicitContextAttachmentWidget extends Disposable { constructor( private readonly attachment: IChatRequestImplicitVariableEntry, private readonly resourceLabels: ResourceLabels, + private readonly attachmentModel: ChatAttachmentModel, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @ILabelService private readonly labelService: ILabelService, @@ -42,6 +48,8 @@ export class ImplicitContextAttachmentWidget extends Disposable { @ILanguageService private readonly languageService: ILanguageService, @IModelService private readonly modelService: IModelService, @IHoverService private readonly hoverService: IHoverService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IConfigurationService private readonly configService: IConfigurationService ) { super(); @@ -80,17 +88,56 @@ export class ImplicitContextAttachmentWidget extends Disposable { this.domNode.ariaLabel = ariaLabel; this.domNode.tabIndex = 0; - const hintLabel = localize('hint.label.current', "Current {0}", attachmentTypeName); + const isSuggestedEnabled = this.configService.getValue('chat.implicitContext.suggestedContext'); + const hintLabel = !this.attachment.isSelection && !isSuggestedEnabled ? localize('hint.label.current', "Current {0}", attachmentTypeName) : ''; const hintElement = dom.append(this.domNode, dom.$('span.chat-implicit-hint', undefined, hintLabel)); this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), hintElement, title)); - const buttonMsg = this.attachment.enabled ? localize('disable', "Disable current {0} context", attachmentTypeName) : localize('enable', "Enable current {0} context", attachmentTypeName); - const toggleButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: buttonMsg })); - toggleButton.icon = this.attachment.enabled ? Codicon.eye : Codicon.eyeClosed; - this.renderDisposables.add(toggleButton.onDidClick((e) => { - e.stopPropagation(); // prevent it from triggering the click handler on the parent immediately after rerendering - this.attachment.enabled = !this.attachment.enabled; - })); + + if (isSuggestedEnabled) { + if (!this.attachment.isSelection) { + const buttonMsg = this.attachment.enabled ? localize('disable', "Disable current {0} context", attachmentTypeName) : localize('enable', "Enable current {0} context", attachmentTypeName); + const toggleButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: buttonMsg })); + toggleButton.icon = this.attachment.enabled ? Codicon.x : Codicon.plus; + this.renderDisposables.add(toggleButton.onDidClick((e) => { + e.stopPropagation(); + e.preventDefault(); + if (!this.attachment.enabled) { + this.convertToRegularAttachment(); + } + this.attachment.enabled = false; + })); + } + + if (!this.attachment.enabled && this.attachment.isSelection) { + this.domNode.classList.remove('disabled'); + } + + this.renderDisposables.add(dom.addDisposableListener(this.domNode, dom.EventType.CLICK, e => { + if (!this.attachment.enabled && !this.attachment.isSelection) { + this.convertToRegularAttachment(); + } + })); + + this.renderDisposables.add(dom.addDisposableListener(this.domNode, dom.EventType.KEY_DOWN, e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { + if (!this.attachment.enabled && !this.attachment.isSelection) { + e.preventDefault(); + e.stopPropagation(); + this.convertToRegularAttachment(); + } + } + })); + } else { + const buttonMsg = this.attachment.enabled ? localize('disable', "Disable current {0} context", attachmentTypeName) : localize('enable', "Enable current {0} context", attachmentTypeName); + const toggleButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: buttonMsg })); + toggleButton.icon = this.attachment.enabled ? Codicon.eye : Codicon.eyeClosed; + this.renderDisposables.add(toggleButton.onDidClick((e) => { + e.stopPropagation(); // prevent it from triggering the click handler on the parent immediately after rerendering + this.attachment.enabled = !this.attachment.enabled; + })); + } // Context menu const scopedContextKeyService = this.renderDisposables.add(this.contextKeyService.createScoped(this.domNode)); @@ -112,4 +159,13 @@ export class ImplicitContextAttachmentWidget extends Disposable { }); })); } + + private convertToRegularAttachment(): void { + if (!this.attachment.value) { + return; + } + const file = URI.isUri(this.attachment.value) ? this.attachment.value : this.attachment.value.uri; + this.attachmentModel.addFile(file); + this.chatWidgetService.lastFocusedWidget?.focusInput(); + } } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index c68b718f8db..95b5d248e1c 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -170,6 +170,12 @@ configurationRegistry.registerConfiguration({ 'panel': 'always', } }, + 'chat.implicitContext.suggestedContext': { + type: 'boolean', + tags: ['experimental'], + markdownDescription: nls.localize('chat.implicitContext.suggestedContext', "Controls whether the new implicit context flow is shown. In Ask and Edit modes, the context will automatically be included. In Agent mode context will be suggested as an attachment. Selections are always included as context."), + default: true, + }, 'chat.editing.autoAcceptDelay': { type: 'number', markdownDescription: nls.localize('chat.editing.autoAcceptDelay', "Delay after which changes made by chat are automatically accepted. Values are in seconds, `0` means disabled and `100` seconds is the maximum."), diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 4a687c90b2f..c7f6b08e50f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -102,6 +102,8 @@ import { ChatRelatedFiles } from './contrib/chatInputRelatedFilesContrib.js'; import { resizeImage } from './imageUtils.js'; import { IModelPickerDelegate, ModelPickerActionItem } from './modelPicker/modelPickerActionItem.js'; import { IModePickerDelegate, ModePickerActionItem } from './modelPicker/modePickerActionItem.js'; +import { isEqual } from '../../../../base/common/resources.js'; +import { isLocation } from '../../../../editor/common/languages.js'; const $ = dom.$; @@ -169,7 +171,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge contextArr.add(...this.attachmentModel.attachments); - if (this.implicitContext?.enabled && this.implicitContext.value) { + if ((this.implicitContext?.enabled && this.implicitContext?.value) || (isLocation(this.implicitContext?.value) && this.configurationService.getValue('chat.implicitContext.suggestedContext'))) { const implicitChatVariables = this.implicitContext.toBaseEntries(); contextArr.add(...implicitChatVariables); } @@ -442,6 +444,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (this._inputEditor) { this._inputEditor.updateOptions({ ariaLabel: this._getAriaLabel() }); } + + if (this.implicitContext && this.configurationService.getValue('chat.implicitContext.suggestedContext')) { + this.implicitContext.enabled = this._currentModeObservable.get() !== ChatMode.Agent; + } })); this._register(this._onDidChangeCurrentLanguageModel.event(() => { if (this._currentLanguageModel?.metadata.name) { @@ -1301,8 +1307,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._indexOfLastOpenedContext = -1; } - if (this.implicitContext?.value) { - const implicitPart = store.add(this.instantiationService.createInstance(ImplicitContextAttachmentWidget, this.implicitContext, this._contextResourceLabels)); + const isSuggestedEnabled = this.configurationService.getValue('chat.implicitContext.suggestedContext'); + + if (this.implicitContext?.value && !isSuggestedEnabled) { + const implicitPart = store.add(this.instantiationService.createInstance(ImplicitContextAttachmentWidget, this.implicitContext, this._contextResourceLabels, this.attachmentModel)); container.appendChild(implicitPart.domNode); } @@ -1355,6 +1363,19 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge })); } + const implicitUri = this.implicitContext?.value; + const isUri = URI.isUri(implicitUri); + + if (isSuggestedEnabled && implicitUri && (isUri || isLocation(implicitUri))) { + const targetUri = isUri ? implicitUri : implicitUri.uri; + const currentlyAttached = attachments.some(([, attachment]) => URI.isUri(attachment.value) && isEqual(attachment.value, targetUri)); + + const shouldShowImplicit = isUri ? !currentlyAttached : implicitUri.range; + if (shouldShowImplicit) { + const implicitPart = store.add(this.instantiationService.createInstance(ImplicitContextAttachmentWidget, this.implicitContext, this._contextResourceLabels, this._attachmentModel)); + container.appendChild(implicitPart.domNode); + } + } if (oldHeight !== this.attachmentsContainer.offsetHeight) { this._onDidChangeHeight.fire(); @@ -1369,9 +1390,18 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._indexOfLastAttachedContextDeletedWithKeyboard = index; } - this._attachmentModel.delete(attachment.id); + + if (this.configurationService.getValue('chat.implicitContext.enableImplicitContext')) { + // if currently opened file is deleted, do not show implicit context + const implicitValue = URI.isUri(this.implicitContext?.value) && URI.isUri(attachment.value) && isEqual(this.implicitContext.value, attachment.value); + + if (this.implicitContext?.isFile && implicitValue) { + this.implicitContext.enabled = false; + } + } + if (this._attachmentModel.size === 0) { this.focus(); } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts index 79d057dc429..7ec2881066c 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts @@ -161,17 +161,21 @@ export class ChatImplicitContextContribution extends Disposable implements IWork newValue = { uri: model.uri, range: selection } satisfies Location; isSelection = true; } else { - const visibleRanges = codeEditor?.getVisibleRanges(); - if (visibleRanges && visibleRanges.length > 0) { - // Merge visible ranges. Maybe the reference value could actually be an array of Locations? - // Something like a Location with an array of Ranges? - let range = visibleRanges[0]; - visibleRanges.slice(1).forEach(r => { - range = range.plusRange(r); - }); - newValue = { uri: model.uri, range } satisfies Location; - } else { + if (this.configurationService.getValue('chat.implicitContext.suggestedContext')) { newValue = model.uri; + } else { + const visibleRanges = codeEditor?.getVisibleRanges(); + if (visibleRanges && visibleRanges.length > 0) { + // Merge visible ranges. Maybe the reference value could actually be an array of Locations? + // Something like a Location with an array of Ranges? + let range = visibleRanges[0]; + visibleRanges.slice(1).forEach(r => { + range = range.plusRange(r); + }); + newValue = { uri: model.uri, range } satisfies Location; + } else { + newValue = model.uri; + } } } } diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index a99fa653618..ed2b27a18a0 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -1255,6 +1255,10 @@ have to be updated for changes to the rules above, or to support more deeply nes outline-offset: -4px; } +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button.codicon.codicon-plus { + font-size: 12px; +} + .chat-related-files .monaco-button.codicon.codicon-add:hover, .action-item.chat-attached-context-attachment.chat-add-files:hover, .interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button:hover { @@ -2061,3 +2065,10 @@ have to be updated for changes to the rules above, or to support more deeply nes width: 1px; height: 1px; } + + +.interactive-session .chat-attached-context .chat-attached-context-attachment.implicit.disabled:hover { + cursor: pointer; + border-style: solid; + background-color: var(--vscode-toolbar-hoverBackground); +} From b2152a7418d0396d9043527dfdb04d72be3d495e Mon Sep 17 00:00:00 2001 From: Gabriel Csapo Date: Wed, 9 Jul 2025 11:29:39 -0700 Subject: [PATCH 266/306] chore: updates server.log enum --- extensions/typescript-language-features/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index a087e162080..7f66178f09c 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -1512,7 +1512,8 @@ "off", "terse", "normal", - "verbose" + "verbose", + "requestTime" ], "default": "off", "description": "%typescript.tsserver.log%", From bd8ade54cd9bb42ccfef33a8ed72b83baef47e6d Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 9 Jul 2025 20:34:11 +0200 Subject: [PATCH 267/306] improves observable dev tools (#254942) --- .../logging/debugger/debuggerApi.d.ts | 6 ++++++ .../logging/debugger/devToolsLogger.ts | 20 ++++++++++++++++++- .../observables/derivedImpl.ts | 8 ++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/vs/base/common/observableInternal/logging/debugger/debuggerApi.d.ts b/src/vs/base/common/observableInternal/logging/debugger/debuggerApi.d.ts index 138732f44b2..10557a75864 100644 --- a/src/vs/base/common/observableInternal/logging/debugger/debuggerApi.d.ts +++ b/src/vs/base/common/observableInternal/logging/debugger/debuggerApi.d.ts @@ -24,9 +24,15 @@ export type ObsDebuggerApi = { getDerivedInfo(instanceId: ObsInstanceId): IDerivedObservableDetailedInfo; getAutorunInfo(instanceId: ObsInstanceId): IAutorunDetailedInfo; getObservableValueInfo(instanceId: ObsInstanceId): IObservableValueInfo; + setValue(instanceId: ObsInstanceId, jsonValue: unknown): void; getValue(instanceId: ObsInstanceId): unknown; + // For autorun and deriveds + rerun(instanceId: ObsInstanceId): void; + + logValue(instanceId: ObsInstanceId): void; + getTransactionState(): ITransactionState | undefined; } }; diff --git a/src/vs/base/common/observableInternal/logging/debugger/devToolsLogger.ts b/src/vs/base/common/observableInternal/logging/debugger/devToolsLogger.ts index 2405a11138c..513921408a5 100644 --- a/src/vs/base/common/observableInternal/logging/debugger/devToolsLogger.ts +++ b/src/vs/base/common/observableInternal/logging/debugger/devToolsLogger.ts @@ -135,7 +135,25 @@ export class DevToolsLogger implements IObservableLogger { } return undefined; - } + }, + logValue: (instanceId) => { + const obs = this._aliveInstances.get(instanceId); + if (obs && 'get' in obs) { + console.log('Logged Value:', obs.get()); + } else { + throw new BugIndicatingError('Observable is not supported'); + } + }, + rerun: (instanceId) => { + const obs = this._aliveInstances.get(instanceId); + if (obs instanceof Derived) { + obs.debugRecompute(); + } else if (obs instanceof AutorunObserver) { + obs.debugRerun(); + } else { + throw new BugIndicatingError('Observable is not supported'); + } + }, } }; }); diff --git a/src/vs/base/common/observableInternal/observables/derivedImpl.ts b/src/vs/base/common/observableInternal/observables/derivedImpl.ts index bbedd1518a6..011e9c76f18 100644 --- a/src/vs/base/common/observableInternal/observables/derivedImpl.ts +++ b/src/vs/base/common/observableInternal/observables/derivedImpl.ts @@ -400,6 +400,14 @@ export class Derived extends BaseObserv this._value = newValue as any; } + public debugRecompute(): void { + if (!this._isComputing) { + this._recompute(); + } else { + this._state = DerivedState.stale; + } + } + public setValue(newValue: T, tx: ITransaction, change: TChange): void { this._value = newValue; const observers = this._observers; From a4495aaf9aab60b86505ada1e1e57f477037aa7c Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 9 Jul 2025 20:45:19 +0200 Subject: [PATCH 268/306] Monaco editor webworker fixes (#254951) --- src/vs/base/browser/webWorkerFactory.ts | 11 +++-- .../browser/services/editorWorkerService.ts | 2 +- .../editor/common/services/editorWebWorker.ts | 2 +- src/vs/editor/editor.worker.start.ts | 47 ++++++++++++------- .../standalone/browser/standaloneWebWorker.ts | 6 ++- src/vs/monaco.d.ts | 4 +- 6 files changed, 45 insertions(+), 27 deletions(-) diff --git a/src/vs/base/browser/webWorkerFactory.ts b/src/vs/base/browser/webWorkerFactory.ts index 58a43681228..d55ef7601cc 100644 --- a/src/vs/base/browser/webWorkerFactory.ts +++ b/src/vs/base/browser/webWorkerFactory.ts @@ -129,13 +129,14 @@ class WebWorker extends Disposable implements IWebWorker { private readonly _onError = this._register(new Emitter()); public readonly onError = this._onError.event; - constructor(descriptorOrWorker: IWebWorkerDescriptor | Worker) { + constructor(descriptorOrWorker: IWebWorkerDescriptor | Worker | Promise) { super(); this.id = ++WebWorker.LAST_WORKER_ID; const workerOrPromise = ( descriptorOrWorker instanceof Worker - ? descriptorOrWorker - : getWorker(descriptorOrWorker, this.id) + ? descriptorOrWorker : + 'then' in descriptorOrWorker ? descriptorOrWorker + : getWorker(descriptorOrWorker, this.id) ); if (isPromiseLike(workerOrPromise)) { this.worker = workerOrPromise; @@ -197,8 +198,8 @@ export class WebWorkerDescriptor implements IWebWorkerDescriptor { } export function createWebWorker(esmModuleLocation: URI, label: string | undefined): IWebWorkerClient; -export function createWebWorker(workerDescriptor: IWebWorkerDescriptor | Worker): IWebWorkerClient; -export function createWebWorker(arg0: URI | IWebWorkerDescriptor | Worker, arg1?: string | undefined): IWebWorkerClient { +export function createWebWorker(workerDescriptor: IWebWorkerDescriptor | Worker | Promise): IWebWorkerClient; +export function createWebWorker(arg0: URI | IWebWorkerDescriptor | Worker | Promise, arg1?: string | undefined): IWebWorkerClient { const workerDescriptorOrWorker = (URI.isUri(arg0) ? new WebWorkerDescriptor(arg0, arg1) : arg0); return new WebWorkerClient(new WebWorker(workerDescriptorOrWorker)); } diff --git a/src/vs/editor/browser/services/editorWorkerService.ts b/src/vs/editor/browser/services/editorWorkerService.ts index 7c0d74d79c6..3475a737bec 100644 --- a/src/vs/editor/browser/services/editorWorkerService.ts +++ b/src/vs/editor/browser/services/editorWorkerService.ts @@ -427,7 +427,7 @@ export class EditorWorkerClient extends Disposable implements IEditorWorkerClien private _disposed = false; constructor( - private readonly _workerDescriptorOrWorker: IWebWorkerDescriptor | Worker, + private readonly _workerDescriptorOrWorker: IWebWorkerDescriptor | Worker | Promise, keepIdleModels: boolean, @IModelService modelService: IModelService, ) { diff --git a/src/vs/editor/common/services/editorWebWorker.ts b/src/vs/editor/common/services/editorWebWorker.ts index ca63478efbd..eac9e795ec3 100644 --- a/src/vs/editor/common/services/editorWebWorker.ts +++ b/src/vs/editor/common/services/editorWebWorker.ts @@ -38,7 +38,7 @@ export interface IMirrorModel extends IMirrorTextModel { getValue(): string; } -export interface IWorkerContext { +export interface IWorkerContext { /** * A proxy to the main thread host object. */ diff --git a/src/vs/editor/editor.worker.start.ts b/src/vs/editor/editor.worker.start.ts index a7304493087..2916b8ae38c 100644 --- a/src/vs/editor/editor.worker.start.ts +++ b/src/vs/editor/editor.worker.start.ts @@ -12,24 +12,37 @@ import { EditorWorkerHost } from './common/services/editorWorkerHost.js'; * @skipMangle * @internal */ -export function start(client: TClient): IWorkerContext { - const webWorkerServer = initialize(() => new EditorWorker(client)); - const editorWorkerHost = EditorWorkerHost.getChannel(webWorkerServer); - const host = new Proxy({}, { - get(target, prop, receiver) { - if (typeof prop !== 'string') { - throw new Error(`Not supported`); +export function start(createClient: (ctx: IWorkerContext) => TClient): TClient { + let client: TClient | undefined; + const webWorkerServer = initialize((workerServer) => { + const editorWorkerHost = EditorWorkerHost.getChannel(workerServer); + + const host = new Proxy({}, { + get(target, prop, receiver) { + if (prop === 'then') { + // Don't forward the call when the proxy is returned in an async function and the runtime tries to .then it. + return undefined; + } + if (typeof prop !== 'string') { + throw new Error(`Not supported`); + } + return (...args: unknown[]) => { + return editorWorkerHost.$fhr(prop, args); + }; } - return (...args: unknown[]) => { - return editorWorkerHost.$fhr(prop, args); - }; - } + }); + + const ctx: IWorkerContext = { + host: host as THost, + getMirrorModels: () => { + return webWorkerServer.requestHandler.getModels(); + } + }; + + client = createClient(ctx); + + return new EditorWorker(client); }); - return { - host: host as THost, - getMirrorModels: () => { - return webWorkerServer.requestHandler.getModels(); - } - }; + return client!; } diff --git a/src/vs/editor/standalone/browser/standaloneWebWorker.ts b/src/vs/editor/standalone/browser/standaloneWebWorker.ts index f8aa5966d51..4cdd6cf5477 100644 --- a/src/vs/editor/standalone/browser/standaloneWebWorker.ts +++ b/src/vs/editor/standalone/browser/standaloneWebWorker.ts @@ -38,7 +38,7 @@ export interface IInternalWebWorkerOptions { /** * The worker. */ - worker: Worker; + worker: Worker | Promise; /** * An object that can be used by the web worker to make calls back to the main thread. */ @@ -61,6 +61,10 @@ class MonacoWebWorkerImpl extends EditorWorkerClient implement this._foreignProxy = this._getProxy().then(proxy => { return new Proxy({}, { get(target, prop, receiver) { + if (prop === 'then') { + // Don't forward the call when the proxy is returned in an async function and the runtime tries to .then it. + return undefined; + } if (typeof prop !== 'string') { throw new Error(`Not supported`); } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index c75078e1976..3fbbdb6d43c 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1223,7 +1223,7 @@ declare namespace monaco.editor { /** * The worker. */ - worker: Worker; + worker: Worker | Promise; /** * An object that can be used by the web worker to make calls back to the main thread. */ @@ -8542,7 +8542,7 @@ declare namespace monaco.worker { getValue(): string; } - export interface IWorkerContext { + export interface IWorkerContext { /** * A proxy to the main thread host object. */ From d2c7ed40fb8295943883e315e489270e3a5861a6 Mon Sep 17 00:00:00 2001 From: Michael Lively <12552271+Yoyokrazy@users.noreply.github.com> Date: Wed, 9 Jul 2025 11:54:02 -0700 Subject: [PATCH 269/306] add Copilot issue template (#254954) * add copilot bug template * remove text about internal backlog or public vscode --- .github/ISSUE_TEMPLATE/copilot_bug_report.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/copilot_bug_report.md diff --git a/.github/ISSUE_TEMPLATE/copilot_bug_report.md b/.github/ISSUE_TEMPLATE/copilot_bug_report.md new file mode 100644 index 00000000000..bf163636a68 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/copilot_bug_report.md @@ -0,0 +1,19 @@ +--- +name: Copilot Bug report +about: Create a report to help us improve Copilot's chat interface in VS Code +--- + + + + +- Copilot Chat Extension Version: +- VS Code Version: +- OS Version: +- Feature (e.g. agent/edit/ask mode): +- Selected model (e.g. GPT 4.1, Claude 3.7 Sonnet): +- Logs: + +Steps to Reproduce: + +1. +2. From d32a677a0387eedd91d26380272e5f0bb5a8b8df Mon Sep 17 00:00:00 2001 From: Michael Lively <12552271+Yoyokrazy@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:10:13 -0700 Subject: [PATCH 270/306] Update issue templates (#254957) --- .github/ISSUE_TEMPLATE/bug_report.md | 7 ++++++- .github/ISSUE_TEMPLATE/copilot_bug_report.md | 4 ++++ .github/ISSUE_TEMPLATE/feature_request.md | 3 +++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index ce9e17c1c96..ad1f2b23812 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,12 @@ --- name: Bug report about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + --- + @@ -19,4 +24,4 @@ Does this issue occur when all extensions are disabled?: Yes/No Steps to Reproduce: 1. -2. +2. diff --git a/.github/ISSUE_TEMPLATE/copilot_bug_report.md b/.github/ISSUE_TEMPLATE/copilot_bug_report.md index bf163636a68..9a77481a8c6 100644 --- a/.github/ISSUE_TEMPLATE/copilot_bug_report.md +++ b/.github/ISSUE_TEMPLATE/copilot_bug_report.md @@ -1,6 +1,10 @@ --- name: Copilot Bug report about: Create a report to help us improve Copilot's chat interface in VS Code +title: '' +labels: chat-ext-issue +assignees: '' + --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index b9c6c83caa3..4e2639b9c6f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,6 +1,9 @@ --- name: Feature request about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' --- From 942dbe295ff4321383d794d7e7a3ecbc72a63f7e Mon Sep 17 00:00:00 2001 From: Elijah King Date: Wed, 9 Jul 2025 12:17:34 -0700 Subject: [PATCH 271/306] Removed 'Copilot' from 1. Right click menu 2. Genrate commit message 3. Search 4. Getting started walkthrough --- .../contrib/chat/browser/actions/chatActions.ts | 2 +- src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 2 +- .../contrib/search/browser/searchResultsView.ts | 9 ++++----- src/vs/workbench/contrib/search/browser/searchView.ts | 4 ++-- .../common/gettingStartedContent.ts | 2 +- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 2ce2ace4d58..e2bd588e29c 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -1086,7 +1086,7 @@ const menuContext = ContextKeyExpr.and( ChatContextKeys.Setup.disabled.negate() ); -const title = localize('copilot', "Copilot"); +const title = localize('ai actions', "AI Actions"); MenuRegistry.appendMenuItem(MenuId.EditorContext, { submenu: MenuId.ChatTextEditorMenu, diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 6f6c7441af7..ef1b10e3eef 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1335,7 +1335,7 @@ registerAction2(class extends Action2 { constructor() { super({ id: SCMInputWidgetCommandId.SetupAction, - title: localize('scmInputGenerateCommitMessage', "Generate Commit Message with Copilot"), + title: localize('scmInputGenerateCommitMessage', "Generate commit message"), icon: Codicon.sparkle, f1: false, menu: { diff --git a/src/vs/workbench/contrib/search/browser/searchResultsView.ts b/src/vs/workbench/contrib/search/browser/searchResultsView.ts index b66f6bae7c7..fb2ec1fc959 100644 --- a/src/vs/workbench/contrib/search/browser/searchResultsView.ts +++ b/src/vs/workbench/contrib/search/browser/searchResultsView.ts @@ -141,20 +141,19 @@ export class TextSearchResultRenderer extends Disposable implements ICompressibl SearchContext.FileFocusKey.bindTo(templateData.contextKeyService).set(false); SearchContext.FolderFocusKey.bindTo(templateData.contextKeyService).set(false); } else { - let aiName = 'Copilot'; try { - aiName = (await node.element.parent().searchModel.getAITextResultProviderName()) || 'Copilot'; + await node.element.parent().searchModel.getAITextResultProviderName(); } catch { // ignore } const localizedLabel = nls.localize({ key: 'searchFolderMatch.aiText.label', - comment: ['This is displayed before the AI text search results, where {0} will be in the place of the AI name (ie: Copilot)'] - }, '{0} Results', aiName); + comment: ['This is displayed before the AI text search results, now always "AI-assisted results".'] + }, 'AI-assisted results'); // todo: make icon extension-contributed. - templateData.label.setLabel(`$(${Codicon.copilot.id}) ${localizedLabel}`); + templateData.label.setLabel(`$(${Codicon.searchSparkle.id}) ${localizedLabel}`); SearchContext.AIResultsTitle.bindTo(templateData.contextKeyService).set(true); SearchContext.MatchFocusKey.bindTo(templateData.contextKeyService).set(false); diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 668140877c8..6069eda7039 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -1746,10 +1746,10 @@ export class SearchView extends ViewPane { if (aiName) { const searchWithAIButtonTooltip = appendKeyBindingLabel( - nls.localize('triggerAISearch.tooltip', "Search with {0}", aiName), + nls.localize('triggerAISearch.tooltip', "Search with AI."), this.keybindingService.lookupKeybinding(Constants.SearchCommandIds.SearchWithAIActionId) ); - const searchWithAIButtonText = nls.localize('searchWithAIButtonTooltip', "Search with {0}.", aiName); + const searchWithAIButtonText = nls.localize('searchWithAIButtonTooltip', "Search with AI."); const searchWithAIButton = this.messageDisposables.add(new SearchLinkButton( searchWithAIButtonText, () => { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index fad3c94a4c5..f2172d92134 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts @@ -700,7 +700,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'copilotSetup.customize', title: localize('gettingStarted.customize.title', "Personalized to how you work"), - description: localize('gettingStarted.customize.description', "Swap models, add agent mode tools, and create personalized instructions.\n{0}", Button(localize('signUp', "Set up AI"), 'command:workbench.action.chat.triggerSetupWithoutDialog')), + description: localize('gettingStarted.customize.description', "Swap models, add agent mode tools, and create personalized instructions.\n{0}", Button(localize('signUp', "Enable AI features"), 'command:workbench.action.chat.triggerSetupWithoutDialog')), media: { type: 'svg', altText: 'Personalize', path: 'customize-ai.svg' }, From 4b8eae3f4e6becc357b5324da2776f6137a611b6 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 9 Jul 2025 21:36:22 +0200 Subject: [PATCH 272/306] Includes delta modified characters and trigger (#254956) --- .../browser/editSourceTrackingImpl.ts | 147 ++++++++++-------- .../editTelemetry/browser/editTracker.ts | 20 ++- 2 files changed, 101 insertions(+), 66 deletions(-) diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts index a6abbd1ead2..6849a9f7d47 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingImpl.ts @@ -101,6 +101,7 @@ class TrackedDocumentInfo extends Disposable { const longtermResetSignal = observableSignal('resetSignal'); + let longtermReason: '10hours' | 'hashChange' | 'branchChange' | 'closed' = 'closed'; this.longtermTracker = derived((reader) => { if (!this._statsEnabled.read(reader)) { return undefined; } longtermResetSignal.read(reader); @@ -109,7 +110,7 @@ class TrackedDocumentInfo extends Disposable { reader.store.add(toDisposable(() => { // send long term document telemetry if (!t.isEmpty()) { - this.sendTelemetry('longterm', t.getTrackedRanges()); + this.sendTelemetry('longterm', longtermReason, t); } t.dispose(); })); @@ -118,7 +119,9 @@ class TrackedDocumentInfo extends Disposable { this._store.add(new IntervalTimer()).cancelAndSet(() => { // Reset after 10 hours + longtermReason = '10hours'; longtermResetSignal.trigger(undefined); + longtermReason = 'closed'; }, 10 * 60 * 60 * 1000); (async () => { @@ -129,10 +132,14 @@ class TrackedDocumentInfo extends Disposable { // Reset on branch change or commit if (repo) { this._store.add(runOnChange(repo.headCommitHashObs, () => { + longtermReason = 'hashChange'; longtermResetSignal.trigger(undefined); + longtermReason = 'closed'; })); this._store.add(runOnChange(repo.headBranchNameObs, () => { + longtermReason = 'branchChange'; longtermResetSignal.trigger(undefined); + longtermReason = 'closed'; })); } @@ -157,7 +164,7 @@ class TrackedDocumentInfo extends Disposable { const t = reader.store.add(new DocumentEditSourceTracker(docWithJustReason, undefined)); reader.store.add(toDisposable(async () => { // send long term document telemetry - this.sendTelemetry('5minWindow', t.getTrackedRanges()); + this.sendTelemetry('5minWindow', 'time', t); t.dispose(); })); @@ -167,16 +174,88 @@ class TrackedDocumentInfo extends Disposable { this._repo = this._scm.getRepo(_doc.uri); } - async sendTelemetry(mode: 'longterm' | '5minWindow', ranges: readonly TrackedEdit[]) { + async sendTelemetry(mode: 'longterm' | '5minWindow', trigger: string, t: DocumentEditSourceTracker) { + const ranges = t.getTrackedRanges(); if (ranges.length === 0) { return; } const data = this.getTelemetryData(ranges); - const isTrackedByGit = await data.isTrackedByGit; + const statsUuid = generateUuid(); + const sourceKeyToRepresentative = new Map(); + for (const r of ranges) { + sourceKeyToRepresentative.set(r.sourceKey, r.sourceRepresentative); + } + + const sums = sumByCategory(ranges, r => r.range.length, r => r.sourceKey); + const entries = Object.entries(sums).filter(([key, value]) => value !== undefined); + entries.sort(reverseOrder(compareBy(([key, value]) => value!, numberComparator))); + entries.length = mode === 'longterm' ? 30 : 10; + + for (const [key, value] of Object.entries(sums)) { + if (value === undefined) { + continue; + } + + + const repr = sourceKeyToRepresentative.get(key); + const cleanedKey = repr?.toKey(1, { $extensionId: false, $extensionVersion: false }); + + const metadata = repr?.metadata; + const extensionId = metadata && '$extensionId' in metadata ? metadata.$extensionId : undefined; + const extensionVersion = metadata && '$extensionVersion' in metadata ? metadata.$extensionVersion : undefined; + + const m = t.getChangedCharactersCount(key); + + this._telemetryService.publicLog2<{ + mode: string; + sourceKey: string; + extensionId: string; + extensionVersion: string; + sourceKeyWithoutExtId: string; + trigger: string; + languageId: string; + statsUuid: string; + modifiedCount: number; + deltaModifiedCount: number; + totalModifiedCount: number; + }, { + owner: 'hediet'; + comment: 'Reports distribution of various edit kinds.'; + + sourceKey: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the edit.' }; + mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'longterm or 5minWindow' }; + languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' }; + statsUuid: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The unique identifier for the telemetry event.' }; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id which provided this inline completion.' }; + extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the extension.' }; + sourceKeyWithoutExtId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the edit.' }; + trigger: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The trigger for the telemetry event.' }; + + modifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of nes modified characters'; isMeasurement: true }; + deltaModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Delta of modified characters'; isMeasurement: true }; + totalModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Total number of characters'; isMeasurement: true }; + + }>('editTelemetry.editSources.details', { + mode, + sourceKey: key, + extensionId: extensionId ?? '', + extensionVersion: extensionVersion ?? '', + sourceKeyWithoutExtId: cleanedKey ?? '', + trigger, + languageId: this._doc.languageId.get(), + statsUuid: statsUuid, + modifiedCount: value, + deltaModifiedCount: m, + totalModifiedCount: data.totalModifiedCharactersInFinalState, + }); + } + + + const isTrackedByGit = await data.isTrackedByGit; this._telemetryService.publicLog2<{ mode: string; languageId: string; @@ -224,66 +303,6 @@ class TrackedDocumentInfo extends Disposable { externalModifiedCount: data.externalModifiedCount, isTrackedByGit: isTrackedByGit ? 1 : 0, }); - - const sourceKeyToRepresentative = new Map(); - for (const r of ranges) { - sourceKeyToRepresentative.set(r.sourceKey, r.sourceRepresentative); - } - - const sums = sumByCategory(ranges, r => r.range.length, r => r.sourceKey); - const entries = Object.entries(sums).filter(([key, value]) => value !== undefined); - entries.sort(reverseOrder(compareBy(([key, value]) => value!, numberComparator))); - entries.length = mode === 'longterm' ? 30 : 10; - - for (const [key, value] of Object.entries(sums)) { - if (value === undefined) { - continue; - } - - - const repr = sourceKeyToRepresentative.get(key); - const cleanedKey = repr?.toKey(1, { $extensionId: false, $extensionVersion: false }); - - const metadata = repr?.metadata; - const extensionId = metadata && '$extensionId' in metadata ? metadata.$extensionId : undefined; - const extensionVersion = metadata && '$extensionVersion' in metadata ? metadata.$extensionVersion : undefined; - - this._telemetryService.publicLog2<{ - mode: string; - sourceKey: string; - extensionId: string; - extensionVersion: string; - sourceKeyWithoutExtId: string; - languageId: string; - statsUuid: string; - modifiedCount: number; - totalModifiedCount: number; - }, { - owner: 'hediet'; - comment: 'Reports distribution of various edit kinds.'; - - sourceKey: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the edit.' }; - mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'longterm or 5minWindow' }; - languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' }; - statsUuid: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The unique identifier for the telemetry event.' }; - extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id which provided this inline completion.' }; - extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the extension.' }; - sourceKeyWithoutExtId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the edit.' }; - - modifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of nes modified characters'; isMeasurement: true }; - totalModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Total number of characters'; isMeasurement: true }; - }>('editTelemetry.editSources.details', { - mode, - sourceKey: key, - extensionId: extensionId ?? '', - extensionVersion: extensionVersion ?? '', - sourceKeyWithoutExtId: cleanedKey ?? '', - languageId: this._doc.languageId.get(), - statsUuid: statsUuid, - modifiedCount: value, - totalModifiedCount: data.totalModifiedCharactersInFinalState, - }); - } } getTelemetryData(ranges: readonly TrackedEdit[]) { diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editTracker.ts b/src/vs/workbench/contrib/editTelemetry/browser/editTracker.ts index a7226cbd693..7346efae6a0 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editTracker.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editTracker.ts @@ -19,6 +19,7 @@ export class DocumentEditSourceTracker extends Disposable { private _pendingExternalEdits: AnnotatedStringEdit = AnnotatedStringEdit.empty; private readonly _update = observableSignal(this); + private readonly _sumAddedCharactersPerKey: Map = new Map(); constructor( private readonly _doc: IDocumentWithAnnotatedEdits, @@ -37,20 +38,35 @@ export class DocumentEditSourceTracker extends Disposable { } } else { if (!this._pendingExternalEdits.isEmpty()) { - this._edits = this._edits.compose(this._pendingExternalEdits); + this._applyEdit(this._pendingExternalEdits); this._pendingExternalEdits = AnnotatedStringEdit.empty; } - this._edits = this._edits.compose(eComposed); + this._applyEdit(eComposed); } this._update.trigger(undefined); })); } + private _applyEdit(e: AnnotatedStringEdit): void { + for (const r of e.replacements) { + const existing = this._sumAddedCharactersPerKey.get(r.data.key) ?? 0; + const newCount = existing + r.getNewLength(); + this._sumAddedCharactersPerKey.set(r.data.key, newCount); + } + + this._edits = this._edits.compose(e); + } + async waitForQueue(): Promise { await this._doc.waitForQueue(); } + public getChangedCharactersCount(key: string): number { + const val = this._sumAddedCharactersPerKey.get(key); + return val ?? 0; + } + getTrackedRanges(reader?: IReader): TrackedEdit[] { this._update.read(reader); const ranges = this._edits.getNewRanges(); From e42d3b2c535d6a9ab9bd9c4d998f1d0fa96ceb98 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 9 Jul 2025 21:49:23 +0200 Subject: [PATCH 273/306] chat - go back to installing only after sign-in has finished (#254960) --- .../workbench/contrib/chat/browser/chatSetup.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 8177d64bde8..09915c27c35 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -1248,13 +1248,13 @@ class ChatSetupController extends Disposable { let session: AuthenticationSession | undefined; let entitlement: ChatEntitlement | undefined; - const installation = this.doInstall(); - // Entitlement Unknown or `forceSignIn`: we need to sign-in user if (this.context.state.entitlement === ChatEntitlement.Unknown || options.forceSignIn) { this.setStep(ChatSetupStep.SigningIn); const result = await this.signIn({ useAlternateProvider: options.useAlternateProvider }); if (!result.session) { + this.doInstall(); // still install the extension in the background to remind the user to sign-in eventually + const provider = options.useAlternateProvider ? defaultChat.alternativeProviderId : options.useEnterpriseProvider ? defaultChat.enterpriseProviderId : defaultChat.providerId; this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider }); return undefined; // treat as cancelled because signing in already triggers an error dialog @@ -1266,7 +1266,7 @@ class ChatSetupController extends Disposable { // Await Install this.setStep(ChatSetupStep.Installing); - success = await this.install(session, entitlement ?? this.context.state.entitlement, providerId, watch, installation, options); + success = await this.install(session, entitlement ?? this.context.state.entitlement, providerId, watch, options); } finally { this.setStep(ChatSetupStep.Initial); this.context.resume(); @@ -1300,7 +1300,7 @@ class ChatSetupController extends Disposable { return { session, entitlement: entitlements?.entitlement }; } - private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, providerId: string, watch: StopWatch, installation: Promise, options: { useAlternateProvider?: boolean; useEnterpriseProvider?: boolean }): Promise { + private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, providerId: string, watch: StopWatch, options: { useAlternateProvider?: boolean; useEnterpriseProvider?: boolean }): Promise { const wasRunning = this.context.state.installed && !this.context.state.disabled; let signUpResult: boolean | { errorCode: number } | undefined = undefined; @@ -1333,7 +1333,7 @@ class ChatSetupController extends Disposable { } } - await this.doInstallWithRetry(installation); + await this.doInstallWithRetry(); } catch (error) { this.logService.error(`[chat setup] install: error ${error}`); this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: isCancellationError(error) ? 'cancelled' : 'failedInstall', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider }); @@ -1351,10 +1351,10 @@ class ChatSetupController extends Disposable { return true; } - private async doInstallWithRetry(installation: Promise): Promise { + private async doInstallWithRetry(): Promise { let error: Error | undefined; try { - await installation; + await this.doInstall(); } catch (e) { this.logService.error(`[chat setup] install: error ${error}`); error = e; @@ -1370,7 +1370,7 @@ class ChatSetupController extends Disposable { }); if (confirmed) { - return this.doInstallWithRetry(this.doInstall()); + return this.doInstallWithRetry(); } } From 20a1aca71ed3be8cbc155a5f086c54cd6a4246d5 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 9 Jul 2025 16:10:57 -0400 Subject: [PATCH 274/306] add listeners to correct disposables (#254891) fix #254890 --- .../api/browser/mainThreadTerminalShellIntegration.ts | 1 - .../workbench/contrib/terminal/browser/terminalService.ts | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts b/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts index 8e40f843454..2d5ede8fd61 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts @@ -114,7 +114,6 @@ export class MainThreadTerminalShellIntegration extends Disposable implements Ma private _enableShellIntegration(instance: ITerminalInstance): void { this._extensionService.activateByEvent('onTerminalShellIntegration:*'); - this._register(instance.onDidChangeShellType(() => this._extensionService.activateByEvent(`onTerminalShellIntegration:${instance.shellType}`))); if (instance.shellType) { this._extensionService.activateByEvent(`onTerminalShellIntegration:${instance.shellType}`); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 6d03b954046..f29abfee746 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -846,6 +846,12 @@ export class TerminalService extends Disposable implements ITerminalService { })); instanceDisposables.add(instance.onDidFocus(this._onDidChangeActiveInstance.fire, this._onDidChangeActiveInstance)); instanceDisposables.add(instance.onRequestAddInstanceToGroup(async e => await this._addInstanceToGroup(instance, e))); + instanceDisposables.add(instance.onDidChangeShellType(() => this._extensionService.activateByEvent(`onTerminal:${instance.shellType}`))); + instanceDisposables.add(Event.runAndSubscribe(instance.capabilities.onDidAddCapability, (() => { + if (instance.capabilities.has(TerminalCapability.CommandDetection)) { + this._extensionService.activateByEvent(`onTerminalShellIntegration:${instance.shellType}`); + } + }))); const disposeListener = this._register(instance.onDisposed(() => { instanceDisposables.dispose(); this._store.delete(disposeListener); @@ -1024,7 +1030,6 @@ export class TerminalService extends Disposable implements ITerminalService { } else { instance = this._createTerminal(shellLaunchConfig, location, options); } - this._register(instance.onDidChangeShellType(() => this._extensionService.activateByEvent(`onTerminal:${instance.shellType}`))); if (instance.shellType) { this._extensionService.activateByEvent(`onTerminal:${instance.shellType}`); } From 72b72d12f49f7ecf9f2f0992af03528d905196ba Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 9 Jul 2025 16:14:17 -0400 Subject: [PATCH 275/306] fix issues --- .../browser/terminalCompletionService.ts | 4 ++-- .../browser/terminalCompletionService.test.ts | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index 06531857053..9f3fb8f1804 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -153,7 +153,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return this._collectCompletions(providers, shellType, promptValue, cursorPosition, allowFallbackCompletions, capabilities, token, explicitlyInvoked); } - providers = this._getEnabledProviders(providers); + providers = this.getEnabledProviders(providers); if (!providers.length) { return; @@ -162,7 +162,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return this._collectCompletions(providers, shellType, promptValue, cursorPosition, allowFallbackCompletions, capabilities, token, explicitlyInvoked); } - protected _getEnabledProviders(providers: ITerminalCompletionProvider[]): ITerminalCompletionProvider[] { + getEnabledProviders(providers: ITerminalCompletionProvider[]): ITerminalCompletionProvider[] { const providerConfig: { [key: string]: boolean } = this._configurationService.getValue(TerminalSuggestSettingId.Providers); return providers.filter(p => { const providerId = p.id; diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index 401f4aa3b35..2125e257364 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -5,7 +5,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { IFileService, IFileStatWithMetadata, IResolveMetadataFileOptions } from '../../../../../../platform/files/common/files.js'; -import { TerminalCompletionService, TerminalResourceRequestConfig } from '../../browser/terminalCompletionService.js'; +import { ITerminalCompletionProvider, TerminalCompletionService, TerminalResourceRequestConfig } from '../../browser/terminalCompletionService.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import assert, { fail } from 'assert'; import { isWindows, type IProcessEnvironment } from '../../../../../../base/common/platform.js'; @@ -21,6 +21,7 @@ import { count } from '../../../../../../base/common/strings.js'; import { WindowsShellType } from '../../../../../../platform/terminal/common/terminal.js'; import { gitBashToWindowsPath, windowsToGitBashPath } from '../../browser/terminalGitBashHelpers.js'; import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; +import { TerminalSuggestSettingId } from '../../common/terminalSuggestConfiguration.js'; const pathSeparator = isWindows ? '\\' : '/'; @@ -774,8 +775,8 @@ suite('TerminalCompletionService', () => { suite('Provider Configuration', () => { // Test class that extends TerminalCompletionService to access protected methods class TestTerminalCompletionService extends TerminalCompletionService { - public _getEnabledProviders(providers: ITerminalCompletionProvider[]): ITerminalCompletionProvider[] { - return super._getEnabledProviders(providers); + override getEnabledProviders(providers: ITerminalCompletionProvider[]): ITerminalCompletionProvider[] { + return super.getEnabledProviders(providers); } } @@ -793,7 +794,8 @@ suite('TerminalCompletionService', () => { label: `completion-from-${id}`, kind: TerminalCompletionItemKind.Method, replacementIndex: 0, - replacementLength: 0 + replacementLength: 0, + provider: id }] }; } @@ -806,7 +808,7 @@ suite('TerminalCompletionService', () => { // Set empty configuration (no provider keys) configurationService.setUserConfiguration(TerminalSuggestSettingId.Providers, {}); - const result = testTerminalCompletionService._getEnabledProviders(providers); + const result = testTerminalCompletionService.getEnabledProviders(providers); // Both providers should be enabled since they're not explicitly disabled assert.strictEqual(result.length, 2, 'Should enable both providers by default'); @@ -824,7 +826,7 @@ suite('TerminalCompletionService', () => { 'provider1': false }); - const result = testTerminalCompletionService._getEnabledProviders(providers); + const result = testTerminalCompletionService.getEnabledProviders(providers); // Only provider2 should be enabled assert.strictEqual(result.length, 1, 'Should enable only one provider'); @@ -842,7 +844,7 @@ suite('TerminalCompletionService', () => { 'provider1': true }); - const result = testTerminalCompletionService._getEnabledProviders(providers); + const result = testTerminalCompletionService.getEnabledProviders(providers); // Both providers should be enabled assert.strictEqual(result.length, 2, 'Should enable both providers'); @@ -862,7 +864,7 @@ suite('TerminalCompletionService', () => { 'provider2': false }); - const result = testTerminalCompletionService._getEnabledProviders(providers); + const result = testTerminalCompletionService.getEnabledProviders(providers); // provider1 and provider3 should be enabled, provider2 should be disabled assert.strictEqual(result.length, 2, 'Should enable two providers'); From c3284e930af7d69236fdff710f20d9c253e09838 Mon Sep 17 00:00:00 2001 From: Michael Lively <12552271+Yoyokrazy@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:34:26 -0700 Subject: [PATCH 276/306] update my-work ghinb (#254964) --- .vscode/notebooks/my-work.github-issues | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index 68c38b3ca49..710a4398b0e 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"May 2025\"\n" + "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"July 2025\"\n" }, { "kind": 1, @@ -82,7 +82,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS is:open assignee:@me label:triage-needed\n" + "value": "$REPOS is:open assignee:@me label:triage-needed,copilot-triage-needed\n" }, { "kind": 1, From b6486d37a49d81f38123220d53c858d48f56b0ea Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:47:06 -0700 Subject: [PATCH 277/306] Revert "fix issues" This reverts commit 72b72d12f49f7ecf9f2f0992af03528d905196ba. --- .../browser/terminalCompletionService.ts | 4 ++-- .../browser/terminalCompletionService.test.ts | 18 ++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index 9f3fb8f1804..06531857053 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -153,7 +153,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return this._collectCompletions(providers, shellType, promptValue, cursorPosition, allowFallbackCompletions, capabilities, token, explicitlyInvoked); } - providers = this.getEnabledProviders(providers); + providers = this._getEnabledProviders(providers); if (!providers.length) { return; @@ -162,7 +162,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return this._collectCompletions(providers, shellType, promptValue, cursorPosition, allowFallbackCompletions, capabilities, token, explicitlyInvoked); } - getEnabledProviders(providers: ITerminalCompletionProvider[]): ITerminalCompletionProvider[] { + protected _getEnabledProviders(providers: ITerminalCompletionProvider[]): ITerminalCompletionProvider[] { const providerConfig: { [key: string]: boolean } = this._configurationService.getValue(TerminalSuggestSettingId.Providers); return providers.filter(p => { const providerId = p.id; diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index 2125e257364..401f4aa3b35 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -5,7 +5,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { IFileService, IFileStatWithMetadata, IResolveMetadataFileOptions } from '../../../../../../platform/files/common/files.js'; -import { ITerminalCompletionProvider, TerminalCompletionService, TerminalResourceRequestConfig } from '../../browser/terminalCompletionService.js'; +import { TerminalCompletionService, TerminalResourceRequestConfig } from '../../browser/terminalCompletionService.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import assert, { fail } from 'assert'; import { isWindows, type IProcessEnvironment } from '../../../../../../base/common/platform.js'; @@ -21,7 +21,6 @@ import { count } from '../../../../../../base/common/strings.js'; import { WindowsShellType } from '../../../../../../platform/terminal/common/terminal.js'; import { gitBashToWindowsPath, windowsToGitBashPath } from '../../browser/terminalGitBashHelpers.js'; import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; -import { TerminalSuggestSettingId } from '../../common/terminalSuggestConfiguration.js'; const pathSeparator = isWindows ? '\\' : '/'; @@ -775,8 +774,8 @@ suite('TerminalCompletionService', () => { suite('Provider Configuration', () => { // Test class that extends TerminalCompletionService to access protected methods class TestTerminalCompletionService extends TerminalCompletionService { - override getEnabledProviders(providers: ITerminalCompletionProvider[]): ITerminalCompletionProvider[] { - return super.getEnabledProviders(providers); + public _getEnabledProviders(providers: ITerminalCompletionProvider[]): ITerminalCompletionProvider[] { + return super._getEnabledProviders(providers); } } @@ -794,8 +793,7 @@ suite('TerminalCompletionService', () => { label: `completion-from-${id}`, kind: TerminalCompletionItemKind.Method, replacementIndex: 0, - replacementLength: 0, - provider: id + replacementLength: 0 }] }; } @@ -808,7 +806,7 @@ suite('TerminalCompletionService', () => { // Set empty configuration (no provider keys) configurationService.setUserConfiguration(TerminalSuggestSettingId.Providers, {}); - const result = testTerminalCompletionService.getEnabledProviders(providers); + const result = testTerminalCompletionService._getEnabledProviders(providers); // Both providers should be enabled since they're not explicitly disabled assert.strictEqual(result.length, 2, 'Should enable both providers by default'); @@ -826,7 +824,7 @@ suite('TerminalCompletionService', () => { 'provider1': false }); - const result = testTerminalCompletionService.getEnabledProviders(providers); + const result = testTerminalCompletionService._getEnabledProviders(providers); // Only provider2 should be enabled assert.strictEqual(result.length, 1, 'Should enable only one provider'); @@ -844,7 +842,7 @@ suite('TerminalCompletionService', () => { 'provider1': true }); - const result = testTerminalCompletionService.getEnabledProviders(providers); + const result = testTerminalCompletionService._getEnabledProviders(providers); // Both providers should be enabled assert.strictEqual(result.length, 2, 'Should enable both providers'); @@ -864,7 +862,7 @@ suite('TerminalCompletionService', () => { 'provider2': false }); - const result = testTerminalCompletionService.getEnabledProviders(providers); + const result = testTerminalCompletionService._getEnabledProviders(providers); // provider1 and provider3 should be enabled, provider2 should be disabled assert.strictEqual(result.length, 2, 'Should enable two providers'); From c34f818e71a44b4f4c54b6ad28f0fa437cd0459d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:48:12 -0700 Subject: [PATCH 278/306] Fix overwrite of super --- .../browser/terminalCompletionService.test.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index 401f4aa3b35..fd8aca5c00e 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -5,7 +5,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { IFileService, IFileStatWithMetadata, IResolveMetadataFileOptions } from '../../../../../../platform/files/common/files.js'; -import { TerminalCompletionService, TerminalResourceRequestConfig } from '../../browser/terminalCompletionService.js'; +import { TerminalCompletionService, TerminalResourceRequestConfig, type ITerminalCompletionProvider } from '../../browser/terminalCompletionService.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import assert, { fail } from 'assert'; import { isWindows, type IProcessEnvironment } from '../../../../../../base/common/platform.js'; @@ -21,6 +21,7 @@ import { count } from '../../../../../../base/common/strings.js'; import { WindowsShellType } from '../../../../../../platform/terminal/common/terminal.js'; import { gitBashToWindowsPath, windowsToGitBashPath } from '../../browser/terminalGitBashHelpers.js'; import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; +import { TerminalSuggestSettingId } from '../../common/terminalSuggestConfiguration.js'; const pathSeparator = isWindows ? '\\' : '/'; @@ -774,7 +775,7 @@ suite('TerminalCompletionService', () => { suite('Provider Configuration', () => { // Test class that extends TerminalCompletionService to access protected methods class TestTerminalCompletionService extends TerminalCompletionService { - public _getEnabledProviders(providers: ITerminalCompletionProvider[]): ITerminalCompletionProvider[] { + public getEnabledProviders(providers: ITerminalCompletionProvider[]): ITerminalCompletionProvider[] { return super._getEnabledProviders(providers); } } @@ -793,7 +794,8 @@ suite('TerminalCompletionService', () => { label: `completion-from-${id}`, kind: TerminalCompletionItemKind.Method, replacementIndex: 0, - replacementLength: 0 + replacementLength: 0, + provider: id }] }; } @@ -806,7 +808,7 @@ suite('TerminalCompletionService', () => { // Set empty configuration (no provider keys) configurationService.setUserConfiguration(TerminalSuggestSettingId.Providers, {}); - const result = testTerminalCompletionService._getEnabledProviders(providers); + const result = testTerminalCompletionService.getEnabledProviders(providers); // Both providers should be enabled since they're not explicitly disabled assert.strictEqual(result.length, 2, 'Should enable both providers by default'); @@ -824,7 +826,7 @@ suite('TerminalCompletionService', () => { 'provider1': false }); - const result = testTerminalCompletionService._getEnabledProviders(providers); + const result = testTerminalCompletionService.getEnabledProviders(providers); // Only provider2 should be enabled assert.strictEqual(result.length, 1, 'Should enable only one provider'); @@ -842,7 +844,7 @@ suite('TerminalCompletionService', () => { 'provider1': true }); - const result = testTerminalCompletionService._getEnabledProviders(providers); + const result = testTerminalCompletionService.getEnabledProviders(providers); // Both providers should be enabled assert.strictEqual(result.length, 2, 'Should enable both providers'); @@ -862,7 +864,7 @@ suite('TerminalCompletionService', () => { 'provider2': false }); - const result = testTerminalCompletionService._getEnabledProviders(providers); + const result = testTerminalCompletionService.getEnabledProviders(providers); // provider1 and provider3 should be enabled, provider2 should be disabled assert.strictEqual(result.length, 2, 'Should enable two providers'); From 660e830fdc04c48f29e7d041b2694a75546468d0 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 9 Jul 2025 23:02:14 +0200 Subject: [PATCH 279/306] Avoid computing deserializeSnapshot twice in SnapshotComparer (#254860) --- .../notebook/chatEditingModifiedNotebookSnapshot.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingModifiedNotebookSnapshot.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingModifiedNotebookSnapshot.ts index 3d70ef8377b..112875ab2f9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingModifiedNotebookSnapshot.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingModifiedNotebookSnapshot.ts @@ -57,8 +57,9 @@ export class SnapshotComparer { private readonly data: NotebookData; private readonly transientOptions: TransientOptions | undefined; constructor(initialCotent: string) { - this.transientOptions = deserializeSnapshot(initialCotent).transientOptions; - this.data = deserializeSnapshot(initialCotent).data; + const { transientOptions, data } = deserializeSnapshot(initialCotent); + this.transientOptions = transientOptions; + this.data = data; } isEqual(notebook: NotebookData | NotebookTextModel): boolean { From ab60c1e28533aeadc741f4dc6d3b3faf92300474 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Wed, 9 Jul 2025 14:25:29 -0700 Subject: [PATCH 280/306] Fix for canceling pending copilot search request --- src/vs/base/browser/ui/tree/asyncDataTree.ts | 45 ++++++++++--------- .../contrib/search/browser/searchView.ts | 23 ++++++---- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 44735ae4b66..015088e035e 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -32,7 +32,7 @@ interface IAsyncDataTreeNode { readonly parent: IAsyncDataTreeNode | null; readonly children: IAsyncDataTreeNode[]; readonly id?: string | null; - refreshPromise: Promise | undefined; + refreshPromise: CancelablePromise | undefined; hasChildren: boolean; stale: boolean; slow: boolean; @@ -528,7 +528,7 @@ export class AsyncDataTree implements IDisposable private readonly findController?: AsyncFindController; private readonly getDefaultCollapseState: { (e: T): undefined | ObjectTreeElementCollapseState.PreserveOrCollapsed | ObjectTreeElementCollapseState.PreserveOrExpanded }; - private readonly subTreeRefreshPromises = new Map, Promise>(); + private readonly subTreeRefreshPromises = new Map, CancelablePromise>(); private readonly refreshPromises = new Map, CancelablePromise>>(); protected readonly identityProvider?: IIdentityProvider; @@ -769,8 +769,7 @@ export class AsyncDataTree implements IDisposable } async setInput(input: TInput, viewState?: IAsyncDataTreeViewState): Promise { - this.refreshPromises.forEach(promise => promise.cancel()); - this.refreshPromises.clear(); + this.cancelAllRefreshPromises(); this.root.element = input!; @@ -792,6 +791,14 @@ export class AsyncDataTree implements IDisposable await this._updateChildren(element, recursive, rerender, undefined, options); } + cancelAllRefreshPromises(): void { + this.refreshPromises.forEach(promise => promise.cancel()); + this.refreshPromises.clear(); + + this.subTreeRefreshPromises.forEach(promise => promise.cancel()); + this.subTreeRefreshPromises.clear(); + } + private async _updateChildren(element: TInput | T = this.root.element, recursive = true, rerender = false, viewStateContext?: IAsyncDataTreeViewStateContext, options?: IAsyncDataTreeUpdateChildrenOptions): Promise { if (typeof this.root.element === 'undefined') { throw new TreeError(this.user, 'Tree input not set'); @@ -875,7 +882,7 @@ export class AsyncDataTree implements IDisposable } if (node.refreshPromise) { - await this.root.refreshPromise; + await node.refreshPromise; await Event.toPromise(this._onDidRender.event); } @@ -886,7 +893,7 @@ export class AsyncDataTree implements IDisposable const result = this.tree.expand(node === this.root ? null : node, recursive); if (node.refreshPromise) { - await this.root.refreshPromise; + await node.refreshPromise; await Event.toPromise(this._onDidRender.event); } @@ -1088,28 +1095,26 @@ export class AsyncDataTree implements IDisposable return; } } - return this.doRefreshSubTree(node, recursive, viewStateContext); } private async doRefreshSubTree(node: IAsyncDataTreeNode, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext): Promise { - let done: () => void; - node.refreshPromise = new Promise(c => done = c); - this.subTreeRefreshPromises.set(node, node.refreshPromise); - - node.refreshPromise.finally(() => { - node.refreshPromise = undefined; - this.subTreeRefreshPromises.delete(node); - }); - - try { + const cancelablePromise = createCancelablePromise(async () => { const childrenToRefresh = await this.doRefreshNode(node, recursive, viewStateContext); node.stale = false; await Promises.settled(childrenToRefresh.map(child => this.doRefreshSubTree(child, recursive, viewStateContext))); - } finally { - done!(); - } + }); + + node.refreshPromise = cancelablePromise; + this.subTreeRefreshPromises.set(node, cancelablePromise); + + cancelablePromise.finally(() => { + node.refreshPromise = undefined; + this.subTreeRefreshPromises.delete(node); + }); + + return cancelablePromise; } private async doRefreshNode(node: IAsyncDataTreeNode, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext): Promise[]> { diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 668140877c8..d5120822ecf 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -543,9 +543,9 @@ export class SearchView extends ViewPane { // Subscribe to AI search result changes and update the tree when new AI results are reported this._onAIResultChangedDisposable?.dispose(); this._onAIResultChangedDisposable = this._register( - this.viewModel.searchResult.aiTextSearchResult.onChange(() => { + this.viewModel.searchResult.aiTextSearchResult.onChange((e) => { // Only refresh the AI node, not the whole tree - if (this.tree && this.tree.hasNode(this.searchResult.aiTextSearchResult)) { + if (this.tree && this.tree.hasNode(this.searchResult.aiTextSearchResult) && !e.removed) { this.tree.updateChildren(this.searchResult.aiTextSearchResult); } }) @@ -1349,6 +1349,7 @@ export class SearchView extends ViewPane { this.searchWidget.clear(); } this.viewModel.cancelSearch(); + this.viewModel.cancelAISearch(); this.tree.ariaLabel = nls.localize('emptySearch', "Empty Search"); this.accessibilitySignalService.playSignal(AccessibilitySignal.clear); @@ -1858,17 +1859,17 @@ export class SearchView extends ViewPane { public clearAIResults() { this.model.searchResult.aiTextSearchResult.hidden = true; - if (!this._pendingSemanticSearchPromise) { - this._cachedResults = undefined; - this._cachedKeywords = []; - this.model.cancelAISearch(true); - this.model.clearAiSearchResults(); - } + this.refreshTreeController.clearAllPending(); + this._pendingSemanticSearchPromise = undefined; + this._cachedResults = undefined; + this._cachedKeywords = []; + this.model.cancelAISearch(true); + this.model.clearAiSearchResults(); } public async requestAIResults() { this.logService.info(`SearchView: Requesting semantic results from keybinding. Cached: ${!!this.cachedResults}`); - if (!this.cachedResults || this.cachedResults.results.length === 0) { + if ((!this.cachedResults || this.cachedResults.results.length === 0) && !this._pendingSemanticSearchPromise) { this.clearAIResults(); } this.model.searchResult.aiTextSearchResult.hidden = false; @@ -2646,6 +2647,10 @@ class RefreshTreeController extends Disposable { private queuedIChangeEvents: IChangeEvent[] = []; + public clearAllPending(): void { + this.searchView.getControl().cancelAllRefreshPromises(); + } + public async queue(e?: IChangeEvent): Promise { if (e) { this.queuedIChangeEvents.push(e); From 200a319a72ff46a7c4e4a3cb8d852fb67d695188 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 9 Jul 2025 15:04:55 -0700 Subject: [PATCH 281/306] tools: fix undo to a request keeping the initial request (#254935) * tools: fix undo to a request keeping the initial request * more timeline improvements - Remove the special `postEdit` stop that was not very undo-able - Improve undo/redo to skip no-op points * update tests --- .../browser/chatEditing/chatEditingSession.ts | 26 ++-- .../chatEditing/chatEditingSessionStorage.ts | 9 +- .../chatEditing/chatEditingTimeline.ts | 133 +++++++++++------- .../browser/chatEditingSessionStorage.test.ts | 4 +- .../test/browser/chatEditingTimeline.test.ts | 85 ++++++++--- 5 files changed, 154 insertions(+), 103 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 50c1b873f35..6151ec33707 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -84,7 +84,7 @@ function getCurrentAndNextStop(requestId: string, stopId: string | undefined, hi const current = snapshot.stops[stopIndex].entries; const next = stopIndex < snapshot.stops.length - 1 ? snapshot.stops[stopIndex + 1].entries - : snapshot.postEdit || history[snapshotIndex + 1]?.stops[0].entries; + : history[snapshotIndex + 1]?.stops[0].entries; if (!next) { @@ -145,6 +145,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio hasHistory && this._state.read(reader) === ChatEditingSessionState.Idle); this.canUndo = this._timeline.canUndo.map((hasHistory, reader) => hasHistory && this._state.read(reader) === ChatEditingSessionState.Idle); + + this._register(autorun(reader => { + const disabled = this._timeline.requestDisablement.read(reader); + this._chatService.getSession(this.chatSessionId)?.setDisabledRequests(disabled); + })); } public async init(): Promise { @@ -226,16 +231,8 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } public getSnapshot(requestId: string, undoStop: string | undefined, snapshotUri: URI): ISnapshotEntry | undefined { - let entries: ResourceMap | undefined; - if (undoStop === ChatEditingTimeline.POST_EDIT_STOP_ID) { - // If postEdit, get from timeline state - const timelineState = this._timeline.getStateForPersistence(); - const snap = timelineState.history.find(s => s.requestId === requestId); - entries = snap?.postEdit; - } else { - const stopRef = this._timeline.getSnapshotForRestore(requestId, undoStop); - entries = stopRef?.stop.entries; - } + const stopRef = this._timeline.getSnapshotForRestore(requestId, undoStop); + const entries = stopRef?.stop.entries; return entries && [...entries.values()].find((e) => isEqual(e.snapshotUri, snapshotUri)); } @@ -268,7 +265,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this._ensurePendingSnapshot(); await this._restoreSnapshot(stopRef.stop); stopRef.apply(); - this._updateRequestHiddenState(); } } else { const pendingSnapshot = this._pendingSnapshot.get(); @@ -470,7 +466,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this._ensurePendingSnapshot(); await this._restoreSnapshot(undo.stop); undo.apply(); - this._updateRequestHiddenState(); } async redoInteraction(): Promise { @@ -485,11 +480,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } else { this._pendingSnapshot.set(undefined, undefined); } - this._updateRequestHiddenState(); - } - - private _updateRequestHiddenState() { - this._chatService.getSession(this.chatSessionId)?.setDisabledRequests(this._timeline.getRequestDisablement()); } private async _acceptStreamingEditsStart(responseModel: IChatResponseModel, undoStop: string | undefined, resource: URI) { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts index e7780b7186f..4a01ea62583 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts @@ -66,11 +66,11 @@ export class ChatEditingSessionStorage { if ('stops' in snapshot) { return snapshot; } - return { requestId: snapshot.requestId, stops: [{ stopId: undefined, entries: snapshot.entries }], postEdit: undefined }; + return { requestId: snapshot.requestId, stops: [{ stopId: undefined, entries: snapshot.entries }] }; }; const deserializeChatEditingSessionSnapshot = async (startIndex: number, snapshot: IChatEditingSessionSnapshotDTO2): Promise => { const stops = await Promise.all(snapshot.stops.map(deserializeChatEditingStopDTO)); - return { startIndex, requestId: snapshot.requestId, stops, postEdit: snapshot.postEdit && await deserializeSnapshotEntriesDTO(snapshot.postEdit) }; + return { startIndex, requestId: snapshot.requestId, stops }; }; const deserializeSnapshotEntry = async (entry: ISnapshotEntryDTO) => { return { @@ -180,7 +180,6 @@ export class ChatEditingSessionStorage { return { requestId: snapshot.requestId, stops: await Promise.all(snapshot.stops.map(serializeChatEditingSessionStop)), - postEdit: snapshot.postEdit ? await Promise.all(Array.from(snapshot.postEdit.values()).map(serializeSnapshotEntry)) : undefined }; }; const serializeSnapshotEntry = async (entry: ISnapshotEntry): Promise => { @@ -242,9 +241,6 @@ export interface IChatEditingSessionSnapshot { * Invariant: never empty. */ readonly stops: IChatEditingSessionStop[]; - - /** Stop that represents changes after the last undo stop, kept for diffing purposes. */ - readonly postEdit: ResourceMap | undefined; } export interface IChatEditingSessionStop { @@ -269,7 +265,6 @@ interface IChatEditingSessionSnapshotDTO { interface IChatEditingSessionSnapshotDTO2 { readonly requestId: string | undefined; readonly stops: IChatEditingSessionStopDTO[]; - readonly postEdit: ISnapshotEntryDTO[] | undefined; } interface ISnapshotEntryDTO { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTimeline.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTimeline.ts index f5dfc91f294..1d802d4ee77 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTimeline.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTimeline.ts @@ -6,7 +6,9 @@ import { equals as arraysEqual, binarySearch2 } from '../../../../../base/common/arrays.js'; +import { equals as objectsEqual } from '../../../../../base/common/objects.js'; import { findLast } from '../../../../../base/common/arraysFind.js'; +import { Iterable } from '../../../../../base/common/iterator.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../base/common/map.js'; import { derived, derivedOpts, IObservable, ITransaction, ObservablePromise, observableValue, transaction } from '../../../../../base/common/observable.js'; @@ -46,6 +48,22 @@ export class ChatEditingTimeline { public readonly canUndo: IObservable; public readonly canRedo: IObservable; + public readonly requestDisablement = derivedOpts({ equalsFn: (a, b) => arraysEqual(a, b, objectsEqual) }, reader => { + const history = this._linearHistory.read(reader); + const index = this._linearHistoryIndex.read(reader); + const undoRequests: IChatRequestDisablement[] = []; + for (const entry of history) { + if (!entry.requestId) { + // ignored + } else if (entry.startIndex >= index) { + undoRequests.push({ requestId: entry.requestId }); + } else if (entry.startIndex + entry.stops.length > index) { + undoRequests.push({ requestId: entry.requestId, afterUndoStop: entry.stops[(index - 1) - entry.startIndex].stopId }); + } + } + return undoRequests; + }); + constructor( @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -76,7 +94,7 @@ export class ChatEditingTimeline { * Get the snapshot and history index for restoring, given requestId and stopId. * If requestId is undefined, returns undefined (pending snapshot is managed by session). */ - public getSnapshotForRestore(requestId: string | undefined, stopId: string | undefined): { stop: IChatEditingSessionStop; historyIndex: number; apply(): void } | undefined { + public getSnapshotForRestore(requestId: string | undefined, stopId: string | undefined): { stop: IChatEditingSessionStop; apply(): void } | undefined { if (requestId === undefined) { return undefined; } @@ -85,7 +103,13 @@ export class ChatEditingTimeline { return undefined; } - return { stop: stopRef.stop, historyIndex: stopRef.historyIndex, apply: () => this._linearHistoryIndex.set(stopRef.historyIndex + 1, undefined) }; + // When rolling back to the first snapshot taken for a request, mark the + // entire request as undone. + const toIndex = stopRef.stop.stopId === undefined ? stopRef.historyIndex : stopRef.historyIndex + 1; + return { + stop: stopRef.stop, + apply: () => this._linearHistoryIndex.set(toIndex, undefined) + }; } /** @@ -119,22 +143,21 @@ export class ChatEditingTimeline { return; } - const snap = history[snapIndex]; + const snap = { ...history[snapIndex] }; let stopIndex = snap.stops.findIndex((s) => s.stopId === undoStop); if (stopIndex === -1) { return; } + let linearHistoryIndexIncr = 0; if (next) { if (stopIndex === snap.stops.length - 1) { - const postEdit = new ResourceMap(snap.postEdit || ChatEditingTimeline.createEmptySnapshot(undefined).entries); - if (!snap.postEdit || !entry.equalsSnapshot(postEdit.get(entry.modifiedURI) as ISnapshotEntry | undefined)) { - postEdit.set(entry.modifiedURI, entry.createSnapshot(requestId, ChatEditingTimeline.POST_EDIT_STOP_ID)); - const newHistory = history.slice(); - newHistory[snapIndex] = { ...snap, postEdit }; - this._linearHistory.set(newHistory, tx); + if (snap.stops[stopIndex].stopId === ChatEditingTimeline.POST_EDIT_STOP_ID) { + throw new Error('cannot duplicate post-edit stop'); } - return; + + snap.stops = snap.stops.concat(ChatEditingTimeline.createEmptySnapshot(ChatEditingTimeline.POST_EDIT_STOP_ID)); + linearHistoryIndexIncr++; } stopIndex++; } @@ -149,10 +172,15 @@ export class ChatEditingTimeline { const newStop = snap.stops.slice(); newStop[stopIndex] = { ...stop, entries: newMap }; + snap.stops = newStop; const newHistory = history.slice(); - newHistory[snapIndex] = { ...snap, stops: newStop }; + newHistory[snapIndex] = snap; + this._linearHistory.set(newHistory, tx); + if (linearHistoryIndexIncr) { + this._linearHistoryIndex.set(this._linearHistoryIndex.get() + linearHistoryIndexIncr, tx); + } } /** @@ -161,23 +189,40 @@ export class ChatEditingTimeline { * pushed into the history. */ public getUndoSnapshot(): { stop: IChatEditingSessionStop; apply(): void } | undefined { - const idx = this._linearHistoryIndex.get() - 2; - const entry = this.getHistoryEntryByLinearIndex(idx); - if (entry) { - return { stop: entry.stop, apply: () => this._linearHistoryIndex.set(idx + 1, undefined) }; - } - return undefined; + return this.getUndoRedoSnapshot(-1); } /** * Get the redo snapshot (next in history), or undefined if at end. */ public getRedoSnapshot(): { stop: IChatEditingSessionStop; apply(): void } | undefined { - const idx = this._linearHistoryIndex.get(); - const entry = this.getHistoryEntryByLinearIndex(idx); + return this.getUndoRedoSnapshot(1); + } + + private getUndoRedoSnapshot(direction: number) { + let idx = this._linearHistoryIndex.get() - 1; + const max = getMaxHistoryIndex(this._linearHistory.get()); + const startEntry = this.getHistoryEntryByLinearIndex(idx); + let entry = startEntry; + if (!startEntry) { + return undefined; + } + + do { + idx += direction; + entry = this.getHistoryEntryByLinearIndex(idx); + } while ( + idx + direction < max && + idx + direction >= 0 && + entry && + !(direction === -1 && entry.entry.requestId !== startEntry.entry.requestId) && + !stopProvidesNewData(startEntry.stop, entry.stop) + ); + if (entry) { return { stop: entry.stop, apply: () => this._linearHistoryIndex.set(idx + 1, undefined) }; } + return undefined; } @@ -221,7 +266,7 @@ export class ChatEditingTimeline { if (entry.startIndex >= linearHistoryPtr) { break; } else if (linearHistoryPtr - entry.startIndex < entry.stops.length) { - newLinearHistory.push({ requestId: entry.requestId, stops: entry.stops.slice(0, linearHistoryPtr - entry.startIndex), startIndex: entry.startIndex, postEdit: undefined }); + newLinearHistory.push({ requestId: entry.requestId, stops: entry.stops.slice(0, linearHistoryPtr - entry.startIndex), startIndex: entry.startIndex }); } else { newLinearHistory.push(entry); } @@ -229,15 +274,19 @@ export class ChatEditingTimeline { const lastEntry = newLinearHistory.at(-1); if (requestId && lastEntry?.requestId === requestId) { - if (lastEntry.postEdit && undoStop) { + const hadPostEditStop = lastEntry.stops.at(-1)?.stopId === ChatEditingTimeline.POST_EDIT_STOP_ID && undoStop; + if (hadPostEditStop) { const rebaseUri = (uri: URI) => uri.with({ query: uri.query.replace(ChatEditingTimeline.POST_EDIT_STOP_ID, undoStop) }); - for (const [uri, prev] of lastEntry.postEdit.entries()) { + for (const [uri, prev] of lastEntry.stops.at(-1)!.entries) { snapshot.entries.set(uri, { ...prev, snapshotUri: rebaseUri(prev.snapshotUri), resource: rebaseUri(prev.resource) }); } } - newLinearHistory[newLinearHistory.length - 1] = { ...lastEntry, stops: [...lastEntry.stops, snapshot], postEdit: undefined }; + newLinearHistory[newLinearHistory.length - 1] = { + ...lastEntry, + stops: [...hadPostEditStop ? lastEntry.stops.slice(0, -1) : lastEntry.stops, snapshot] + }; } else { - newLinearHistory.push({ requestId, startIndex: lastEntry ? lastEntry.startIndex + lastEntry.stops.length : 0, stops: [snapshot], postEdit: undefined }); + newLinearHistory.push({ requestId, startIndex: lastEntry ? lastEntry.startIndex + lastEntry.stops.length : 0, stops: [snapshot] }); } transaction((tx) => { @@ -247,25 +296,6 @@ export class ChatEditingTimeline { }); } - /** - * Gets chat disablement entries for the current timeline state. - */ - public getRequestDisablement() { - const history = this._linearHistory.get(); - const index = this._linearHistoryIndex.get(); - const undoRequests: IChatRequestDisablement[] = []; - for (const entry of history) { - if (!entry.requestId) { - // ignored - } else if (entry.startIndex >= index) { - undoRequests.push({ requestId: entry.requestId }); - } else if (entry.startIndex + entry.stops.length > index) { - undoRequests.push({ requestId: entry.requestId, afterUndoStop: entry.stops[index - entry.startIndex].stopId }); - } - } - return undoRequests; - } - /** * Gets diff for text entries between stops. * @param entriesContent Observable that observes either snapshot entry @@ -390,6 +420,10 @@ export class ChatEditingTimeline { } } +function stopProvidesNewData(origin: IChatEditingSessionStop, target: IChatEditingSessionStop) { + return Iterable.some(target.entries, ([uri, e]) => origin.entries.get(uri)?.current !== e.current); +} + function getMaxHistoryIndex(history: readonly IChatEditingSessionSnapshot[]) { const lastHistory = history.at(-1); return lastHistory ? lastHistory.startIndex + lastHistory.stops.length : 0; @@ -415,14 +449,11 @@ function getCurrentAndNextStop(requestId: string, stopId: string | undefined, hi const nextStop = stopIndex < snapshot.stops.length - 1 ? snapshot.stops[stopIndex + 1] : undefined; - const next = nextStop?.entries || snapshot.postEdit; - - - if (!next) { + if (!nextStop) { return undefined; } - return { current, currentStopId: currentStop.stopId, next, nextStopId: nextStop?.stopId || ChatEditingTimeline.POST_EDIT_STOP_ID }; + return { current, currentStopId: currentStop.stopId, next: nextStop.entries, nextStopId: nextStop.stopId }; } function getFirstAndLastStop(uri: URI, history: readonly IChatEditingSessionSnapshot[]) { @@ -439,12 +470,6 @@ function getFirstAndLastStop(uri: URI, history: readonly IChatEditingSessionSnap let lastStopWithUriId: string | undefined; for (let i = history.length - 1; i >= 0; i--) { const snapshot = history[i]; - if (snapshot.postEdit?.has(uri)) { - lastStopWithUri = snapshot.postEdit; - lastStopWithUriId = ChatEditingTimeline.POST_EDIT_STOP_ID; - break; - } - const stop = findLast(snapshot.stops, s => s.entries.has(uri)); if (stop) { lastStopWithUri = stop.entries; diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts index 7777341548b..50d651b7bce 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts @@ -65,8 +65,8 @@ suite('ChatEditingSessionStorage', () => { recentSnapshot: makeStop(undefined, 'd', 'e'), linearHistoryIndex: 3, linearHistory: [ - { startIndex: 0, requestId: r1, stops: [makeStop(r1, 'a', 'b')], postEdit: makeStop(r1, 'b', 'c').entries }, - { startIndex: 1, requestId: r2, stops: [makeStop(r2, 'c', 'd'), makeStop(r2, 'd', 'd')], postEdit: makeStop(r2, 'd', 'd').entries }, + { startIndex: 0, requestId: r1, stops: [makeStop(r1, 'a', 'b')] }, + { startIndex: 1, requestId: r2, stops: [makeStop(r2, 'c', 'd'), makeStop(r2, 'd', 'd')] }, ] }; } diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditingTimeline.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditingTimeline.test.ts index c5d5b851e03..00e0b5c759e 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatEditingTimeline.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatEditingTimeline.test.ts @@ -9,6 +9,10 @@ import { workbenchInstantiationService } from '../../../../test/browser/workbenc import { ChatEditingTimeline } from '../../browser/chatEditing/chatEditingTimeline.js'; import { IChatEditingSessionStop } from '../../browser/chatEditing/chatEditingSessionStorage.js'; import { transaction } from '../../../../../base/common/observable.js'; +import { IChatRequestDisablement } from '../../common/chatModel.js'; +import { ResourceMap } from '../../../../../base/common/map.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ISnapshotEntry } from '../../common/chatEditingService.js'; suite('ChatEditingTimeline', () => { const ds = ensureNoDisposablesAreLeakedInTestSuite(); @@ -28,10 +32,13 @@ suite('ChatEditingTimeline', () => { }); }); - function createSnapshot(stopId = 'stop', entries?: any): IChatEditingSessionStop { + function createSnapshot(stopId: string | undefined, requestId = 'req1'): IChatEditingSessionStop { return { stopId, - entries: entries || new Map(), + entries: stopId === undefined ? new ResourceMap() : new ResourceMap([[ + URI.file(`file:///path/to/${stopId}`), + { requestId, current: `Content for ${stopId}` } as Partial as ISnapshotEntry + ]]), }; } @@ -99,12 +106,12 @@ suite('ChatEditingTimeline', () => { test('getRequestDisablement returns correct requests', () => { timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); - timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2')); + timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2', 'req2')); // Move back to first timeline.getUndoSnapshot()?.apply(); - const disables = timeline.getRequestDisablement(); + const disables = timeline.requestDisablement.get(); assert.ok(Array.isArray(disables)); assert.ok(disables.some(d => d.requestId === 'req2')); }); @@ -113,7 +120,7 @@ suite('ChatEditingTimeline', () => { suite('Multiple requests', () => { test('handles multiple requests with separate snapshots', () => { timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); - timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2')); + timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2', 'req2')); timeline.pushSnapshot('req3', 'stop3', createSnapshot('stop3')); assert.strictEqual(timeline.canUndo.get(), true); @@ -144,8 +151,8 @@ suite('ChatEditingTimeline', () => { test('mixed requests and stops', () => { timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); timeline.pushSnapshot('req1', 'stop2', createSnapshot('stop2')); - timeline.pushSnapshot('req2', 'stop3', createSnapshot('stop3')); - timeline.pushSnapshot('req2', 'stop4', createSnapshot('stop4')); + timeline.pushSnapshot('req2', 'stop3', createSnapshot('stop3', 'req2')); + timeline.pushSnapshot('req2', 'stop4', createSnapshot('stop4', 'req2')); const state = timeline.getStateForPersistence(); assert.strictEqual(state.history.length, 2); @@ -190,7 +197,7 @@ suite('ChatEditingTimeline', () => { test('branching from middle of history creates new branch', () => { timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); - timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2')); + timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2', 'req2')); timeline.pushSnapshot('req3', 'stop3', createSnapshot('stop3')); // Undo to middle @@ -208,7 +215,7 @@ suite('ChatEditingTimeline', () => { suite('State persistence', () => { test('getStateForPersistence returns complete state', () => { timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); - timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2')); + timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2', 'req2')); const state = timeline.getStateForPersistence(); assert.ok(state.history); @@ -230,7 +237,7 @@ suite('ChatEditingTimeline', () => { // Create complex state timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); timeline.pushSnapshot('req1', 'stop2', createSnapshot('stop2')); - timeline.pushSnapshot('req2', 'stop3', createSnapshot('stop3')); + timeline.pushSnapshot('req2', 'stop3', createSnapshot('stop3', 'req2')); const originalState = timeline.getStateForPersistence(); @@ -248,36 +255,36 @@ suite('ChatEditingTimeline', () => { suite('Request disablement', () => { test('getRequestDisablement at various positions', () => { timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); - timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2')); + timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2', 'req2')); timeline.pushSnapshot('req3', 'stop3', createSnapshot('stop3')); // At end - no disabled requests - let disables = timeline.getRequestDisablement(); + let disables = timeline.requestDisablement.get(); assert.strictEqual(disables.length, 0); // Move back one timeline.getUndoSnapshot()?.apply(); - disables = timeline.getRequestDisablement(); + disables = timeline.requestDisablement.get(); assert.strictEqual(disables.length, 1); assert.strictEqual(disables[0].requestId, 'req3'); // Move back to beginning timeline.getUndoSnapshot()?.apply(); timeline.getUndoSnapshot()?.apply(); - disables = timeline.getRequestDisablement(); + disables = timeline.requestDisablement.get(); assert.strictEqual(disables.length, 2); }); test('getRequestDisablement with mixed request/stop structure', () => { timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); timeline.pushSnapshot('req1', 'stop2', createSnapshot('stop2')); - timeline.pushSnapshot('req2', 'stop3', createSnapshot('stop3')); + timeline.pushSnapshot('req2', 'stop3', createSnapshot('stop3', 'req2')); // Move to middle of req1 timeline.getUndoSnapshot()?.apply(); timeline.getUndoSnapshot()?.apply(); - const disables = timeline.getRequestDisablement(); + const disables = timeline.requestDisablement.get(); assert.strictEqual(disables.length, 2); // Should have partial disable for req1 and full disable for req2 @@ -311,7 +318,7 @@ suite('ChatEditingTimeline', () => { test('multiple undos and redos', () => { timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); - timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2')); + timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2', 'req2')); timeline.pushSnapshot('req3', 'stop3', createSnapshot('stop3')); // Undo all @@ -334,6 +341,40 @@ suite('ChatEditingTimeline', () => { } assert.deepStrictEqual(redoStops, ['stop2', 'stop3']); }); + + test('getRequestDisablement with root request ID', () => { + timeline.pushSnapshot('req1', undefined, createSnapshot(undefined)); + timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); + timeline.pushSnapshot('req1', 'stop2', createSnapshot('stop2')); + + timeline.pushSnapshot('req2', undefined, createSnapshot(undefined, 'req2')); + timeline.pushSnapshot('req2', 'stop1-2', createSnapshot('stop1-2', 'req2')); + timeline.pushSnapshot('req2', 'stop2-2', createSnapshot('stop2-2', 'req2')); + + const expected: IChatRequestDisablement[][] = [ + [{ requestId: 'req2', afterUndoStop: 'stop1-2' }], + [{ requestId: 'req2' }], + // stop2 is not in this because we're at stop2 when undoing req2 + [{ requestId: 'req1', afterUndoStop: 'stop1' }, { requestId: 'req2' }], + [{ requestId: 'req1', afterUndoStop: undefined }, { requestId: 'req2' }], + ]; + + let ei = 0; + while (timeline.canUndo.get()) { + timeline.getUndoSnapshot()!.apply(); + const actual = timeline.requestDisablement.get(); + + assert.deepStrictEqual(actual, expected[ei++]); + } + + expected.unshift([]); + + while (timeline.canRedo.get()) { + timeline.getRedoSnapshot()!.apply(); + const actual = timeline.requestDisablement.get(); + assert.deepStrictEqual(actual, expected[--ei]); + } + }); }); suite('Static methods', () => { @@ -384,7 +425,7 @@ suite('ChatEditingTimeline', () => { suite('Complex scenarios', () => { test('interleaved requests and undos', () => { timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); - timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2')); + timeline.pushSnapshot('req2', 'stop2', createSnapshot('stop2', 'req2')); // Undo req2 timeline.getUndoSnapshot()?.apply(); @@ -416,9 +457,9 @@ suite('ChatEditingTimeline', () => { timeline.pushSnapshot('req1', 'stop1', createSnapshot('stop1')); // Multi-stop request - timeline.pushSnapshot('req2', 'stop2a', createSnapshot('stop2a')); - timeline.pushSnapshot('req2', 'stop2b', createSnapshot('stop2b')); - timeline.pushSnapshot('req2', 'stop2c', createSnapshot('stop2c')); + timeline.pushSnapshot('req2', 'stop2a', createSnapshot('stop2a', 'req2')); + timeline.pushSnapshot('req2', 'stop2b', createSnapshot('stop2b', 'req2')); + timeline.pushSnapshot('req2', 'stop2c', createSnapshot('stop2c', 'req2')); // Single stop request timeline.pushSnapshot('req3', 'stop3', createSnapshot('stop3')); @@ -460,7 +501,7 @@ suite('ChatEditingTimeline', () => { // Should be safe to call methods on empty timeline assert.strictEqual(timeline.getUndoSnapshot(), undefined); assert.strictEqual(timeline.getRedoSnapshot(), undefined); - assert.deepStrictEqual(timeline.getRequestDisablement(), []); + assert.deepStrictEqual(timeline.requestDisablement.get(), []); }); }); }); From 430f91e41b1c382c87f4c5487b06a56e70ab5374 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:42:03 -0700 Subject: [PATCH 282/306] chore: bump get-func-name (#254989) Resolves an npm audit issue --- test/monaco/package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/monaco/package-lock.json b/test/monaco/package-lock.json index 224680dd597..0b5274978bb 100644 --- a/test/monaco/package-lock.json +++ b/test/monaco/package-lock.json @@ -68,10 +68,11 @@ } }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } From 01d401258a44186d19e993cd98224efac6a1196b Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 9 Jul 2025 20:40:46 -0700 Subject: [PATCH 283/306] Remove agent killswitch (#255007) --- .../contrib/chat/common/chatServiceImpl.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index df2842c459e..a9f81d447c6 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -25,11 +25,10 @@ import { Progress } from '../../../../platform/progress/common/progress.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from './chatAgents.js'; import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, normalizeSerializableChatData, toChatHistoryContent, updateRanges } from './chatModel.js'; -import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, getPromptText } from './chatParserTypes.js'; +import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, chatSubcommandLeader, getPromptText, IParsedChatRequest } from './chatParserTypes.js'; import { ChatRequestParser } from './chatRequestParser.js'; import { IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from './chatService.js'; import { ChatServiceTelemetry } from './chatServiceTelemetry.js'; @@ -161,7 +160,6 @@ export class ChatService extends Disposable implements IChatService { @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, @IChatAgentService private readonly chatAgentService: IChatAgentService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, @IChatTransferService private readonly chatTransferService: IChatTransferService, @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, ) { @@ -804,7 +802,6 @@ export class ChatService extends Disposable implements IChatService { const agent = (detectedAgent ?? agentPart?.agent ?? defaultAgent)!; const command = detectedCommand ?? agentSlashCommandPart?.command; await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`); - await this.checkAgentAllowed(agent); // Recompute history in case the agent or command changed const history = this.getHistoryEntriesFromModel(requests, model.sessionId, location, agent.id); @@ -959,15 +956,6 @@ export class ChatService extends Disposable implements IChatService { return attachedContextVariables; } - private async checkAgentAllowed(agent: IChatAgentData): Promise { - if (agent.modes.includes(ChatModeKind.Agent)) { - const enabled = await this.experimentService.getTreatment('chatAgentEnabled'); - if (enabled === false) { - throw new Error('Agent is currently disabled'); - } - } - } - private attachmentKindsForTelemetry(variableData: IChatRequestVariableData): string[] { // TODO this shows why attachments still have to be cleaned up somewhat return variableData.variables.map(v => { From af0a171e61fa7bce22b53002bc2f3970315f7b73 Mon Sep 17 00:00:00 2001 From: Jason Kuo Date: Wed, 9 Jul 2025 23:44:16 -0500 Subject: [PATCH 284/306] Fix popup message when hovering over an instruction breakpoint (#254925) * Fix popup message when hovering over an instruction breakpoint * Update src/vs/workbench/contrib/debug/browser/breakpointsView.ts Co-authored-by: Connor Peet --------- Co-authored-by: Connor Peet --- src/vs/workbench/contrib/debug/browser/breakpointsView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 19f796fd9ee..9f52557a0ae 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -913,7 +913,7 @@ class InstructionBreakpointsRenderer implements IListRenderer Date: Thu, 10 Jul 2025 18:22:07 +1000 Subject: [PATCH 285/306] Stable sorting of notebook cell diff info (#255005) --- .../notebook/notebookCellChanges.ts | 18 ++++++++++++++++-- .../chatEditingModifiedNotebookEntry.test.ts | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/notebookCellChanges.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/notebookCellChanges.ts index 376d0ed8d5f..f978bec7d68 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/notebookCellChanges.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/notebookCellChanges.ts @@ -83,6 +83,8 @@ export function countChanges(changes: ICellDiffInfo[]): number { } export function sortCellChanges(changes: ICellDiffInfo[]): ICellDiffInfo[] { + const indexes = new Map(); + changes.forEach((c, i) => indexes.set(c, i)); return [...changes].sort((a, b) => { // For unchanged and modified, use modifiedCellIndex if ((a.type === 'unchanged' || a.type === 'modified') && @@ -101,10 +103,22 @@ export function sortCellChanges(changes: ICellDiffInfo[]): ICellDiffInfo[] { } if (a.type === 'delete' && b.type === 'insert') { - return -1; + // If the deleted cell comes before the inserted cell, we want the delete to come first + // As this means the cell was deleted before it was inserted + // We would like to see the deleted cell first in the list + // Else in the UI it would look weird to see an inserted cell before a deleted cell, + // When the users operation was to first delete the cell and then insert a new one + // I.e. this is merely just a simple way to ensure we have a stable sort. + return indexes.get(a)! - indexes.get(b)!; } if (a.type === 'insert' && b.type === 'delete') { - return 1; + // If the deleted cell comes before the inserted cell, we want the delete to come first + // As this means the cell was deleted before it was inserted + // We would like to see the deleted cell first in the list + // Else in the UI it would look weird to see an inserted cell before a deleted cell, + // When the users operation was to first delete the cell and then insert a new one + // I.e. this is merely just a simple way to ensure we have a stable sort. + return indexes.get(a)! - indexes.get(b)!; } if ((a.type === 'delete' && b.type !== 'insert') || (a.type !== 'insert' && b.type === 'delete')) { diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditingModifiedNotebookEntry.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditingModifiedNotebookEntry.test.ts index 86903354543..42270fb0be7 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatEditingModifiedNotebookEntry.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatEditingModifiedNotebookEntry.test.ts @@ -680,7 +680,7 @@ suite('ChatEditingModifiedNotebookEntry', function () { ]); }); - test.skip('Revert first deleted with multiple cells', async function () { + test('Revert first deleted with multiple cells', async function () { const cellsDiffInfo: ICellDiffInfo[] = [ { diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, From 8ce742c78878fc0ba62b59982e0f59633e1fef88 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 10 Jul 2025 12:38:20 +0200 Subject: [PATCH 286/306] Restoring to a maximised secondary sidebar on startup needs workarounds (fix #252465) (#255046) --- src/vs/workbench/browser/layout.ts | 80 +++++++++--------------------- 1 file changed, 24 insertions(+), 56 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index c01c75c25d3..caadf4f7ac2 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -1565,16 +1565,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.workbenchGrid = workbenchGrid; this.workbenchGrid.edgeSnapping = this.state.runtime.mainWindowFullscreen; - if (this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_WAS_LAST_MAXIMIZED)) { - // TODO@benibenj this is a workaround for the grid not being able to - // restore the maximized auxiliary bar on startup when it was maximised - // It seems that since editor and panel are hidden, the parent node is - // also hidden and not present, breaking the layout. - // Workaround is to make editor visible so that its parent view gets - // added properly and then enter maximized mode of auxiliary bar. - this.setAuxiliaryBarMaximized(true, true /* fromInit */); - } - for (const part of [titleBar, editorPart, activityBar, panelPart, sideBar, statusBar, auxiliaryBarPart, bannerPart]) { this._register(part.onDidVisibilityChange(visible => { if (!this.inMaximizedAuxiliaryBarTransition) { @@ -2023,63 +2013,42 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } } - private maximizedAuxiliaryBarState: { - sideBarVisible: boolean; - editorVisible: boolean; - panelVisible: boolean; - auxiliaryBarVisible: boolean; - } | undefined = undefined; - private inMaximizedAuxiliaryBarTransition = false; isAuxiliaryBarMaximized(): boolean { - return !!this.maximizedAuxiliaryBarState; + return this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_WAS_LAST_MAXIMIZED); } toggleMaximizedAuxiliaryBar(): void { this.setAuxiliaryBarMaximized(!this.isAuxiliaryBarMaximized()); } - setAuxiliaryBarMaximized(maximized: boolean, fromInit?: boolean): boolean { + setAuxiliaryBarMaximized(maximized: boolean): boolean { if ( - this.inMaximizedAuxiliaryBarTransition || // prevent re-entrance - (!maximized && !this.maximizedAuxiliaryBarState) // return early if not maximizing and no state + this.inMaximizedAuxiliaryBarTransition || // prevent re-entrance + (maximized === this.isAuxiliaryBarMaximized()) // return early if state is already present ) { return false; } if (maximized) { - let state: typeof this.maximizedAuxiliaryBarState; - if (fromInit) { - - // TODO workaround for a bug with grid, see above in `createWorkbenchLayout` - const stateMixin = { editorVisible: true }; - this.setEditorHidden(false); - // TODO workaround - - state = { - ...this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_LAST_NON_MAXIMIZED_VISIBILITY), - ...stateMixin - }; - } else { - state = { - sideBarVisible: this.isVisible(Parts.SIDEBAR_PART), - editorVisible: this.isVisible(Parts.EDITOR_PART), - panelVisible: this.isVisible(Parts.PANEL_PART), - auxiliaryBarVisible: this.isVisible(Parts.AUXILIARYBAR_PART) - }; - } - this.maximizedAuxiliaryBarState = state; + const state = { + sideBarVisible: this.isVisible(Parts.SIDEBAR_PART), + editorVisible: this.isVisible(Parts.EDITOR_PART), + panelVisible: this.isVisible(Parts.PANEL_PART), + auxiliaryBarVisible: this.isVisible(Parts.AUXILIARYBAR_PART) + }; + this.stateModel.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_WAS_LAST_MAXIMIZED, true); this.inMaximizedAuxiliaryBarTransition = true; try { if (!state.auxiliaryBarVisible) { this.setAuxiliaryBarHidden(false); } - if (!fromInit) { - const size = this.workbenchGrid.getViewSize(this.auxiliaryBarPartView).width; - this.stateModel.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_LAST_NON_MAXIMIZED_SIZE, size); - } + + const size = this.workbenchGrid.getViewSize(this.auxiliaryBarPartView).width; + this.stateModel.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_LAST_NON_MAXIMIZED_SIZE, size); + if (state.sideBarVisible) { this.setSideBarHidden(true); } @@ -2090,15 +2059,13 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.setEditorHidden(true); } - if (!fromInit) { - this.stateModel.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_LAST_NON_MAXIMIZED_VISIBILITY, state); - } + this.stateModel.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_LAST_NON_MAXIMIZED_VISIBILITY, state); } finally { this.inMaximizedAuxiliaryBarTransition = false; } } else { - const state = assertReturnsDefined(this.maximizedAuxiliaryBarState); - this.maximizedAuxiliaryBarState = undefined; + const state = assertReturnsDefined(this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_LAST_NON_MAXIMIZED_VISIBILITY)); + this.stateModel.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_WAS_LAST_MAXIMIZED, false); this.inMaximizedAuxiliaryBarTransition = true; try { @@ -2118,8 +2085,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.focusPart(Parts.AUXILIARYBAR_PART); - this.stateModel.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_WAS_LAST_MAXIMIZED, maximized); - this._onDidChangeAuxiliaryBarMaximized.fire(); return true; @@ -2451,7 +2416,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return { type: 'branch', data: result, - size: availableHeight + size: availableHeight, + visible: result.some(node => node.visible) }; } @@ -2496,10 +2462,12 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi auxiliaryBar: auxiliaryBarNextToEditor ? nodes.auxiliaryBar : undefined }, availableHeight - panelSize, editorSectionWidth); + const data = panelPostion === Position.BOTTOM ? [editorNodes, nodes.panel] : [nodes.panel, editorNodes]; result.push({ type: 'branch', - data: panelPostion === Position.BOTTOM ? [editorNodes, nodes.panel] : [nodes.panel, editorNodes], - size: editorSectionWidth + data, + size: editorSectionWidth, + visible: data.some(node => node.visible) }); if (!sideBarNextToEditor) { From 5d2088c19dd27269e8b64804d60e17dfa6260fc3 Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Thu, 10 Jul 2025 05:39:47 -0500 Subject: [PATCH 287/306] Context key `availableEditorIds` for diff editors (#250198) --- src/vs/workbench/common/contextkeys.ts | 36 +++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index 55a0db1fc70..e3b386ba967 100644 --- a/src/vs/workbench/common/contextkeys.ts +++ b/src/vs/workbench/common/contextkeys.ts @@ -15,6 +15,7 @@ import { Schemas } from '../../base/common/network.js'; import { EditorInput } from './editor/editorInput.js'; import { IEditorResolverService } from '../services/editor/common/editorResolverService.js'; import { DEFAULT_EDITOR_ASSOCIATION } from './editor.js'; +import { DiffEditorInput } from './editor/diffEditorInput.js'; //#region < --- Workbench --- > @@ -303,13 +304,30 @@ export function applyAvailableEditorIds(contextKey: IContextKey, editor: return; } - const editorResource = editor.resource; - if (editorResource?.scheme === Schemas.untitled && editor.editorId !== DEFAULT_EDITOR_ASSOCIATION.id) { - // Non text editor untitled files cannot be easily serialized between extensions - // so instead we disable this context key to prevent common commands that act on the active editor - contextKey.set(''); - } else { - const editors = editorResource ? editorResolverService.getEditors(editorResource).map(editor => editor.id) : []; - contextKey.set(editors.join(',')); - } + const editors = getAvailableEditorIds(editor, editorResolverService); + contextKey.set(editors.join(',')); +} + +function getAvailableEditorIds(editor: EditorInput, editorResolverService: IEditorResolverService): string[] { + // Non text editor untitled files cannot be easily serialized between + // extensions so instead we disable this context key to prevent common + // commands that act on the active editor. + if (editor.resource?.scheme === Schemas.untitled && editor.editorId !== DEFAULT_EDITOR_ASSOCIATION.id) { + return []; + } + + // Diff editors. The original and modified resources of a diff editor + // *should* be the same, but calculate the set intersection just to be safe. + if (editor instanceof DiffEditorInput) { + const original = getAvailableEditorIds(editor.original, editorResolverService); + const modified = new Set(getAvailableEditorIds(editor.modified, editorResolverService)); + return original.filter(editor => modified.has(editor)); + } + + // Normal editors. + if (editor.resource) { + return editorResolverService.getEditors(editor.resource).map(editor => editor.id); + } + + return []; } From 8665a3e8ae0f521624a067c1e11d387af5e5dc81 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 10 Jul 2025 12:42:17 +0200 Subject: [PATCH 288/306] chat - always refresh tokens when setup ran (#255100) --- src/vs/workbench/contrib/chat/browser/chatSetup.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 09915c27c35..d4751a21054 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -1344,7 +1344,11 @@ class ChatSetupController extends Disposable { this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: wasRunning && !signUpResult ? 'alreadyInstalled' : 'installed', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider }); } - if (wasRunning && signUpResult === true) { + if (wasRunning) { + // We always trigger refresh of tokens to help the user + // get out of authentication issues that can happen when + // for example the sign-up ran after the extension tried + // to use the authentication information to mint a token refreshTokens(this.commandService); } From c195156400a06e82f7cb0519396d72f8605fd61e Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 10 Jul 2025 21:05:02 +1000 Subject: [PATCH 289/306] Use open and show notebook instead of executeCommand in tests (#255106) --- .../src/singlefolder-tests/notebook.api.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts index e4489090017..0903b12a14a 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts @@ -244,7 +244,7 @@ const apiTestSerializer: vscode.NotebookSerializer = { // no kernel -> no default language assert.strictEqual(getFocusedCell(editor)?.document.languageId, 'typescript'); - await vscode.commands.executeCommand('vscode.openWith', notebook.uri, 'default'); + await vscode.window.showNotebookDocument(await vscode.workspace.openNotebookDocument(notebook.uri)); assert.strictEqual(vscode.window.activeTextEditor?.document.uri.path, notebook.uri.path); }); From e31bfcad9dd70ce11dc45188f47824089f6fd362 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 10 Jul 2025 21:05:27 +1000 Subject: [PATCH 290/306] Re-enabled some of the skipped notebook tests (#255087) * Identify flaky test failures * Updates --- .../src/singlefolder-tests/ipynb.test.ts | 37 ++++++++++++++++--- .../notebook.kernel.test.ts | 2 +- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/ipynb.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/ipynb.test.ts index b73025ab2e7..c3e28b4c303 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/ipynb.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/ipynb.test.ts @@ -6,13 +6,40 @@ import * as assert from 'assert'; import 'mocha'; import * as vscode from 'vscode'; +import { assertNoRpc, closeAllEditors, createRandomFile } from '../utils'; + +const ipynbContent = JSON.stringify({ + "cells": [ + { + "cell_type": "markdown", + "source": ["## Header"], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 2, + "source": ["print('hello 1')\n", "print('hello 2')"], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": ["hello 1\n", "hello 2\n"] + } + ], + "metadata": {} + } + ] +}); + +suite('ipynb NotebookSerializer', function () { + teardown(async function () { + assertNoRpc(); + await closeAllEditors(); + }); -(vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('ipynb NotebookSerializer', function () { test('Can open an ipynb notebook', async () => { - assert.ok(vscode.workspace.workspaceFolders); - const workspace = vscode.workspace.workspaceFolders[0]; - const uri = vscode.Uri.joinPath(workspace.uri, 'test.ipynb'); - const notebook = await vscode.workspace.openNotebookDocument(uri); + const file = await createRandomFile(ipynbContent, undefined, '.ipynb'); + const notebook = await vscode.workspace.openNotebookDocument(file); await vscode.window.showNotebookDocument(notebook); const notebookEditor = vscode.window.activeNotebookEditor; diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts index d1fafae7591..03045a033fc 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts @@ -123,7 +123,7 @@ const apiTestSerializer: vscode.NotebookSerializer = { } }; -(vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('Notebook Kernel API tests', function () { +suite('Notebook Kernel API tests', function () { const testDisposables: vscode.Disposable[] = []; const suiteDisposables: vscode.Disposable[] = []; From de49b03b7cbd64bf6ad9d67c3a81cd55c0c403ca Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 10 Jul 2025 04:21:39 -0700 Subject: [PATCH 291/306] Use log service in TaskQueue Fixes #242078 --- src/vs/editor/browser/gpu/atlas/textureAtlas.ts | 2 +- src/vs/editor/browser/gpu/taskQueue.ts | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts index 252a44596ef..13127f80e87 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts @@ -170,7 +170,7 @@ export class TextureAtlas extends Disposable { throw new BugIndicatingError('Cannot warm atlas without color map'); } this._warmUpTask.value?.clear(); - const taskQueue = this._warmUpTask.value = new IdleTaskQueue(); + const taskQueue = this._warmUpTask.value = this._instantiationService.createInstance(IdleTaskQueue); // Warm up using roughly the larger glyphs first to help optimize atlas allocation // A-Z for (let code = CharCode.A; code <= CharCode.Z; code++) { diff --git a/src/vs/editor/browser/gpu/taskQueue.ts b/src/vs/editor/browser/gpu/taskQueue.ts index a6cf28c8c86..7d7e7318900 100644 --- a/src/vs/editor/browser/gpu/taskQueue.ts +++ b/src/vs/editor/browser/gpu/taskQueue.ts @@ -5,6 +5,8 @@ import { getActiveWindow } from '../../../base/browser/dom.js'; import { Disposable, toDisposable, type IDisposable } from '../../../base/common/lifecycle.js'; +import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../platform/log/common/log.js'; /** * Copyright (c) 2022 The xterm.js authors. All rights reserved. @@ -41,7 +43,9 @@ abstract class TaskQueue extends Disposable implements ITaskQueue { private _idleCallback?: number; private _i = 0; - constructor() { + constructor( + @ILogService private readonly _logService: ILogService + ) { super(); this._register(toDisposable(() => this.clear())); } @@ -101,7 +105,7 @@ abstract class TaskQueue extends Disposable implements ITaskQueue { // Warn when the time exceeding the deadline is over 20ms, if this happens in practice the // task should be split into sub-tasks to ensure the UI remains responsive. if (lastDeadlineRemaining - taskDuration < -20) { - console.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(lastDeadlineRemaining - taskDuration))}ms`); + this._logService.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(lastDeadlineRemaining - taskDuration))}ms`); } this._start(); return; @@ -161,8 +165,10 @@ export const IdleTaskQueue = ('requestIdleCallback' in getActiveWindow()) ? Idle export class DebouncedIdleTask { private _queue: ITaskQueue; - constructor() { - this._queue = new IdleTaskQueue(); + constructor( + @IInstantiationService instantiationService: IInstantiationService + ) { + this._queue = instantiationService.createInstance(IdleTaskQueue); } public set(task: () => boolean | void): void { From 7b269dac035ca2439d3bce193483e44dfba9127b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 10 Jul 2025 21:22:38 +1000 Subject: [PATCH 292/306] Await on notebook.cell.execute command in tests (#255111) --- .../src/singlefolder-tests/notebook.kernel.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts index 03045a033fc..f12629d2619 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts @@ -213,7 +213,7 @@ suite('Notebook Kernel API tests', function () { } })); - vscode.commands.executeCommand('notebook.cell.execute', { document: notebook.uri, ranges: [{ start: 0, end: 1 }, { start: 1, end: 2 }] }); + await vscode.commands.executeCommand('notebook.cell.execute', { document: notebook.uri, ranges: [{ start: 0, end: 1 }, { start: 1, end: 2 }] }); await def.p; await saveAllFilesAndCloseAll(); From 2ff34581ea1ee3c0677a41984308b395da6a7933 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 10 Jul 2025 21:57:40 +1000 Subject: [PATCH 293/306] Skip flaky Notebook Kernel API tests (#255119) * Skip flaky Notebook Kernel API tests * updates --- .../src/singlefolder-tests/notebook.kernel.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts index f12629d2619..b232de35ffb 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts @@ -123,7 +123,7 @@ const apiTestSerializer: vscode.NotebookSerializer = { } }; -suite('Notebook Kernel API tests', function () { +(vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('Notebook Kernel API tests', function () { const testDisposables: vscode.Disposable[] = []; const suiteDisposables: vscode.Disposable[] = []; From ac3d4703b2b1bd8dd5da23eb138d240600cd1fbd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 10 Jul 2025 05:06:22 -0700 Subject: [PATCH 294/306] Eliminate some flakiness when not verifying files save This test was flaking because before the suite runs a bunch of settings are added which are critical to making the test reliable. Inside the settings part it opens the editor, edits it and saves the file via ctrl/cmd+s. This is all fine, but it doesn't verify anything so the editor may end up closing before ctrl/cmd+s actually gets handled. We disable the modal in smoke tests since it needs to run headlessly, so it's difficult to see that the file never actually saves and a dirty file is closed and discarded. The fix is to verify settings.json actually does save by changing the shared Editors.saveOpenedFile mechanism to ensure the dirty indicator isn't present on the active tab. Fixes #254893 Part of #246731 --- test/automation/src/editors.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/automation/src/editors.ts b/test/automation/src/editors.ts index 0f2e54722d0..4fcee5e14af 100644 --- a/test/automation/src/editors.ts +++ b/test/automation/src/editors.ts @@ -15,6 +15,7 @@ export class Editors { } else { await this.code.sendKeybinding('ctrl+s'); } + await this.code.waitForElements('.tab.active.dirty', false, results => results.length === 0); } async selectTab(fileName: string): Promise { From 40603817d71c2e28786802fa16dd177dd3b34b08 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 10 Jul 2025 15:59:38 +0200 Subject: [PATCH 295/306] TypeError thrown in instanceof DocumentSymbol (#255144) --- src/vs/workbench/api/common/extHostTypes.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index c4e454261a3..5ea598a3f50 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1422,9 +1422,6 @@ export class DocumentSymbol extends AbstractDocumentSymbol { } static override[Symbol.hasInstance](candidate: unknown): boolean { - if (!isObject(candidate)) { - throw new TypeError(); - } return candidate instanceof AbstractDocumentSymbol || candidate instanceof SymbolInformationAndDocumentSymbol; } From 73fbb0cccd17f892e8ade51199f7476ddd760ee1 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 10 Jul 2025 09:49:51 -0700 Subject: [PATCH 296/306] trying new empty state! (#253689) * exp welcome view * use exp setting * use exp setting * positioned disclaimers under title to avoid overlap * Updated chat placeholder text * some fixes * fix hygiene * make sure to add additional message * swap exp is active * add new setting --------- Co-authored-by: Elijah King --- .../contrib/chat/browser/chat.contribution.ts | 6 +++ .../contrib/chat/browser/chatWidget.ts | 41 +++++++++++++------ .../browser/contrib/chatInputEditorContrib.ts | 15 ++++++- .../chat/browser/media/chatViewWelcome.css | 16 ++++++++ .../viewsWelcome/chatViewWelcomeController.ts | 6 ++- .../contrib/chat/common/chatModes.ts | 6 +-- 6 files changed, 72 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 95b5d248e1c..fa11055a422 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -262,6 +262,12 @@ configurationRegistry.registerConfiguration({ default: 'inline', tags: ['experimental', 'onExp'], }, + 'chat.emptyChatState.enabled': { + type: 'boolean', + default: true, + description: nls.localize('chat.emptyChatState', "Shows a modified empty chat state with hints in the input placeholder text."), + tags: ['experimental', 'onExp'], + }, [mcpEnabledSection]: { type: 'boolean', description: nls.localize('chat.mcp.enabled', "Enables integration with Model Context Protocol servers to provide additional tools and functionality."), diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index dee43656492..b49bc87252d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -751,12 +751,16 @@ export class ChatWidget extends Disposable implements IChatWidget { const configuration = this.configurationService.inspect('workbench.secondarySideBar.defaultVisibility'); const expIsActive = configuration.defaultValue !== 'hidden'; + const expEmptyState = this.configurationService.getValue('chat.emptyChatState.enabled'); + const chatSetupTriggerContext = ContextKeyExpr.or( ChatContextKeys.Setup.installed.negate(), ChatContextKeys.Entitlement.canSignUp ); let welcomeContent: IChatViewWelcomeContent; + const defaultAgent = this.chatAgentService.getDefaultAgent(this.location, this.input.currentModeKind); + const additionalMessage = defaultAgent?.metadata.additionalWelcomeMessage; if ((startupExpValue === StartupExperimentGroup.MaximizedChat || startupExpValue === StartupExperimentGroup.SplitEmptyEditorChat || startupExpValue === StartupExperimentGroup.SplitWelcomeChat @@ -764,9 +768,10 @@ export class ChatWidget extends Disposable implements IChatWidget { welcomeContent = this.getExpWelcomeViewContent(); this.container.classList.add('experimental-welcome-view'); } + else if (expEmptyState) { + welcomeContent = this.getWelcomeViewContent(additionalMessage, expEmptyState); + } else { - const defaultAgent = this.chatAgentService.getDefaultAgent(this.location, this.input.currentModeKind); - const additionalMessage = defaultAgent?.metadata.additionalWelcomeMessage; const tips = this.input.currentModeKind === ChatModeKind.Ask ? new MarkdownString(localize('chatWidget.tips', "{0} or type {1} to attach context\n\n{2} to chat with extensions\n\nType {3} to use commands", '$(attach)', '#', '$(mention)', '/'), { supportThemeIcons: true }) : new MarkdownString(localize('chatWidget.tips.withoutParticipants', "{0} or type {1} to attach context", '$(attach)', '#'), { supportThemeIcons: true }); @@ -790,27 +795,37 @@ export class ChatWidget extends Disposable implements IChatWidget { } } - private getWelcomeViewContent(additionalMessage: string | IMarkdownString | undefined): IChatViewWelcomeContent { - const baseMessage = localize('chatMessage', "Copilot is powered by AI, so mistakes are possible. Review output carefully before use."); + private getWelcomeViewContent(additionalMessage: string | IMarkdownString | undefined, expEmptyState?: boolean): IChatViewWelcomeContent { + const disclaimerMessage = expEmptyState + ? localize('chatDisclaimer', "AI responses may be inaccurate.") + : localize('chatMessage', "Copilot is powered by AI, so mistakes are possible. Review output carefully before use."); + const icon = expEmptyState ? Codicon.chatSparkle : Codicon.copilotLarge; + if (this.input.currentModeKind === ChatModeKind.Ask) { return { - title: localize('chatDescription', "Ask Copilot"), - message: new MarkdownString(baseMessage), - icon: Codicon.copilotLarge, + title: localize('chatDescription', "Ask about your code."), + message: new MarkdownString(disclaimerMessage), + icon, additionalMessage, }; } else if (this.input.currentModeKind === ChatModeKind.Edit) { + const editsHelpMessage = localize('editsHelp', "Start your editing session by defining a set of files that you want to work with. Then ask Copilot for the changes you want to make."); + const message = expEmptyState ? disclaimerMessage : `${editsHelpMessage}\n\n${disclaimerMessage}`; + return { - title: localize('editsTitle', "Edit with Copilot"), - message: new MarkdownString(localize('editsMessage', "Start your editing session by defining a set of files that you want to work with. Then ask Copilot for the changes you want to make.") + `\n\n${baseMessage}`), - icon: Codicon.copilotLarge, + title: localize('editsTitle', "Edit in context."), + message: new MarkdownString(message), + icon, additionalMessage }; } else { + const agentHelpMessage = localize('agentMessage', "Ask Copilot to edit your files in [agent mode]({0}). Copilot will automatically use multiple requests to pick files to edit, run terminal commands, and iterate on errors.", 'https://aka.ms/vscode-copilot-agent'); + const message = expEmptyState ? disclaimerMessage : `${agentHelpMessage}\n\n${disclaimerMessage}`; + return { - title: localize('editsTitle', "Edit with Copilot"), - message: new MarkdownString(localize('agentMessage', "Ask Copilot to edit your files in [agent mode]({0}). Copilot will automatically use multiple requests to pick files to edit, run terminal commands, and iterate on errors.", 'https://aka.ms/vscode-copilot-agent') + `\n\n${baseMessage}`), - icon: Codicon.copilotLarge, + title: localize('agentTitle', "Build with agent mode."), + message: new MarkdownString(message), + icon, additionalMessage }; } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index cf529794f3a..f61e7d6b3e9 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -11,6 +11,8 @@ import { ICodeEditorService } from '../../../../../editor/browser/services/codeE import { Range } from '../../../../../editor/common/core/range.js'; import { IDecorationOptions } from '../../../../../editor/common/editorCommon.js'; import { TrackedRangeStickiness } from '../../../../../editor/common/model.js'; +import { localize } from '../../../../../nls.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { inputPlaceholderForeground } from '../../../../../platform/theme/common/colorRegistry.js'; import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; @@ -18,6 +20,7 @@ import { IChatAgentCommand, IChatAgentData, IChatAgentService } from '../../comm import { chatSlashCommandBackground, chatSlashCommandForeground } from '../../common/chatColors.js'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestToolSetPart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader } from '../../common/chatParserTypes.js'; import { ChatRequestParser } from '../../common/chatRequestParser.js'; +import { ChatModeKind } from '../../common/constants.js'; import { IChatWidget } from '../chat.js'; import { ChatWidget } from '../chatWidget.js'; import { dynamicVariableDecorationType } from './chatDynamicVariables.js'; @@ -44,6 +47,7 @@ class InputEditorDecorations extends Disposable { @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @IThemeService private readonly themeService: IThemeService, @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); @@ -126,7 +130,16 @@ class InputEditorDecorations extends Disposable { } if (!inputValue) { - const description = this.widget.input.currentModeObs.get().description.get(); + const mode = this.widget.input.currentModeObs.get(); + let description = mode.description.get(); + if (this.configurationService.getValue('chat.emptyChatState.enabled')) { + if (mode.kind === ChatModeKind.Ask) { + description += ` ${localize('askPlaceholderHint', "# context, @ extensions, / commands")}`; + } else if (mode.kind === ChatModeKind.Edit || mode.kind === ChatModeKind.Agent) { + description += ` ${localize('editPlaceholderHint', "# context")}`; + } + } + const decoration: IDecorationOptions[] = [ { range: { diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css index d6d06e188c0..6523d2807b4 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css @@ -80,6 +80,22 @@ div.chat-welcome-view { } } + & > .chat-welcome-view-message.experimental-empty-state { + position: relative; + text-align: center; + max-width: 100%; + margin: 0 auto; + color: var(--vscode-input-placeholderForeground); + + a { + color: var(--vscode-textLink-foreground); + } + p{ + margin-top: 8px; + margin-bottom: 8px; + } + } + .monaco-button { display: inline-block; width: initial; diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index 603baeeb982..46d8ccadeea 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -20,6 +20,7 @@ import { ITelemetryService } from '../../../../../platform/telemetry/common/tele import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { IChatWidgetService } from '../chat.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { chatViewsWelcomeRegistry, IChatViewsWelcomeDescriptor } from './chatViewsWelcome.js'; const $ = dom.$; @@ -142,6 +143,7 @@ export class ChatViewWelcomePart extends Disposable { @ILogService private logService: ILogService, @IChatWidgetService private chatWidgetService: IChatWidgetService, @ITelemetryService private telemetryService: ITelemetryService, + @IConfigurationService private configurationService: IConfigurationService, ) { super(); this.element = dom.$('.chat-welcome-view'); @@ -160,13 +162,15 @@ export class ChatViewWelcomePart extends Disposable { title.textContent = content.title; // Preview indicator - if (typeof content.message !== 'function' && options?.isWidgetAgentWelcomeViewContent) { + const expEmptyState = this.configurationService.getValue('chat.emptyChatState.enabled'); + if (typeof content.message !== 'function' && options?.isWidgetAgentWelcomeViewContent && !expEmptyState) { const container = dom.append(this.element, $('.chat-welcome-view-indicator-container')); dom.append(container, $('.chat-welcome-view-subtitle', undefined, localize('agentModeSubtitle', "Agent Mode"))); } // Message const message = dom.append(this.element, content.isExperimental ? $('.chat-welcome-experimental-view-message') : $('.chat-welcome-view-message')); + message.classList.toggle('experimental-empty-state', expEmptyState); if (typeof content.message === 'function') { dom.append(message, content.message(this._register(new DisposableStore()))); } else { diff --git a/src/vs/workbench/contrib/chat/common/chatModes.ts b/src/vs/workbench/contrib/chat/common/chatModes.ts index f805a65d42e..85d0bbb9750 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -333,9 +333,9 @@ export class BuiltinChatMode implements IChatMode { } export namespace ChatMode { - export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Ask Copilot")); - export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit files in your workspace")); - export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Edit files in your workspace in agent mode")); + export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Ask a question.")); + export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit files.")); + export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Build autonomously.")); } export function isBuiltinChatMode(mode: IChatMode): boolean { From 6b3bbd966c02dab6380c86420c981f75b754fe46 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 10 Jul 2025 11:06:03 -0700 Subject: [PATCH 297/306] Clean up implementation --- src/vs/base/browser/markdownRenderer.ts | 50 +++++++++++++++++-- .../browser}/markedKatexSupport.ts | 36 ++++++------- .../chatMarkdownContentPart.ts | 18 ++----- 3 files changed, 68 insertions(+), 36 deletions(-) rename src/vs/{workbench/contrib/chat/browser/chatContentParts => base/browser}/markedKatexSupport.ts (81%) diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index d4dc25f94f6..7b69a04b78a 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -23,18 +23,20 @@ import dompurify from './dompurify/dompurify.js'; import { DomEmitter } from './event.js'; import { createElement, FormattedTextRenderOptions } from './formattedTextRenderer.js'; import { StandardKeyboardEvent } from './keyboardEvent.js'; +import { MarkedKatexSupport } from './markedKatexSupport.js'; import { StandardMouseEvent } from './mouseEvent.js'; import { renderLabelWithIcons } from './ui/iconLabel/iconLabels.js'; +import { CodeWindow } from './window.js'; -export interface MarkedOptions extends Readonly> { - readonly markedExtensions?: marked.MarkedExtension[]; -} +export interface MarkedOptions extends Readonly> { } export interface MarkdownRenderOptions extends FormattedTextRenderOptions { + readonly enableMath?: { readonly window: CodeWindow }; + readonly fillInIncompleteTokens?: boolean; + readonly codeBlockRenderer?: (languageId: string, value: string) => Promise; readonly codeBlockRendererSync?: (languageId: string, value: string, raw?: string) => HTMLElement; readonly asyncRenderCallback?: () => void; - readonly fillInIncompleteTokens?: boolean; readonly remoteImageIsAllowed?: (uri: URI) => boolean; readonly sanitizerOptions?: ISanitizerOptions; } @@ -108,7 +110,45 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende const element = createElement(options); - const markedInstance = new marked.Marked(...(markedOptions.markedExtensions ?? [])); + const markedExtensions: marked.MarkedExtension[] = []; + + if (options.enableMath) { + const existing = MarkedKatexSupport.getExtension(options.enableMath.window, { + throwOnError: false + }); + + if (existing) { + markedExtensions.push(existing); + } else { + // We need to load the extension + // However we don't want to make `renderMarkdown` async, so we kick off the loading in parallel and then + // insert the async rendered into the sync result + let disposed = false; + let disposable: IDisposable | undefined; + + MarkedKatexSupport.loadExtension(options.enableMath.window, { + throwOnError: false + }).then(() => { + if (disposed) { + return; + } + + const result = renderMarkdown(markdown, options, markedOptions); + disposable = result; + element.replaceChildren(...result.element.childNodes); + }); + + return { + element, + dispose: () => { + disposed = true; + disposable?.dispose(); + } + }; + } + } + + const markedInstance = new marked.Marked(...markedExtensions); const { renderer, codeBlocks, syncCodeBlocks } = createMarkdownRenderer(markedInstance, options, markedOptions, markdown); const value = preprocessMarkdownString(markdown); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts b/src/vs/base/browser/markedKatexSupport.ts similarity index 81% rename from src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts rename to src/vs/base/browser/markedKatexSupport.ts index 4594f999aea..d290cb03213 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts +++ b/src/vs/base/browser/markedKatexSupport.ts @@ -3,36 +3,36 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { importAMDNodeModule, resolveAmdNodeModulePath } from '../../../../../amdX.js'; -import * as dom from '../../../../../base/browser/dom.js'; -import type * as marked from '../../../../../base/common/marked/marked.js'; +import { importAMDNodeModule, resolveAmdNodeModulePath } from '../../amdX.js'; +import { Lazy } from '../common/lazy.js'; +import type * as marked from '../common/marked/marked.js'; +import { CodeWindow } from './window.js'; export class MarkedKatexSupport { public static _katex?: typeof import('katex').default; + public static _katexPromise = new Lazy(async () => { + this._katex = await importAMDNodeModule('katex', 'dist/katex.min.js'); + return this._katex; + }); - static { - // TODO: figure out a better way to do this - // I ran into two issues: - // - We don't support to level imports of node_modules so you have to use `import(...)` - // - I also didn't want to make all the callers to markdown rendering async to properly await - // loading of the extension, especially because many of them are ctors. - importAMDNodeModule('katex', 'dist/katex.min.js').then(katex => { - this._katex = katex; - }); - } - - public static getExtension(container: HTMLElement, options: MarkedKatexExtension.MarkedKatexOptions = {}): marked.MarkedExtension | undefined { + public static getExtension(window: CodeWindow, options: MarkedKatexExtension.MarkedKatexOptions = {}): marked.MarkedExtension | undefined { if (!this._katex) { return undefined; } - this.ensureKatexStyles(container); + this.ensureKatexStyles(window); return MarkedKatexExtension.extension(this._katex, options); } - public static ensureKatexStyles(container: HTMLElement) { - const doc = dom.getWindow(container).document; + public static async loadExtension(window: CodeWindow, options: MarkedKatexExtension.MarkedKatexOptions = {}): Promise { + const katex = await this._katexPromise.value; + this.ensureKatexStyles(window); + return MarkedKatexExtension.extension(katex, options); + } + + public static ensureKatexStyles(window: CodeWindow) { + const doc = window.document; if (!doc.querySelector('link.katex')) { const katexStyle = document.createElement('link'); katexStyle.classList.add('katex'); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index 7c64e11bbb4..f25893c1919 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -7,7 +7,6 @@ import * as dom from '../../../../../base/browser/dom.js'; import { MarkedOptions } from '../../../../../base/browser/markdownRenderer.js'; import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js'; -import { coalesce } from '../../../../../base/common/arrays.js'; import { findLast } from '../../../../../base/common/arraysFind.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../base/common/event.js'; @@ -51,7 +50,6 @@ import '../media/chatCodeBlockPill.css'; import { IDisposableReference, ResourcePool } from './chatCollections.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; import { ChatExtensionsContentPart } from './chatExtensionsContentPart.js'; -import { MarkedKatexSupport } from './markedKatexSupport.js'; import './media/chatMarkdownPart.css'; const $ = dom.$; @@ -161,22 +159,16 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP let globalCodeBlockIndexStart = codeBlockStartIndex; let thisPartCodeBlockIndexStart = 0; - const markedExtensions = configurationService.getValue(ChatConfiguration.EnableMath) - ? coalesce([MarkedKatexSupport.getExtension(context.container, { - throwOnError: false - })]) - : []; - // Don't set to 'false' for responses, respect defaults - const markedOpts: MarkedOptions = isRequestVM(element) || true ? { + const markedOpts: MarkedOptions = isRequestVM(element) ? { gfm: true, breaks: true, - markedExtensions, - } : { - markedExtensions, - }; + } : {}; const result = this._register(renderer.render(markdown.content, { + enableMath: configurationService.getValue(ChatConfiguration.EnableMath) ? { + window: dom.getWindow(context.container), + } : undefined, sanitizerOptions: { allowedTags: [ ...dom.basicMarkupHtmlTags, From c2058a02513ce3986c0541e8ee64726e1960a452 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 10 Jul 2025 11:08:24 -0700 Subject: [PATCH 298/306] Fix merge conflict --- src/vs/base/browser/markdownRenderer.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 7b69a04b78a..c550a95b5df 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -45,7 +45,6 @@ export interface ISanitizerOptions { readonly replaceWithPlaintext?: boolean; readonly allowedTags?: readonly string[]; readonly customAttrSanitizer?: (attrName: string, attrValue: string) => boolean | string; - readonly allowedSchemes?: readonly string[]; readonly allowedProductProtocols?: readonly string[]; } From 658bdeff2bbf6a70c6b9b1e56fbd224a46b8a3cb Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 10 Jul 2025 11:11:02 -0700 Subject: [PATCH 299/306] Inline type for now --- src/vs/base/browser/markedKatexSupport.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/base/browser/markedKatexSupport.ts b/src/vs/base/browser/markedKatexSupport.ts index d290cb03213..e65547fdfb1 100644 --- a/src/vs/base/browser/markedKatexSupport.ts +++ b/src/vs/base/browser/markedKatexSupport.ts @@ -45,7 +45,9 @@ export class MarkedKatexSupport { namespace MarkedKatexExtension { - type KatexOptions = import('katex').KatexOptions; + type KatexOptions = { + throwOnError?: boolean; + }; // From https://github.com/UziTech/marked-katex-extension/blob/main/src/index.js export interface MarkedKatexOptions extends KatexOptions { From 10b3a655ba5608785fa6c87479a7155cb36ca949 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 10 Jul 2025 11:26:43 -0700 Subject: [PATCH 300/306] Disable more checking for now --- src/vs/base/browser/markedKatexSupport.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/vs/base/browser/markedKatexSupport.ts b/src/vs/base/browser/markedKatexSupport.ts index e65547fdfb1..2a3138a1d87 100644 --- a/src/vs/base/browser/markedKatexSupport.ts +++ b/src/vs/base/browser/markedKatexSupport.ts @@ -8,11 +8,13 @@ import { Lazy } from '../common/lazy.js'; import type * as marked from '../common/marked/marked.js'; import { CodeWindow } from './window.js'; +type KatexLib = any; + export class MarkedKatexSupport { - public static _katex?: typeof import('katex').default; + public static _katex?: KatexLib; public static _katexPromise = new Lazy(async () => { - this._katex = await importAMDNodeModule('katex', 'dist/katex.min.js'); + this._katex = await importAMDNodeModule('katex', 'dist/katex.min.js'); return this._katex; }); @@ -63,7 +65,7 @@ namespace MarkedKatexExtension { const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/; - export function extension(katex: typeof import('katex').default, options: MarkedKatexOptions = {}): marked.MarkedExtension { + export function extension(katex: KatexLib, options: MarkedKatexOptions = {}): marked.MarkedExtension { return { extensions: [ inlineKatex(options, createRenderer(katex, options, false)), @@ -72,7 +74,7 @@ namespace MarkedKatexExtension { }; } - function createRenderer(katex: typeof import('katex').default, options: MarkedKatexOptions, newlineAfter: boolean): marked.RendererExtensionFunction { + function createRenderer(katex: KatexLib, options: MarkedKatexOptions, newlineAfter: boolean): marked.RendererExtensionFunction { return (token: marked.Tokens.Generic) => { return katex.renderToString(token.text, { ...options, From 762754e6b86ea9df80fe554bda5fc966bdc1d39b Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Thu, 10 Jul 2025 12:48:04 -0700 Subject: [PATCH 301/306] chore: bump webpack (#254987) Brings in the latest fixes including perf improvements. Co-authored-by: Benjamin Pasero --- package-lock.json | 383 ++++++++++++++++++++++++---------------------- 1 file changed, 197 insertions(+), 186 deletions(-) diff --git a/package-lock.json b/package-lock.json index 09d8dc7afb4..ee7c8d6f393 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1286,34 +1286,33 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz", + "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -2186,11 +2185,23 @@ "@types/json-schema": "*" } }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" }, "node_modules/@types/expect": { "version": "1.20.4", @@ -3537,148 +3548,163 @@ "hasInstallScript": true }, "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", - "dev": true + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, + "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -3830,13 +3856,15 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/abbrev": { "version": "1.1.1", @@ -3858,10 +3886,11 @@ } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -3869,13 +3898,17 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "node_modules/acorn-import-phases": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.3.tgz", + "integrity": "sha512-jtKLnfoOzm28PazuQ4dVBcE9Jeo6ha1GAJvq3N0LlNOszmTfx+wSycBehn+FN0RnyeR77IBxN/qVYMw0Rlj0Xw==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, "peerDependencies": { - "acorn": "^8" + "acorn": "^8.14.0" } }, "node_modules/acorn-jsx": { @@ -6471,10 +6504,11 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -11137,6 +11171,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -11151,6 +11186,7 @@ "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" } @@ -11160,6 +11196,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -12263,7 +12300,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", @@ -15243,18 +15281,19 @@ "dev": true }, "node_modules/schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", + "ajv": "^8.9.0", "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" + "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -16682,13 +16721,14 @@ } }, "node_modules/terser": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.0.tgz", - "integrity": "sha512-Y/SblUl5kEyEFzhMAQdsxVHh+utAxd4IuRNJzKywY/4uzSogh3G219jqbDDxYu4MXO9CzY3tSEqmZvW6AoEDJw==", + "version": "5.43.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -16700,16 +16740,17 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -16733,35 +16774,19 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/terser/node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -18003,20 +18028,23 @@ "dev": true }, "node_modules/webpack": { - "version": "5.94.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", - "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "version": "5.100.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.100.0.tgz", + "integrity": "sha512-H8yBSBTk+BqxrINJnnRzaxU94SVP2bjd7WmA+PfCphoIdDpeQMJ77pq9/4I7xjLq38cB1bNKfzYPZu8pB3zKtg==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", + "enhanced-resolve": "^5.17.2", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -18026,11 +18054,11 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", + "schema-utils": "^4.3.2", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", + "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" @@ -18137,10 +18165,11 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.13.0" } @@ -18239,24 +18268,6 @@ "node": ">=4.0" } }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", From 141aa8582abb96f671badbb100bbd6dab3f85a7c Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 10 Jul 2025 16:03:07 -0400 Subject: [PATCH 302/306] check if thisArgs are defined (#255214) --- src/vs/workbench/api/common/extHost.api.impl.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 87fc94481a3..0f44ca6f3c1 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1359,7 +1359,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, onDidStartTask: (listeners, thisArgs?, disposables?) => { if (!isProposedApiEnabled(extension, 'taskExecutionTerminal')) { - thisArgs.terminal = undefined; + if (thisArgs) { + thisArgs.terminal = undefined; + } } return _asExtensionEvent(extHostTask.onDidStartTask)(listeners, thisArgs, disposables); }, From a9882f1344b73e407e4ef213242f45d3b9338a8f Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 10 Jul 2025 13:21:32 -0700 Subject: [PATCH 303/306] Different approach to async loading --- src/vs/base/browser/markdownRenderer.ts | 51 +--- .../chatMarkdownContentPart.ts | 288 ++++++++++-------- .../chatContentParts}/markedKatexSupport.ts | 22 +- 3 files changed, 171 insertions(+), 190 deletions(-) rename src/vs/{base/browser => workbench/contrib/chat/browser/chatContentParts}/markedKatexSupport.ts (86%) diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index c550a95b5df..d4dc25f94f6 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -23,20 +23,18 @@ import dompurify from './dompurify/dompurify.js'; import { DomEmitter } from './event.js'; import { createElement, FormattedTextRenderOptions } from './formattedTextRenderer.js'; import { StandardKeyboardEvent } from './keyboardEvent.js'; -import { MarkedKatexSupport } from './markedKatexSupport.js'; import { StandardMouseEvent } from './mouseEvent.js'; import { renderLabelWithIcons } from './ui/iconLabel/iconLabels.js'; -import { CodeWindow } from './window.js'; -export interface MarkedOptions extends Readonly> { } +export interface MarkedOptions extends Readonly> { + readonly markedExtensions?: marked.MarkedExtension[]; +} export interface MarkdownRenderOptions extends FormattedTextRenderOptions { - readonly enableMath?: { readonly window: CodeWindow }; - readonly fillInIncompleteTokens?: boolean; - readonly codeBlockRenderer?: (languageId: string, value: string) => Promise; readonly codeBlockRendererSync?: (languageId: string, value: string, raw?: string) => HTMLElement; readonly asyncRenderCallback?: () => void; + readonly fillInIncompleteTokens?: boolean; readonly remoteImageIsAllowed?: (uri: URI) => boolean; readonly sanitizerOptions?: ISanitizerOptions; } @@ -45,6 +43,7 @@ export interface ISanitizerOptions { readonly replaceWithPlaintext?: boolean; readonly allowedTags?: readonly string[]; readonly customAttrSanitizer?: (attrName: string, attrValue: string) => boolean | string; + readonly allowedSchemes?: readonly string[]; readonly allowedProductProtocols?: readonly string[]; } @@ -109,45 +108,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende const element = createElement(options); - const markedExtensions: marked.MarkedExtension[] = []; - - if (options.enableMath) { - const existing = MarkedKatexSupport.getExtension(options.enableMath.window, { - throwOnError: false - }); - - if (existing) { - markedExtensions.push(existing); - } else { - // We need to load the extension - // However we don't want to make `renderMarkdown` async, so we kick off the loading in parallel and then - // insert the async rendered into the sync result - let disposed = false; - let disposable: IDisposable | undefined; - - MarkedKatexSupport.loadExtension(options.enableMath.window, { - throwOnError: false - }).then(() => { - if (disposed) { - return; - } - - const result = renderMarkdown(markdown, options, markedOptions); - disposable = result; - element.replaceChildren(...result.element.childNodes); - }); - - return { - element, - dispose: () => { - disposed = true; - disposable?.dispose(); - } - }; - } - } - - const markedInstance = new marked.Marked(...markedExtensions); + const markedInstance = new marked.Marked(...(markedOptions.markedExtensions ?? [])); const { renderer, codeBlocks, syncCodeBlocks } = createMarkdownRenderer(markedInstance, options, markedOptions, markdown); const value = preprocessMarkdownString(markdown); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index f25893c1919..ebae5f2da05 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -7,6 +7,7 @@ import * as dom from '../../../../../base/browser/dom.js'; import { MarkedOptions } from '../../../../../base/browser/markdownRenderer.js'; import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js'; +import { coalesce } from '../../../../../base/common/arrays.js'; import { findLast } from '../../../../../base/common/arraysFind.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../base/common/event.js'; @@ -50,6 +51,7 @@ import '../media/chatCodeBlockPill.css'; import { IDisposableReference, ResourcePool } from './chatCollections.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; import { ChatExtensionsContentPart } from './chatExtensionsContentPart.js'; +import { MarkedKatexSupport } from './markedKatexSupport.js'; import './media/chatMarkdownPart.css'; const $ = dom.$; @@ -159,147 +161,169 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP let globalCodeBlockIndexStart = codeBlockStartIndex; let thisPartCodeBlockIndexStart = 0; - // Don't set to 'false' for responses, respect defaults - const markedOpts: MarkedOptions = isRequestVM(element) ? { - gfm: true, - breaks: true, - } : {}; + this.domNode = document.createElement('div'); + this.domNode.classList.add('chat-markdown-part'); - const result = this._register(renderer.render(markdown.content, { - enableMath: configurationService.getValue(ChatConfiguration.EnableMath) ? { - window: dom.getWindow(context.container), - } : undefined, - sanitizerOptions: { - allowedTags: [ - ...dom.basicMarkupHtmlTags, - ...dom.trustedMathMlTags, - ], - customAttrSanitizer: (attrName, attrValue) => { - if (attrName === 'class') { - return true; // TODO: allows all classes for now since we don't have a list of possible katex classes - } else if (attrName === 'style') { - return ChatMarkdownContentPart.sanitizeKatexStyles(attrValue); - } + const enableMath = configurationService.getValue(ChatConfiguration.EnableMath); - return false; + const doRenderMarkdown = () => { + const markedExtensions = enableMath + ? coalesce([MarkedKatexSupport.getExtension(dom.getWindow(context.container), { + throwOnError: false + })]) + : []; + + // Don't set to 'false' for responses, respect defaults + const markedOpts: MarkedOptions = isRequestVM(element) ? { + gfm: true, + breaks: true, + markedExtensions, + } : { + markedExtensions, + }; + + const result = this._register(renderer.render(markdown.content, { + sanitizerOptions: { + allowedTags: [ + ...dom.basicMarkupHtmlTags, + ...dom.trustedMathMlTags, + ], + customAttrSanitizer: (attrName, attrValue) => { + if (attrName === 'class') { + return true; // TODO: allows all classes for now since we don't have a list of possible katex classes + } else if (attrName === 'style') { + return ChatMarkdownContentPart.sanitizeKatexStyles(attrValue); + } + + return false; + }, }, - }, - fillInIncompleteTokens, - codeBlockRendererSync: (languageId, text, raw) => { - const isCodeBlockComplete = !isResponseVM(context.element) || context.element.isComplete || !raw || codeblockHasClosingBackticks(raw); - if ((!text || (text.startsWith(' this._onDidChangeHeight.fire())); - return chatExtensions.domNode; - } - const globalIndex = globalCodeBlockIndexStart++; - const thisPartIndex = thisPartCodeBlockIndexStart++; - let textModel: Promise; - let range: Range | undefined; - let vulns: readonly IMarkdownVulnerability[] | undefined; - let codeblockEntry: CodeBlockEntry | undefined; - if (equalsIgnoreCase(languageId, localFileLanguageId)) { - try { - const parsedBody = parseLocalFileData(text); - range = parsedBody.range && Range.lift(parsedBody.range); - textModel = this.textModelService.createModelReference(parsedBody.uri).then(ref => ref.object.textEditorModel); - } catch (e) { - return $('div'); + fillInIncompleteTokens, + codeBlockRendererSync: (languageId, text, raw) => { + const isCodeBlockComplete = !isResponseVM(context.element) || context.element.isComplete || !raw || codeblockHasClosingBackticks(raw); + if ((!text || (text.startsWith(' this._onDidChangeHeight.fire())); - - const ownerMarkdownPartId = this.codeblocksPartId; - const info: IChatCodeBlockInfo = new class implements IChatCodeBlockInfo { - readonly ownerMarkdownPartId = ownerMarkdownPartId; - readonly codeBlockIndex = globalIndex; - readonly elementId = element.id; - readonly isStreaming = false; - readonly chatSessionId = element.sessionId; - codemapperUri = undefined; // will be set async - public get uri() { - // here we must do a getter because the ref.object is rendered - // async and the uri might be undefined when it's read immediately - return ref.object.uri; - } - readonly uriPromise = textModel.then(model => model.uri); - public focus() { - ref.object.focus(); - } - }(); - this.codeblocks.push(info); - orderedDisposablesList.push(ref); - return ref.object.element; - } else { - const requestId = isRequestVM(element) ? element.id : element.requestId; - const ref = this.renderCodeBlockPill(element.sessionId, requestId, inUndoStop, codeBlockInfo.codemapperUri, !isCodeBlockComplete); - if (isResponseVM(codeBlockInfo.element)) { - // TODO@joyceerhl: remove this code when we change the codeblockUri API to make the URI available synchronously - this.codeBlockModelCollection.update(codeBlockInfo.element.sessionId, codeBlockInfo.element, codeBlockInfo.codeBlockIndex, { text, languageId: codeBlockInfo.languageId, isComplete: isCodeBlockComplete }).then((e) => { - // Update the existing object's codemapperUri - this.codeblocks[codeBlockInfo.codeBlockPartIndex].codemapperUri = e.codemapperUri; - this._onDidChangeHeight.fire(); - }); + if (languageId === 'vscode-extensions') { + const chatExtensions = this._register(instantiationService.createInstance(ChatExtensionsContentPart, { kind: 'extensions', extensions: text.split(',') })); + this._register(chatExtensions.onDidChangeHeight(() => this._onDidChangeHeight.fire())); + return chatExtensions.domNode; } - this.allRefs.push(ref); - const ownerMarkdownPartId = this.codeblocksPartId; - const info: IChatCodeBlockInfo = new class implements IChatCodeBlockInfo { - readonly ownerMarkdownPartId = ownerMarkdownPartId; - readonly codeBlockIndex = globalIndex; - readonly elementId = element.id; - readonly isStreaming = !isCodeBlockComplete; - readonly codemapperUri = codeblockEntry?.codemapperUri; - readonly chatSessionId = element.sessionId; - public get uri() { - return undefined; + const globalIndex = globalCodeBlockIndexStart++; + const thisPartIndex = thisPartCodeBlockIndexStart++; + let textModel: Promise; + let range: Range | undefined; + let vulns: readonly IMarkdownVulnerability[] | undefined; + let codeblockEntry: CodeBlockEntry | undefined; + if (equalsIgnoreCase(languageId, localFileLanguageId)) { + try { + const parsedBody = parseLocalFileData(text); + range = parsedBody.range && Range.lift(parsedBody.range); + textModel = this.textModelService.createModelReference(parsedBody.uri).then(ref => ref.object.textEditorModel); + } catch (e) { + return $('div'); } - readonly uriPromise = Promise.resolve(undefined); - public focus() { - return ref.object.element.focus(); + } else { + const sessionId = isResponseVM(element) || isRequestVM(element) ? element.sessionId : ''; + const modelEntry = this.codeBlockModelCollection.getOrCreate(sessionId, element, globalIndex); + const fastUpdateModelEntry = this.codeBlockModelCollection.updateSync(sessionId, element, globalIndex, { text, languageId, isComplete: isCodeBlockComplete }); + vulns = modelEntry.vulns; + codeblockEntry = fastUpdateModelEntry; + textModel = modelEntry.model; + } + + const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered; + const renderOptions = { + ...this.rendererOptions.codeBlockRenderOptions, + }; + if (hideToolbar !== undefined) { + renderOptions.hideToolbar = hideToolbar; + } + const codeBlockInfo: ICodeBlockData = { languageId, textModel, codeBlockIndex: globalIndex, codeBlockPartIndex: thisPartIndex, element, range, parentContextKeyService: contextKeyService, vulns, codemapperUri: codeblockEntry?.codemapperUri, renderOptions, chatSessionId: element.sessionId }; + + if (element.isCompleteAddedRequest || !codeblockEntry?.codemapperUri || !codeblockEntry.isEdit) { + const ref = this.renderCodeBlock(codeBlockInfo, text, isCodeBlockComplete, currentWidth); + this.allRefs.push(ref); + + // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) + // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) + this._register(ref.object.onDidChangeContentHeight(() => this._onDidChangeHeight.fire())); + + const ownerMarkdownPartId = this.codeblocksPartId; + const info: IChatCodeBlockInfo = new class implements IChatCodeBlockInfo { + readonly ownerMarkdownPartId = ownerMarkdownPartId; + readonly codeBlockIndex = globalIndex; + readonly elementId = element.id; + readonly isStreaming = false; + readonly chatSessionId = element.sessionId; + codemapperUri = undefined; // will be set async + public get uri() { + // here we must do a getter because the ref.object is rendered + // async and the uri might be undefined when it's read immediately + return ref.object.uri; + } + readonly uriPromise = textModel.then(model => model.uri); + public focus() { + ref.object.focus(); + } + }(); + this.codeblocks.push(info); + orderedDisposablesList.push(ref); + return ref.object.element; + } else { + const requestId = isRequestVM(element) ? element.id : element.requestId; + const ref = this.renderCodeBlockPill(element.sessionId, requestId, inUndoStop, codeBlockInfo.codemapperUri, !isCodeBlockComplete); + if (isResponseVM(codeBlockInfo.element)) { + // TODO@joyceerhl: remove this code when we change the codeblockUri API to make the URI available synchronously + this.codeBlockModelCollection.update(codeBlockInfo.element.sessionId, codeBlockInfo.element, codeBlockInfo.codeBlockIndex, { text, languageId: codeBlockInfo.languageId, isComplete: isCodeBlockComplete }).then((e) => { + // Update the existing object's codemapperUri + this.codeblocks[codeBlockInfo.codeBlockPartIndex].codemapperUri = e.codemapperUri; + this._onDidChangeHeight.fire(); + }); } - }(); - this.codeblocks.push(info); - orderedDisposablesList.push(ref); - return ref.object.element; - } - }, - asyncRenderCallback: () => this._onDidChangeHeight.fire(), - }, markedOpts)); + this.allRefs.push(ref); + const ownerMarkdownPartId = this.codeblocksPartId; + const info: IChatCodeBlockInfo = new class implements IChatCodeBlockInfo { + readonly ownerMarkdownPartId = ownerMarkdownPartId; + readonly codeBlockIndex = globalIndex; + readonly elementId = element.id; + readonly isStreaming = !isCodeBlockComplete; + readonly codemapperUri = codeblockEntry?.codemapperUri; + readonly chatSessionId = element.sessionId; + public get uri() { + return undefined; + } + readonly uriPromise = Promise.resolve(undefined); + public focus() { + return ref.object.element.focus(); + } + }(); + this.codeblocks.push(info); + orderedDisposablesList.push(ref); + return ref.object.element; + } + }, + asyncRenderCallback: () => this._onDidChangeHeight.fire(), + }, markedOpts)); - const markdownDecorationsRenderer = instantiationService.createInstance(ChatMarkdownDecorationsRenderer); - this._register(markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(markdown, result.element)); + const markdownDecorationsRenderer = instantiationService.createInstance(ChatMarkdownDecorationsRenderer); + this._register(markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(markdown, result.element)); - orderedDisposablesList.reverse().forEach(d => this._register(d)); - result.element.classList.add('chat-markdown-part'); - this.domNode = result.element; + orderedDisposablesList.reverse().forEach(d => this._register(d)); + + this.domNode.replaceChildren(...result.element.children); + }; + + if (enableMath && !MarkedKatexSupport.getExtension(dom.getWindow(context.container))) { + // Need to load async + MarkedKatexSupport.loadExtension(dom.getWindow(context.container)).then(() => { + doRenderMarkdown(); + }); + } else { + doRenderMarkdown(); + } } private renderCodeBlockPill(sessionId: string, requestId: string, inUndoStop: string | undefined, codemapperUri: URI | undefined, isStreaming: boolean): IDisposableReference { diff --git a/src/vs/base/browser/markedKatexSupport.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts similarity index 86% rename from src/vs/base/browser/markedKatexSupport.ts rename to src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts index 2a3138a1d87..6d35651a9f5 100644 --- a/src/vs/base/browser/markedKatexSupport.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/markedKatexSupport.ts @@ -3,17 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { importAMDNodeModule, resolveAmdNodeModulePath } from '../../amdX.js'; -import { Lazy } from '../common/lazy.js'; -import type * as marked from '../common/marked/marked.js'; -import { CodeWindow } from './window.js'; - -type KatexLib = any; +import { importAMDNodeModule, resolveAmdNodeModulePath } from '../../../../../amdX.js'; +import { CodeWindow } from '../../../../../base/browser/window.js'; +import { Lazy } from '../../../../../base/common/lazy.js'; +import type * as marked from '../../../../../base/common/marked/marked.js'; export class MarkedKatexSupport { - public static _katex?: KatexLib; - public static _katexPromise = new Lazy(async () => { + private static _katex?: typeof import('katex').default; + private static _katexPromise = new Lazy(async () => { this._katex = await importAMDNodeModule('katex', 'dist/katex.min.js'); return this._katex; }); @@ -47,9 +45,7 @@ export class MarkedKatexSupport { namespace MarkedKatexExtension { - type KatexOptions = { - throwOnError?: boolean; - }; + type KatexOptions = import('katex').KatexOptions; // From https://github.com/UziTech/marked-katex-extension/blob/main/src/index.js export interface MarkedKatexOptions extends KatexOptions { @@ -65,7 +61,7 @@ namespace MarkedKatexExtension { const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/; - export function extension(katex: KatexLib, options: MarkedKatexOptions = {}): marked.MarkedExtension { + export function extension(katex: typeof import('katex').default, options: MarkedKatexOptions = {}): marked.MarkedExtension { return { extensions: [ inlineKatex(options, createRenderer(katex, options, false)), @@ -74,7 +70,7 @@ namespace MarkedKatexExtension { }; } - function createRenderer(katex: KatexLib, options: MarkedKatexOptions, newlineAfter: boolean): marked.RendererExtensionFunction { + function createRenderer(katex: typeof import('katex').default, options: MarkedKatexOptions, newlineAfter: boolean): marked.RendererExtensionFunction { return (token: marked.Tokens.Generic) => { return katex.renderToString(token.text, { ...options, From 008e8cf20f3c04a3e39538eaccd6b42dc14c4e4e Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 10 Jul 2025 14:27:29 -0700 Subject: [PATCH 304/306] Fix possible disposable leak Fixes #254829 --- .../browser/copyPasteController.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index f995539bb62..3c01e74c1a6 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -409,19 +409,14 @@ export class CopyPasteController extends Disposable implements IEditorContributi this._pasteProgressManager.showWhile(selections[0].getEndPosition(), localize('pasteIntoEditorProgress', "Running paste handlers. Click to cancel and do basic paste"), p, { cancel: async () => { - try { - p.cancel(); - - if (editorStateCts.token.isCancellationRequested) { - return; - } - - await this.applyDefaultPasteHandler(dataTransfer, metadata, editorStateCts.token, clipboardEvent); - } finally { - editorStateCts.dispose(); + p.cancel(); + if (editorStateCts.token.isCancellationRequested) { + return; } + + await this.applyDefaultPasteHandler(dataTransfer, metadata, editorStateCts.token, clipboardEvent); } - }).then(() => { + }).finally(() => { editorStateCts.dispose(); }); this._currentPasteOperation = p; From 2188c90df51a8557c2bf62a0eb97bfe52d6dac78 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 10 Jul 2025 23:43:10 +0200 Subject: [PATCH 305/306] Add sign in option for Apple (microsoft/vscode-internalbacklog#5578) (#255155) --- extensions/github-authentication/src/flows.ts | 6 +- package.json | 2 +- src/vs/base/common/product.ts | 13 +- .../chat/browser/actions/chatActions.ts | 4 +- .../contrib/chat/browser/chat.contribution.ts | 2 +- .../contrib/chat/browser/chatSetup.ts | 129 +++++++----------- .../contrib/chat/browser/media/apple-dark.svg | 3 + .../chat/browser/media/apple-light.svg | 3 + .../contrib/chat/browser/media/chatSetup.css | 14 +- .../chat/browser/media/google-mono-dark.svg | 3 - .../chat/browser/media/google-mono-light.svg | 3 - .../chat/common/chatEntitlementService.ts | 16 +-- .../common/gettingStartedContent.ts | 11 +- 13 files changed, 95 insertions(+), 114 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/media/apple-dark.svg create mode 100644 src/vs/workbench/contrib/chat/browser/media/apple-light.svg delete mode 100644 src/vs/workbench/contrib/chat/browser/media/google-mono-dark.svg delete mode 100644 src/vs/workbench/contrib/chat/browser/media/google-mono-light.svg diff --git a/extensions/github-authentication/src/flows.ts b/extensions/github-authentication/src/flows.ts index 0c470b1d65d..e9954dc2d02 100644 --- a/extensions/github-authentication/src/flows.ts +++ b/extensions/github-authentication/src/flows.ts @@ -571,14 +571,14 @@ export function getFlows(query: IFlowQuery) { */ export const enum GitHubSocialSignInProvider { Google = 'google', - // Apple = 'apple', + Apple = 'apple', } const GitHubSocialSignInProviderLabels = { [GitHubSocialSignInProvider.Google]: l10n.t('Google'), - // [GitHubSocialSignInProvider.Apple]: l10n.t('Apple'), + [GitHubSocialSignInProvider.Apple]: l10n.t('Apple'), }; export function isSocialSignInProvider(provider: unknown): provider is GitHubSocialSignInProvider { - return provider === GitHubSocialSignInProvider.Google; // || provider === GitHubSocialSignInProvider.Apple; + return provider === GitHubSocialSignInProvider.Google || provider === GitHubSocialSignInProvider.Apple; } diff --git a/package.json b/package.json index dd51bb0b769..b4b408cd82c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.103.0", - "distro": "7fd50e9bdc1a124eb32a184ee4bc0d437cdae9f0", + "distro": "e050762924418e1fb937b0aee594b586defeac82", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 4f18173228a..0743b45df6c 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -337,12 +337,13 @@ export interface IDefaultChatAgent { readonly upgradePlanUrl: string; readonly signUpUrl: string; - readonly providerId: string; - readonly providerName: string; - readonly enterpriseProviderId: string; - readonly enterpriseProviderName: string; - readonly alternativeProviderId: string; - readonly alternativeProviderName: string; + readonly provider: { + default: { id: string; name: string }; + enterprise: { id: string; name: string }; + google: { id: string; name: string }; + apple: { id: string; name: string }; + }; + readonly providerUriSetting: string; readonly providerScopes: string[][]; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index e2bd588e29c..e14d3084197 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -673,7 +673,7 @@ export function registerChatActions() { } }); - const nonEnterpriseCopilotUsers = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.notEquals(`config.${defaultChat.completionsAdvancedSetting}.authProvider`, defaultChat.enterpriseProviderId)); + const nonEnterpriseCopilotUsers = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.notEquals(`config.${defaultChat.completionsAdvancedSetting}.authProvider`, defaultChat.provider?.enterprise.id)); registerAction2(class extends Action2 { constructor() { super({ @@ -901,7 +901,7 @@ const defaultChat = { documentationUrl: product.defaultChatAgent?.documentationUrl ?? '', manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '', managePlanUrl: product.defaultChatAgent?.managePlanUrl ?? '', - enterpriseProviderId: product.defaultChatAgent?.enterpriseProviderId ?? '', + provider: product.defaultChatAgent?.provider ?? { enterprise: { id: '' } }, completionsAdvancedSetting: product.defaultChatAgent?.completionsAdvancedSetting ?? '', completionsMenuCommand: product.defaultChatAgent?.completionsMenuCommand ?? '', }; diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index fa11055a422..f3f4164c855 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -478,7 +478,7 @@ configurationRegistry.registerConfiguration({ }, 'chat.setup.signInDialogVariant': { // TODO@bpasero remove me eventually type: 'string', - enum: ['default', 'alternate-first', 'alternate-color', 'alternate-monochrome'], + enum: ['default', 'apple'], description: nls.localize('chat.signInDialogVariant', "Control variations of the sign-in dialog."), default: 'default', tags: ['onExp', 'experimental'] diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index d4751a21054..8a619025797 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -80,13 +80,7 @@ const defaultChat = { publicCodeMatchesUrl: product.defaultChatAgent?.publicCodeMatchesUrl ?? '', manageOveragesUrl: product.defaultChatAgent?.manageOverageUrl ?? '', upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '', - signUpUrl: product.defaultChatAgent?.signUpUrl ?? '', - providerId: product.defaultChatAgent?.providerId ?? '', - providerName: product.defaultChatAgent?.providerName ?? '', - enterpriseProviderId: product.defaultChatAgent?.enterpriseProviderId ?? '', - enterpriseProviderName: product.defaultChatAgent?.enterpriseProviderName ?? '', - alternativeProviderId: product.defaultChatAgent?.alternativeProviderId ?? '', - alternativeProviderName: product.defaultChatAgent?.alternativeProviderName ?? '', + provider: product.defaultChatAgent?.provider ?? { default: { id: '', name: '' }, enterprise: { id: '', name: '' }, apple: { id: '', name: '' }, google: { id: '', name: '' } }, providerUriSetting: product.defaultChatAgent?.providerUriSetting ?? '', providerScopes: product.defaultChatAgent?.providerScopes ?? [[]], manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '', @@ -134,7 +128,7 @@ class SetupAgent extends Disposable implements IChatAgentImplementation { break; } - return SetupAgent.doRegisterAgent(instantiationService, chatAgentService, id, `${defaultChat.providerName} Copilot`, true, description, location, mode, context, controller); + return SetupAgent.doRegisterAgent(instantiationService, chatAgentService, id, `${defaultChat.provider?.default.name} Copilot`, true, description, location, mode, context, controller); }); } @@ -307,9 +301,9 @@ class SetupAgent extends Disposable implements IChatAgentImplementation { if (ready === 'error' || ready === 'timedout') { let warningMessage: string; if (ready === 'timedout') { - warningMessage = localize('copilotTookLongWarning', "Copilot took too long to get ready. Please ensure you are signed in to {0} and that the extension `{1}` is installed and enabled.", defaultChat.providerName, defaultChat.chatExtensionId); + warningMessage = localize('copilotTookLongWarning', "Copilot took too long to get ready. Please ensure you are signed in to {0} and that the extension `{1}` is installed and enabled.", defaultChat.provider?.default.name, defaultChat.chatExtensionId); } else { - warningMessage = localize('copilotFailedWarning', "Copilot failed to get ready. Please ensure you are signed in to {0} and that the extension `{1}` is installed and enabled.", defaultChat.providerName, defaultChat.chatExtensionId); + warningMessage = localize('copilotFailedWarning', "Copilot failed to get ready. Please ensure you are signed in to {0} and that the extension `{1}` is installed and enabled.", defaultChat.provider?.default.name, defaultChat.chatExtensionId); } progress({ @@ -398,7 +392,7 @@ class SetupAgent extends Disposable implements IChatAgentImplementation { case ChatSetupStep.SigningIn: progress({ kind: 'progressMessage', - content: new MarkdownString(localize('setupChatSignIn2', "Signing in to {0}.", ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId ? defaultChat.enterpriseProviderName : defaultChat.providerName)), + content: new MarkdownString(localize('setupChatSignIn2', "Signing in to {0}.", ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.provider?.enterprise.id ? defaultChat.provider?.enterprise.name : defaultChat.provider?.default.name)), }); break; case ChatSetupStep.Installing: @@ -575,8 +569,8 @@ enum ChatSetupStrategy { DefaultSetup = 1, SetupWithoutEnterpriseProvider = 2, SetupWithEnterpriseProvider = 3, - SetupWithAccountCreate = 4, - SetupWithAlternateProvider = 5 + SetupWithGoogleProvider = 4, + SetupWithAppleProvider = 5 } type ChatSetupResultValue = boolean /* success */ | undefined /* canceled */; @@ -593,7 +587,7 @@ class ChatSetup { let instance = ChatSetup.instance; if (!instance) { instance = ChatSetup.instance = instantiationService.invokeFunction(accessor => { - return new ChatSetup(context, controller, instantiationService, accessor.get(ITelemetryService), accessor.get(IWorkbenchLayoutService), accessor.get(IKeybindingService), accessor.get(IChatEntitlementService) as ChatEntitlementService, accessor.get(ILogService), accessor.get(IConfigurationService), accessor.get(IViewsService), accessor.get(IOpenerService), accessor.get(IWorkspaceTrustRequestService)); + return new ChatSetup(context, controller, instantiationService, accessor.get(ITelemetryService), accessor.get(IWorkbenchLayoutService), accessor.get(IKeybindingService), accessor.get(IChatEntitlementService) as ChatEntitlementService, accessor.get(ILogService), accessor.get(IConfigurationService), accessor.get(IViewsService), accessor.get(IWorkspaceTrustRequestService)); }); } @@ -615,7 +609,6 @@ class ChatSetup { @ILogService private readonly logService: ILogService, @IConfigurationService private readonly configurationService: IConfigurationService, @IViewsService private readonly viewsService: IViewsService, - @IOpenerService private readonly openerService: IOpenerService, @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService ) { } @@ -660,7 +653,7 @@ class ChatSetup { setupStrategy = await this.showDialog(); } - if (setupStrategy === ChatSetupStrategy.DefaultSetup && ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId) { + if (setupStrategy === ChatSetupStrategy.DefaultSetup && ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.provider?.enterprise.id) { setupStrategy = ChatSetupStrategy.SetupWithEnterpriseProvider; // users with a configured provider go through provider setup } @@ -674,20 +667,20 @@ class ChatSetup { try { switch (setupStrategy) { case ChatSetupStrategy.SetupWithEnterpriseProvider: - success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: true, useAlternateProvider: false }); + success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: true, useSocialProvider: undefined }); break; case ChatSetupStrategy.SetupWithoutEnterpriseProvider: - success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: false, useAlternateProvider: false }); + success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: false, useSocialProvider: undefined }); break; - case ChatSetupStrategy.SetupWithAlternateProvider: - success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: false, useAlternateProvider: true }); + case ChatSetupStrategy.SetupWithAppleProvider: + success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: false, useSocialProvider: 'apple' }); + break; + case ChatSetupStrategy.SetupWithGoogleProvider: + success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: false, useSocialProvider: 'google' }); break; case ChatSetupStrategy.DefaultSetup: success = await this.controller.value.setup(); break; - case ChatSetupStrategy.SetupWithAccountCreate: - this.openerService.open(URI.parse(defaultChat.signUpUrl)); - return this.doRun(options); // open dialog again case ChatSetupStrategy.Canceled: this.context.update({ later: true }); this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedMaybeLater', installDuration: 0, signUpErrorCode: undefined, provider: undefined }); @@ -704,7 +697,7 @@ class ChatSetup { private async showDialog(): Promise { const disposables = new DisposableStore(); - const dialogVariant = this.configurationService.getValue<'default' | 'alternate-first' | 'alternate-color' | 'alternate-monochrome' | unknown>('chat.setup.signInDialogVariant'); + const dialogVariant = this.configurationService.getValue<'default' | 'apple' | unknown>('chat.setup.signInDialogVariant'); const buttons = this.getButtons(dialogVariant); const dialog = disposables.add(new Dialog( @@ -730,59 +723,41 @@ class ChatSetup { return buttons[button]?.[1] ?? ChatSetupStrategy.Canceled; } - private getButtons(variant: 'default' | 'alternate-first' | 'alternate-color' | 'alternate-monochrome' | unknown): Array<[string, ChatSetupStrategy, { styleButton?: (button: IButton) => void } | undefined]> { - let buttons: Array<[string, ChatSetupStrategy, { styleButton?: (button: IButton) => void } | undefined]>; + private getButtons(variant: 'default' | 'apple' | unknown): Array<[string, ChatSetupStrategy, { styleButton?: (button: IButton) => void } | undefined]> { + type ContinueWithButton = [string, ChatSetupStrategy, { styleButton?: (button: IButton) => void } | undefined]; + const styleButton = (...classes: string[]) => ({ styleButton: (button: IButton) => button.element.classList.add(...classes) }); + let buttons: Array; if (this.context.state.entitlement === ChatEntitlement.Unknown) { - let alternateProvider: 'off' | 'monochrome' | 'colorful' | 'first' = 'off'; - if (defaultChat.alternativeProviderId) { - switch (variant) { - case 'alternate-first': - alternateProvider = 'first'; - break; - case 'alternate-color': - alternateProvider = 'colorful'; - break; - case 'alternate-monochrome': - alternateProvider = 'monochrome'; - break; - } - } + const defaultProviderButton: ContinueWithButton = [localize('continueWith', "Continue with {0}", defaultChat.provider?.default.name), ChatSetupStrategy.SetupWithoutEnterpriseProvider, styleButton('continue-button', 'default')]; + const defaultProviderLink: ContinueWithButton = [defaultProviderButton[0], defaultProviderButton[1], styleButton('link-button')]; - if (ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId) { + const enterpriseProviderButton: ContinueWithButton = [localize('continueWith', "Continue with {0}", defaultChat.provider?.enterprise.name), ChatSetupStrategy.SetupWithEnterpriseProvider, styleButton('continue-button', 'default')]; + const enterpriseProviderLink: ContinueWithButton = [enterpriseProviderButton[0], enterpriseProviderButton[1], styleButton('link-button')]; + + const googleProviderButton: ContinueWithButton = [localize('continueWith', "Continue with {0}", defaultChat.provider?.google.name), ChatSetupStrategy.SetupWithGoogleProvider, styleButton('continue-button', 'google')]; + const appleProviderButton: ContinueWithButton = [localize('continueWith', "Continue with {0}", defaultChat.provider?.apple.name), ChatSetupStrategy.SetupWithAppleProvider, styleButton('continue-button', 'apple')]; + + if (ChatEntitlementRequests.providerId(this.configurationService) !== defaultChat.provider?.enterprise.id) { buttons = coalesce([ - [localize('continueWith', "Continue with {0}", defaultChat.enterpriseProviderName), ChatSetupStrategy.SetupWithEnterpriseProvider, { - styleButton: button => button.element.classList.add('continue-button', 'default') - }], - alternateProvider !== 'off' ? [localize('continueWith', "Continue with {0}", defaultChat.alternativeProviderName), ChatSetupStrategy.SetupWithAlternateProvider, { - styleButton: button => button.element.classList.add('continue-button', 'alternate', alternateProvider) - }] : undefined, - [localize('signInWithProvider', "Sign in with a {0} account", defaultChat.providerName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, { - styleButton: button => button.element.classList.add('link-button') - }] + defaultProviderButton, + googleProviderButton, + variant === 'apple' ? appleProviderButton : undefined, + enterpriseProviderLink ]); } else { buttons = coalesce([ - [localize('continueWith', "Continue with {0}", defaultChat.providerName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, { - styleButton: button => button.element.classList.add('continue-button', 'default') - }], - alternateProvider !== 'off' ? [localize('continueWith', "Continue with {0}", defaultChat.alternativeProviderName), ChatSetupStrategy.SetupWithAlternateProvider, { - styleButton: button => button.element.classList.add('continue-button', 'alternate', alternateProvider) - }] : undefined, - [localize('signInWithProvider', "Sign in with a {0} account", defaultChat.enterpriseProviderName), ChatSetupStrategy.SetupWithEnterpriseProvider, { - styleButton: button => button.element.classList.add('link-button') - }] + enterpriseProviderButton, + googleProviderButton, + variant === 'apple' ? appleProviderButton : undefined, + defaultProviderLink ]); } - - if (alternateProvider === 'first') { - [buttons[0], buttons[1]] = [buttons[1], buttons[0]]; - } } else { buttons = [[localize('setupCopilotButton', "Set up Copilot"), ChatSetupStrategy.DefaultSetup, undefined]]; } - buttons.push([localize('skipForNow', "Skip for now"), ChatSetupStrategy.Canceled, { styleButton: button => button.element.classList.add('link-button', 'skip-button') }]); + buttons.push([localize('skipForNow', "Skip for now"), ChatSetupStrategy.Canceled, styleButton('link-button', 'skip-button')]); return buttons; } @@ -801,7 +776,7 @@ class ChatSetup { const markdown = this.instantiationService.createInstance(MarkdownRenderer, {}); // SKU Settings - const settings = localize({ key: 'settings', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "{0} Copilot Free, Pro and Pro+ may show [public code]({1}) suggestions and we may use your data for product improvement. You can change these [settings]({2}) at any time.", defaultChat.providerName, defaultChat.publicCodeMatchesUrl, defaultChat.manageSettingsUrl); + const settings = localize({ key: 'settings', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "{0} Copilot Free, Pro and Pro+ may show [public code]({1}) suggestions and we may use your data for product improvement. You can change these [settings]({2}) at any time.", defaultChat.provider?.default.name, defaultChat.publicCodeMatchesUrl, defaultChat.manageSettingsUrl); element.appendChild($('p', undefined, disposables.add(markdown.render(new MarkdownString(settings, { isTrusted: true }))).element)); return element; @@ -1221,7 +1196,7 @@ class ChatSetupController extends Disposable { this._onDidChange.fire(); } - async setup(options?: { forceSignIn?: boolean; useAlternateProvider?: boolean; useEnterpriseProvider?: boolean }): Promise { + async setup(options?: { forceSignIn?: boolean; useSocialProvider?: string; useEnterpriseProvider?: boolean }): Promise { const watch = new StopWatch(false); const title = localize('setupChatProgress', "Getting Copilot ready..."); const badge = this.activityService.showViewContainerActivity(CHAT_SIDEBAR_PANEL_ID, { @@ -1239,7 +1214,7 @@ class ChatSetupController extends Disposable { } } - private async doSetup(options: { forceSignIn?: boolean; useAlternateProvider?: boolean; useEnterpriseProvider?: boolean }, watch: StopWatch): Promise { + private async doSetup(options: { forceSignIn?: boolean; useSocialProvider?: string; useEnterpriseProvider?: boolean }, watch: StopWatch): Promise { this.context.suspend(); // reduces flicker let success: ChatSetupResultValue = false; @@ -1251,11 +1226,11 @@ class ChatSetupController extends Disposable { // Entitlement Unknown or `forceSignIn`: we need to sign-in user if (this.context.state.entitlement === ChatEntitlement.Unknown || options.forceSignIn) { this.setStep(ChatSetupStep.SigningIn); - const result = await this.signIn({ useAlternateProvider: options.useAlternateProvider }); + const result = await this.signIn(options); if (!result.session) { this.doInstall(); // still install the extension in the background to remind the user to sign-in eventually - const provider = options.useAlternateProvider ? defaultChat.alternativeProviderId : options.useEnterpriseProvider ? defaultChat.enterpriseProviderId : defaultChat.providerId; + const provider = options.useSocialProvider ?? options.useEnterpriseProvider ? defaultChat.provider?.enterprise.id : defaultChat.provider?.default.id; this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider }); return undefined; // treat as cancelled because signing in already triggers an error dialog } @@ -1275,7 +1250,7 @@ class ChatSetupController extends Disposable { return success; } - private async signIn(options: { useAlternateProvider?: boolean }): Promise<{ session: AuthenticationSession | undefined; entitlement: ChatEntitlement | undefined }> { + private async signIn(options: { useSocialProvider?: string }): Promise<{ session: AuthenticationSession | undefined; entitlement: ChatEntitlement | undefined }> { let session: AuthenticationSession | undefined; let entitlements; try { @@ -1287,7 +1262,7 @@ class ChatSetupController extends Disposable { if (!session && !this.lifecycleService.willShutdown) { const { confirmed } = await this.dialogService.confirm({ type: Severity.Error, - message: localize('unknownSignInError', "Failed to sign in to {0}. Would you like to try again?", options?.useAlternateProvider ? defaultChat.alternativeProviderName : ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId ? defaultChat.enterpriseProviderName : defaultChat.providerName), + message: localize('unknownSignInError', "Failed to sign in to {0}. Would you like to try again?", ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.provider?.enterprise.id ? defaultChat.provider?.enterprise.name : defaultChat.provider?.default.name), detail: localize('unknownSignInErrorDetail', "You must be signed in to use Copilot."), primaryButton: localize('retry', "Retry") }); @@ -1300,11 +1275,11 @@ class ChatSetupController extends Disposable { return { session, entitlement: entitlements?.entitlement }; } - private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, providerId: string, watch: StopWatch, options: { useAlternateProvider?: boolean; useEnterpriseProvider?: boolean }): Promise { + private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, providerId: string, watch: StopWatch, options: { useSocialProvider?: string; useEnterpriseProvider?: boolean }): Promise { const wasRunning = this.context.state.installed && !this.context.state.disabled; let signUpResult: boolean | { errorCode: number } | undefined = undefined; - const provider = options.useAlternateProvider ? defaultChat.alternativeProviderId : options.useEnterpriseProvider ? defaultChat.enterpriseProviderId : defaultChat.providerId; + const provider = options.useSocialProvider ?? options.useEnterpriseProvider ? defaultChat.provider?.enterprise.id : defaultChat.provider?.default.id; try { @@ -1392,7 +1367,7 @@ class ChatSetupController extends Disposable { }, ChatViewId); } - async setupWithProvider(options: { useEnterpriseProvider: boolean; useAlternateProvider: boolean }): Promise { + async setupWithProvider(options: { useEnterpriseProvider: boolean; useSocialProvider: string | undefined }): Promise { const registry = Registry.as(ConfigurationExtensions.Configuration); registry.registerConfiguration({ 'id': 'copilot.setup', @@ -1427,7 +1402,7 @@ class ChatSetupController extends Disposable { if (options.useEnterpriseProvider) { await this.configurationService.updateValue(`${defaultChat.completionsAdvancedSetting}`, { ...existingAdvancedSetting, - 'authProvider': defaultChat.enterpriseProviderId + 'authProvider': defaultChat.provider?.enterprise.id }, ConfigurationTarget.USER); } else { await this.configurationService.updateValue(`${defaultChat.completionsAdvancedSetting}`, Object.keys(existingAdvancedSetting).length > 0 ? { @@ -1450,7 +1425,7 @@ class ChatSetupController extends Disposable { let isSingleWord = false; const result = await this.quickInputService.input({ - prompt: localize('enterpriseInstance', "What is your {0} instance?", defaultChat.enterpriseProviderName), + prompt: localize('enterpriseInstance', "What is your {0} instance?", defaultChat.provider?.enterprise.name), placeHolder: localize('enterpriseInstancePlaceholder', 'i.e. "octocat" or "https://octocat.ghe.com"...'), ignoreFocusLost: true, value: uri, @@ -1468,7 +1443,7 @@ class ChatSetupController extends Disposable { }; } if (!fullUriRegEx.test(value)) { return { - content: localize('invalidEnterpriseInstance', 'You must enter a valid {0} instance (i.e. "octocat" or "https://octocat.ghe.com")', defaultChat.enterpriseProviderName), + content: localize('invalidEnterpriseInstance', 'You must enter a valid {0} instance (i.e. "octocat" or "https://octocat.ghe.com")', defaultChat.provider?.enterprise.name), severity: Severity.Error }; } diff --git a/src/vs/workbench/contrib/chat/browser/media/apple-dark.svg b/src/vs/workbench/contrib/chat/browser/media/apple-dark.svg new file mode 100644 index 00000000000..be725db24f3 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/media/apple-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/workbench/contrib/chat/browser/media/apple-light.svg b/src/vs/workbench/contrib/chat/browser/media/apple-light.svg new file mode 100644 index 00000000000..f51d23005c3 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/media/apple-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/workbench/contrib/chat/browser/media/chatSetup.css b/src/vs/workbench/contrib/chat/browser/media/chatSetup.css index 055e3a8f9f2..7bd6ce82a2f 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatSetup.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatSetup.css @@ -23,7 +23,7 @@ background-image: url('./github.svg'); } - .dialog-buttons-row > .dialog-buttons > .monaco-button.continue-button.alternate::before { + .dialog-buttons-row > .dialog-buttons > .monaco-button.continue-button.google::before { background-image: url('./google.svg'); } @@ -51,12 +51,12 @@ } } -.monaco-workbench.hc-black .chat-setup-dialog .dialog-buttons-row > .dialog-buttons > .monaco-button.continue-button.alternate.monochrome::before, -.monaco-workbench.vs-dark .chat-setup-dialog .dialog-buttons-row > .dialog-buttons > .monaco-button.continue-button.alternate.monochrome::before { - background-image: url('./google-mono-dark.svg'); +.monaco-workbench.hc-black .chat-setup-dialog .dialog-buttons-row > .dialog-buttons > .monaco-button.continue-button.apple::before, +.monaco-workbench.vs-dark .chat-setup-dialog .dialog-buttons-row > .dialog-buttons > .monaco-button.continue-button.apple::before { + background-image: url('./apple-dark.svg'); } -.monaco-workbench.hc-light .chat-setup-dialog .dialog-buttons-row > .dialog-buttons > .monaco-button.continue-button.alternate.monochrome::before, -.monaco-workbench.vs .chat-setup-dialog .dialog-buttons-row > .dialog-buttons > .monaco-button.continue-button.alternate.monochrome::before { - background-image: url('./google-mono-light.svg'); +.monaco-workbench.hc-light .chat-setup-dialog .dialog-buttons-row > .dialog-buttons > .monaco-button.continue-button.apple::before, +.monaco-workbench.vs .chat-setup-dialog .dialog-buttons-row > .dialog-buttons > .monaco-button.continue-button.apple::before { + background-image: url('./apple-light.svg'); } diff --git a/src/vs/workbench/contrib/chat/browser/media/google-mono-dark.svg b/src/vs/workbench/contrib/chat/browser/media/google-mono-dark.svg deleted file mode 100644 index 7c91281c36a..00000000000 --- a/src/vs/workbench/contrib/chat/browser/media/google-mono-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/chat/browser/media/google-mono-light.svg b/src/vs/workbench/contrib/chat/browser/media/google-mono-light.svg deleted file mode 100644 index 264c6ce8c5c..00000000000 --- a/src/vs/workbench/contrib/chat/browser/media/google-mono-light.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts b/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts index f8e2b2f4e17..2850f656171 100644 --- a/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts @@ -134,9 +134,7 @@ const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', chatExtensionId: product.defaultChatAgent?.chatExtensionId ?? '', upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '', - providerId: product.defaultChatAgent?.providerId ?? '', - enterpriseProviderId: product.defaultChatAgent?.enterpriseProviderId ?? '', - alternativeProviderId: product.defaultChatAgent?.alternativeProviderId ?? '', + provider: product.defaultChatAgent?.provider ?? { default: { id: '' }, enterprise: { id: '' } }, providerScopes: product.defaultChatAgent?.providerScopes ?? [[]], entitlementUrl: product.defaultChatAgent?.entitlementUrl ?? '', entitlementSignupLimitedUrl: product.defaultChatAgent?.entitlementSignupLimitedUrl ?? '', @@ -421,11 +419,11 @@ interface IQuotas { export class ChatEntitlementRequests extends Disposable { static providerId(configurationService: IConfigurationService): string { - if (configurationService.getValue(`${defaultChat.completionsAdvancedSetting}.authProvider`) === defaultChat.enterpriseProviderId) { - return defaultChat.enterpriseProviderId; + if (configurationService.getValue(`${defaultChat.completionsAdvancedSetting}.authProvider`) === defaultChat.provider?.enterprise.id) { + return defaultChat.provider!.enterprise.id; } - return defaultChat.providerId; + return defaultChat.provider!.default.id; } private state: IEntitlements; @@ -568,7 +566,7 @@ export class ChatEntitlementRequests extends Disposable { } private async doResolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise { - if (ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId) { + if (ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.provider?.enterprise.id) { this.logService.trace('[chat entitlement]: enterprise provider, assuming Enterprise plan'); return { entitlement: ChatEntitlement.Enterprise }; } @@ -857,9 +855,9 @@ export class ChatEntitlementRequests extends Disposable { } } - async signIn(options?: { useAlternateProvider?: boolean }) { + async signIn(options?: { useSocialProvider?: string }) { const providerId = ChatEntitlementRequests.providerId(this.configurationService); - const session = await this.authenticationService.createSession(providerId, defaultChat.providerScopes[0], options?.useAlternateProvider ? { provider: defaultChat.alternativeProviderId } : undefined); + const session = await this.authenticationService.createSession(providerId, defaultChat.providerScopes[0], options?.useSocialProvider ? { provider: options.useSocialProvider } : undefined); this.authenticationExtensionsService.updateAccountPreference(defaultChat.extensionId, providerId, session.account); this.authenticationExtensionsService.updateAccountPreference(defaultChat.chatExtensionId, providerId, session.account); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index f2172d92134..a7a2eb28265 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts @@ -19,7 +19,14 @@ interface IGettingStartedContentProvider { (): string; } -export const copilotSettingsMessage = localize({ key: 'settings', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "{0} Copilot Free, Pro and Pro+ may show [public code]({1}) suggestions and we may use your data for product improvement. You can change these [settings]({2}) at any time.", product.defaultChatAgent?.providerName, product.defaultChatAgent?.publicCodeMatchesUrl, product.defaultChatAgent?.manageSettingsUrl); +const defaultChat = { + documentationUrl: product.defaultChatAgent?.documentationUrl ?? '', + manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '', + provider: product.defaultChatAgent?.provider ?? { default: { name: '' } }, + publicCodeMatchesUrl: product.defaultChatAgent?.publicCodeMatchesUrl ?? '', +}; + +export const copilotSettingsMessage = localize({ key: 'settings', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "{0} Copilot Free, Pro and Pro+ may show [public code]({1}) suggestions and we may use your data for product improvement. You can change these [settings]({2}) at any time.", defaultChat.provider.default.name, defaultChat.publicCodeMatchesUrl, defaultChat.manageSettingsUrl); class GettingStartedContentProviderRegistry { @@ -217,7 +224,7 @@ export const startEntries: GettingStartedStartEntryContent = [ const Button = (title: string, href: string) => `[${title}](${href})`; const CopilotStepTitle = localize('gettingStarted.copilotSetup.title', "Use AI features with Copilot for free"); -const CopilotDescription = localize({ key: 'gettingStarted.copilotSetup.description', comment: ['{Locked="["}', '{Locked="]({0})"}'] }, "You can use [Copilot]({0}) to generate code across multiple files, fix errors, ask questions about your code and much more using natural language.", product.defaultChatAgent?.documentationUrl ?? ''); +const CopilotDescription = localize({ key: 'gettingStarted.copilotSetup.description', comment: ['{Locked="["}', '{Locked="]({0})"}'] }, "You can use [Copilot]({0}) to generate code across multiple files, fix errors, ask questions about your code and much more using natural language.", defaultChat.documentationUrl ?? ''); const CopilotSignedOutButton = Button(localize('setupCopilotButton.signIn', "Set up Copilot"), `command:workbench.action.chat.triggerSetup`); const CopilotSignedInButton = Button(localize('setupCopilotButton.setup', "Set up Copilot"), `command:workbench.action.chat.triggerSetup`); const CopilotCompleteButton = Button(localize('setupCopilotButton.chatWithCopilot', "Chat with Copilot"), 'command:workbench.action.chat.open'); From 379182c2c765086f6cfc937ab976f620a60773bb Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 10 Jul 2025 18:04:20 -0400 Subject: [PATCH 306/306] change anchor for suggest widget if it overflows the container's size (#255194) fix #244535 --- .../suggest/browser/simpleSuggestWidget.ts | 143 ++++++++++-------- 1 file changed, 76 insertions(+), 67 deletions(-) diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts index ab471ff40a2..fc285392044 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts @@ -719,76 +719,85 @@ export class SimpleSuggestWidget, TI this._status.element.style.height = `${info.itemHeight}px`; } - // if (this._state === State.Empty || this._state === State.Loading) { - // // showing a message only - // height = info.itemHeight + info.borderHeight; - // width = info.defaultSize.width / 2; - // this.element.enableSashes(false, false, false, false); - // this.element.minSize = this.element.maxSize = new dom.Dimension(width, height); - // this._contentWidget.setPreference(ContentWidgetPositionPreference.BELOW); - - // } else { - // showing items - - // width math - const maxWidth = bodyBox.width - info.borderHeight - 2 * info.horizontalPadding; - if (width > maxWidth) { - width = maxWidth; - } - const preferredWidth = this._completionModel ? this._completionModel.stats.pLabelLen * info.typicalHalfwidthCharacterWidth : width; - - // height math - const fullHeight = info.statusBarHeight + this._list.contentHeight + this._messageElement.clientHeight + info.borderHeight; - const minHeight = info.itemHeight + info.statusBarHeight; - // const editorBox = dom.getDomNodePagePosition(this.editor.getDomNode()); - // const cursorBox = this.editor.getScrolledVisiblePosition(this.editor.getPosition()); - const editorBox = dom.getDomNodePagePosition(this._container); - const cursorBox = this._cursorPosition; //this.editor.getScrolledVisiblePosition(this.editor.getPosition()); - const cursorBottom = editorBox.top + cursorBox.top + cursorBox.height; - const maxHeightBelow = Math.min(bodyBox.height - cursorBottom - info.verticalPadding, fullHeight); - const availableSpaceAbove = editorBox.top + cursorBox.top - info.verticalPadding; - const maxHeightAbove = Math.min(availableSpaceAbove, fullHeight); - let maxHeight = Math.min(Math.max(maxHeightAbove, maxHeightBelow) + info.borderHeight, fullHeight); - - if (height === this._cappedHeight?.capped) { - // Restore the old (wanted) height when the current - // height is capped to fit - height = this._cappedHeight.wanted; - } - - if (height < minHeight) { - height = minHeight; - } - if (height > maxHeight) { - height = maxHeight; - } - - const forceRenderingAboveRequiredSpace = 150; - if (height > maxHeightBelow || (this._forceRenderingAbove && availableSpaceAbove > forceRenderingAboveRequiredSpace)) { - this._preference = WidgetPositionPreference.Above; - this.element.enableSashes(true, true, false, false); - maxHeight = maxHeightAbove; - } else { + if (this._state === State.Empty || this._state === State.Loading) { + // showing a message only + height = info.itemHeight + info.borderHeight; + width = info.defaultSize.width / 2; + this.element.enableSashes(false, false, false, false); + this.element.minSize = this.element.maxSize = new dom.Dimension(width, height); this._preference = WidgetPositionPreference.Below; - this.element.enableSashes(false, true, true, false); - maxHeight = maxHeightBelow; - } - this.element.preferredSize = new dom.Dimension(preferredWidth, info.defaultSize.height); - this.element.maxSize = new dom.Dimension(maxWidth, maxHeight); - this.element.minSize = new dom.Dimension(220, minHeight); - // Know when the height was capped to fit and remember - // the wanted height for later. This is required when going - // left to widen suggestions. - this._cappedHeight = height === fullHeight - ? { wanted: this._cappedHeight?.wanted ?? size.height, capped: height } - : undefined; - // } - this.element.domNode.style.left = `${this._cursorPosition.left}px`; - if (this._preference === WidgetPositionPreference.Above) { - this.element.domNode.style.top = `${this._cursorPosition.top - height - info.borderHeight}px`; } else { - this.element.domNode.style.top = `${this._cursorPosition.top + this._cursorPosition.height}px`; + // showing items + + // width math + const maxWidth = bodyBox.width - info.borderHeight - 2 * info.horizontalPadding; + if (width > maxWidth) { + width = maxWidth; + } + const preferredWidth = this._completionModel ? this._completionModel.stats.pLabelLen * info.typicalHalfwidthCharacterWidth : width; + + // height math + const fullHeight = info.statusBarHeight + this._list.contentHeight + this._messageElement.clientHeight + info.borderHeight; + const minHeight = info.itemHeight + info.statusBarHeight; + // const editorBox = dom.getDomNodePagePosition(this.editor.getDomNode()); + // const cursorBox = this.editor.getScrolledVisiblePosition(this.editor.getPosition()); + const editorBox = dom.getDomNodePagePosition(this._container); + const cursorBox = this._cursorPosition; //this.editor.getScrolledVisiblePosition(this.editor.getPosition()); + const cursorBottom = editorBox.top + cursorBox.top + cursorBox.height; + const maxHeightBelow = Math.min(bodyBox.height - cursorBottom - info.verticalPadding, fullHeight); + const availableSpaceAbove = editorBox.top + cursorBox.top - info.verticalPadding; + const maxHeightAbove = Math.min(availableSpaceAbove, fullHeight); + let maxHeight = Math.min(Math.max(maxHeightAbove, maxHeightBelow) + info.borderHeight, fullHeight); + + if (height === this._cappedHeight?.capped) { + // Restore the old (wanted) height when the current + // height is capped to fit + height = this._cappedHeight.wanted; + } + + if (height < minHeight) { + height = minHeight; + } + if (height > maxHeight) { + height = maxHeight; + } + + const forceRenderingAboveRequiredSpace = 150; + if (height > maxHeightBelow || (this._forceRenderingAbove && availableSpaceAbove > forceRenderingAboveRequiredSpace)) { + this._preference = WidgetPositionPreference.Above; + this.element.enableSashes(true, true, false, false); + maxHeight = maxHeightAbove; + } else { + this._preference = WidgetPositionPreference.Below; + this.element.enableSashes(false, true, true, false); + maxHeight = maxHeightBelow; + } + this.element.preferredSize = new dom.Dimension(preferredWidth, info.defaultSize.height); + this.element.maxSize = new dom.Dimension(maxWidth, maxHeight); + this.element.minSize = new dom.Dimension(220, minHeight); + + // Know when the height was capped to fit and remember + // the wanted height for later. This is required when going + // left to widen suggestions. + this._cappedHeight = height === fullHeight + ? { wanted: this._cappedHeight?.wanted ?? size.height, capped: height } + : undefined; + // } + this.element.domNode.style.left = `${this._cursorPosition.left}px`; + + // Move anchor if widget will overflow the edge of the container + const containerWidth = this._container.clientWidth; + let anchorLeft = this._cursorPosition.left; + if (width > containerWidth) { + anchorLeft = Math.max(0, this._cursorPosition.left - width + containerWidth); + this.element.domNode.style.left = `${anchorLeft}px`; + } + if (this._preference === WidgetPositionPreference.Above) { + this.element.domNode.style.top = `${this._cursorPosition.top - height - info.borderHeight}px`; + } else { + this.element.domNode.style.top = `${this._cursorPosition.top + this._cursorPosition.height}px`; + } } this._resize(width, height); }