From be3c70f6d27b2ed5cd4d1d3059191127ec07eb2d Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 17 Feb 2026 20:07:31 -0800 Subject: [PATCH 01/13] Fix some jumping on scrolling up - Chat session loads, katex has not yet been loaded - ListView really wants to render from the top down so it checks the heights of some elements at the top - Katex not loaded, so we don't render markdown at all - Measured element height is very wrong - But we initialize scroll all the way down so those elements don't get a chance to fix their height - When scrolling up, we render an element and it is resized massively from the previous estimate, content shifts down --- .../chatMarkdownContentPart.ts | 44 ++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts index d4b4cec56b25f..d97b47e8c2ea2 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts @@ -120,11 +120,6 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP const element = context.element; const inUndoStop = (findLast(context.content, e => e.kind === 'undoStop', context.contentIndex) as IChatUndoStop | undefined)?.id; - // We release editors in order so that it's more likely that the same editor will - // be assigned if this element is re-rendered right away, like it often is during - // progressive rendering - const orderedDisposablesList: IDisposable[] = []; - // Need to track the index of the codeblock within the response so it can have a unique ID, // and within this part to find it within the codeblocks array let globalCodeBlockIndexStart = codeBlockStartIndex; @@ -141,11 +136,28 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP const enableMath = configurationService.getValue(ChatConfiguration.EnableMath); + const renderStore = this._register(new MutableDisposable()); + const doRenderMarkdown = () => { if (this._store.isDisposed) { return; } + // Dispose previous render and reset state for re-render + const store = new DisposableStore(); + renderStore.value = store; + dom.clearNode(this.domNode); + this.allRefs.length = 0; + this._codeblocks.length = 0; + this.mathLayoutParticipants.clear(); + globalCodeBlockIndexStart = codeBlockStartIndex; + thisPartCodeBlockIndexStart = 0; + + // We release editors in order so that it's more likely that the same editor will + // be assigned if this element is re-rendered right away, like it often is during + // progressive rendering + const orderedDisposablesList: IDisposable[] = []; + // TODO: Move katex support into chatMarkdownRenderer const markedExtensions = enableMath ? coalesce([MarkedKatexSupport.getExtension(dom.getWindow(context.container), { @@ -160,7 +172,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP breaks: true, }; - const result = this._register(renderer.render(markdown.content, { + const result = store.add(renderer.render(markdown.content, { sanitizerConfig: MarkedKatexSupport.getSanitizerOptions({ allowedTags: allowedChatMarkdownHtmlTags, allowedAttributes: allowedMarkdownHtmlAttributes, @@ -201,7 +213,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP } } if (languageId === 'vscode-extensions') { - const chatExtensions = this._register(instantiationService.createInstance(ChatExtensionsContentPart, { kind: 'extensions', extensions: text.split(',') })); + const chatExtensions = store.add(instantiationService.createInstance(ChatExtensionsContentPart, { kind: 'extensions', extensions: text.split(',') })); return chatExtensions.domNode; } const globalIndex = globalCodeBlockIndexStart++; @@ -330,12 +342,12 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP } const markdownDecorationsRenderer = instantiationService.createInstance(ChatMarkdownDecorationsRenderer); - this._register(markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(markdown, result.element)); + store.add(markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(markdown, result.element)); const layoutParticipants = new Lazy(() => { const observer = new ResizeObserver(() => this.mathLayoutParticipants.forEach(layout => layout())); observer.observe(this.domNode); - this._register(toDisposable(() => observer.disconnect())); + store.add(toDisposable(() => observer.disconnect())); return this.mathLayoutParticipants; }); @@ -357,19 +369,21 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP scrollable.scanDomNode(); } - orderedDisposablesList.reverse().forEach(d => this._register(d)); + orderedDisposablesList.reverse().forEach(d => store.add(d)); }; + // Always render immediately + doRenderMarkdown(); + if (enableMath && !MarkedKatexSupport.getExtension(dom.getWindow(context.container))) { - // Need to load async + // KaTeX not yet loaded - load it and re-render when ready MarkedKatexSupport.loadExtension(dom.getWindow(context.container)) + .then(() => { + doRenderMarkdown(); + }) .catch(e => { console.error('Failed to load MarkedKatexSupport extension:', e); - }).finally(() => { - doRenderMarkdown(); }); - } else { - doRenderMarkdown(); } } From bceeaccee1828b15e36e7d83a441127e472978ee Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 17 Feb 2026 20:15:58 -0800 Subject: [PATCH 02/13] fix --- .../widget/chatContentParts/chatMarkdownContentPart.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts index d97b47e8c2ea2..812f076e628f4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts @@ -228,8 +228,8 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP range = parsedBody.range && Range.lift(parsedBody.range); const modelRefPromise = this.textModelService.createModelReference(parsedBody.uri); textModel = modelRefPromise.then(ref => { - if (!this._store.isDisposed) { - this._register(ref); + if (!store.isDisposed) { + store.add(ref); } return ref.object.textEditorModel; }); From f32f3306fd3c291e444be7426229d485198d87d7 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:12:26 -0800 Subject: [PATCH 03/13] Enable kitty graphics protocol, bump xterm.js (#295701) * Bump xterm to enable kitty images * edit comment about gpu acceleration+kitty * Update to 162 * Update please * Mention enable transparency mode * Stop messing with git * Update to 165 --- package-lock.json | 96 +++++++++---------- package.json | 20 ++-- remote/package-lock.json | 96 +++++++++---------- remote/package.json | 20 ++-- remote/web/package-lock.json | 88 ++++++++--------- remote/web/package.json | 18 ++-- .../terminal/browser/xterm/xtermTerminal.ts | 2 + .../terminal/common/terminalConfiguration.ts | 2 +- 8 files changed, 172 insertions(+), 170 deletions(-) diff --git a/package-lock.json b/package-lock.json index 32f7e1bae0a78..e6adfda164d62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,16 +30,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.3.0-beta.152", - "@xterm/addon-image": "^0.10.0-beta.152", - "@xterm/addon-ligatures": "^0.11.0-beta.152", - "@xterm/addon-progress": "^0.3.0-beta.152", - "@xterm/addon-search": "^0.17.0-beta.152", - "@xterm/addon-serialize": "^0.15.0-beta.152", - "@xterm/addon-unicode11": "^0.10.0-beta.152", - "@xterm/addon-webgl": "^0.20.0-beta.151", - "@xterm/headless": "^6.1.0-beta.152", - "@xterm/xterm": "^6.1.0-beta.152", + "@xterm/addon-clipboard": "^0.3.0-beta.165", + "@xterm/addon-image": "^0.10.0-beta.165", + "@xterm/addon-ligatures": "^0.11.0-beta.165", + "@xterm/addon-progress": "^0.3.0-beta.165", + "@xterm/addon-search": "^0.17.0-beta.165", + "@xterm/addon-serialize": "^0.15.0-beta.165", + "@xterm/addon-unicode11": "^0.10.0-beta.165", + "@xterm/addon-webgl": "^0.20.0-beta.164", + "@xterm/headless": "^6.1.0-beta.165", + "@xterm/xterm": "^6.1.0-beta.165", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -3909,30 +3909,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.152.tgz", - "integrity": "sha512-D+wFHTTNj1qzlSL1h15tgFh6JgK/SSaotkohtaKykkKFmkdGrtJq8PpINaFipRDrZXX0d9eOD+wrMfz6IG+5Yw==", + "version": "0.3.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.165.tgz", + "integrity": "sha512-48GUTZg7sKB7tQvtC7FcH22GxxO0cIUVM4hw068Oi3cJnxDLLPQDicPv70fFG7zysGxxEKE7A39GMtHhwFI75Q==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.152.tgz", - "integrity": "sha512-pyQ/hQr3O0gY1La+6ZXdh0tI/+6MmNo2eFPNyWzB21J02xMu6nc30+B/H9VlPSR3AXHno5U67AWra5Y4FrE+5A==", + "version": "0.10.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.165.tgz", + "integrity": "sha512-DwYvKRgytc1OYoJVwA/doOTT92K8asgvnt3FzsHt5D+XgniwdvM5nwjxv95p6UXv0kEOxQWFy3sNJl/4g/5pew==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.152.tgz", - "integrity": "sha512-DglTaxmWHolTfryequU/7+Q4bjpDywt7UDsE3SdbC7O/9fa1qaOZMVlxKtRBtMBBzX5PXa+Ha4qAaMS2psr3UQ==", + "version": "0.11.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.165.tgz", + "integrity": "sha512-3nuPBH4ZrGYF+yj/tBB/+YaLRnn8qqbR9J9OcvM6aeDfboEeaFAYIpmdqjh+2Rl2JFTIgZoiS3dKLWaUUpk0Tw==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -3942,7 +3942,7 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-ligatures/node_modules/lru-cache": { @@ -3964,63 +3964,63 @@ "license": "ISC" }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.152.tgz", - "integrity": "sha512-H3qNwUaTNDRm51s8IzcYRinnQBSf7QDXWkcyAuDlprDJlR5BFhmGr9hpMV/KlCo2s6nhWrFjiwkd642DJ7McMg==", + "version": "0.3.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.165.tgz", + "integrity": "sha512-Jl+dhHkFBUafrXCECI/EepcGV1GYuU1X/0oXkPYu/VYfbmkjQSAidmfBAEyS+4+AUK5Lkf6yLdb1N13tZVexyg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.152.tgz", - "integrity": "sha512-0T/xDg0yh3PlS9HWOioIrNGP0OfUp4MtBJ7M2sfR+h23KKa4gl1ec7S1TsGU4gsvEMBKG1TB6jReX4vlKGYc4A==", + "version": "0.17.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.165.tgz", + "integrity": "sha512-3KjonTDJl/8M6jI5nTJITVT+Z528d/5CgqRmn6IV+sDgRfr3W84RZNDsaxXsLoc0GDsxQIB74/FmnNykUQ5Yew==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.152.tgz", - "integrity": "sha512-GnhwKg0dkpAI1gZmm9L69Xjseal5pKwXFaMUxzm+Viajcp/PdqK1pEBJX5RndToNF0Ti3xu4e6BFO7dqY/J9TA==", + "version": "0.15.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.165.tgz", + "integrity": "sha512-NjXE+of4NJagrtHlzePBuWQ8a9pBFhhmQuvOhPj9W3CSi+VanuMoM/oRaT1TbR3efHk2JdCsKVDJScEzY8kdjw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.152.tgz", - "integrity": "sha512-2HSpMjbckGAmU/56CTGuEbCZJZHxUlfNJ2uzR4akZyVVLEmavC4thHVSGT7Ei1zzpHZsAg0y4WMbcp4wzpPv3g==", + "version": "0.10.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.165.tgz", + "integrity": "sha512-a6myeixOXDYeuOj0GK+/LWXbXXWanFVMvQRUMgC7wmUNGgSZiyJ8NPWzhAq6Vib4jSQ02pd+ux4ZtWs5kyvFLg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.151", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.151.tgz", - "integrity": "sha512-3ogsmZKPKc8n9Mjik4jTmNYT2Nbboe/zqcjDNG7RONO3w/tUyoKQshYCMBxxGMNLDwvh3BQ/D9/6JvdNWA1ShA==", + "version": "0.20.0-beta.164", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.164.tgz", + "integrity": "sha512-wXTi281yTWY1iAmRh21N6AhcEMopTjIm4xsdDdNmS5LbhxNuhVKNNIGKm5Zhd/G9fpn/vrfC4yZ6KA0lI/ZAxg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/headless": { - "version": "6.1.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.152.tgz", - "integrity": "sha512-Hkt+KPuifM8kqDKtbHq1uIhqdZMQazKTl9zaqjcWY3Vogx7+JVr6F+eN89KHnrvhUUOmhAM0JQAIRv1O+upfUw==", + "version": "6.1.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.165.tgz", + "integrity": "sha512-GjAqUhEiY7gb12+yIItptgMKUwHMa7o39HpezD7sfNjYLjmvWQcB02jqUdVMsvjjAKTe2YJMXp3RkApeXdMRVg==", "license": "MIT", "workspaces": [ "addons/*" ] }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.152.tgz", - "integrity": "sha512-XHJ5ab19V6tmcHmBE7k9IYjXSwTxUd0c7oKLa5J+ZO0+aiXE8UKh9OEDw1oyl5ZQhw9gn71cGEo4TpB58KhfoQ==", + "version": "6.1.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.165.tgz", + "integrity": "sha512-OUszO4HSmGPEw3EhboyIcNLQKJQKCDsYHv9kYFcaiK3biuNjGP0VAPVUJOLbf3V9fa1GLUUq+t985blqvTApoA==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/package.json b/package.json index 20b139291a3a7..0180a71488240 100644 --- a/package.json +++ b/package.json @@ -95,16 +95,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.3.0-beta.152", - "@xterm/addon-image": "^0.10.0-beta.152", - "@xterm/addon-ligatures": "^0.11.0-beta.152", - "@xterm/addon-progress": "^0.3.0-beta.152", - "@xterm/addon-search": "^0.17.0-beta.152", - "@xterm/addon-serialize": "^0.15.0-beta.152", - "@xterm/addon-unicode11": "^0.10.0-beta.152", - "@xterm/addon-webgl": "^0.20.0-beta.151", - "@xterm/headless": "^6.1.0-beta.152", - "@xterm/xterm": "^6.1.0-beta.152", + "@xterm/addon-clipboard": "^0.3.0-beta.165", + "@xterm/addon-image": "^0.10.0-beta.165", + "@xterm/addon-ligatures": "^0.11.0-beta.165", + "@xterm/addon-progress": "^0.3.0-beta.165", + "@xterm/addon-search": "^0.17.0-beta.165", + "@xterm/addon-serialize": "^0.15.0-beta.165", + "@xterm/addon-unicode11": "^0.10.0-beta.165", + "@xterm/addon-webgl": "^0.20.0-beta.164", + "@xterm/headless": "^6.1.0-beta.165", + "@xterm/xterm": "^6.1.0-beta.165", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", diff --git a/remote/package-lock.json b/remote/package-lock.json index de6d200c162a1..4cec83e52e86a 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -22,16 +22,16 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.3.0-beta.152", - "@xterm/addon-image": "^0.10.0-beta.152", - "@xterm/addon-ligatures": "^0.11.0-beta.152", - "@xterm/addon-progress": "^0.3.0-beta.152", - "@xterm/addon-search": "^0.17.0-beta.152", - "@xterm/addon-serialize": "^0.15.0-beta.152", - "@xterm/addon-unicode11": "^0.10.0-beta.152", - "@xterm/addon-webgl": "^0.20.0-beta.151", - "@xterm/headless": "^6.1.0-beta.152", - "@xterm/xterm": "^6.1.0-beta.152", + "@xterm/addon-clipboard": "^0.3.0-beta.165", + "@xterm/addon-image": "^0.10.0-beta.165", + "@xterm/addon-ligatures": "^0.11.0-beta.165", + "@xterm/addon-progress": "^0.3.0-beta.165", + "@xterm/addon-search": "^0.17.0-beta.165", + "@xterm/addon-serialize": "^0.15.0-beta.165", + "@xterm/addon-unicode11": "^0.10.0-beta.165", + "@xterm/addon-webgl": "^0.20.0-beta.164", + "@xterm/headless": "^6.1.0-beta.165", + "@xterm/xterm": "^6.1.0-beta.165", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -577,30 +577,30 @@ "license": "MIT" }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.152.tgz", - "integrity": "sha512-D+wFHTTNj1qzlSL1h15tgFh6JgK/SSaotkohtaKykkKFmkdGrtJq8PpINaFipRDrZXX0d9eOD+wrMfz6IG+5Yw==", + "version": "0.3.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.165.tgz", + "integrity": "sha512-48GUTZg7sKB7tQvtC7FcH22GxxO0cIUVM4hw068Oi3cJnxDLLPQDicPv70fFG7zysGxxEKE7A39GMtHhwFI75Q==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.152.tgz", - "integrity": "sha512-pyQ/hQr3O0gY1La+6ZXdh0tI/+6MmNo2eFPNyWzB21J02xMu6nc30+B/H9VlPSR3AXHno5U67AWra5Y4FrE+5A==", + "version": "0.10.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.165.tgz", + "integrity": "sha512-DwYvKRgytc1OYoJVwA/doOTT92K8asgvnt3FzsHt5D+XgniwdvM5nwjxv95p6UXv0kEOxQWFy3sNJl/4g/5pew==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.152.tgz", - "integrity": "sha512-DglTaxmWHolTfryequU/7+Q4bjpDywt7UDsE3SdbC7O/9fa1qaOZMVlxKtRBtMBBzX5PXa+Ha4qAaMS2psr3UQ==", + "version": "0.11.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.165.tgz", + "integrity": "sha512-3nuPBH4ZrGYF+yj/tBB/+YaLRnn8qqbR9J9OcvM6aeDfboEeaFAYIpmdqjh+2Rl2JFTIgZoiS3dKLWaUUpk0Tw==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -610,67 +610,67 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.152.tgz", - "integrity": "sha512-H3qNwUaTNDRm51s8IzcYRinnQBSf7QDXWkcyAuDlprDJlR5BFhmGr9hpMV/KlCo2s6nhWrFjiwkd642DJ7McMg==", + "version": "0.3.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.165.tgz", + "integrity": "sha512-Jl+dhHkFBUafrXCECI/EepcGV1GYuU1X/0oXkPYu/VYfbmkjQSAidmfBAEyS+4+AUK5Lkf6yLdb1N13tZVexyg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.152.tgz", - "integrity": "sha512-0T/xDg0yh3PlS9HWOioIrNGP0OfUp4MtBJ7M2sfR+h23KKa4gl1ec7S1TsGU4gsvEMBKG1TB6jReX4vlKGYc4A==", + "version": "0.17.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.165.tgz", + "integrity": "sha512-3KjonTDJl/8M6jI5nTJITVT+Z528d/5CgqRmn6IV+sDgRfr3W84RZNDsaxXsLoc0GDsxQIB74/FmnNykUQ5Yew==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.152.tgz", - "integrity": "sha512-GnhwKg0dkpAI1gZmm9L69Xjseal5pKwXFaMUxzm+Viajcp/PdqK1pEBJX5RndToNF0Ti3xu4e6BFO7dqY/J9TA==", + "version": "0.15.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.165.tgz", + "integrity": "sha512-NjXE+of4NJagrtHlzePBuWQ8a9pBFhhmQuvOhPj9W3CSi+VanuMoM/oRaT1TbR3efHk2JdCsKVDJScEzY8kdjw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.152.tgz", - "integrity": "sha512-2HSpMjbckGAmU/56CTGuEbCZJZHxUlfNJ2uzR4akZyVVLEmavC4thHVSGT7Ei1zzpHZsAg0y4WMbcp4wzpPv3g==", + "version": "0.10.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.165.tgz", + "integrity": "sha512-a6myeixOXDYeuOj0GK+/LWXbXXWanFVMvQRUMgC7wmUNGgSZiyJ8NPWzhAq6Vib4jSQ02pd+ux4ZtWs5kyvFLg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.151", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.151.tgz", - "integrity": "sha512-3ogsmZKPKc8n9Mjik4jTmNYT2Nbboe/zqcjDNG7RONO3w/tUyoKQshYCMBxxGMNLDwvh3BQ/D9/6JvdNWA1ShA==", + "version": "0.20.0-beta.164", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.164.tgz", + "integrity": "sha512-wXTi281yTWY1iAmRh21N6AhcEMopTjIm4xsdDdNmS5LbhxNuhVKNNIGKm5Zhd/G9fpn/vrfC4yZ6KA0lI/ZAxg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/headless": { - "version": "6.1.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.152.tgz", - "integrity": "sha512-Hkt+KPuifM8kqDKtbHq1uIhqdZMQazKTl9zaqjcWY3Vogx7+JVr6F+eN89KHnrvhUUOmhAM0JQAIRv1O+upfUw==", + "version": "6.1.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.165.tgz", + "integrity": "sha512-GjAqUhEiY7gb12+yIItptgMKUwHMa7o39HpezD7sfNjYLjmvWQcB02jqUdVMsvjjAKTe2YJMXp3RkApeXdMRVg==", "license": "MIT", "workspaces": [ "addons/*" ] }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.152.tgz", - "integrity": "sha512-XHJ5ab19V6tmcHmBE7k9IYjXSwTxUd0c7oKLa5J+ZO0+aiXE8UKh9OEDw1oyl5ZQhw9gn71cGEo4TpB58KhfoQ==", + "version": "6.1.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.165.tgz", + "integrity": "sha512-OUszO4HSmGPEw3EhboyIcNLQKJQKCDsYHv9kYFcaiK3biuNjGP0VAPVUJOLbf3V9fa1GLUUq+t985blqvTApoA==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/remote/package.json b/remote/package.json index 603dc5559ba96..afaaabaf5c8c6 100644 --- a/remote/package.json +++ b/remote/package.json @@ -17,16 +17,16 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.3.0-beta.152", - "@xterm/addon-image": "^0.10.0-beta.152", - "@xterm/addon-ligatures": "^0.11.0-beta.152", - "@xterm/addon-progress": "^0.3.0-beta.152", - "@xterm/addon-search": "^0.17.0-beta.152", - "@xterm/addon-serialize": "^0.15.0-beta.152", - "@xterm/addon-unicode11": "^0.10.0-beta.152", - "@xterm/addon-webgl": "^0.20.0-beta.151", - "@xterm/headless": "^6.1.0-beta.152", - "@xterm/xterm": "^6.1.0-beta.152", + "@xterm/addon-clipboard": "^0.3.0-beta.165", + "@xterm/addon-image": "^0.10.0-beta.165", + "@xterm/addon-ligatures": "^0.11.0-beta.165", + "@xterm/addon-progress": "^0.3.0-beta.165", + "@xterm/addon-search": "^0.17.0-beta.165", + "@xterm/addon-serialize": "^0.15.0-beta.165", + "@xterm/addon-unicode11": "^0.10.0-beta.165", + "@xterm/addon-webgl": "^0.20.0-beta.164", + "@xterm/headless": "^6.1.0-beta.165", + "@xterm/xterm": "^6.1.0-beta.165", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index f443df36ae51d..eeed651e576c2 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -14,15 +14,15 @@ "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.3.0-beta.152", - "@xterm/addon-image": "^0.10.0-beta.152", - "@xterm/addon-ligatures": "^0.11.0-beta.152", - "@xterm/addon-progress": "^0.3.0-beta.152", - "@xterm/addon-search": "^0.17.0-beta.152", - "@xterm/addon-serialize": "^0.15.0-beta.152", - "@xterm/addon-unicode11": "^0.10.0-beta.152", - "@xterm/addon-webgl": "^0.20.0-beta.151", - "@xterm/xterm": "^6.1.0-beta.152", + "@xterm/addon-clipboard": "^0.3.0-beta.165", + "@xterm/addon-image": "^0.10.0-beta.165", + "@xterm/addon-ligatures": "^0.11.0-beta.165", + "@xterm/addon-progress": "^0.3.0-beta.165", + "@xterm/addon-search": "^0.17.0-beta.165", + "@xterm/addon-serialize": "^0.15.0-beta.165", + "@xterm/addon-unicode11": "^0.10.0-beta.165", + "@xterm/addon-webgl": "^0.20.0-beta.164", + "@xterm/xterm": "^6.1.0-beta.165", "jschardet": "3.1.4", "katex": "^0.16.22", "tas-client": "0.3.1", @@ -99,30 +99,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.152.tgz", - "integrity": "sha512-D+wFHTTNj1qzlSL1h15tgFh6JgK/SSaotkohtaKykkKFmkdGrtJq8PpINaFipRDrZXX0d9eOD+wrMfz6IG+5Yw==", + "version": "0.3.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.165.tgz", + "integrity": "sha512-48GUTZg7sKB7tQvtC7FcH22GxxO0cIUVM4hw068Oi3cJnxDLLPQDicPv70fFG7zysGxxEKE7A39GMtHhwFI75Q==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.152.tgz", - "integrity": "sha512-pyQ/hQr3O0gY1La+6ZXdh0tI/+6MmNo2eFPNyWzB21J02xMu6nc30+B/H9VlPSR3AXHno5U67AWra5Y4FrE+5A==", + "version": "0.10.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.165.tgz", + "integrity": "sha512-DwYvKRgytc1OYoJVwA/doOTT92K8asgvnt3FzsHt5D+XgniwdvM5nwjxv95p6UXv0kEOxQWFy3sNJl/4g/5pew==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.152.tgz", - "integrity": "sha512-DglTaxmWHolTfryequU/7+Q4bjpDywt7UDsE3SdbC7O/9fa1qaOZMVlxKtRBtMBBzX5PXa+Ha4qAaMS2psr3UQ==", + "version": "0.11.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.165.tgz", + "integrity": "sha512-3nuPBH4ZrGYF+yj/tBB/+YaLRnn8qqbR9J9OcvM6aeDfboEeaFAYIpmdqjh+2Rl2JFTIgZoiS3dKLWaUUpk0Tw==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -132,58 +132,58 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.152.tgz", - "integrity": "sha512-H3qNwUaTNDRm51s8IzcYRinnQBSf7QDXWkcyAuDlprDJlR5BFhmGr9hpMV/KlCo2s6nhWrFjiwkd642DJ7McMg==", + "version": "0.3.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.165.tgz", + "integrity": "sha512-Jl+dhHkFBUafrXCECI/EepcGV1GYuU1X/0oXkPYu/VYfbmkjQSAidmfBAEyS+4+AUK5Lkf6yLdb1N13tZVexyg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.152.tgz", - "integrity": "sha512-0T/xDg0yh3PlS9HWOioIrNGP0OfUp4MtBJ7M2sfR+h23KKa4gl1ec7S1TsGU4gsvEMBKG1TB6jReX4vlKGYc4A==", + "version": "0.17.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.165.tgz", + "integrity": "sha512-3KjonTDJl/8M6jI5nTJITVT+Z528d/5CgqRmn6IV+sDgRfr3W84RZNDsaxXsLoc0GDsxQIB74/FmnNykUQ5Yew==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.152.tgz", - "integrity": "sha512-GnhwKg0dkpAI1gZmm9L69Xjseal5pKwXFaMUxzm+Viajcp/PdqK1pEBJX5RndToNF0Ti3xu4e6BFO7dqY/J9TA==", + "version": "0.15.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.165.tgz", + "integrity": "sha512-NjXE+of4NJagrtHlzePBuWQ8a9pBFhhmQuvOhPj9W3CSi+VanuMoM/oRaT1TbR3efHk2JdCsKVDJScEzY8kdjw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.152.tgz", - "integrity": "sha512-2HSpMjbckGAmU/56CTGuEbCZJZHxUlfNJ2uzR4akZyVVLEmavC4thHVSGT7Ei1zzpHZsAg0y4WMbcp4wzpPv3g==", + "version": "0.10.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.165.tgz", + "integrity": "sha512-a6myeixOXDYeuOj0GK+/LWXbXXWanFVMvQRUMgC7wmUNGgSZiyJ8NPWzhAq6Vib4jSQ02pd+ux4ZtWs5kyvFLg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.151", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.151.tgz", - "integrity": "sha512-3ogsmZKPKc8n9Mjik4jTmNYT2Nbboe/zqcjDNG7RONO3w/tUyoKQshYCMBxxGMNLDwvh3BQ/D9/6JvdNWA1ShA==", + "version": "0.20.0-beta.164", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.164.tgz", + "integrity": "sha512-wXTi281yTWY1iAmRh21N6AhcEMopTjIm4xsdDdNmS5LbhxNuhVKNNIGKm5Zhd/G9fpn/vrfC4yZ6KA0lI/ZAxg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.152" + "@xterm/xterm": "^6.1.0-beta.165" } }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.152", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.152.tgz", - "integrity": "sha512-XHJ5ab19V6tmcHmBE7k9IYjXSwTxUd0c7oKLa5J+ZO0+aiXE8UKh9OEDw1oyl5ZQhw9gn71cGEo4TpB58KhfoQ==", + "version": "6.1.0-beta.165", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.165.tgz", + "integrity": "sha512-OUszO4HSmGPEw3EhboyIcNLQKJQKCDsYHv9kYFcaiK3biuNjGP0VAPVUJOLbf3V9fa1GLUUq+t985blqvTApoA==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/remote/web/package.json b/remote/web/package.json index fa7ad6cca706f..35cf04c172a41 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -9,15 +9,15 @@ "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.3.0-beta.152", - "@xterm/addon-image": "^0.10.0-beta.152", - "@xterm/addon-ligatures": "^0.11.0-beta.152", - "@xterm/addon-progress": "^0.3.0-beta.152", - "@xterm/addon-search": "^0.17.0-beta.152", - "@xterm/addon-serialize": "^0.15.0-beta.152", - "@xterm/addon-unicode11": "^0.10.0-beta.152", - "@xterm/addon-webgl": "^0.20.0-beta.151", - "@xterm/xterm": "^6.1.0-beta.152", + "@xterm/addon-clipboard": "^0.3.0-beta.165", + "@xterm/addon-image": "^0.10.0-beta.165", + "@xterm/addon-ligatures": "^0.11.0-beta.165", + "@xterm/addon-progress": "^0.3.0-beta.165", + "@xterm/addon-search": "^0.17.0-beta.165", + "@xterm/addon-serialize": "^0.15.0-beta.165", + "@xterm/addon-unicode11": "^0.10.0-beta.165", + "@xterm/addon-webgl": "^0.20.0-beta.164", + "@xterm/xterm": "^6.1.0-beta.165", "jschardet": "3.1.4", "katex": "^0.16.22", "tas-client": "0.3.1", diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index d66f1c39a3824..be33e2b306bf8 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -256,6 +256,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach kittyKeyboard: config.enableKittyKeyboardProtocol, win32InputMode: config.enableWin32InputMode, }, + allowTransparency: config.enableImages, windowOptions: { getWinSizePixels: true, getCellSizePixels: true, @@ -551,6 +552,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach this.raw.options.wordSeparator = config.wordSeparators; this.raw.options.ignoreBracketedPasteMode = config.ignoreBracketedPasteMode; this.raw.options.rescaleOverlappingGlyphs = config.rescaleOverlappingGlyphs; + this.raw.options.allowTransparency = config.enableImages; this.raw.options.vtExtensions = { kittyKeyboard: config.enableKittyKeyboardProtocol, win32InputMode: config.enableWin32InputMode, diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index a99f9628499c4..3500a7243e18b 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -654,7 +654,7 @@ const terminalConfiguration: IStringDictionary = { }, [TerminalSettingId.EnableImages]: { restricted: true, - markdownDescription: localize('terminal.integrated.enableImages', "Enables image support in the terminal, this will only work when {0} is enabled. Both sixel and iTerm's inline image protocol are supported on Linux and macOS. This will only work on Windows for versions of ConPTY >= v2 which is shipped with Windows itself, see also {1}. Images will currently not be restored between window reloads/reconnects.", `\`#${TerminalSettingId.GpuAcceleration}#\``, `\`#${TerminalSettingId.WindowsUseConptyDll}#\``), + markdownDescription: localize('terminal.integrated.enableImages', "Enables image support in the terminal, this will only work when {0} is enabled. Both sixel and iTerm's inline image protocol are supported on Linux and macOS. This will only work on Windows for versions of ConPTY >= v2 which is shipped with Windows itself, see also {1}. Images will currently not be restored between window reloads/reconnects. When enabled, transparency mode is also turned on in the terminal.", `\`#${TerminalSettingId.GpuAcceleration}#\``, `\`#${TerminalSettingId.WindowsUseConptyDll}#\``), type: 'boolean', default: false }, From ebbe486b2efaae8f217844cf436fd6f122370a25 Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:48:15 -0800 Subject: [PATCH 04/13] Browser: Managed CDP context groups (#295676) * Browser: Managed CDP context groups * layering * feedback * Fail fast * two-way lookup * feedback, cleanup --- src/vs/code/electron-main/app.ts | 8 + .../sharedProcess/sharedProcessMain.ts | 2 + .../browserView/common/browserView.ts | 5 - .../browserView/common/browserViewGroup.ts | 86 +++++ .../browserViewCDPProxyServer.ts | 95 ++++-- .../electron-main/browserViewGroup.ts | 202 +++++++++++ .../browserViewGroupMainService.ts | 88 +++++ .../electron-main/browserViewMainService.ts | 8 +- .../node/browserViewGroupRemoteService.ts | 109 ++++++ .../browserView/node/playwrightService.ts | 318 ++++++++++++++++-- .../contrib/browserView/common/browserView.ts | 5 - .../browserViewWorkbenchService.ts | 4 - 12 files changed, 858 insertions(+), 72 deletions(-) create mode 100644 src/vs/platform/browserView/common/browserViewGroup.ts create mode 100644 src/vs/platform/browserView/electron-main/browserViewGroup.ts create mode 100644 src/vs/platform/browserView/electron-main/browserViewGroupMainService.ts create mode 100644 src/vs/platform/browserView/node/browserViewGroupRemoteService.ts diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 5b64a6cbb1d83..a055db7e3dde8 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -37,7 +37,9 @@ import { IEncryptionMainService } from '../../platform/encryption/common/encrypt import { EncryptionMainService } from '../../platform/encryption/electron-main/encryptionMainService.js'; import { NativeBrowserElementsMainService, INativeBrowserElementsMainService } from '../../platform/browserElements/electron-main/nativeBrowserElementsMainService.js'; import { ipcBrowserViewChannelName } from '../../platform/browserView/common/browserView.js'; +import { ipcBrowserViewGroupChannelName } from '../../platform/browserView/common/browserViewGroup.js'; import { BrowserViewMainService, IBrowserViewMainService } from '../../platform/browserView/electron-main/browserViewMainService.js'; +import { BrowserViewGroupMainService, IBrowserViewGroupMainService } from '../../platform/browserView/electron-main/browserViewGroupMainService.js'; import { BrowserViewCDPProxyServer, IBrowserViewCDPProxyServer } from '../../platform/browserView/electron-main/browserViewCDPProxyServer.js'; import { NativeParsedArgs } from '../../platform/environment/common/argv.js'; import { IEnvironmentMainService } from '../../platform/environment/electron-main/environmentMainService.js'; @@ -1043,6 +1045,7 @@ export class CodeApplication extends Disposable { // Browser View services.set(IBrowserViewCDPProxyServer, new SyncDescriptor(BrowserViewCDPProxyServer, undefined, true)); services.set(IBrowserViewMainService, new SyncDescriptor(BrowserViewMainService, undefined, false /* proxied to other processes */)); + services.set(IBrowserViewGroupMainService, new SyncDescriptor(BrowserViewGroupMainService, undefined, false /* proxied to other processes */)); // Keyboard Layout services.set(IKeyboardLayoutMainService, new SyncDescriptor(KeyboardLayoutMainService)); @@ -1206,6 +1209,11 @@ export class CodeApplication extends Disposable { mainProcessElectronServer.registerChannel(ipcBrowserViewChannelName, browserViewChannel); sharedProcessClient.then(client => client.registerChannel(ipcBrowserViewChannelName, browserViewChannel)); + // Browser View Group + const browserViewGroupChannel = ProxyChannel.fromService(accessor.get(IBrowserViewGroupMainService), disposables); + mainProcessElectronServer.registerChannel(ipcBrowserViewGroupChannelName, browserViewGroupChannel); + sharedProcessClient.then(client => client.registerChannel(ipcBrowserViewGroupChannelName, browserViewGroupChannel)); + // Signing const signChannel = ProxyChannel.fromService(accessor.get(ISignService), disposables); mainProcessElectronServer.registerChannel('sign', signChannel); diff --git a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts index 6569ef1fbc808..e112b958d730e 100644 --- a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts @@ -136,6 +136,7 @@ import { IMeteredConnectionService } from '../../../platform/meteredConnection/c import { MeteredConnectionChannelClient, METERED_CONNECTION_CHANNEL } from '../../../platform/meteredConnection/common/meteredConnectionIpc.js'; import { IPlaywrightService } from '../../../platform/browserView/common/playwrightService.js'; import { PlaywrightService } from '../../../platform/browserView/node/playwrightService.js'; +import { IBrowserViewGroupRemoteService, BrowserViewGroupRemoteService } from '../../../platform/browserView/node/browserViewGroupRemoteService.js'; class SharedProcessMain extends Disposable implements IClientConnectionFilter { @@ -404,6 +405,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { services.set(ISharedWebContentExtractorService, new SyncDescriptor(SharedWebContentExtractorService)); // Playwright + services.set(IBrowserViewGroupRemoteService, new SyncDescriptor(BrowserViewGroupRemoteService)); services.set(IPlaywrightService, new SyncDescriptor(PlaywrightService)); return new InstantiationService(services); diff --git a/src/vs/platform/browserView/common/browserView.ts b/src/vs/platform/browserView/common/browserView.ts index 5c8c9517dfb4a..f22fd39e70b0c 100644 --- a/src/vs/platform/browserView/common/browserView.ts +++ b/src/vs/platform/browserView/common/browserView.ts @@ -275,9 +275,4 @@ export interface IBrowserViewService { * @param id The browser view identifier */ clearStorage(id: string): Promise; - - /** - * Get a CDP WebSocket endpoint URL. - */ - getDebugWebSocketEndpoint(): Promise; } diff --git a/src/vs/platform/browserView/common/browserViewGroup.ts b/src/vs/platform/browserView/common/browserViewGroup.ts new file mode 100644 index 0000000000000..0f43b98c8b080 --- /dev/null +++ b/src/vs/platform/browserView/common/browserViewGroup.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../base/common/event.js'; +import { IDisposable } from '../../../base/common/lifecycle.js'; + +export const ipcBrowserViewGroupChannelName = 'browserViewGroup'; + +/** + * Fired when a browser view is added to or removed from a group. + */ +export interface IBrowserViewGroupViewEvent { + /** The ID of the browser view that was added or removed. */ + readonly viewId: string; +} + +/** + * A browser view group - an isolated collection of browser views. + * + * This interface is shared between the main-process entity and remote proxies. + */ +export interface IBrowserViewGroup extends IDisposable { + readonly id: string; + + readonly onDidAddView: Event; + readonly onDidRemoveView: Event; + readonly onDidDestroy: Event; + + addView(viewId: string): Promise; + removeView(viewId: string): Promise; + getDebugWebSocketEndpoint(): Promise; +} + +/** + * Common service for managing browser view groups across processes. + * + * A browser view group is an isolated collection of browser views that can be + * independently exposed to different services or CDP clients. + * + * This interface is consumed via {@link ProxyChannel}. + * The main-process implementation is {@link BrowserViewGroupMainService}. + */ +export interface IBrowserViewGroupService { + + // Dynamic events - one per group instance, keyed by group ID. + onDynamicDidAddView(groupId: string): Event; + onDynamicDidRemoveView(groupId: string): Event; + onDynamicDidDestroy(groupId: string): Event; + + /** + * Create a new browser view group. + * @returns The id of the newly created group. + */ + createGroup(): Promise; + + /** + * Destroy a browser view group. + * Views in the group are **not** destroyed - they are simply detached. + * @param groupId The group identifier. + */ + destroyGroup(groupId: string): Promise; + + /** + * Add a browser view to a group. + * A view can belong to multiple groups simultaneously. + * @param groupId The group identifier. + * @param viewId The browser view identifier. + */ + addViewToGroup(groupId: string, viewId: string): Promise; + + /** + * Remove a browser view from a group. + * @param groupId The group identifier. + * @param viewId The browser view identifier. + */ + removeViewFromGroup(groupId: string, viewId: string): Promise; + + /** + * Get a short-lived CDP WebSocket endpoint URL for a specific group. + * The returned URL contains a single-use token. + * @param groupId The group identifier. + */ + getDebugWebSocketEndpoint(groupId: string): Promise; +} diff --git a/src/vs/platform/browserView/electron-main/browserViewCDPProxyServer.ts b/src/vs/platform/browserView/electron-main/browserViewCDPProxyServer.ts index 9142b497166dd..30ad512c042d0 100644 --- a/src/vs/platform/browserView/electron-main/browserViewCDPProxyServer.ts +++ b/src/vs/platform/browserView/electron-main/browserViewCDPProxyServer.ts @@ -22,44 +22,60 @@ export interface IBrowserViewCDPProxyServer { readonly _serviceBrand: undefined; /** - * Returns a debug endpoint with a short-lived, single-use token. + * Returns a debug endpoint with a short-lived, single-use token for a specific browser target. */ - getWebSocketEndpoint(): Promise; + getWebSocketEndpointForTarget(target: ICDPBrowserTarget): Promise; + + /** + * Unregister a previously registered browser target. + */ + removeTarget(target: ICDPBrowserTarget): Promise; } /** * WebSocket server that provides CDP debugging for browser views. + * + * Manages a registry of {@link ICDPBrowserTarget} instances, each reachable + * at its own `/devtools/browser/{id}` WebSocket endpoint. */ export class BrowserViewCDPProxyServer extends Disposable implements IBrowserViewCDPProxyServer { declare readonly _serviceBrand: undefined; private server: http.Server | undefined; private port: number | undefined; - private readonly tokens: TokenManager; + + private readonly tokens = this._register(new TokenManager()); + private readonly targets = new Map(); constructor( - private readonly browserTarget: ICDPBrowserTarget, @ILogService private readonly logService: ILogService ) { super(); - - this.tokens = this._register(new TokenManager()); } /** - * Returns a debug endpoint with a short-lived, single-use token in the - * WebSocket URL. The token is revoked once a WebSocket connection is made - * or after 30 seconds, whichever comes first. + * Register a browser target and return a WebSocket endpoint URL for it. + * The target is reachable at `/devtools/browser/{targetId}`. */ - async getWebSocketEndpoint(): Promise { + async getWebSocketEndpointForTarget(target: ICDPBrowserTarget): Promise { await this.ensureServerStarted(); - const token = await this.tokens.issueToken(); - return this.getWebSocketUrl(token); + const targetInfo = await target.getTargetInfo(); + const targetId = targetInfo.targetId; + + // Register (or re-register) the target + this.targets.set(targetId, target); + + const token = await this.tokens.issueToken(targetId); + return `ws://localhost:${this.port}/devtools/browser/${targetId}?token=${token}`; } - private getWebSocketUrl(token: string): string { - return `ws://localhost:${this.port}/devtools/browser?token=${token}`; + /** + * Unregister a previously registered browser target. + */ + async removeTarget(target: ICDPBrowserTarget): Promise { + const targetInfo = await target.getTargetInfo(); + this.targets.delete(targetInfo.targetId); } private async ensureServerStarted(): Promise { @@ -93,19 +109,30 @@ export class BrowserViewCDPProxyServer extends Disposable implements IBrowserVie private handleWebSocketUpgrade(req: http.IncomingMessage, socket: Socket): void { const [pathname, params] = (req.url || '').split('?'); - const token = new URLSearchParams(params).get('token'); - if (!token || !this.tokens.consumeToken(token)) { - socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); + const browserMatch = pathname.match(/^\/devtools\/browser\/([^/?]+)$/); + + this.logService.debug(`[BrowserViewDebugProxy] WebSocket upgrade requested: ${pathname}`); + + if (!browserMatch) { + this.logService.warn(`[BrowserViewDebugProxy] Rejecting WebSocket on unknown path: ${pathname}`); + socket.write('HTTP/1.1 404 Not Found\r\n\r\n'); socket.end(); return; } - const browserMatch = pathname.match(/^\/devtools\/browser(\/.*)?$/); + const targetId = browserMatch[1]; - this.logService.debug(`[BrowserViewDebugProxy] WebSocket upgrade requested: ${pathname}`); + const token = new URLSearchParams(params).get('token'); + const tokenTargetId = token && this.tokens.consumeToken(token); + if (!tokenTargetId || tokenTargetId !== targetId) { + socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); + socket.end(); + return; + } - if (!browserMatch) { - this.logService.warn(`[BrowserViewDebugProxy] Rejecting WebSocket on unknown path: ${pathname}`); + const target = this.targets.get(targetId); + if (!target) { + this.logService.warn(`[BrowserViewDebugProxy] Browser target not found: ${targetId}`); socket.write('HTTP/1.1 404 Not Found\r\n\r\n'); socket.end(); return; @@ -122,7 +149,7 @@ export class BrowserViewCDPProxyServer extends Disposable implements IBrowserVie return; } - const proxy = new CDPBrowserProxy(this.browserTarget); + const proxy = new CDPBrowserProxy(target); const disposables = this.wireWebSocket(upgraded, proxy); this._register(disposables); this._register(upgraded); @@ -200,31 +227,35 @@ export class BrowserViewCDPProxyServer extends Disposable implements IBrowserVie } } -class TokenManager extends Disposable { - /** Map of currently valid single-use tokens. Each expires after 30 seconds. */ - private readonly tokens = new Map(); +class TokenManager extends Disposable { + /** Map of currently valid single-use tokens to their associated details. */ + private readonly tokens = new Map(); /** - * Creates a short-lived, single-use token. + * Creates a short-lived, single-use token bound to a specific target. * The token is revoked once consumed or after 30 seconds. */ - async issueToken(): Promise { + async issueToken(details: TDetails): Promise { const token = this.makeToken(); - this.tokens.set(token, { expiresAt: Date.now() + 30_000 }); + this.tokens.set(token, { details: Object.freeze(details), expiresAt: Date.now() + 30_000 }); this._register(disposableTimeout(() => this.tokens.delete(token), 30_000)); return token; } - consumeToken(token: string): boolean { + /** + * Consume a token. Returns the details it was issued with, or + * `undefined` if the token is invalid or expired. + */ + consumeToken(token: string): TDetails | undefined { if (!token) { - return false; + return undefined; } const info = this.tokens.get(token); if (!info) { - return false; + return undefined; } this.tokens.delete(token); - return Date.now() <= info.expiresAt; + return Date.now() <= info.expiresAt ? info.details : undefined; } private makeToken(): string { diff --git a/src/vs/platform/browserView/electron-main/browserViewGroup.ts b/src/vs/platform/browserView/electron-main/browserViewGroup.ts new file mode 100644 index 0000000000000..efd4d01abf3fb --- /dev/null +++ b/src/vs/platform/browserView/electron-main/browserViewGroup.ts @@ -0,0 +1,202 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; +import { Emitter, Event } from '../../../base/common/event.js'; +import { BrowserView } from './browserView.js'; +import { ICDPTarget, CDPBrowserVersion, CDPWindowBounds, CDPTargetInfo, ICDPConnection, ICDPBrowserTarget } from '../common/cdp/types.js'; +import { CDPBrowserProxy } from '../common/cdp/proxy.js'; +import { IBrowserViewGroup, IBrowserViewGroupViewEvent } from '../common/browserViewGroup.js'; +import { IBrowserViewCDPProxyServer } from './browserViewCDPProxyServer.js'; +import { IBrowserViewMainService } from './browserViewMainService.js'; + +/** + * An isolated group of {@link BrowserView} instances exposed as CDP targets. + * + * Each group represents an independent CDP "browser" endpoint + * (`/devtools/browser/{id}`). Different groups can expose different + * subsets of browser views, enabling selective target visibility across + * CDP sessions. + * + * Created via {@link BrowserViewGroupMainService.createGroup}. + */ +export class BrowserViewGroup extends Disposable implements ICDPBrowserTarget, IBrowserViewGroup { + + private readonly views = new Map(); + private readonly viewListeners = this._register(new DisposableStore()); + + /** All context IDs known to this group, including those from views added to it. */ + private readonly knownContextIds = new Set(); + /** Browser context IDs created by this group via {@link createBrowserContext}. */ + private readonly ownedContextIds = new Set(); + + private readonly _onTargetCreated = this._register(new Emitter()); + readonly onTargetCreated: Event = this._onTargetCreated.event; + + private readonly _onTargetDestroyed = this._register(new Emitter()); + readonly onTargetDestroyed: Event = this._onTargetDestroyed.event; + + private readonly _onDidAddView = this._register(new Emitter()); + readonly onDidAddView: Event = this._onDidAddView.event; + + private readonly _onDidRemoveView = this._register(new Emitter()); + readonly onDidRemoveView: Event = this._onDidRemoveView.event; + + private readonly _onDidDestroy = this._register(new Emitter()); + readonly onDidDestroy: Event = this._onDidDestroy.event; + + constructor( + readonly id: string, + @IBrowserViewMainService private readonly browserViewMainService: IBrowserViewMainService, + @IBrowserViewCDPProxyServer private readonly cdpProxyServer: IBrowserViewCDPProxyServer, + ) { + super(); + } + + // #region View management + + /** + * Add a {@link BrowserView} to this group. + * Fires {@link onDidAddView} and {@link onTargetCreated}. + * Automatically removes the view when it closes. + */ + async addView(viewId: string): Promise { + if (this.views.has(viewId)) { + return; + } + const view = this.browserViewMainService.tryGetBrowserView(viewId); + if (!view) { + throw new Error(`Browser view ${viewId} not found`); + } + this.views.set(view.id, view); + this.knownContextIds.add(view.session.id); + this._onDidAddView.fire({ viewId: view.id }); + this._onTargetCreated.fire(view); + + this.viewListeners.add(Event.once(view.onDidClose)(() => { + this.removeView(viewId); + })); + } + + /** + * Remove a {@link BrowserView} from this group. + * Fires {@link onDidRemoveView} and {@link onTargetDestroyed} if the view was tracked. + */ + async removeView(viewId: string): Promise { + const view = this.views.get(viewId); + if (view && this.views.delete(viewId)) { + this._onDidRemoveView.fire({ viewId: view.id }); + this._onTargetDestroyed.fire(view); + } + } + + // #endregion + + // #region ICDPBrowserTarget implementation + + getVersion(): CDPBrowserVersion { + return this.browserViewMainService.getVersion(); + } + + getWindowForTarget(target: ICDPTarget): { windowId: number; bounds: CDPWindowBounds } { + return this.browserViewMainService.getWindowForTarget(target); + } + + async attach(): Promise { + return new CDPBrowserProxy(this); + } + + async getTargetInfo(): Promise { + return { + targetId: this.id, + type: 'browser', + title: this.getVersion().product, + url: '', + attached: true, + canAccessOpener: false + }; + } + + getTargets(): IterableIterator { + return this.views.values(); + } + + async createTarget(url: string, browserContextId?: string): Promise { + if (browserContextId && !this.knownContextIds.has(browserContextId)) { + throw new Error(`Unknown browser context ${browserContextId}`); + } + + const target = await this.browserViewMainService.createTarget(url, browserContextId); + if (target instanceof BrowserView) { + await this.addView(target.id); + } + return target; + } + + async activateTarget(target: ICDPTarget): Promise { + return this.browserViewMainService.activateTarget(target); + } + + async closeTarget(target: ICDPTarget): Promise { + if (target instanceof BrowserView) { + await this.removeView(target.id); + } + return this.browserViewMainService.closeTarget(target); + } + + // Browser context management + + /** + * Returns only the browser context IDs that are visible to this group, + * i.e. contexts used by views currently in the group. + */ + getBrowserContexts(): string[] { + return [...this.knownContextIds]; + } + + async createBrowserContext(): Promise { + const contextId = await this.browserViewMainService.createBrowserContext(); + this.knownContextIds.add(contextId); + this.ownedContextIds.add(contextId); + return contextId; + } + + async disposeBrowserContext(browserContextId: string): Promise { + if (!this.ownedContextIds.has(browserContextId)) { + throw new Error('Can only dispose browser contexts created by this group'); + } + + // Close views in this group that belong to the context before disposing + for (const view of this.views.values()) { + if (view.session.id === browserContextId) { + await this.removeView(view.id); + } + } + + this.knownContextIds.delete(browserContextId); + this.ownedContextIds.delete(browserContextId); + return this.browserViewMainService.disposeBrowserContext(browserContextId); + } + + // #endregion + + // #region CDP endpoint + + /** + * Get a WebSocket endpoint URL for connecting to this group's CDP + * session. The URL contains a short-lived, single-use token. + */ + async getDebugWebSocketEndpoint(): Promise { + return this.cdpProxyServer.getWebSocketEndpointForTarget(this); + } + + // #endregion + + override dispose(): void { + this._onDidDestroy.fire(); + this.cdpProxyServer.removeTarget(this); + super.dispose(); + } +} diff --git a/src/vs/platform/browserView/electron-main/browserViewGroupMainService.ts b/src/vs/platform/browserView/electron-main/browserViewGroupMainService.ts new file mode 100644 index 0000000000000..20dd6331c0ea5 --- /dev/null +++ b/src/vs/platform/browserView/electron-main/browserViewGroupMainService.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; +import { Event } from '../../../base/common/event.js'; +import { createDecorator, IInstantiationService } from '../../instantiation/common/instantiation.js'; +import { generateUuid } from '../../../base/common/uuid.js'; +import { IBrowserViewGroupService, IBrowserViewGroupViewEvent } from '../common/browserViewGroup.js'; +import { BrowserViewGroup } from './browserViewGroup.js'; + +export const IBrowserViewGroupMainService = createDecorator('browserViewGroupMainService'); + +export interface IBrowserViewGroupMainService extends IBrowserViewGroupService { + readonly _serviceBrand: undefined; +} + +/** + * Main-process service that manages {@link BrowserViewGroup} instances. + * + * Implements {@link IBrowserViewGroupService} so it can be surfaced to + * the workbench/shared process via {@link ProxyChannel}. + */ +export class BrowserViewGroupMainService extends Disposable implements IBrowserViewGroupMainService { + declare readonly _serviceBrand: undefined; + + private readonly groups = this._register(new DisposableMap()); + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(); + } + + async createGroup(): Promise { + const id = generateUuid(); + const group = this.instantiationService.createInstance(BrowserViewGroup, id); + this.groups.set(id, group); + + // Auto-cleanup when the group disposes itself + Event.once(group.onDidDestroy)(() => { + this.groups.deleteAndLeak(id); + }); + + return id; + } + + async destroyGroup(groupId: string): Promise { + this.groups.deleteAndDispose(groupId); + } + + async addViewToGroup(groupId: string, viewId: string): Promise { + return this._getGroup(groupId).addView(viewId); + } + + async removeViewFromGroup(groupId: string, viewId: string): Promise { + return this._getGroup(groupId).removeView(viewId); + } + + async getDebugWebSocketEndpoint(groupId: string): Promise { + return this._getGroup(groupId).getDebugWebSocketEndpoint(); + } + + onDynamicDidAddView(groupId: string): Event { + return this._getGroup(groupId).onDidAddView; + } + + onDynamicDidRemoveView(groupId: string): Event { + return this._getGroup(groupId).onDidRemoveView; + } + + onDynamicDidDestroy(groupId: string): Event { + return this._getGroup(groupId).onDidDestroy; + } + + /** + * Get a group or throw if not found. + */ + private _getGroup(groupId: string): BrowserViewGroup { + const group = this.groups.get(groupId); + if (!group) { + throw new Error(`Browser view group ${groupId} not found`); + } + return group; + } +} + diff --git a/src/vs/platform/browserView/electron-main/browserViewMainService.ts b/src/vs/platform/browserView/electron-main/browserViewMainService.ts index f29b4c18a438c..68e97b58b4f47 100644 --- a/src/vs/platform/browserView/electron-main/browserViewMainService.ts +++ b/src/vs/platform/browserView/electron-main/browserViewMainService.ts @@ -15,7 +15,6 @@ import { generateUuid } from '../../../base/common/uuid.js'; import { BrowserViewUri } from '../common/browserViewUri.js'; import { IWindowsMainService } from '../../windows/electron-main/windows.js'; import { BrowserSession } from './browserSession.js'; -import { IBrowserViewCDPProxyServer } from './browserViewCDPProxyServer.js'; import { IProductService } from '../../product/common/productService.js'; import { CDPBrowserProxy } from '../common/cdp/proxy.js'; @@ -51,8 +50,7 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - @IProductService private readonly productService: IProductService, - @IBrowserViewCDPProxyServer private readonly cdpProxyServer: IBrowserViewCDPProxyServer, + @IProductService private readonly productService: IProductService ) { super(); } @@ -363,8 +361,4 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa ); await browserSession.electronSession.clearData(); } - - async getDebugWebSocketEndpoint(): Promise { - return this.cdpProxyServer.getWebSocketEndpoint(); - } } diff --git a/src/vs/platform/browserView/node/browserViewGroupRemoteService.ts b/src/vs/platform/browserView/node/browserViewGroupRemoteService.ts new file mode 100644 index 0000000000000..b4aaffb612d17 --- /dev/null +++ b/src/vs/platform/browserView/node/browserViewGroupRemoteService.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../base/common/event.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; +import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js'; +import { createDecorator } from '../../instantiation/common/instantiation.js'; +import { IMainProcessService } from '../../ipc/common/mainProcessService.js'; +import { IBrowserViewGroup, IBrowserViewGroupService, IBrowserViewGroupViewEvent, ipcBrowserViewGroupChannelName } from '../common/browserViewGroup.js'; + +export const IBrowserViewGroupRemoteService = createDecorator('browserViewGroupRemoteService'); + +/** + * Remote-process service for managing browser view groups. + * + * Connects to the main-process {@link BrowserViewGroupMainService} via + * IPC and provides {@link IBrowserViewGroup} instances for + * interacting with groups. + * + * Usable from the shared process. + */ +export interface IBrowserViewGroupRemoteService { + readonly _serviceBrand: undefined; + + /** + * Create a new browser view group. + */ + createGroup(): Promise; +} + +/** + * Remote proxy for a browser view group living in the main process. + */ +class RemoteBrowserViewGroup extends Disposable implements IBrowserViewGroup { + constructor( + readonly id: string, + private readonly groupService: IBrowserViewGroupService, + ) { + super(); + + this._register(groupService.onDynamicDidDestroy(this.id)(() => { + // Avoid loops + this.dispose(true); + })); + } + + get onDidAddView(): Event { + return this.groupService.onDynamicDidAddView(this.id); + } + + get onDidRemoveView(): Event { + return this.groupService.onDynamicDidRemoveView(this.id); + } + + get onDidDestroy(): Event { + return this.groupService.onDynamicDidDestroy(this.id); + } + + async addView(viewId: string): Promise { + return this.groupService.addViewToGroup(this.id, viewId); + } + + async removeView(viewId: string): Promise { + return this.groupService.removeViewFromGroup(this.id, viewId); + } + + async getDebugWebSocketEndpoint(): Promise { + return this.groupService.getDebugWebSocketEndpoint(this.id); + } + + override dispose(fromService = false): void { + if (!fromService) { + this.groupService.destroyGroup(this.id); + } + super.dispose(); + } +} + +export class BrowserViewGroupRemoteService implements IBrowserViewGroupRemoteService { + declare readonly _serviceBrand: undefined; + + private readonly _groupService: IBrowserViewGroupService; + private readonly _groups = new Map(); + + constructor( + @IMainProcessService mainProcessService: IMainProcessService, + ) { + const channel = mainProcessService.getChannel(ipcBrowserViewGroupChannelName); + this._groupService = ProxyChannel.toService(channel); + } + + async createGroup(): Promise { + const id = await this._groupService.createGroup(); + return this._wrap(id); + } + + private _wrap(id: string): IBrowserViewGroup { + const group = new RemoteBrowserViewGroup(id, this._groupService); + this._groups.set(id, group); + + Event.once(group.onDidDestroy)(() => { + this._groups.delete(id); + }); + + return group; + } +} diff --git a/src/vs/platform/browserView/node/playwrightService.ts b/src/vs/platform/browserView/node/playwrightService.ts index 2dcd8fbd04e9c..f19ee20bd94ea 100644 --- a/src/vs/platform/browserView/node/playwrightService.ts +++ b/src/vs/platform/browserView/node/playwrightService.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../base/common/lifecycle.js'; -import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js'; +import { DeferredPromise } from '../../../base/common/async.js'; import { ILogService } from '../../log/common/log.js'; -import { IBrowserViewService, ipcBrowserViewChannelName } from '../common/browserView.js'; import { IPlaywrightService } from '../common/playwrightService.js'; -import { IMainProcessService } from '../../ipc/common/mainProcessService.js'; +import { IBrowserViewGroupRemoteService } from '../node/browserViewGroupRemoteService.js'; +import { IBrowserViewGroup } from '../common/browserViewGroup.js'; // eslint-disable-next-line local/code-import-patterns -import type { Browser } from 'playwright-core'; +import type { Browser, BrowserContext, Page } from 'playwright-core'; /** * Shared-process implementation of {@link IPlaywrightService}. @@ -19,22 +19,22 @@ import type { Browser } from 'playwright-core'; export class PlaywrightService extends Disposable implements IPlaywrightService { declare readonly _serviceBrand: undefined; - private readonly browserViewService: IBrowserViewService; private _browser: Browser | undefined; + private _pages: PlaywrightPageManager | undefined; private _initPromise: Promise | undefined; constructor( - @IMainProcessService mainProcessService: IMainProcessService, + @IBrowserViewGroupRemoteService private readonly browserViewGroupRemoteService: IBrowserViewGroupRemoteService, @ILogService private readonly logService: ILogService, ) { super(); - - const channel = mainProcessService.getChannel(ipcBrowserViewChannelName); - this.browserViewService = ProxyChannel.toService(channel); } + /** + * Ensure the Playwright browser connection and page map are initialized. + */ async initialize(): Promise { - if (this._browser?.isConnected()) { + if (this._pages) { return; } @@ -44,30 +44,41 @@ export class PlaywrightService extends Disposable implements IPlaywrightService this._initPromise = (async () => { try { - this.logService.debug('[PlaywrightService] Connecting to browser via CDP'); + this.logService.debug('[PlaywrightService] Creating browser view group'); + const group = this._register(await this.browserViewGroupRemoteService.createGroup()); + this.logService.debug('[PlaywrightService] Connecting to browser via CDP'); const playwright = await import('playwright-core'); - const endpoint = await this.browserViewService.getDebugWebSocketEndpoint(); + const endpoint = await group.getDebugWebSocketEndpoint(); const browser = await playwright.chromium.connectOverCDP(endpoint); this.logService.debug('[PlaywrightService] Connected to browser'); + // This can happen if the service was disposed while we were waiting for the connection. In that case, clean up immediately. + if (this._initPromise === undefined) { + browser.close().catch(() => { /* ignore */ }); + throw new Error('PlaywrightService was disposed during initialization'); + } + + const pageManager = this._register(new PlaywrightPageManager(group, browser, this.logService)); + browser.on('disconnected', () => { this.logService.debug('[PlaywrightService] Browser disconnected'); if (this._browser === browser) { + group.dispose(); + pageManager.dispose(); + this._browser = undefined; + this._pages = undefined; + this._initPromise = undefined; } }); - // This can happen if the service was disposed while we were waiting for the connection. In that case, clean up immediately. - if (this._initPromise === undefined) { - browser.close().catch(() => { /* ignore */ }); - throw new Error('PlaywrightService was disposed during initialization'); - } - this._browser = browser; - } finally { + this._pages = pageManager; + } catch (e) { this._initPromise = undefined; + throw e; } })(); @@ -83,3 +94,272 @@ export class PlaywrightService extends Disposable implements IPlaywrightService super.dispose(); } } + +/** + * Correlates browser view IDs with Playwright {@link Page} instances. + * + * When a browser view is added to a group, two asynchronous events follow + * through independent channels: + * + * 1. The group fires {@link IBrowserViewGroup.onDidAddView} (via IPC). + * 2. Playwright receives a CDP `Target.targetCreated` event (via WebSocket) + * and fires a `page` event on the matching {@link BrowserContext}. + * + * This class pairs the two event streams by FIFO ordering: the first view-ID + * received is matched with the first page event received. + * + * A periodic scan handles the case where Playwright creates a new + * {@link BrowserContext} for a target whose session was previously unknown. + */ +class PlaywrightPageManager extends Disposable { + + private readonly _viewIdToPage = new Map(); + private readonly _pageToViewId = new WeakMap(); + + /** View IDs received from the group but not yet matched with a page. */ + private _viewIdQueue: Array<{ + viewId: string; + page: DeferredPromise; + }> = []; + + /** Pages received from Playwright but not yet matched with a view ID. */ + private _pageQueue: Array<{ + page: Page; + viewId: DeferredPromise; + }> = []; + + private readonly _watchedContexts = new WeakSet(); + private _scanTimer: ReturnType | undefined; + + constructor( + private readonly _group: IBrowserViewGroup, + private readonly _browser: Browser, + private readonly logService: ILogService, + ) { + super(); + + this._register(_group.onDidAddView(e => this.onViewAdded(e.viewId))); + this._register(_group.onDidRemoveView(e => this.onViewRemoved(e.viewId))); + this.scanForNewContexts(); + } + + /** + * Create a new page in the browser and return its associated page and view ID. + */ + async newPage(): Promise<{ viewId: string; page: Page }> { + const page = await this._browser.newPage(); + const viewId = await this.onPageAdded(page); + + return { viewId, page }; + } + + /** + * Explicitly add an existing browser view to the CDP group. + */ + async addPage(viewId: string): Promise { + if (this._viewIdToPage.has(viewId)) { + return; + } + if (this._viewIdQueue.some(item => item.viewId === viewId)) { + return; + } + + // ensure the viewId is queued so we can immediately fetch the promise via getPage(). + this.onViewAdded(viewId); + + try { + await this._group.addView(viewId); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : String(err); + this.logService.error('[PlaywrightPageMap] Failed to add view:', errorMessage); + this.onViewRemoved(viewId); + } + } + + /** + * Remove a browser view from the CDP group. + */ + async removePage(viewId: string): Promise { + this.onViewRemoved(viewId); + try { + await this._group.removeView(viewId); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : String(err); + this.logService.error('[PlaywrightPageMap] Failed to remove view:', errorMessage); + } + } + + /** + * Get the Playwright {@link Page} for a browser view that has already been added. + * Throws if the view has not been added. + */ + async getPage(viewId: string): Promise { + const resolved = this._viewIdToPage.get(viewId); + if (resolved) { + return resolved; + } + const queued = this._viewIdQueue.find(item => item.viewId === viewId); + if (queued) { + return queued.page.p; + } + + throw new Error(`Page "${viewId}" has not been added to the Playwright service`); + } + + /** + * Called when the group fires onDidAddView. Creates a deferred entry in + * the view ID queue and attempts to match it with a page. + */ + private onViewAdded(viewId: string, timeoutMs = 10000): Promise { + const resolved = this._viewIdToPage.get(viewId); + if (resolved) { + return Promise.resolve(resolved); + } + const queued = this._viewIdQueue.find(item => item.viewId === viewId); + if (queued) { + return queued.page.p; + } + + const deferred = new DeferredPromise(); + const timeout = setTimeout(() => deferred.error(new Error(`Timed out waiting for page`)), timeoutMs); + + deferred.p.finally(() => { + clearTimeout(timeout); + this._viewIdQueue = this._viewIdQueue.filter(item => item.viewId !== viewId); + if (this._viewIdQueue.length === 0) { + this.stopScanning(); + } + }); + + this._viewIdQueue.push({ viewId, page: deferred }); + this.tryMatch(); + this.ensureScanning(); + + return deferred.p; + } + + private onViewRemoved(viewId: string): void { + this._viewIdQueue = this._viewIdQueue.filter(item => item.viewId !== viewId); + const page = this._viewIdToPage.get(viewId); + if (page) { + this._pageToViewId.delete(page); + } + this._viewIdToPage.delete(viewId); + } + + private onPageAdded(page: Page, timeoutMs = 10000): Promise { + const resolved = this._pageToViewId.get(page); + if (resolved) { + return Promise.resolve(resolved); + } + const queued = this._pageQueue.find(item => item.page === page); + if (queued) { + return queued.viewId.p; + } + + this.onContextAdded(page.context()); + page.once('close', () => this.onPageRemoved(page)); + + const deferred = new DeferredPromise(); + const timeout = setTimeout(() => deferred.error(new Error(`Timed out waiting for browser view`)), timeoutMs); + deferred.p.finally(() => { + clearTimeout(timeout); + this._pageQueue = this._pageQueue.filter(item => item.page !== page); + }); + + this._pageQueue.push({ page, viewId: deferred }); + this.tryMatch(); + + return deferred.p; + } + + private onPageRemoved(page: Page): void { + this._pageQueue = this._pageQueue.filter(item => item.page !== page); + const viewId = this._pageToViewId.get(page); + if (viewId) { + this._viewIdToPage.delete(viewId); + } + this._pageToViewId.delete(page); + } + + private onContextAdded(context: BrowserContext): void { + if (this._watchedContexts.has(context)) { + return; + } + this._watchedContexts.add(context); + + context.on('page', (page: Page) => this.onPageAdded(page)); + context.on('close', () => this.onContextRemoved(context)); + + for (const page of context.pages()) { + this.onPageAdded(page); + } + } + + private onContextRemoved(context: BrowserContext): void { + this._watchedContexts.delete(context); + } + + // --- Matching --- + + /** + * Pair up queued view IDs with queued pages in FIFO order and resolve + * any callers waiting for the matched view IDs. + */ + private tryMatch(): void { + while (this._viewIdQueue.length > 0 && this._pageQueue.length > 0) { + const viewIdItem = this._viewIdQueue.shift()!; + const pageItem = this._pageQueue.shift()!; + + this._viewIdToPage.set(viewIdItem.viewId, pageItem.page); + this._pageToViewId.set(pageItem.page, viewIdItem.viewId); + + viewIdItem.page.complete(pageItem.page); + pageItem.viewId.complete(viewIdItem.viewId); + + this.logService.debug(`[PlaywrightPageMap] Matched view ${viewIdItem.viewId} → page`); + } + + if (this._viewIdQueue.length === 0) { + this.stopScanning(); + } + } + + // --- Context scanning --- + + /** + * Watch all current {@link BrowserContext BrowserContexts} for new pages. + * Also processes any existing pages in newly discovered contexts. + */ + private scanForNewContexts(): void { + for (const context of this._browser.contexts()) { + this.onContextAdded(context); + } + } + + private ensureScanning(): void { + if (this._scanTimer === undefined) { + this._scanTimer = setInterval(() => this.scanForNewContexts(), 100); + } + } + + private stopScanning(): void { + if (this._scanTimer !== undefined) { + clearInterval(this._scanTimer); + this._scanTimer = undefined; + } + } + + override dispose(): void { + this.stopScanning(); + for (const { page } of this._viewIdQueue) { + page.error(new Error('PlaywrightPageMap disposed')); + } + for (const { viewId } of this._pageQueue) { + viewId.error(new Error('PlaywrightPageMap disposed')); + } + this._viewIdQueue = []; + this._pageQueue = []; + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/browserView/common/browserView.ts b/src/vs/workbench/contrib/browserView/common/browserView.ts index d717ada29b959..732fa1974e481 100644 --- a/src/vs/workbench/contrib/browserView/common/browserView.ts +++ b/src/vs/workbench/contrib/browserView/common/browserView.ts @@ -68,11 +68,6 @@ export interface IBrowserViewWorkbenchService { * Clear all storage data for the current workspace browser session */ clearWorkspaceStorage(): Promise; - - /** - * Get the endpoint for connecting to a browser view's CDP proxy server - */ - getDebugWebSocketEndpoint(): Promise; } diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts index 30199e27cbf40..68d2376c587d1 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts @@ -54,8 +54,4 @@ export class BrowserViewWorkbenchService implements IBrowserViewWorkbenchService const workspaceId = this.workspaceContextService.getWorkspace().id; return this._browserViewService.clearWorkspaceStorage(workspaceId); } - - async getDebugWebSocketEndpoint() { - return this._browserViewService.getDebugWebSocketEndpoint(); - } } From 31659231a3e3bed2f627b79243426e34a340cc9d Mon Sep 17 00:00:00 2001 From: Robo Date: Wed, 18 Feb 2026 15:30:08 +0900 Subject: [PATCH 05/13] fix: crash with run_as_node in sub app (#295927) * fix: crash with run_as_node in sub app * chore: bump distro --- .npmrc | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.npmrc b/.npmrc index e2305b402ea33..b07eade64d573 100644 --- a/.npmrc +++ b/.npmrc @@ -1,6 +1,6 @@ disturl="https://electronjs.org/headers" target="39.6.0" -ms_build_id="13312042" +ms_build_id="13330601" runtime="electron" ignore-scripts=false build_from_source="true" diff --git a/package.json b/package.json index 0180a71488240..7ee40bb9d7ebb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.110.0", - "distro": "16be71799bd7ef33ea9b0206fb548ce74a47daa4", + "distro": "d5c0e77a0214208f36b56d42e8e787de88d02ea4", "author": { "name": "Microsoft Corporation" }, From ddf7d0b4c255fc822afbe12f3091bf6cd99fee24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Ruales?= <1588988+jruales@users.noreply.github.com> Date: Tue, 17 Feb 2026 23:38:29 -0800 Subject: [PATCH 06/13] Browser: Disable toolbar buttons when not possible to use them (#295942) Disable buttons when not possible to use them --- .../browserView/electron-browser/browserEditor.ts | 5 +++++ .../electron-browser/browserViewActions.ts | 13 +++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts index cea4ababe30a6..e9f065deb3e64 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts @@ -52,6 +52,7 @@ export const CONTEXT_BROWSER_CAN_GO_FORWARD = new RawContextKey('browse export const CONTEXT_BROWSER_FOCUSED = new RawContextKey('browserFocused', true, localize('browser.editorFocused', "Whether the browser editor is focused")); export const CONTEXT_BROWSER_STORAGE_SCOPE = new RawContextKey('browserStorageScope', '', localize('browser.storageScope', "The storage scope of the current browser view")); export const CONTEXT_BROWSER_HAS_URL = new RawContextKey('browserHasUrl', false, localize('browser.hasUrl', "Whether the browser has a URL loaded")); +export const CONTEXT_BROWSER_HAS_ERROR = new RawContextKey('browserHasError', false, localize('browser.hasError', "Whether the browser has a load error")); export const CONTEXT_BROWSER_DEVTOOLS_OPEN = new RawContextKey('browserDevToolsOpen', false, localize('browser.devToolsOpen', "Whether developer tools are open for the current browser view")); export const CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE = new RawContextKey('browserElementSelectionActive', false, localize('browser.elementSelectionActive', "Whether element selection is currently active")); @@ -188,6 +189,7 @@ export class BrowserEditor extends EditorPane { private _canGoForwardContext!: IContextKey; private _storageScopeContext!: IContextKey; private _hasUrlContext!: IContextKey; + private _hasErrorContext!: IContextKey; private _devToolsOpenContext!: IContextKey; private _elementSelectionActiveContext!: IContextKey; @@ -227,6 +229,7 @@ export class BrowserEditor extends EditorPane { this._canGoForwardContext = CONTEXT_BROWSER_CAN_GO_FORWARD.bindTo(contextKeyService); this._storageScopeContext = CONTEXT_BROWSER_STORAGE_SCOPE.bindTo(contextKeyService); this._hasUrlContext = CONTEXT_BROWSER_HAS_URL.bindTo(contextKeyService); + this._hasErrorContext = CONTEXT_BROWSER_HAS_ERROR.bindTo(contextKeyService); this._devToolsOpenContext = CONTEXT_BROWSER_DEVTOOLS_OPEN.bindTo(contextKeyService); this._elementSelectionActiveContext = CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE.bindTo(contextKeyService); @@ -526,6 +529,7 @@ export class BrowserEditor extends EditorPane { } const error: IBrowserViewLoadError | undefined = this._model.error; + this._hasErrorContext.set(!!error); if (error) { // Update error content @@ -990,6 +994,7 @@ export class BrowserEditor extends EditorPane { this._canGoBackContext.reset(); this._canGoForwardContext.reset(); this._hasUrlContext.reset(); + this._hasErrorContext.reset(); this._storageScopeContext.reset(); this._devToolsOpenContext.reset(); this._elementSelectionActiveContext.reset(); diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts index dd212d04485c4..39305f4c57bce 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts @@ -11,7 +11,7 @@ import { KeybindingWeight } from '../../../../platform/keybinding/common/keybind import { KeyMod, KeyCode } from '../../../../base/common/keyCodes.js'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { BrowserEditor, CONTEXT_BROWSER_CAN_GO_BACK, CONTEXT_BROWSER_CAN_GO_FORWARD, CONTEXT_BROWSER_DEVTOOLS_OPEN, CONTEXT_BROWSER_FOCUSED, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_STORAGE_SCOPE, CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE, CONTEXT_BROWSER_FIND_WIDGET_FOCUSED, CONTEXT_BROWSER_FIND_WIDGET_VISIBLE } from './browserEditor.js'; +import { BrowserEditor, CONTEXT_BROWSER_CAN_GO_BACK, CONTEXT_BROWSER_CAN_GO_FORWARD, CONTEXT_BROWSER_DEVTOOLS_OPEN, CONTEXT_BROWSER_FOCUSED, CONTEXT_BROWSER_HAS_ERROR, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_STORAGE_SCOPE, CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE, CONTEXT_BROWSER_FIND_WIDGET_FOCUSED, CONTEXT_BROWSER_FIND_WIDGET_VISIBLE } from './browserEditor.js'; import { BrowserViewUri } from '../../../../platform/browserView/common/browserViewUri.js'; import { IBrowserViewWorkbenchService } from '../common/browserView.js'; import { BrowserViewStorageScope } from '../../../../platform/browserView/common/browserView.js'; @@ -228,7 +228,7 @@ class AddElementToChatAction extends Action2 { category: BrowserCategory, icon: Codicon.inspect, f1: true, - precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, enabled), + precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_HAS_ERROR.negate(), enabled), toggled: CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE, menu: { id: MenuId.BrowserActionsToolbar, @@ -265,7 +265,7 @@ class AddConsoleLogsToChatAction extends Action2 { category: BrowserCategory, icon: Codicon.output, f1: true, - precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL, enabled), + precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_HAS_ERROR.negate(), enabled), menu: { id: MenuId.BrowserActionsToolbar, group: 'actions', @@ -292,7 +292,7 @@ class ToggleDevToolsAction extends Action2 { category: BrowserCategory, icon: Codicon.terminal, f1: true, - precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL), + precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_HAS_ERROR.negate()), toggled: ContextKeyExpr.equals(CONTEXT_BROWSER_DEVTOOLS_OPEN.key, true), menu: { id: MenuId.BrowserActionsToolbar, @@ -323,7 +323,8 @@ class OpenInExternalBrowserAction extends Action2 { category: BrowserCategory, icon: Codicon.linkExternal, f1: true, - precondition: BROWSER_EDITOR_ACTIVE, + // Note: We do allow opening in an external browser even if there is an error page shown + precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL), menu: { id: MenuId.BrowserActionsToolbar, group: ActionGroupPage, @@ -455,7 +456,7 @@ class ShowBrowserFindAction extends Action2 { title: localize2('browser.showFindAction', 'Find in Page'), category: BrowserCategory, f1: true, - precondition: BROWSER_EDITOR_ACTIVE, + precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_HAS_ERROR.negate()), menu: { id: MenuId.BrowserActionsToolbar, group: ActionGroupPage, From cb12b134bc76db38d4eb8d1bd5d2e8ee75432cf3 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 17 Feb 2026 23:54:45 -0800 Subject: [PATCH 07/13] Fix instructions reading in WSL (#295898) --- .../computeAutomaticInstructions.ts | 29 ++++++++-- .../computeAutomaticInstructions.test.ts | 58 ++++++++++++++++++- .../service/promptsService.test.ts | 5 ++ 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index 50c007fc1168d..3c3e2597382c7 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -7,6 +7,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { match, splitGlobAware } from '../../../../../base/common/glob.js'; import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js'; import { Schemas } from '../../../../../base/common/network.js'; +import { OperatingSystem } from '../../../../../base/common/platform.js'; import { basename, dirname } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; @@ -14,6 +15,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { IFileService } from '../../../../../platform/files/common/files.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IRemoteAgentService } from '../../../../services/remote/common/remoteAgentService.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, toPromptFileVariableEntry, toPromptTextVariableEntry, PromptFileVariableKind, IPromptTextVariableEntry, ChatRequestToolReferenceEntry, toToolVariableEntry } from '../attachments/chatVariableEntries.js'; @@ -68,6 +70,7 @@ export class ComputeAutomaticInstructions { @IConfigurationService private readonly _configurationService: IConfigurationService, @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, @IFileService private readonly _fileService: IFileService, + @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService, ) { @@ -294,6 +297,10 @@ export class ComputeAutomaticInstructions { const readTool = this._getTool('readFile'); const runSubagentTool = this._getTool(VSCodeToolReference.runSubagent); + const remoteEnv = await this._remoteAgentService.getEnvironment(); + const remoteOS = remoteEnv?.os; + const filePath = (uri: URI) => getFilePath(uri, remoteOS); + const entries: string[] = []; if (readTool) { @@ -316,13 +323,13 @@ export class ComputeAutomaticInstructions { if (description) { entries.push(`${description}`); } - entries.push(`${getFilePath(uri)}`); + entries.push(`${filePath(uri)}`); const applyToPattern = this._getApplyToPattern(applyTo, paths); if (applyToPattern) { entries.push(`${applyToPattern}`); } } else { - entries.push(`${getFilePath(uri)}`); + entries.push(`${filePath(uri)}`); } entries.push(''); hasContent = true; @@ -335,7 +342,7 @@ export class ComputeAutomaticInstructions { const description = folderName.trim().length === 0 ? localize('instruction.file.description.agentsmd.root', 'Instructions for the workspace') : localize('instruction.file.description.agentsmd.folder', 'Instructions for folder \'{0}\'', folderName); entries.push(''); entries.push(`${description}`); - entries.push(`${getFilePath(uri)}`); + entries.push(`${filePath(uri)}`); entries.push(''); hasContent = true; @@ -378,7 +385,7 @@ export class ComputeAutomaticInstructions { if (skill.description) { entries.push(`${skill.description}`); } - entries.push(`${getFilePath(skill.uri)}`); + entries.push(`${filePath(skill.uri)}`); entries.push(''); } entries.push('', '', ''); // add trailing newline @@ -492,9 +499,19 @@ export class ComputeAutomaticInstructions { } -function getFilePath(uri: URI): string { +export function getFilePath(uri: URI, remoteOS: OperatingSystem | undefined): string { if (uri.scheme === Schemas.file || uri.scheme === Schemas.vscodeRemote) { - return uri.fsPath; + const fsPath = uri.fsPath; + // uri.fsPath uses the local OS's path separators, but the path + // may belong to a remote with a different OS. Normalize separators + // to match the remote OS (idempotent when local and remote match). + if (remoteOS !== undefined) { + if (remoteOS === OperatingSystem.Windows) { + return fsPath.replace(/\//g, '\\'); + } + return fsPath.replace(/\\/g, '/'); + } + return fsPath; } return uri.toString(); } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts index 3cb6fe2ad58fe..2d008a2567f07 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts @@ -8,6 +8,7 @@ import * as sinon from 'sinon'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Event } from '../../../../../../base/common/event.js'; import { Schemas } from '../../../../../../base/common/network.js'; +import { OperatingSystem } from '../../../../../../base/common/platform.js'; import { URI } from '../../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; @@ -27,7 +28,7 @@ import { testWorkspace } from '../../../../../../platform/workspace/test/common/ import { IUserDataProfileService } from '../../../../../services/userDataProfile/common/userDataProfile.js'; import { TestContextService, TestUserDataProfileService } from '../../../../../test/common/workbenchTestServices.js'; import { ChatRequestVariableSet, isPromptFileVariableEntry, isPromptTextVariableEntry, toFileVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; -import { ComputeAutomaticInstructions, InstructionsCollectionEvent } from '../../../common/promptSyntax/computeAutomaticInstructions.js'; +import { ComputeAutomaticInstructions, getFilePath, InstructionsCollectionEvent } from '../../../common/promptSyntax/computeAutomaticInstructions.js'; import { PromptsConfig } from '../../../common/promptSyntax/config/config.js'; import { AGENTS_SOURCE_FOLDER, CLAUDE_RULES_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../common/promptSyntax/config/promptFileLocations.js'; import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../../../common/promptSyntax/promptTypes.js'; @@ -39,6 +40,7 @@ import { IPathService } from '../../../../../services/path/common/pathService.js import { IFileQuery, ISearchService } from '../../../../../services/search/common/search.js'; import { IExtensionService } from '../../../../../services/extensions/common/extensions.js'; import { ILanguageModelToolsService } from '../../../common/tools/languageModelToolsService.js'; +import { IRemoteAgentService } from '../../../../../../workbench/services/remote/common/remoteAgentService.js'; import { basename } from '../../../../../../base/common/resources.js'; import { match } from '../../../../../../base/common/glob.js'; import { ChatModeKind } from '../../../common/constants.js'; @@ -169,6 +171,10 @@ suite('ComputeAutomaticInstructions', () => { } as unknown as ILanguageModelToolsService; instaService.stub(ILanguageModelToolsService, toolsService); + instaService.stub(IRemoteAgentService, { + getEnvironment: () => Promise.resolve(null), + }); + service = disposables.add(instaService.createInstance(PromptsService)); instaService.stub(IPromptsService, service); }); @@ -2011,3 +2017,53 @@ suite('ComputeAutomaticInstructions', () => { assert.ok(!paths.includes(claudeMdUri.path), 'Should not include CLAUDE.md (symlink to copilot)'); }); }); + +suite('getFilePath', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('should return fsPath for file:// URIs', () => { + const uri = URI.file('/workspace/src/file.ts'); + const result = getFilePath(uri, undefined); + assert.strictEqual(result, uri.fsPath); + }); + + test('should return fsPath for vscode-remote URIs', () => { + const uri = URI.from({ scheme: Schemas.vscodeRemote, path: '/workspace/src/file.ts' }); + const result = getFilePath(uri, undefined); + assert.strictEqual(result, uri.fsPath); + }); + + test('should return uri.toString() for other schemes', () => { + const uri = URI.from({ scheme: 'untitled', path: '/workspace/src/file.ts' }); + const result = getFilePath(uri, undefined); + assert.strictEqual(result, uri.toString()); + }); + + test('should use backslashes when remote is Windows', () => { + const uri = URI.from({ scheme: Schemas.vscodeRemote, path: '/C:/Users/dev/project/file.ts' }); + const result = getFilePath(uri, OperatingSystem.Windows); + assert.ok(!result.includes('/'), 'Should not contain forward slashes'); + assert.ok(result.includes('\\'), 'Should contain backslashes'); + }); + + test('should use forward slashes when remote is Linux', () => { + const uri = URI.from({ scheme: Schemas.vscodeRemote, path: '/home/user/project/file.ts' }); + const result = getFilePath(uri, OperatingSystem.Linux); + assert.ok(!result.includes('\\'), 'Should not contain backslashes'); + assert.ok(result.includes('/home/user/project/file.ts'), 'Should contain the forward-slash path'); + }); + + test('should use forward slashes when remote is macOS', () => { + const uri = URI.from({ scheme: Schemas.vscodeRemote, path: '/Users/dev/project/file.ts' }); + const result = getFilePath(uri, OperatingSystem.Macintosh); + assert.ok(!result.includes('\\'), 'Should not contain backslashes'); + assert.ok(result.includes('/Users/dev/project/file.ts'), 'Should contain the forward-slash path'); + }); + + test('should not replace slashes when remoteOS is undefined', () => { + const uri = URI.file('/workspace/src/file.ts'); + const result = getFilePath(uri, undefined); + assert.strictEqual(result, uri.fsPath); + }); +}); 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 05eabacb54dec..dd8f81d4dd3a8 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 @@ -47,6 +47,7 @@ import { InMemoryStorageService, IStorageService } from '../../../../../../../pl import { IPathService } from '../../../../../../services/path/common/pathService.js'; import { IFileMatch, IFileQuery, ISearchService } from '../../../../../../services/search/common/search.js'; import { IExtensionService } from '../../../../../../services/extensions/common/extensions.js'; +import { IRemoteAgentService } from '../../../../../../services/remote/common/remoteAgentService.js'; import { ChatModeKind } from '../../../../common/constants.js'; import { HookType } from '../../../../common/promptSyntax/hookSchema.js'; @@ -153,6 +154,10 @@ suite('PromptsService', () => { } }); + instaService.stub(IRemoteAgentService, { + getEnvironment: () => Promise.resolve(null), + }); + service = disposables.add(instaService.createInstance(PromptsService)); instaService.stub(IPromptsService, service); }); From ed52ab8bc106774535a98575bcf58745941fd9ed Mon Sep 17 00:00:00 2001 From: Zhichao Li <57812115+zhichli@users.noreply.github.com> Date: Tue, 17 Feb 2026 23:55:27 -0800 Subject: [PATCH 08/13] perf: defer expensive repo diff capture to export time (#295936) * perf: defer expensive repo diff capture to export time Split repo info capture into two paths: 1. On first chat message (lightweight, no file I/O): - captureRepoMetadata() reads only from already-loaded SCM provider observables (branch name, commit hash, remote refs) - Synchronous, zero file I/O, no diff computation - Stores ~200 bytes of metadata per session 2. At export time only (on-demand, user-initiated): - captureRepoInfo() performs full file reads and diff generation - Only runs when user explicitly triggers 'Export Chat as Zip' Additional changes: - Remove repoData from toJSON() serialization. Repo state does not need to survive VS Code restarts, only matters for current session export. This eliminates serialization overhead in saveState(). - Remove trimOldSessionDiffs(). No longer needed since diffs are never stored on the model. - Remove IFileService dependency from ChatRepoInfoContribution - Export action no longer gated on chat.repoInfo.enabled for diff capture (always captures at export time since it is user-initiated) Addresses feedback from #286812 about per-message overhead. Fixes #294863 root cause (expensive file I/O on every message). * fix: address PR review feedback - Fix undefined === undefined falsely reporting 'synced' by requiring both localHeadCommit and remoteHeadCommit to be defined - Use 'local-git' / 'local-only' when no remote refs are available instead of always claiming 'remote-git' - Update setting description to reflect metadata-only capture --- .../contrib/chat/browser/chatRepoInfo.ts | 147 +++++++++++------- .../contrib/chat/common/model/chatModel.ts | 1 - .../electron-browser/actions/chatExportZip.ts | 55 +++---- 3 files changed, 117 insertions(+), 86 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts b/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts index e5848bf941ace..494c639a67a38 100644 --- a/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts +++ b/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts @@ -23,7 +23,6 @@ import * as nls from '../../../../nls.js'; const MAX_CHANGES = 100; const MAX_DIFFS_SIZE_BYTES = 900 * 1024; -const MAX_SESSIONS_WITH_FULL_DIFFS = 5; const MAX_FILE_SIZE_BYTES = 1 * 1024 * 1024; // 1 MB per file /** * Regex to match `url = ` lines in git config. @@ -374,7 +373,83 @@ function computeDiffHunks( } /** - * Captures repository state from the first available SCM repository. + * Captures lightweight repository metadata (branch, commit, remote) from SCM providers. + * No file I/O or diff computation - reads only from already-loaded SCM observables. + * Used on chat message submission to record the point-in-time commit state. + */ +export function captureRepoMetadata(scmService: ISCMService): IExportableRepoData | undefined { + const repositories = [...scmService.repositories]; + if (repositories.length === 0) { + return undefined; + } + + const repository = repositories[0]; + const rootUri = repository.provider.rootUri; + if (!rootUri) { + return undefined; + } + + let localBranch: string | undefined; + let localHeadCommit: string | undefined; + let remoteTrackingBranch: string | undefined; + let remoteHeadCommit: string | undefined; + let remoteBaseBranch: string | undefined; + + const historyProvider = repository.provider.historyProvider?.get(); + if (historyProvider) { + const historyItemRef = historyProvider.historyItemRef.get(); + localBranch = historyItemRef?.name; + localHeadCommit = historyItemRef?.revision; + + const historyItemRemoteRef = historyProvider.historyItemRemoteRef.get(); + if (historyItemRemoteRef) { + remoteTrackingBranch = historyItemRemoteRef.name; + remoteHeadCommit = historyItemRemoteRef.revision; + } + + const historyItemBaseRef = historyProvider.historyItemBaseRef.get(); + if (historyItemBaseRef) { + remoteBaseBranch = historyItemBaseRef.name; + } + } + + // Determine workspace type and sync status without file I/O. + // Cannot determine remoteUrl/remoteVendor or detect plain-folder here (requires reading .git/config). + // The full captureRepoInfo at export time will produce accurate classification. + let workspaceType: IExportableRepoData['workspaceType']; + let syncStatus: IExportableRepoData['syncStatus']; + + if (remoteTrackingBranch || remoteHeadCommit || remoteBaseBranch) { + workspaceType = 'remote-git'; + + if (!remoteTrackingBranch) { + syncStatus = 'unpublished'; + } else if (localHeadCommit && remoteHeadCommit && localHeadCommit === remoteHeadCommit) { + syncStatus = 'synced'; + } else { + syncStatus = 'unpushed'; + } + } else { + // No remote refs available; conservatively classify as local-git + workspaceType = 'local-git'; + syncStatus = 'local-only'; + } + + return { + workspaceType, + syncStatus, + localBranch, + remoteTrackingBranch, + remoteBaseBranch, + localHeadCommit, + remoteHeadCommit, + diffsStatus: 'notCaptured', + }; +} + +/** + * Captures full repository state including working tree diffs. + * Performs file I/O and diff computation - should only be called on explicit user action (e.g., export). */ export async function captureRepoInfo(scmService: ISCMService, fileService: IFileService): Promise { const repositories = [...scmService.repositories]; @@ -564,7 +639,9 @@ export async function captureRepoInfo(scmService: ISCMService, fileService: IFil } /** - * Captures repository information for chat sessions on creation and first message. + * Captures lightweight repository metadata for chat sessions on first message. + * Only reads from already-loaded SCM provider observables, no file I/O. + * Full diff capture is deferred to export time (see chatExportZip.ts). */ export class ChatRepoInfoContribution extends Disposable implements IWorkbenchContribution { @@ -576,7 +653,6 @@ export class ChatRepoInfoContribution extends Disposable implements IWorkbenchCo @IChatService private readonly chatService: IChatService, @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, @ISCMService private readonly scmService: ISCMService, - @IFileService private readonly fileService: IFileService, @ILogService private readonly logService: ILogService, @IConfigurationService private readonly configurationService: IConfigurationService, ) { @@ -586,12 +662,12 @@ export class ChatRepoInfoContribution extends Disposable implements IWorkbenchCo this.registerConfigurationIfInternal(); })); - this._register(this.chatService.onDidSubmitRequest(async ({ chatSessionResource }) => { + this._register(this.chatService.onDidSubmitRequest(({ chatSessionResource }) => { const model = this.chatService.getSession(chatSessionResource); if (!model) { return; } - await this.captureAndSetRepoData(model); + this.captureAndSetRepoMetadata(model); })); } @@ -612,7 +688,7 @@ export class ChatRepoInfoContribution extends Disposable implements IWorkbenchCo properties: { [ChatConfiguration.RepoInfoEnabled]: { type: 'boolean', - description: nls.localize('chat.repoInfo.enabled', "Controls whether repository information (branch, commit, working tree diffs) is captured at the start of chat sessions for internal diagnostics."), + description: nls.localize('chat.repoInfo.enabled', "Controls whether lightweight repository metadata (branch, commit, remotes) is captured when a chat request is submitted for internal diagnostics."), default: false, } } @@ -622,12 +698,15 @@ export class ChatRepoInfoContribution extends Disposable implements IWorkbenchCo this.logService.debug('[ChatRepoInfo] Configuration registered for internal user'); } - private async captureAndSetRepoData(model: IChatModel): Promise { + /** + * Captures lightweight metadata (branch, commit, remote refs) on first message. + * Synchronous, no file I/O. Reads only from SCM provider observables. + */ + private captureAndSetRepoMetadata(model: IChatModel): void { if (!this.chatEntitlementService.isInternal) { return; } - // Check if repo info capture is enabled via configuration if (!this.configurationService.getValue(ChatConfiguration.RepoInfoEnabled)) { return; } @@ -637,55 +716,17 @@ export class ChatRepoInfoContribution extends Disposable implements IWorkbenchCo } try { - const repoData = await captureRepoInfo(this.scmService, this.fileService); - if (repoData) { - model.setRepoData(repoData); - if (!repoData.localHeadCommit && repoData.workspaceType !== 'plain-folder') { - this.logService.warn('[ChatRepoInfo] Captured repo data without commit hash - git history may not be ready'); + const metadata = captureRepoMetadata(this.scmService); + if (metadata) { + model.setRepoData(metadata); + if (!metadata.localHeadCommit) { + this.logService.warn('[ChatRepoInfo] Captured repo metadata without commit hash - git history may not be ready'); } - - // Trim diffs from older sessions to manage storage - this.trimOldSessionDiffs(); } else { this.logService.debug('[ChatRepoInfo] No SCM repository available for chat session'); } } catch (error) { - this.logService.warn('[ChatRepoInfo] Failed to capture repo info:', error); - } - } - - /** - * Trims diffs from older sessions, keeping full diffs only for the most recent sessions. - */ - private trimOldSessionDiffs(): void { - try { - // Get all sessions with repoData that has diffs - const sessionsWithDiffs: { model: IChatModel; timestamp: number }[] = []; - - for (const model of this.chatService.chatModels.get()) { - if (model.repoData?.diffs && model.repoData.diffs.length > 0 && model.repoData.diffsStatus === 'included') { - sessionsWithDiffs.push({ model, timestamp: model.timestamp }); - } - } - - // Sort by timestamp descending (most recent first) - sessionsWithDiffs.sort((a, b) => b.timestamp - a.timestamp); - - // Trim diffs from sessions beyond the limit - for (let i = MAX_SESSIONS_WITH_FULL_DIFFS; i < sessionsWithDiffs.length; i++) { - const { model } = sessionsWithDiffs[i]; - if (model.repoData) { - const trimmedRepoData: IExportableRepoData = { - ...model.repoData, - diffs: undefined, - diffsStatus: 'trimmedForStorage' - }; - model.setRepoData(trimmedRepoData); - this.logService.trace(`[ChatRepoInfo] Trimmed diffs from older session: ${model.sessionResource.toString()}`); - } - } - } catch (error) { - this.logService.warn('[ChatRepoInfo] Failed to trim old session diffs:', error); + this.logService.warn('[ChatRepoInfo] Failed to capture repo metadata:', error); } } } diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index e804c91874405..c5d858de46c67 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -2652,7 +2652,6 @@ export class ChatModel extends Disposable implements IChatModel { creationDate: this._timestamp, customTitle: this._customTitle, inputState: this.inputModel.toJSON(), - repoData: this._repoData, }; } diff --git a/src/vs/workbench/contrib/chat/electron-browser/actions/chatExportZip.ts b/src/vs/workbench/contrib/chat/electron-browser/actions/chatExportZip.ts index 7de3b5ea61e8d..a32ee2b588aeb 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/actions/chatExportZip.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/actions/chatExportZip.ts @@ -7,7 +7,6 @@ import { joinPath } from '../../../../../base/common/resources.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { localize, localize2 } from '../../../../../nls.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; @@ -19,7 +18,6 @@ import { IChatWidgetService } from '../../browser/chat.js'; import { captureRepoInfo } from '../../browser/chatRepoInfo.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { IChatService } from '../../common/chatService/chatService.js'; -import { ChatConfiguration } from '../../common/constants.js'; import { ISCMService } from '../../../scm/common/scm.js'; export function registerChatExportZipAction() { @@ -42,9 +40,6 @@ export function registerChatExportZipAction() { const notificationService = accessor.get(INotificationService); const scmService = accessor.get(ISCMService); const fileService = accessor.get(IFileService); - const configurationService = accessor.get(IConfigurationService); - - const repoInfoEnabled = configurationService.getValue(ChatConfiguration.RepoInfoEnabled) ?? false; const widget = widgetService.lastFocusedWidget; if (!widget || !widget.viewModel) { @@ -83,36 +78,32 @@ export function registerChatExportZipAction() { }); } - if (repoInfoEnabled) { - const currentRepoData = await captureRepoInfo(scmService, fileService); - if (currentRepoData) { - files.push({ - path: 'chat.repo.end.json', - contents: JSON.stringify(currentRepoData, undefined, 2) - }); - } + const currentRepoData = await captureRepoInfo(scmService, fileService); + if (currentRepoData) { + files.push({ + path: 'chat.repo.end.json', + contents: JSON.stringify(currentRepoData, undefined, 2) + }); + } - if (!model.repoData && !currentRepoData) { - notificationService.notify({ - severity: Severity.Warning, - message: localize('chatExportZip.noRepoData', "Exported chat without repository context. No Git repository was detected.") - }); - } + if (!model.repoData && !currentRepoData) { + notificationService.notify({ + severity: Severity.Warning, + message: localize('chatExportZip.noRepoData', "Exported chat without repository context. No Git repository was detected.") + }); } } else { - if (repoInfoEnabled) { - const currentRepoData = await captureRepoInfo(scmService, fileService); - if (currentRepoData) { - files.push({ - path: 'chat.repo.begin.json', - contents: JSON.stringify(currentRepoData, undefined, 2) - }); - } else { - notificationService.notify({ - severity: Severity.Warning, - message: localize('chatExportZip.noRepoData', "Exported chat without repository context. No Git repository was detected.") - }); - } + const currentRepoData = await captureRepoInfo(scmService, fileService); + if (currentRepoData) { + files.push({ + path: 'chat.repo.begin.json', + contents: JSON.stringify(currentRepoData, undefined, 2) + }); + } else { + notificationService.notify({ + severity: Severity.Warning, + message: localize('chatExportZip.noRepoData', "Exported chat without repository context. No Git repository was detected.") + }); } } From e5895d4db74ed3177053d2a3701e4eb7aa142cfe Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 18 Feb 2026 09:38:29 +0100 Subject: [PATCH 09/13] :up: distro (#295946) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7ee40bb9d7ebb..b29648bcd2c7a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.110.0", - "distro": "d5c0e77a0214208f36b56d42e8e787de88d02ea4", + "distro": "3dc22e559f766d4c609a675d88819589e9876d9c", "author": { "name": "Microsoft Corporation" }, From 2500a2c880f61bb9c3c5c7b2951bc0f91798c336 Mon Sep 17 00:00:00 2001 From: Prasanth Pulavarthi Date: Wed, 18 Feb 2026 00:39:46 -0800 Subject: [PATCH 10/13] A/B experiment: close button vs Skip for now on sign-in dialog (#295867) A/B experiment: close button vs Skip for now button on sign-in dialog Add experiment 'chatSetupDialogCloseButton' using IWorkbenchAssignmentService: - Treatment (true): show standard X close button, hide Skip for now button - Control (default): show Skip for now button, hide X close button Both variants produce the same 'failedMaybeLater' telemetry on dismiss. Test locally via: experiments.override.chatSetupDialogCloseButton: true --- .../chat/browser/chatSetup/chatSetupRunner.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupRunner.ts b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupRunner.ts index 9929aa7064e18..8922f2cbf35d3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupRunner.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupRunner.ts @@ -31,6 +31,7 @@ import { ChatSetupController } from './chatSetupController.js'; import { IChatSetupResult, ChatSetupAnonymous, InstallChatEvent, InstallChatClassification, ChatSetupStrategy, ChatSetupResultValue } from './chatSetup.js'; import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; import { IHostService } from '../../../../services/host/browser/host.js'; +import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; const defaultChat = { publicCodeMatchesUrl: product.defaultChatAgent?.publicCodeMatchesUrl ?? '', @@ -49,7 +50,7 @@ export class ChatSetup { let instance = ChatSetup.instance; if (!instance) { instance = ChatSetup.instance = instantiationService.invokeFunction(accessor => { - return new ChatSetup(context, controller, accessor.get(ITelemetryService), accessor.get(IWorkbenchLayoutService), accessor.get(IKeybindingService), accessor.get(IChatEntitlementService) as ChatEntitlementService, accessor.get(ILogService), accessor.get(IChatWidgetService), accessor.get(IWorkspaceTrustRequestService), accessor.get(IMarkdownRendererService), accessor.get(IDefaultAccountService), accessor.get(IHostService)); + return new ChatSetup(context, controller, accessor.get(ITelemetryService), accessor.get(IWorkbenchLayoutService), accessor.get(IKeybindingService), accessor.get(IChatEntitlementService) as ChatEntitlementService, accessor.get(ILogService), accessor.get(IChatWidgetService), accessor.get(IWorkspaceTrustRequestService), accessor.get(IMarkdownRendererService), accessor.get(IDefaultAccountService), accessor.get(IHostService), accessor.get(IWorkbenchAssignmentService)); }); } @@ -72,7 +73,8 @@ export class ChatSetup { @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, @IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService, @IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService, - @IHostService private readonly hostService: IHostService + @IHostService private readonly hostService: IHostService, + @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, ) { } skipDialog(): void { @@ -162,7 +164,8 @@ export class ChatSetup { private async showDialog(options?: { forceSignInDialog?: boolean; forceAnonymous?: ChatSetupAnonymous }): Promise { const disposables = new DisposableStore(); - const buttons = this.getButtons(options); + const useCloseButton = await this.experimentService.getTreatment('chatSetupDialogCloseButton'); + const buttons = this.getButtons(options, useCloseButton); const dialog = disposables.add(new Dialog( this.layoutService.activeContainer, @@ -174,8 +177,8 @@ export class ChatSetup { detail: ' ', // workaround allowing us to render the message in large icon: Codicon.copilotLarge, alignment: DialogContentsAlignment.Vertical, - cancelId: buttons.length - 1, - disableCloseButton: true, + cancelId: useCloseButton ? buttons.length : buttons.length - 1, + disableCloseButton: !useCloseButton, renderFooter: footer => footer.appendChild(this.createDialogFooter(disposables, options)), buttonOptions: buttons.map(button => button[2]) }, this.keybindingService, this.layoutService, this.hostService) @@ -187,7 +190,7 @@ export class ChatSetup { return buttons[button]?.[1] ?? ChatSetupStrategy.Canceled; } - private getButtons(options?: { forceSignInDialog?: boolean; forceAnonymous?: ChatSetupAnonymous }): Array<[string, ChatSetupStrategy, { styleButton?: (button: IButton) => void } | undefined]> { + private getButtons(options?: { forceSignInDialog?: boolean; forceAnonymous?: ChatSetupAnonymous }, useCloseButton?: boolean): 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) }); @@ -221,7 +224,9 @@ export class ChatSetup { buttons = [[localize('setupAIButton', "Use AI Features"), ChatSetupStrategy.DefaultSetup, undefined]]; } - buttons.push([localize('skipForNow', "Skip for now"), ChatSetupStrategy.Canceled, styleButton('link-button', 'skip-button')]); + if (!useCloseButton) { + buttons.push([localize('skipForNow', "Skip for now"), ChatSetupStrategy.Canceled, styleButton('link-button', 'skip-button')]); + } return buttons; } From 1874f55074600e40785cc447b6ecc1bca38f7565 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 18 Feb 2026 10:44:30 +0100 Subject: [PATCH 11/13] Potential fix for https://github.com/microsoft/vscode/issues/295379 (#295967) --- .../browser/model/inlineSuggestionItem.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts index 475902a836e4c..68e5f4502e216 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts @@ -513,6 +513,8 @@ export class InlineEditItem extends InlineSuggestionItemBase { let inlineEditModelVersion = this._inlineEditModelVersion; let newAction: InlineSuggestionAction | undefined; + const updatedTarget = TextModelValueReference.snapshot(textModel); + if (this.action?.kind === 'edit') { // TODO What about rename? edits = edits.map(innerEdit => innerEdit.applyTextModelChanges(textModelChanges)); @@ -546,7 +548,7 @@ export class InlineEditItem extends InlineSuggestionItemBase { snippetInfo: this.snippetInfo, stringEdit: newEdit, alternativeAction: this.action.alternativeAction, - target: this.originalTextRef, + target: updatedTarget, }; } else if (this.action?.kind === 'jumpTo') { const jumpToOffset = this.action.offset; @@ -560,7 +562,7 @@ export class InlineEditItem extends InlineSuggestionItemBase { kind: 'jumpTo', position: newJumpToPosition, offset: newJumpToOffset, - target: this.originalTextRef, + target: updatedTarget, }; } else { newAction = undefined; @@ -582,7 +584,7 @@ export class InlineEditItem extends InlineSuggestionItemBase { newDisplayLocation, lastChangePartOfInlineEdit, inlineEditModelVersion, - this.originalTextRef, + updatedTarget, ); } From 9ae007d1bb19a9d6a509276b9574dab62429f074 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 18 Feb 2026 10:53:11 +0100 Subject: [PATCH 12/13] Prevent static contribution API file from getting line ending changes (#295962) --- build/lib/compilation.ts | 12 +++++++++++- build/lib/extractExtensionPoints.ts | 13 +++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index 0d6209e66c862..047e83eb7ff2b 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -368,8 +368,18 @@ function generateExtensionPointNames() { }, function () { collectedNames.sort(); const content = JSON.stringify(collectedNames, undefined, '\t') + '\n'; + const filePath = 'vs/workbench/services/extensions/common/extensionPoints.json'; + try { + const existing = fs.readFileSync(path.join('src', filePath), 'utf-8'); + if (existing.replace(/\r\n/g, '\n') === content) { + this.emit('end'); + return; + } + } catch { + // File doesn't exist yet, emit it + } this.emit('data', new File({ - path: 'vs/workbench/services/extensions/common/extensionPoints.json', + path: filePath, contents: Buffer.from(content) })); this.emit('end'); diff --git a/build/lib/extractExtensionPoints.ts b/build/lib/extractExtensionPoints.ts index 1c442564026b3..97dbda0645222 100644 --- a/build/lib/extractExtensionPoints.ts +++ b/build/lib/extractExtensionPoints.ts @@ -211,10 +211,23 @@ function scanDirectory(dir: string): string[] { return names; } +function normalize(s: string): string { + return s.replace(/\r\n/g, '\n'); +} + function main(): void { const names = scanDirectory(path.join(srcDir, 'vs', 'workbench')); names.sort(); const output = JSON.stringify(names, undefined, '\t') + '\n'; + try { + const existing = fs.readFileSync(outputPath, 'utf-8'); + if (normalize(existing) === normalize(output)) { + console.log(`No changes to ${path.relative(rootDir, outputPath)}`); + return; + } + } catch { + // File doesn't exist yet, write it + } fs.writeFileSync(outputPath, output, 'utf-8'); console.log(`Wrote ${names.length} extension points to ${path.relative(rootDir, outputPath)}`); } From 4a2f9d4ad6194299f004a7ea94ec654db53fc849 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 18 Feb 2026 11:10:12 +0100 Subject: [PATCH 13/13] chore - comment out session exclusion in settings (#295971) --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 0a58b8a5bf849..7307607bef73f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -70,7 +70,7 @@ "test/smoke/out/**": true, "test/automation/out/**": true, "test/integration/browser/out/**": true, - "src/vs/sessions/**": true + // "src/vs/sessions/**": true }, // --- Search --- "search.exclude": {