From b859b7de3a53a04ac13a20fe0d101ff1e287d533 Mon Sep 17 00:00:00 2001 From: Bryan Chen Date: Mon, 12 Jan 2026 15:22:08 -0800 Subject: [PATCH 001/387] fix: prevent setting viewport when there are no visible lines --- src/vs/editor/common/viewModel/viewModelImpl.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 3fab2ddee2e..30a60796b1c 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -768,6 +768,10 @@ export class ViewModel extends Disposable implements IViewModel { * Gives a hint that a lot of requests are about to come in for these line numbers. */ public setViewport(startLineNumber: number, endLineNumber: number, centeredLineNumber: number): void { + if (this._lines.getViewLineCount() === 0) { + // No visible lines to set viewport on + return; + } this._viewportStart.update(this, startLineNumber); } From 9c0fffb260d4834e196fd262a1de16365b0b9a7d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Jan 2026 07:03:38 -0800 Subject: [PATCH 002/387] @xterm/xterm@6.1.0-beta.102 Part of #286870 (main) --- 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 +++---- 6 files changed, 169 insertions(+), 169 deletions(-) diff --git a/package-lock.json b/package-lock.json index c5874a4622b..d663bdb5e60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,16 +27,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.101", - "@xterm/addon-image": "^0.10.0-beta.101", - "@xterm/addon-ligatures": "^0.11.0-beta.101", - "@xterm/addon-progress": "^0.3.0-beta.101", - "@xterm/addon-search": "^0.17.0-beta.101", - "@xterm/addon-serialize": "^0.15.0-beta.101", - "@xterm/addon-unicode11": "^0.10.0-beta.101", - "@xterm/addon-webgl": "^0.20.0-beta.100", - "@xterm/headless": "^6.1.0-beta.101", - "@xterm/xterm": "^6.1.0-beta.101", + "@xterm/addon-clipboard": "^0.3.0-beta.102", + "@xterm/addon-image": "^0.10.0-beta.102", + "@xterm/addon-ligatures": "^0.11.0-beta.102", + "@xterm/addon-progress": "^0.3.0-beta.102", + "@xterm/addon-search": "^0.17.0-beta.102", + "@xterm/addon-serialize": "^0.15.0-beta.102", + "@xterm/addon-unicode11": "^0.10.0-beta.102", + "@xterm/addon-webgl": "^0.20.0-beta.101", + "@xterm/headless": "^6.1.0-beta.102", + "@xterm/xterm": "^6.1.0-beta.102", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -3809,30 +3809,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.101.tgz", - "integrity": "sha512-xuEqMUlvC6UR4HEa1OHSgF0LUEH7K5rS0fYjMJ9Tj/9Fsb84Z9LWwk5O5kYB4njEToX+mbm78Dhy7huXUcs8Ug==", + "version": "0.3.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.102.tgz", + "integrity": "sha512-VIqI/GP/OF4XX2nZaub3HJ59ysfR/t1BTy9695zgTecmX+RXPqp4s4rPmy3yv1k1MK/fqPu/HITGHjGvDZgKdg==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.101.tgz", - "integrity": "sha512-Gd8ZpfyzvisG+08+mXynufQHfaWWxGhhtRMSQXV2FyPNa3MNXNrowgjeXhpaRObOOsxSZnAlB8qDW8OHTjzG6A==", + "version": "0.10.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.102.tgz", + "integrity": "sha512-1zoOF08yrtXyK8YDd9h0PA7IFoaTyygV0Cip2/6fQb7j9QD6TMrqWsNC+g8T8yfLB31q6NS5CnG+TELVoNlfnw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.101.tgz", - "integrity": "sha512-FIO3S/f3K1nnmQs/oJ3ILI9p1Vb4sSK7J4UhROBj5JOyZtmRwhdUFr9MozfPVdc2VZPRJJJJq6vaPDdLioeJyQ==", + "version": "0.11.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.102.tgz", + "integrity": "sha512-P6o8BBv4+gaAp7JNsiHyoMb1NKkW/KSMJyY6H7nhgUMtY6dT+skp3fbIkzP2M57XOKziLK9K3oRB0OcHjivrHQ==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -3842,7 +3842,7 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-ligatures/node_modules/lru-cache": { @@ -3864,63 +3864,63 @@ "license": "ISC" }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.101.tgz", - "integrity": "sha512-QsJLvH3tc9KywOXb9O/mxkPoYmL2cCvKUa/YckriuZmEh5WMw+3cgNa+BC0aA73htdWE8UMUBvigE786KErajA==", + "version": "0.3.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.102.tgz", + "integrity": "sha512-Mcsse/9drif/4OuzB10SpkiWmXJObWs+wDPRYtgH9uX4cK31Z2Y+Fm6UMnsDw9taIRMmizhhKYbgR+CBJGSoZA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.101.tgz", - "integrity": "sha512-Y1yWv2baZdqP6AH/aKmMXSs6VNr7FGrpOe+DKNKq77H+QZbUoUXfgI/qKZpqlByl7FGuo8OT9g0AqH3zykkkUw==", + "version": "0.17.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.102.tgz", + "integrity": "sha512-61XYIk/Mbxc1gEL4pBVSTbxu1IV0kLtNrc8JHqp7rgVWKyNQMVKwovMUuzx2yR4y1h/Obu+P+OS06zsbrExg8A==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.101.tgz", - "integrity": "sha512-NJXY4WBV9tG2IG85xMlmUX6YFIa63NO0qF+JzDM9+3AaNDEmTXd+Lg5ucWtJjZRDW3AHUOLAQos4osLvBfyhYQ==", + "version": "0.15.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.102.tgz", + "integrity": "sha512-fm5UA65Lk+ScucYtuZRsuFWI2MAUNYUh3w+9qOlfJRnoxP0PgsiCxXgXO6T6zB3/K9tO3De/X7PcfKvgP6zPZg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.101.tgz", - "integrity": "sha512-WM9Rs8ZBQ1nY5nb4OxXsOLVIZ2DQvGmtSIKkUueDB9mSQOg05mz2dHbEdW63MrX8sMQAbEx+o/kyxvl7oFDS/A==", + "version": "0.10.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.102.tgz", + "integrity": "sha512-+h/tIFEkGbTSlKCM9u3+PWs2P3bQWDYshy3JeJ+kYzgKdsCqfu8PNdQDekVVT/SeqW17hpQKl2OWHfIsNix9Xw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.100", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.100.tgz", - "integrity": "sha512-h17XiyERE+LpuYEPUAm2ux6g2Iy34BT/tfwxOckZ+RrhjM8bZMeN3u6/q28viBqAKWhhD3JbxlcDfKMN8sE3sg==", + "version": "0.20.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.101.tgz", + "integrity": "sha512-X6u76IQ/yFkWgX0Qh7OjdHJzDB/Wu03ZfMTRb/ipUCx7J0w/6Xs8TQ/wqSCLyE8JeZC/Pshhrg+acUcJLwC8Mw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/headless": { - "version": "6.1.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.101.tgz", - "integrity": "sha512-m+5Gyiy72wry8wJQPueUojcF8bMzK983owwOCyFp0I6qrHk+VuKh84FQXAvq5Gu9C0irL99iP5K54xGjDZ7Zkg==", + "version": "6.1.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.102.tgz", + "integrity": "sha512-aiuSRacrnCHf1a/eMlB+3wRhLJ4Iy4NMPVCnHbB0KV+eIoMIGmdUAXJvrFkm2Qd4mF4wXlb6okdtFievi+g5Fw==", "license": "MIT", "workspaces": [ "addons/*" ] }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.101.tgz", - "integrity": "sha512-fINmTdz6WkLkMkwwpwuWu4h1gn4uQVnnJJsdKYy+Wwr7mzazx2uK22Lrgc6B/2AZZjw+CfK2mkK5+AuR5J2YRw==", + "version": "6.1.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.102.tgz", + "integrity": "sha512-53vBNI1onToMiVCxh+pq1QkS8w3fEdeGfN/wp76GitHNgkLSDeTghGITsHeXGoEEgbnhR7+HhX4b5TGdN6u5vw==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/package.json b/package.json index a4a014d7ae2..ba3477b0565 100644 --- a/package.json +++ b/package.json @@ -90,16 +90,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.101", - "@xterm/addon-image": "^0.10.0-beta.101", - "@xterm/addon-ligatures": "^0.11.0-beta.101", - "@xterm/addon-progress": "^0.3.0-beta.101", - "@xterm/addon-search": "^0.17.0-beta.101", - "@xterm/addon-serialize": "^0.15.0-beta.101", - "@xterm/addon-unicode11": "^0.10.0-beta.101", - "@xterm/addon-webgl": "^0.20.0-beta.100", - "@xterm/headless": "^6.1.0-beta.101", - "@xterm/xterm": "^6.1.0-beta.101", + "@xterm/addon-clipboard": "^0.3.0-beta.102", + "@xterm/addon-image": "^0.10.0-beta.102", + "@xterm/addon-ligatures": "^0.11.0-beta.102", + "@xterm/addon-progress": "^0.3.0-beta.102", + "@xterm/addon-search": "^0.17.0-beta.102", + "@xterm/addon-serialize": "^0.15.0-beta.102", + "@xterm/addon-unicode11": "^0.10.0-beta.102", + "@xterm/addon-webgl": "^0.20.0-beta.101", + "@xterm/headless": "^6.1.0-beta.102", + "@xterm/xterm": "^6.1.0-beta.102", "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 44adad2a826..0508f773eac 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -20,16 +20,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.101", - "@xterm/addon-image": "^0.10.0-beta.101", - "@xterm/addon-ligatures": "^0.11.0-beta.101", - "@xterm/addon-progress": "^0.3.0-beta.101", - "@xterm/addon-search": "^0.17.0-beta.101", - "@xterm/addon-serialize": "^0.15.0-beta.101", - "@xterm/addon-unicode11": "^0.10.0-beta.101", - "@xterm/addon-webgl": "^0.20.0-beta.100", - "@xterm/headless": "^6.1.0-beta.101", - "@xterm/xterm": "^6.1.0-beta.101", + "@xterm/addon-clipboard": "^0.3.0-beta.102", + "@xterm/addon-image": "^0.10.0-beta.102", + "@xterm/addon-ligatures": "^0.11.0-beta.102", + "@xterm/addon-progress": "^0.3.0-beta.102", + "@xterm/addon-search": "^0.17.0-beta.102", + "@xterm/addon-serialize": "^0.15.0-beta.102", + "@xterm/addon-unicode11": "^0.10.0-beta.102", + "@xterm/addon-webgl": "^0.20.0-beta.101", + "@xterm/headless": "^6.1.0-beta.102", + "@xterm/xterm": "^6.1.0-beta.102", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -518,30 +518,30 @@ "license": "MIT" }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.101.tgz", - "integrity": "sha512-xuEqMUlvC6UR4HEa1OHSgF0LUEH7K5rS0fYjMJ9Tj/9Fsb84Z9LWwk5O5kYB4njEToX+mbm78Dhy7huXUcs8Ug==", + "version": "0.3.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.102.tgz", + "integrity": "sha512-VIqI/GP/OF4XX2nZaub3HJ59ysfR/t1BTy9695zgTecmX+RXPqp4s4rPmy3yv1k1MK/fqPu/HITGHjGvDZgKdg==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.101.tgz", - "integrity": "sha512-Gd8ZpfyzvisG+08+mXynufQHfaWWxGhhtRMSQXV2FyPNa3MNXNrowgjeXhpaRObOOsxSZnAlB8qDW8OHTjzG6A==", + "version": "0.10.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.102.tgz", + "integrity": "sha512-1zoOF08yrtXyK8YDd9h0PA7IFoaTyygV0Cip2/6fQb7j9QD6TMrqWsNC+g8T8yfLB31q6NS5CnG+TELVoNlfnw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.101.tgz", - "integrity": "sha512-FIO3S/f3K1nnmQs/oJ3ILI9p1Vb4sSK7J4UhROBj5JOyZtmRwhdUFr9MozfPVdc2VZPRJJJJq6vaPDdLioeJyQ==", + "version": "0.11.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.102.tgz", + "integrity": "sha512-P6o8BBv4+gaAp7JNsiHyoMb1NKkW/KSMJyY6H7nhgUMtY6dT+skp3fbIkzP2M57XOKziLK9K3oRB0OcHjivrHQ==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -551,67 +551,67 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.101.tgz", - "integrity": "sha512-QsJLvH3tc9KywOXb9O/mxkPoYmL2cCvKUa/YckriuZmEh5WMw+3cgNa+BC0aA73htdWE8UMUBvigE786KErajA==", + "version": "0.3.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.102.tgz", + "integrity": "sha512-Mcsse/9drif/4OuzB10SpkiWmXJObWs+wDPRYtgH9uX4cK31Z2Y+Fm6UMnsDw9taIRMmizhhKYbgR+CBJGSoZA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.101.tgz", - "integrity": "sha512-Y1yWv2baZdqP6AH/aKmMXSs6VNr7FGrpOe+DKNKq77H+QZbUoUXfgI/qKZpqlByl7FGuo8OT9g0AqH3zykkkUw==", + "version": "0.17.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.102.tgz", + "integrity": "sha512-61XYIk/Mbxc1gEL4pBVSTbxu1IV0kLtNrc8JHqp7rgVWKyNQMVKwovMUuzx2yR4y1h/Obu+P+OS06zsbrExg8A==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.101.tgz", - "integrity": "sha512-NJXY4WBV9tG2IG85xMlmUX6YFIa63NO0qF+JzDM9+3AaNDEmTXd+Lg5ucWtJjZRDW3AHUOLAQos4osLvBfyhYQ==", + "version": "0.15.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.102.tgz", + "integrity": "sha512-fm5UA65Lk+ScucYtuZRsuFWI2MAUNYUh3w+9qOlfJRnoxP0PgsiCxXgXO6T6zB3/K9tO3De/X7PcfKvgP6zPZg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.101.tgz", - "integrity": "sha512-WM9Rs8ZBQ1nY5nb4OxXsOLVIZ2DQvGmtSIKkUueDB9mSQOg05mz2dHbEdW63MrX8sMQAbEx+o/kyxvl7oFDS/A==", + "version": "0.10.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.102.tgz", + "integrity": "sha512-+h/tIFEkGbTSlKCM9u3+PWs2P3bQWDYshy3JeJ+kYzgKdsCqfu8PNdQDekVVT/SeqW17hpQKl2OWHfIsNix9Xw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.100", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.100.tgz", - "integrity": "sha512-h17XiyERE+LpuYEPUAm2ux6g2Iy34BT/tfwxOckZ+RrhjM8bZMeN3u6/q28viBqAKWhhD3JbxlcDfKMN8sE3sg==", + "version": "0.20.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.101.tgz", + "integrity": "sha512-X6u76IQ/yFkWgX0Qh7OjdHJzDB/Wu03ZfMTRb/ipUCx7J0w/6Xs8TQ/wqSCLyE8JeZC/Pshhrg+acUcJLwC8Mw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/headless": { - "version": "6.1.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.101.tgz", - "integrity": "sha512-m+5Gyiy72wry8wJQPueUojcF8bMzK983owwOCyFp0I6qrHk+VuKh84FQXAvq5Gu9C0irL99iP5K54xGjDZ7Zkg==", + "version": "6.1.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.102.tgz", + "integrity": "sha512-aiuSRacrnCHf1a/eMlB+3wRhLJ4Iy4NMPVCnHbB0KV+eIoMIGmdUAXJvrFkm2Qd4mF4wXlb6okdtFievi+g5Fw==", "license": "MIT", "workspaces": [ "addons/*" ] }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.101.tgz", - "integrity": "sha512-fINmTdz6WkLkMkwwpwuWu4h1gn4uQVnnJJsdKYy+Wwr7mzazx2uK22Lrgc6B/2AZZjw+CfK2mkK5+AuR5J2YRw==", + "version": "6.1.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.102.tgz", + "integrity": "sha512-53vBNI1onToMiVCxh+pq1QkS8w3fEdeGfN/wp76GitHNgkLSDeTghGITsHeXGoEEgbnhR7+HhX4b5TGdN6u5vw==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/remote/package.json b/remote/package.json index 479adcd5410..35ae9dba303 100644 --- a/remote/package.json +++ b/remote/package.json @@ -15,16 +15,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.101", - "@xterm/addon-image": "^0.10.0-beta.101", - "@xterm/addon-ligatures": "^0.11.0-beta.101", - "@xterm/addon-progress": "^0.3.0-beta.101", - "@xterm/addon-search": "^0.17.0-beta.101", - "@xterm/addon-serialize": "^0.15.0-beta.101", - "@xterm/addon-unicode11": "^0.10.0-beta.101", - "@xterm/addon-webgl": "^0.20.0-beta.100", - "@xterm/headless": "^6.1.0-beta.101", - "@xterm/xterm": "^6.1.0-beta.101", + "@xterm/addon-clipboard": "^0.3.0-beta.102", + "@xterm/addon-image": "^0.10.0-beta.102", + "@xterm/addon-ligatures": "^0.11.0-beta.102", + "@xterm/addon-progress": "^0.3.0-beta.102", + "@xterm/addon-search": "^0.17.0-beta.102", + "@xterm/addon-serialize": "^0.15.0-beta.102", + "@xterm/addon-unicode11": "^0.10.0-beta.102", + "@xterm/addon-webgl": "^0.20.0-beta.101", + "@xterm/headless": "^6.1.0-beta.102", + "@xterm/xterm": "^6.1.0-beta.102", "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 6fef77cf22c..c1f40004d83 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -13,15 +13,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.101", - "@xterm/addon-image": "^0.10.0-beta.101", - "@xterm/addon-ligatures": "^0.11.0-beta.101", - "@xterm/addon-progress": "^0.3.0-beta.101", - "@xterm/addon-search": "^0.17.0-beta.101", - "@xterm/addon-serialize": "^0.15.0-beta.101", - "@xterm/addon-unicode11": "^0.10.0-beta.101", - "@xterm/addon-webgl": "^0.20.0-beta.100", - "@xterm/xterm": "^6.1.0-beta.101", + "@xterm/addon-clipboard": "^0.3.0-beta.102", + "@xterm/addon-image": "^0.10.0-beta.102", + "@xterm/addon-ligatures": "^0.11.0-beta.102", + "@xterm/addon-progress": "^0.3.0-beta.102", + "@xterm/addon-search": "^0.17.0-beta.102", + "@xterm/addon-serialize": "^0.15.0-beta.102", + "@xterm/addon-unicode11": "^0.10.0-beta.102", + "@xterm/addon-webgl": "^0.20.0-beta.101", + "@xterm/xterm": "^6.1.0-beta.102", "jschardet": "3.1.4", "katex": "^0.16.22", "tas-client": "0.3.1", @@ -92,30 +92,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.101.tgz", - "integrity": "sha512-xuEqMUlvC6UR4HEa1OHSgF0LUEH7K5rS0fYjMJ9Tj/9Fsb84Z9LWwk5O5kYB4njEToX+mbm78Dhy7huXUcs8Ug==", + "version": "0.3.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.102.tgz", + "integrity": "sha512-VIqI/GP/OF4XX2nZaub3HJ59ysfR/t1BTy9695zgTecmX+RXPqp4s4rPmy3yv1k1MK/fqPu/HITGHjGvDZgKdg==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.101.tgz", - "integrity": "sha512-Gd8ZpfyzvisG+08+mXynufQHfaWWxGhhtRMSQXV2FyPNa3MNXNrowgjeXhpaRObOOsxSZnAlB8qDW8OHTjzG6A==", + "version": "0.10.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.102.tgz", + "integrity": "sha512-1zoOF08yrtXyK8YDd9h0PA7IFoaTyygV0Cip2/6fQb7j9QD6TMrqWsNC+g8T8yfLB31q6NS5CnG+TELVoNlfnw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.101.tgz", - "integrity": "sha512-FIO3S/f3K1nnmQs/oJ3ILI9p1Vb4sSK7J4UhROBj5JOyZtmRwhdUFr9MozfPVdc2VZPRJJJJq6vaPDdLioeJyQ==", + "version": "0.11.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.102.tgz", + "integrity": "sha512-P6o8BBv4+gaAp7JNsiHyoMb1NKkW/KSMJyY6H7nhgUMtY6dT+skp3fbIkzP2M57XOKziLK9K3oRB0OcHjivrHQ==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -125,58 +125,58 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.101.tgz", - "integrity": "sha512-QsJLvH3tc9KywOXb9O/mxkPoYmL2cCvKUa/YckriuZmEh5WMw+3cgNa+BC0aA73htdWE8UMUBvigE786KErajA==", + "version": "0.3.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.102.tgz", + "integrity": "sha512-Mcsse/9drif/4OuzB10SpkiWmXJObWs+wDPRYtgH9uX4cK31Z2Y+Fm6UMnsDw9taIRMmizhhKYbgR+CBJGSoZA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.101.tgz", - "integrity": "sha512-Y1yWv2baZdqP6AH/aKmMXSs6VNr7FGrpOe+DKNKq77H+QZbUoUXfgI/qKZpqlByl7FGuo8OT9g0AqH3zykkkUw==", + "version": "0.17.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.102.tgz", + "integrity": "sha512-61XYIk/Mbxc1gEL4pBVSTbxu1IV0kLtNrc8JHqp7rgVWKyNQMVKwovMUuzx2yR4y1h/Obu+P+OS06zsbrExg8A==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.101.tgz", - "integrity": "sha512-NJXY4WBV9tG2IG85xMlmUX6YFIa63NO0qF+JzDM9+3AaNDEmTXd+Lg5ucWtJjZRDW3AHUOLAQos4osLvBfyhYQ==", + "version": "0.15.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.102.tgz", + "integrity": "sha512-fm5UA65Lk+ScucYtuZRsuFWI2MAUNYUh3w+9qOlfJRnoxP0PgsiCxXgXO6T6zB3/K9tO3De/X7PcfKvgP6zPZg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.101.tgz", - "integrity": "sha512-WM9Rs8ZBQ1nY5nb4OxXsOLVIZ2DQvGmtSIKkUueDB9mSQOg05mz2dHbEdW63MrX8sMQAbEx+o/kyxvl7oFDS/A==", + "version": "0.10.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.102.tgz", + "integrity": "sha512-+h/tIFEkGbTSlKCM9u3+PWs2P3bQWDYshy3JeJ+kYzgKdsCqfu8PNdQDekVVT/SeqW17hpQKl2OWHfIsNix9Xw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.100", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.100.tgz", - "integrity": "sha512-h17XiyERE+LpuYEPUAm2ux6g2Iy34BT/tfwxOckZ+RrhjM8bZMeN3u6/q28viBqAKWhhD3JbxlcDfKMN8sE3sg==", + "version": "0.20.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.101.tgz", + "integrity": "sha512-X6u76IQ/yFkWgX0Qh7OjdHJzDB/Wu03ZfMTRb/ipUCx7J0w/6Xs8TQ/wqSCLyE8JeZC/Pshhrg+acUcJLwC8Mw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.101" + "@xterm/xterm": "^6.1.0-beta.102" } }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.101.tgz", - "integrity": "sha512-fINmTdz6WkLkMkwwpwuWu4h1gn4uQVnnJJsdKYy+Wwr7mzazx2uK22Lrgc6B/2AZZjw+CfK2mkK5+AuR5J2YRw==", + "version": "6.1.0-beta.102", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.102.tgz", + "integrity": "sha512-53vBNI1onToMiVCxh+pq1QkS8w3fEdeGfN/wp76GitHNgkLSDeTghGITsHeXGoEEgbnhR7+HhX4b5TGdN6u5vw==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/remote/web/package.json b/remote/web/package.json index a90d2e5b957..171f09a5f44 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -8,15 +8,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.101", - "@xterm/addon-image": "^0.10.0-beta.101", - "@xterm/addon-ligatures": "^0.11.0-beta.101", - "@xterm/addon-progress": "^0.3.0-beta.101", - "@xterm/addon-search": "^0.17.0-beta.101", - "@xterm/addon-serialize": "^0.15.0-beta.101", - "@xterm/addon-unicode11": "^0.10.0-beta.101", - "@xterm/addon-webgl": "^0.20.0-beta.100", - "@xterm/xterm": "^6.1.0-beta.101", + "@xterm/addon-clipboard": "^0.3.0-beta.102", + "@xterm/addon-image": "^0.10.0-beta.102", + "@xterm/addon-ligatures": "^0.11.0-beta.102", + "@xterm/addon-progress": "^0.3.0-beta.102", + "@xterm/addon-search": "^0.17.0-beta.102", + "@xterm/addon-serialize": "^0.15.0-beta.102", + "@xterm/addon-unicode11": "^0.10.0-beta.102", + "@xterm/addon-webgl": "^0.20.0-beta.101", + "@xterm/xterm": "^6.1.0-beta.102", "jschardet": "3.1.4", "katex": "^0.16.22", "tas-client": "0.3.1", From 45db6d6e9fa73a75ecff826169ea0cc10f457f07 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 13 Jan 2026 15:53:23 +0000 Subject: [PATCH 003/387] Add 2026 theme files and customization documentation --- extensions/theme-2026/.vscodeignore | 5 + extensions/theme-2026/CUSTOMIZATION.md | 91 ++++++ extensions/theme-2026/README.md | 55 ++++ extensions/theme-2026/cgmanifest.json | 5 + extensions/theme-2026/package.json | 30 ++ extensions/theme-2026/package.nls.json | 6 + extensions/theme-2026/themes/2026-dark.json | 320 +++++++++++++++++++ extensions/theme-2026/themes/2026-light.json | 318 ++++++++++++++++++ 8 files changed, 830 insertions(+) create mode 100644 extensions/theme-2026/.vscodeignore create mode 100644 extensions/theme-2026/CUSTOMIZATION.md create mode 100644 extensions/theme-2026/README.md create mode 100644 extensions/theme-2026/cgmanifest.json create mode 100644 extensions/theme-2026/package.json create mode 100644 extensions/theme-2026/package.nls.json create mode 100644 extensions/theme-2026/themes/2026-dark.json create mode 100644 extensions/theme-2026/themes/2026-light.json diff --git a/extensions/theme-2026/.vscodeignore b/extensions/theme-2026/.vscodeignore new file mode 100644 index 00000000000..7ef29eaaabf --- /dev/null +++ b/extensions/theme-2026/.vscodeignore @@ -0,0 +1,5 @@ +CUSTOMIZATION.md +node_modules/** +.vscode/** +.gitignore +**/*.map diff --git a/extensions/theme-2026/CUSTOMIZATION.md b/extensions/theme-2026/CUSTOMIZATION.md new file mode 100644 index 00000000000..03c07d0e2c0 --- /dev/null +++ b/extensions/theme-2026/CUSTOMIZATION.md @@ -0,0 +1,91 @@ +# Theme Customization Guide + +The 2026 theme supports granular customization through **variables** and **overrides** in the config files. + +## Variables + +Define reusable color values in the `variables` section that can be referenced throughout your entire config: + +```json +{ + "variables": { + "myBlue": "#0066DD", + "myRed": "#DD0000", + "sidebarBg": "#161616" + } +} +``` + +Variables can be used in: +- **Config sections**: `textConfig`, `backgroundConfig`, `editorConfig`, `panelConfig`, `baseColors` +- **Overrides**: Any VS Code theme color property + +## Overrides + +Override any VS Code theme color property in the `overrides` section. You can use: +- Hex colors directly: `"#161616"` +- Variable references: `"${myBlue}"` + +```json +{ + "overrides": { + "sideBar.background": "${sidebarBg}", + "activityBar.background": "#161616", + "statusBar.background": "${myBlue}" + } +} +``` + +## Example Configuration + +**theme.config.dark.json:** + +```json +{ + "paletteScale": 21, + "accentUsage": "interactive-and-status", + ... + "editorConfig": { + "background": "${darkBlue}", + "foreground": "${textPrimary}" + }, + "backgroundConfig": { + "primary": "${primaryBg}", + "secondary": "${secondaryBg}" + }, + "variables": { + "darkBlue": "#001133", + "brightAccent": "#00AAFF", + "primaryBg": "#161616", + "secondaryBg": "#222222", + "textPrimary": "#cccccc" + }, + "overrides": { + "focusBorder": "${brightAccent}", + "button.background": "#007ACC" + } +} +``` + +## Finding Theme Properties + +To find available theme color properties: +1. Open Command Palette (Cmd+Shift+P) +2. Run "Developer: Generate Color Theme From Current Settings" +3. View generated theme to see all available properties + +Or refer to the [VS Code Theme Color Reference](https://code.visualstudio.com/api/references/theme-color). + +## Workflow + +1. Edit `theme.config.dark.json` or `theme.config.light.json` +2. Add variables and overrides sections +3. Run `npm run build` to regenerate themes +4. Reload VS Code to see changes + +## Tips + +- **Variables** help avoid repeating the same color values +- Use **overrides** to fine-tune specific elements without modifying the generator code +- Changes in config files persist across theme updates +- Both variants (light/dark) support independent variables and overrides diff --git a/extensions/theme-2026/README.md b/extensions/theme-2026/README.md new file mode 100644 index 00000000000..59d0ab9b7d5 --- /dev/null +++ b/extensions/theme-2026/README.md @@ -0,0 +1,55 @@ +# 2026 Themes + +Modern, minimal light and dark themes for VS Code with a consistent neutral palette and accessible color contrast. + +> **Note**: These themes are generated using an external theme generator. The source code for the generator is maintained in a separate repository: [vscode-2026-theme-generator](../../../vscode-2026-theme-generator) + +## Design Philosophy + +- **Minimal and modern**: Clean, distraction-free interface +- **Consistent palette**: Limited base colors (5 neutral shades + accent) for visual coherence +- **Accessible**: WCAG AA compliant contrast ratios (minimum 4.5:1 for text) +- **Generated externally**: Themes are generated from a TypeScript-based generator with configurable color palettes + +## Color Palette + +### Light Theme + +| Purpose | Color | Usage | +|---------|-------|-------| +| Text Primary | `#1A1A1A` | Main text content | +| Text Secondary | `#6B6B6B` | Secondary text, line numbers | +| Background Primary | `#FFFFFF` | Main editor background | +| Background Secondary | `#F5F5F5` | Sidebars, inactive tabs | +| Border Default | `#848484` | Component borders | +| Accent | `#0066CC` | Interactive elements, focus states | + +### Dark Theme + +| Purpose | Color | Usage | +|---------|-------|-------| +| Text Primary | `#bbbbbb` | Main text content | +| Text Secondary | `#888888` | Secondary text, line numbers | +| Background Primary | `#191919` | Main editor background | +| Background Secondary | `#242424` | Sidebars, inactive tabs | +| Border Default | `#848484` | Component borders | +| Accent | `#007ACC` | Interactive elements, focus states | + +## Modifying These Themes + +These theme files are **generated** and should not be edited directly. To customize or modify the themes: + +1. Navigate to the theme generator repository (one level up from vscode root) +2. Modify the configuration files (`theme.config.light.json` and `theme.config.dark.json`) +3. Run the generator to create new theme files +4. Copy the generated files back to this directory + +See the [theme generator README](../../../vscode-2026-theme-generator/README.md) for detailed documentation on configuration options, color customization, and the generation process. + +## Accessibility + +All text/background combinations meet WCAG AA standards (4.5:1 contrast ratio minimum). + +## License + +MIT diff --git a/extensions/theme-2026/cgmanifest.json b/extensions/theme-2026/cgmanifest.json new file mode 100644 index 00000000000..74291602f06 --- /dev/null +++ b/extensions/theme-2026/cgmanifest.json @@ -0,0 +1,5 @@ +{ + "Version": 1, + "Registrations": [], + "Comments": [] +} diff --git a/extensions/theme-2026/package.json b/extensions/theme-2026/package.json new file mode 100644 index 00000000000..593169de867 --- /dev/null +++ b/extensions/theme-2026/package.json @@ -0,0 +1,30 @@ +{ + "name": "theme-2026", + "displayName": "2026 Themes", + "description": "Modern, minimal light and dark themes for 2026 with consistent neutral palette and accessible color contrast", + "version": "0.1.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "^1.85.0" + }, + "categories": [ + "Themes" + ], + "contributes": { + "themes": [ + { + "id": "2026-light", + "label": "2026 Light", + "uiTheme": "vs", + "path": "./themes/2026-light.json" + }, + { + "id": "2026-dark", + "label": "2026 Dark", + "uiTheme": "vs-dark", + "path": "./themes/2026-dark.json" + } + ] + } +} diff --git a/extensions/theme-2026/package.nls.json b/extensions/theme-2026/package.nls.json new file mode 100644 index 00000000000..639cf87f44e --- /dev/null +++ b/extensions/theme-2026/package.nls.json @@ -0,0 +1,6 @@ +{ + "displayName": "2026 Themes", + "description": "Modern, minimal light and dark themes for 2026 with consistent neutral palette and accessible color contrast", + "2026-light-label": "2026 Light", + "2026-dark-label": "2026 Dark" +} diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json new file mode 100644 index 00000000000..8114bf52582 --- /dev/null +++ b/extensions/theme-2026/themes/2026-dark.json @@ -0,0 +1,320 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "2026 Dark", + "type": "dark", + "colors": { + "foreground": "#B9BABB", + "disabledForeground": "#434545", + "errorForeground": "#007ACC", + "descriptionForeground": "#878889", + "icon.foreground": "#878889", + "focusBorder": "#007ACCB3", + "textBlockQuote.background": "#232627", + "textBlockQuote.border": "#262C30FF", + "textCodeBlock.background": "#232627", + "textLink.foreground": "#0092F5", + "textLink.activeForeground": "#3EB1FE", + "textPreformat.foreground": "#878889", + "textSeparator.foreground": "#28292AFF", + "button.background": "#007ACC", + "button.foreground": "#FCFEFE", + "button.hoverBackground": "#0A9CFE", + "button.border": "#262C30FF", + "button.secondaryBackground": "#232627", + "button.secondaryForeground": "#B9BABB", + "button.secondaryHoverBackground": "#007ACC", + "checkbox.background": "#232627", + "checkbox.border": "#262C30FF", + "checkbox.foreground": "#B9BABB", + "dropdown.background": "#191B1D", + "dropdown.border": "#262C30FF", + "dropdown.foreground": "#B9BABB", + "dropdown.listBackground": "#1F2223", + "input.background": "#191B1D", + "input.border": "#262C30FF", + "input.foreground": "#B9BABB", + "input.placeholderForeground": "#767778", + "inputOption.activeBackground": "#007ACC33", + "inputOption.activeForeground": "#B9BABB", + "inputOption.activeBorder": "#262C30FF", + "inputValidation.errorBackground": "#191B1D", + "inputValidation.errorBorder": "#262C30FF", + "inputValidation.errorForeground": "#B9BABB", + "inputValidation.infoBackground": "#191B1D", + "inputValidation.infoBorder": "#262C30FF", + "inputValidation.infoForeground": "#B9BABB", + "inputValidation.warningBackground": "#191B1D", + "inputValidation.warningBorder": "#262C30FF", + "inputValidation.warningForeground": "#B9BABB", + "scrollbar.shadow": "#191B1D4D", + "scrollbarSlider.background": "#81848566", + "scrollbarSlider.hoverBackground": "#81848599", + "scrollbarSlider.activeBackground": "#818485CC", + "badge.background": "#007ACC", + "badge.foreground": "#FCFEFE", + "progressBar.background": "#858889", + "list.activeSelectionBackground": "#007ACC26", + "list.activeSelectionForeground": "#B9BABB", + "list.inactiveSelectionBackground": "#232627", + "list.inactiveSelectionForeground": "#B9BABB", + "list.hoverBackground": "#202324", + "list.hoverForeground": "#B9BABB", + "list.dropBackground": "#007ACC1A", + "list.focusBackground": "#007ACC26", + "list.focusForeground": "#B9BABB", + "list.focusOutline": "#007ACCB3", + "list.highlightForeground": "#B9BABB", + "list.invalidItemForeground": "#434545", + "list.errorForeground": "#007ACC", + "list.warningForeground": "#007ACC", + "activityBar.background": "#191B1D", + "activityBar.foreground": "#B9BABB", + "activityBar.inactiveForeground": "#878889", + "activityBar.border": "#262C30FF", + "activityBar.activeBorder": "#262C30FF", + "activityBar.activeFocusBorder": "#007ACCB3", + "activityBarBadge.background": "#007ACC", + "activityBarBadge.foreground": "#FCFEFE", + "sideBar.background": "#191B1D", + "sideBar.foreground": "#B9BABB", + "sideBar.border": "#262C30FF", + "sideBarTitle.foreground": "#B9BABB", + "sideBarSectionHeader.background": "#191B1D", + "sideBarSectionHeader.foreground": "#B9BABB", + "sideBarSectionHeader.border": "#262C30FF", + "titleBar.activeBackground": "#191B1D", + "titleBar.activeForeground": "#B9BABB", + "titleBar.inactiveBackground": "#191B1D", + "titleBar.inactiveForeground": "#878889", + "titleBar.border": "#262C30FF", + "menubar.selectionBackground": "#232627", + "menubar.selectionForeground": "#B9BABB", + "menu.background": "#1F2223", + "menu.foreground": "#B9BABB", + "menu.selectionBackground": "#007ACC26", + "menu.selectionForeground": "#B9BABB", + "menu.separatorBackground": "#818485", + "menu.border": "#262C30FF", + "editor.background": "#151719", + "editor.foreground": "#B7BABB", + "editorLineNumber.foreground": "#858889", + "editorLineNumber.activeForeground": "#B7BABB", + "editorCursor.foreground": "#B7BABB", + "editor.selectionBackground": "#007ACC33", + "editor.inactiveSelectionBackground": "#007ACC80", + "editor.selectionHighlightBackground": "#007ACC1A", + "editor.wordHighlightBackground": "#007ACCB3", + "editor.wordHighlightStrongBackground": "#007ACCE6", + "editor.findMatchBackground": "#007ACC4D", + "editor.findMatchHighlightBackground": "#007ACC26", + "editor.findRangeHighlightBackground": "#232627", + "editor.hoverHighlightBackground": "#232627", + "editor.lineHighlightBackground": "#232627", + "editor.rangeHighlightBackground": "#232627", + "editorLink.activeForeground": "#007ACC", + "editorWhitespace.foreground": "#8788894D", + "editorIndentGuide.background": "#8184854D", + "editorIndentGuide.activeBackground": "#818485", + "editorRuler.foreground": "#838485", + "editorCodeLens.foreground": "#878889", + "editorBracketMatch.background": "#007ACC80", + "editorBracketMatch.border": "#262C30FF", + "editorWidget.background": "#1F2223", + "editorWidget.border": "#262C30FF", + "editorWidget.foreground": "#B9BABB", + "editorSuggestWidget.background": "#1F2223", + "editorSuggestWidget.border": "#262C30FF", + "editorSuggestWidget.foreground": "#B9BABB", + "editorSuggestWidget.highlightForeground": "#B9BABB", + "editorSuggestWidget.selectedBackground": "#007ACC26", + "editorHoverWidget.background": "#1F2223", + "editorHoverWidget.border": "#262C30FF", + "peekView.border": "#262C30FF", + "peekViewEditor.background": "#191B1D", + "peekViewEditor.matchHighlightBackground": "#007ACC33", + "peekViewResult.background": "#232627", + "peekViewResult.fileForeground": "#B9BABB", + "peekViewResult.lineForeground": "#878889", + "peekViewResult.matchHighlightBackground": "#007ACC33", + "peekViewResult.selectionBackground": "#007ACC26", + "peekViewResult.selectionForeground": "#B9BABB", + "peekViewTitle.background": "#232627", + "peekViewTitleDescription.foreground": "#878889", + "peekViewTitleLabel.foreground": "#B9BABB", + "editorGutter.background": "#151719", + "editorGutter.addedBackground": "#007ACC", + "editorGutter.deletedBackground": "#007ACC", + "diffEditor.insertedTextBackground": "#007ACC26", + "diffEditor.removedTextBackground": "#43454726", + "editorOverviewRuler.border": "#262C30FF", + "editorOverviewRuler.findMatchForeground": "#007ACC99", + "editorOverviewRuler.modifiedForeground": "#007ACC", + "editorOverviewRuler.addedForeground": "#007ACC", + "editorOverviewRuler.deletedForeground": "#007ACC", + "editorOverviewRuler.errorForeground": "#007ACC", + "editorOverviewRuler.warningForeground": "#007ACC", + "panel.background": "#191B1D", + "panel.border": "#262C30FF", + "panelTitle.activeBorder": "#007ACC", + "panelTitle.activeForeground": "#B9BABB", + "panelTitle.inactiveForeground": "#878889", + "statusBar.background": "#191B1D", + "statusBar.foreground": "#B9BABB", + "statusBar.border": "#262C30FF", + "statusBar.focusBorder": "#007ACCB3", + "statusBar.debuggingBackground": "#007ACC", + "statusBar.debuggingForeground": "#FCFEFE", + "statusBar.noFolderBackground": "#191B1D", + "statusBar.noFolderForeground": "#B9BABB", + "statusBarItem.activeBackground": "#007ACC", + "statusBarItem.hoverBackground": "#202324", + "statusBarItem.focusBorder": "#007ACCB3", + "statusBarItem.prominentBackground": "#007ACC", + "statusBarItem.prominentForeground": "#FCFEFE", + "statusBarItem.prominentHoverBackground": "#007ACC", + "tab.activeBackground": "#151719", + "tab.activeForeground": "#B9BABB", + "tab.inactiveBackground": "#191B1D", + "tab.inactiveForeground": "#878889", + "tab.border": "#262C30FF", + "tab.lastPinnedBorder": "#262C30FF", + "tab.activeBorder": "#141A1E", + "tab.hoverBackground": "#202324", + "tab.hoverForeground": "#B9BABB", + "tab.unfocusedActiveBackground": "#151719", + "tab.unfocusedActiveForeground": "#878889", + "tab.unfocusedInactiveBackground": "#191B1D", + "tab.unfocusedInactiveForeground": "#434545", + "editorGroupHeader.tabsBackground": "#191B1D", + "editorGroupHeader.tabsBorder": "#262C30FF", + "breadcrumb.foreground": "#878889", + "breadcrumb.background": "#191B1D", + "breadcrumb.focusForeground": "#B9BABB", + "breadcrumb.activeSelectionForeground": "#B9BABB", + "breadcrumbPicker.background": "#1F2223", + "notificationCenter.border": "#262C30FF", + "notificationCenterHeader.foreground": "#B9BABB", + "notificationCenterHeader.background": "#232627", + "notificationToast.border": "#262C30FF", + "notifications.foreground": "#B9BABB", + "notifications.background": "#1F2223", + "notifications.border": "#262C30FF", + "notificationLink.foreground": "#007ACC", + "extensionButton.prominentBackground": "#007ACC", + "extensionButton.prominentForeground": "#FCFEFE", + "extensionButton.prominentHoverBackground": "#0A9CFE", + "pickerGroup.border": "#262C30FF", + "pickerGroup.foreground": "#B9BABB", + "quickInput.background": "#1F2223", + "quickInput.foreground": "#B9BABB", + "quickInputList.focusBackground": "#007ACC26", + "quickInputList.focusForeground": "#B9BABB", + "quickInputList.focusIconForeground": "#B9BABB", + "terminal.foreground": "#B9BABB", + "terminal.background": "#191B1D", + "terminal.selectionBackground": "#007ACC33", + "terminalCursor.foreground": "#B9BABB", + "terminalCursor.background": "#191B1D", + "breadcrum.background": "#151719", + "quickInputTitle.background": "#1F2223" + }, + "tokenColors": [ + { + "scope": [ + "comment" + ], + "settings": { + "foreground": "#6A9955" + } + }, + { + "scope": [ + "keyword", + "storage.modifier", + "storage.type" + ], + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": [ + "string" + ], + "settings": { + "foreground": "#ce9178" + } + }, + { + "name": "Function declarations", + "scope": [ + "entity.name.function", + "support.function", + "support.constant.handlebars" + ], + "settings": { + "foreground": "#DCDCAA" + } + }, + { + "name": "Types declaration and references", + "scope": [ + "support.class", + "support.type", + "entity.name.type", + "entity.name.namespace", + "entity.name.class" + ], + "settings": { + "foreground": "#4EC9B0" + } + }, + { + "name": "Control flow keywords", + "scope": [ + "keyword.control" + ], + "settings": { + "foreground": "#C586C0" + } + }, + { + "name": "Variable and parameter name", + "scope": [ + "variable", + "meta.definition.variable.name", + "support.variable", + "entity.name.variable" + ], + "settings": { + "foreground": "#9CDCFE" + } + }, + { + "name": "Constants and enums", + "scope": [ + "variable.other.constant", + "variable.other.enummember" + ], + "settings": { + "foreground": "#4FC1FF" + } + }, + { + "name": "Numbers", + "scope": [ + "constant.numeric" + ], + "settings": { + "foreground": "#b5cea8" + } + } + ], + "semanticHighlighting": true, + "semanticTokenColors": { + "newOperator": "#C586C0", + "stringLiteral": "#ce9178", + "customLiteral": "#DCDCAA", + "numberLiteral": "#b5cea8" + } +} \ No newline at end of file diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json new file mode 100644 index 00000000000..71739c4eee0 --- /dev/null +++ b/extensions/theme-2026/themes/2026-light.json @@ -0,0 +1,318 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "2026 Light", + "type": "light", + "colors": { + "foreground": "#1A1A1A", + "disabledForeground": "#999999", + "errorForeground": "#0066CC", + "descriptionForeground": "#6B6B6B", + "icon.foreground": "#6B6B6B", + "focusBorder": "#D0D0D099", + "textBlockQuote.background": "#F5F5F5", + "textBlockQuote.border": "#D0D0D099", + "textCodeBlock.background": "#F5F5F5", + "textLink.foreground": "#007AF5", + "textLink.activeForeground": "#3F9FFF", + "textPreformat.foreground": "#6B6B6B", + "textSeparator.foreground": "#D0D0D099", + "button.background": "#0066CC", + "button.foreground": "#FFFFFF", + "button.hoverBackground": "#0A85FF", + "button.border": "#D0D0D099", + "button.secondaryBackground": "#F5F5F5", + "button.secondaryForeground": "#1A1A1A", + "button.secondaryHoverBackground": "#0066CC", + "checkbox.background": "#F5F5F5", + "checkbox.border": "#D0D0D099", + "checkbox.foreground": "#1A1A1A", + "dropdown.background": "#FFFFFF", + "dropdown.border": "#D0D0D099", + "dropdown.foreground": "#1A1A1A", + "dropdown.listBackground": "#EEEEEE", + "input.background": "#FFFFFF", + "input.border": "#D0D0D099", + "input.foreground": "#1A1A1A", + "input.placeholderForeground": "#AAAAAA", + "inputOption.activeBackground": "#0066CC33", + "inputOption.activeForeground": "#1A1A1A", + "inputOption.activeBorder": "#D0D0D099", + "inputValidation.errorBackground": "#FFFFFF", + "inputValidation.errorBorder": "#D0D0D099", + "inputValidation.errorForeground": "#1A1A1A", + "inputValidation.infoBackground": "#FFFFFF", + "inputValidation.infoBorder": "#D0D0D099", + "inputValidation.infoForeground": "#1A1A1A", + "inputValidation.warningBackground": "#FFFFFF", + "inputValidation.warningBorder": "#D0D0D099", + "inputValidation.warningForeground": "#1A1A1A", + "scrollbar.shadow": "#FFFFFF4D", + "scrollbarSlider.background": "#84848466", + "scrollbarSlider.hoverBackground": "#84848499", + "scrollbarSlider.activeBackground": "#848484CC", + "badge.background": "#0066CC", + "badge.foreground": "#FFFFFF", + "progressBar.background": "#6B6B6B", + "list.activeSelectionBackground": "#0066CC26", + "list.activeSelectionForeground": "#1A1A1A", + "list.inactiveSelectionBackground": "#F5F5F5", + "list.inactiveSelectionForeground": "#1A1A1A", + "list.hoverBackground": "#FFFFFF", + "list.hoverForeground": "#1A1A1A", + "list.dropBackground": "#0066CC1A", + "list.focusBackground": "#0066CC26", + "list.focusForeground": "#1A1A1A", + "list.focusOutline": "#D0D0D099", + "list.highlightForeground": "#1A1A1A", + "list.invalidItemForeground": "#999999", + "list.errorForeground": "#0066CC", + "list.warningForeground": "#0066CC", + "activityBar.background": "#FFFFFF", + "activityBar.foreground": "#1A1A1A", + "activityBar.inactiveForeground": "#6B6B6B", + "activityBar.border": "#D0D0D099", + "activityBar.activeBorder": "#D0D0D099", + "activityBar.activeFocusBorder": "#0066CC99", + "activityBarBadge.background": "#0066CC", + "activityBarBadge.foreground": "#FFFFFF", + "sideBar.background": "#FFFFFF", + "sideBar.foreground": "#1A1A1A", + "sideBar.border": "#D0D0D099", + "sideBarTitle.foreground": "#1A1A1A", + "sideBarSectionHeader.background": "#FFFFFF", + "sideBarSectionHeader.foreground": "#1A1A1A", + "sideBarSectionHeader.border": "#D0D0D099", + "titleBar.activeBackground": "#FFFFFF", + "titleBar.activeForeground": "#1A1A1A", + "titleBar.inactiveBackground": "#FFFFFF", + "titleBar.inactiveForeground": "#6B6B6B", + "titleBar.border": "#D0D0D099", + "menubar.selectionBackground": "#F5F5F5", + "menubar.selectionForeground": "#1A1A1A", + "menu.background": "#EEEEEE", + "menu.foreground": "#1A1A1A", + "menu.selectionBackground": "#0066CC26", + "menu.selectionForeground": "#1A1A1A", + "menu.separatorBackground": "#848484", + "menu.border": "#D0D0D099", + "editor.background": "#FFFFFF", + "editor.foreground": "#1A1A1A", + "editorLineNumber.foreground": "#6B6B6B", + "editorLineNumber.activeForeground": "#1A1A1A", + "editorCursor.foreground": "#1A1A1A", + "editor.selectionBackground": "#0066CC33", + "editor.inactiveSelectionBackground": "#0066CC80", + "editor.selectionHighlightBackground": "#0066CC1A", + "editor.wordHighlightBackground": "#0066CCB3", + "editor.wordHighlightStrongBackground": "#0066CCE6", + "editor.findMatchBackground": "#0066CC4D", + "editor.findMatchHighlightBackground": "#0066CC26", + "editor.findRangeHighlightBackground": "#F5F5F5", + "editor.hoverHighlightBackground": "#F5F5F5", + "editor.lineHighlightBackground": "#F5F5F5", + "editor.rangeHighlightBackground": "#F5F5F5", + "editorLink.activeForeground": "#0066CC", + "editorWhitespace.foreground": "#6B6B6B4D", + "editorIndentGuide.background": "#8484844D", + "editorIndentGuide.activeBackground": "#848484", + "editorRuler.foreground": "#848484", + "editorCodeLens.foreground": "#6B6B6B", + "editorBracketMatch.background": "#0066CC80", + "editorBracketMatch.border": "#D0D0D099", + "editorWidget.background": "#EEEEEE", + "editorWidget.border": "#D0D0D099", + "editorWidget.foreground": "#1A1A1A", + "editorSuggestWidget.background": "#EEEEEE", + "editorSuggestWidget.border": "#D0D0D099", + "editorSuggestWidget.foreground": "#1A1A1A", + "editorSuggestWidget.highlightForeground": "#1A1A1A", + "editorSuggestWidget.selectedBackground": "#0066CC26", + "editorHoverWidget.background": "#EEEEEE", + "editorHoverWidget.border": "#D0D0D099", + "peekView.border": "#D0D0D099", + "peekViewEditor.background": "#FFFFFF", + "peekViewEditor.matchHighlightBackground": "#0066CC33", + "peekViewResult.background": "#F5F5F5", + "peekViewResult.fileForeground": "#1A1A1A", + "peekViewResult.lineForeground": "#6B6B6B", + "peekViewResult.matchHighlightBackground": "#0066CC33", + "peekViewResult.selectionBackground": "#0066CC26", + "peekViewResult.selectionForeground": "#1A1A1A", + "peekViewTitle.background": "#F5F5F5", + "peekViewTitleDescription.foreground": "#6B6B6B", + "peekViewTitleLabel.foreground": "#1A1A1A", + "editorGutter.background": "#FFFFFF", + "editorGutter.addedBackground": "#0066CC", + "editorGutter.deletedBackground": "#0066CC", + "diffEditor.insertedTextBackground": "#0066CC26", + "diffEditor.removedTextBackground": "#99999926", + "editorOverviewRuler.border": "#D0D0D099", + "editorOverviewRuler.findMatchForeground": "#0066CC99", + "editorOverviewRuler.modifiedForeground": "#0066CC", + "editorOverviewRuler.addedForeground": "#0066CC", + "editorOverviewRuler.deletedForeground": "#0066CC", + "editorOverviewRuler.errorForeground": "#0066CC", + "editorOverviewRuler.warningForeground": "#0066CC", + "panel.background": "#F5F5F5", + "panel.border": "#D0D0D099", + "panelTitle.activeBorder": "#0066CC", + "panelTitle.activeForeground": "#1A1A1A", + "panelTitle.inactiveForeground": "#6B6B6B", + "statusBar.background": "#FFFFFF", + "statusBar.foreground": "#1A1A1A", + "statusBar.border": "#D0D0D099", + "statusBar.focusBorder": "#D0D0D099", + "statusBar.debuggingBackground": "#0066CC", + "statusBar.debuggingForeground": "#FFFFFF", + "statusBar.noFolderBackground": "#FFFFFF", + "statusBar.noFolderForeground": "#1A1A1A", + "statusBarItem.activeBackground": "#0066CC", + "statusBarItem.hoverBackground": "#FFFFFF", + "statusBarItem.focusBorder": "#D0D0D099", + "statusBarItem.prominentBackground": "#0066CC", + "statusBarItem.prominentForeground": "#FFFFFF", + "statusBarItem.prominentHoverBackground": "#0066CC", + "tab.activeBackground": "#FFFFFF", + "tab.activeForeground": "#1A1A1A", + "tab.inactiveBackground": "#FFFFFF", + "tab.inactiveForeground": "#6B6B6B", + "tab.border": "#D0D0D099", + "tab.lastPinnedBorder": "#D0D0D099", + "tab.activeBorder": "#FFFFFF", + "tab.hoverBackground": "#FFFFFF", + "tab.hoverForeground": "#1A1A1A", + "tab.unfocusedActiveBackground": "#FFFFFF", + "tab.unfocusedActiveForeground": "#6B6B6B", + "tab.unfocusedInactiveBackground": "#FFFFFF", + "tab.unfocusedInactiveForeground": "#999999", + "editorGroupHeader.tabsBackground": "#FFFFFF", + "editorGroupHeader.tabsBorder": "#D0D0D099", + "breadcrumb.foreground": "#6B6B6B", + "breadcrumb.background": "#FFFFFF", + "breadcrumb.focusForeground": "#1A1A1A", + "breadcrumb.activeSelectionForeground": "#1A1A1A", + "breadcrumbPicker.background": "#EEEEEE", + "notificationCenter.border": "#D0D0D099", + "notificationCenterHeader.foreground": "#1A1A1A", + "notificationCenterHeader.background": "#F5F5F5", + "notificationToast.border": "#D0D0D099", + "notifications.foreground": "#1A1A1A", + "notifications.background": "#EEEEEE", + "notifications.border": "#D0D0D099", + "notificationLink.foreground": "#0066CC", + "extensionButton.prominentBackground": "#0066CC", + "extensionButton.prominentForeground": "#FFFFFF", + "extensionButton.prominentHoverBackground": "#0A85FF", + "pickerGroup.border": "#D0D0D099", + "pickerGroup.foreground": "#1A1A1A", + "quickInput.background": "#EEEEEE", + "quickInput.foreground": "#1A1A1A", + "quickInputList.focusBackground": "#0066CC26", + "quickInputList.focusForeground": "#1A1A1A", + "quickInputList.focusIconForeground": "#1A1A1A", + "terminal.foreground": "#1A1A1A", + "terminal.background": "#FFFFFF", + "terminal.selectionBackground": "#0066CC33", + "terminalCursor.foreground": "#1A1A1A", + "terminalCursor.background": "#FFFFFF" + }, + "tokenColors": [ + { + "scope": [ + "comment" + ], + "settings": { + "foreground": "#008000" + } + }, + { + "scope": [ + "keyword", + "storage.modifier", + "storage.type" + ], + "settings": { + "foreground": "#0000ff" + } + }, + { + "scope": [ + "string" + ], + "settings": { + "foreground": "#a31515" + } + }, + { + "name": "Function declarations", + "scope": [ + "entity.name.function", + "support.function", + "support.constant.handlebars" + ], + "settings": { + "foreground": "#795E26" + } + }, + { + "name": "Types declaration and references", + "scope": [ + "support.class", + "support.type", + "entity.name.type", + "entity.name.namespace", + "entity.name.class" + ], + "settings": { + "foreground": "#267f99" + } + }, + { + "name": "Control flow keywords", + "scope": [ + "keyword.control" + ], + "settings": { + "foreground": "#AF00DB" + } + }, + { + "name": "Variable and parameter name", + "scope": [ + "variable", + "meta.definition.variable.name", + "support.variable", + "entity.name.variable" + ], + "settings": { + "foreground": "#001080" + } + }, + { + "name": "Constants and enums", + "scope": [ + "variable.other.constant", + "variable.other.enummember" + ], + "settings": { + "foreground": "#0070C1" + } + }, + { + "name": "Numbers", + "scope": [ + "constant.numeric" + ], + "settings": { + "foreground": "#098658" + } + } + ], + "semanticHighlighting": true, + "semanticTokenColors": { + "newOperator": "#AF00DB", + "stringLiteral": "#a31515", + "customLiteral": "#795E26", + "numberLiteral": "#098658" + } +} \ No newline at end of file From a9f018e8b9e0f7286a12895b203796c6ca7a06d6 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 14 Jan 2026 11:53:33 +0000 Subject: [PATCH 004/387] Update 2026 theme colors for improved visibility and consistency --- extensions/theme-2026/themes/2026-dark.json | 429 ++++++++++--------- extensions/theme-2026/themes/2026-light.json | 64 +-- 2 files changed, 258 insertions(+), 235 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 8114bf52582..e073247c68e 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -3,220 +3,231 @@ "name": "2026 Dark", "type": "dark", "colors": { - "foreground": "#B9BABB", - "disabledForeground": "#434545", - "errorForeground": "#007ACC", - "descriptionForeground": "#878889", - "icon.foreground": "#878889", - "focusBorder": "#007ACCB3", - "textBlockQuote.background": "#232627", - "textBlockQuote.border": "#262C30FF", - "textCodeBlock.background": "#232627", - "textLink.foreground": "#0092F5", - "textLink.activeForeground": "#3EB1FE", - "textPreformat.foreground": "#878889", - "textSeparator.foreground": "#28292AFF", - "button.background": "#007ACC", - "button.foreground": "#FCFEFE", - "button.hoverBackground": "#0A9CFE", - "button.border": "#262C30FF", - "button.secondaryBackground": "#232627", - "button.secondaryForeground": "#B9BABB", - "button.secondaryHoverBackground": "#007ACC", - "checkbox.background": "#232627", - "checkbox.border": "#262C30FF", - "checkbox.foreground": "#B9BABB", - "dropdown.background": "#191B1D", - "dropdown.border": "#262C30FF", - "dropdown.foreground": "#B9BABB", - "dropdown.listBackground": "#1F2223", - "input.background": "#191B1D", - "input.border": "#262C30FF", - "input.foreground": "#B9BABB", - "input.placeholderForeground": "#767778", - "inputOption.activeBackground": "#007ACC33", - "inputOption.activeForeground": "#B9BABB", - "inputOption.activeBorder": "#262C30FF", - "inputValidation.errorBackground": "#191B1D", - "inputValidation.errorBorder": "#262C30FF", - "inputValidation.errorForeground": "#B9BABB", - "inputValidation.infoBackground": "#191B1D", - "inputValidation.infoBorder": "#262C30FF", - "inputValidation.infoForeground": "#B9BABB", - "inputValidation.warningBackground": "#191B1D", - "inputValidation.warningBorder": "#262C30FF", - "inputValidation.warningForeground": "#B9BABB", + "foreground": "#bbbbbb", + "disabledForeground": "#444444", + "errorForeground": "#f48771", + "descriptionForeground": "#888888", + "icon.foreground": "#888888", + "focusBorder": "#007ABBB3", + "textBlockQuote.background": "#242424", + "textBlockQuote.border": "#252627FF", + "textCodeBlock.background": "#242424", + "textLink.foreground": "#0092E0", + "textLink.activeForeground": "#009AEB", + "textPreformat.foreground": "#888888", + "textSeparator.foreground": "#252525FF", + "button.background": "#007ABB", + "button.foreground": "#FFFFFF", + "button.hoverBackground": "#0080C4", + "button.border": "#252627FF", + "button.secondaryBackground": "#242424", + "button.secondaryForeground": "#bbbbbb", + "button.secondaryHoverBackground": "#007ABB", + "checkbox.background": "#242424", + "checkbox.border": "#252627FF", + "checkbox.foreground": "#bbbbbb", + "dropdown.background": "#191919", + "dropdown.border": "#252627FF", + "dropdown.foreground": "#bbbbbb", + "dropdown.listBackground": "#202020", + "input.background": "#191919", + "input.border": "#323435FF", + "input.foreground": "#bbbbbb", + "input.placeholderForeground": "#777777", + "inputOption.activeBackground": "#007ABB33", + "inputOption.activeForeground": "#bbbbbb", + "inputOption.activeBorder": "#252627FF", + "inputValidation.errorBackground": "#191919", + "inputValidation.errorBorder": "#252627FF", + "inputValidation.errorForeground": "#bbbbbb", + "inputValidation.infoBackground": "#191919", + "inputValidation.infoBorder": "#252627FF", + "inputValidation.infoForeground": "#bbbbbb", + "inputValidation.warningBackground": "#191919", + "inputValidation.warningBorder": "#252627FF", + "inputValidation.warningForeground": "#bbbbbb", "scrollbar.shadow": "#191B1D4D", - "scrollbarSlider.background": "#81848566", - "scrollbarSlider.hoverBackground": "#81848599", - "scrollbarSlider.activeBackground": "#818485CC", - "badge.background": "#007ACC", - "badge.foreground": "#FCFEFE", - "progressBar.background": "#858889", - "list.activeSelectionBackground": "#007ACC26", - "list.activeSelectionForeground": "#B9BABB", - "list.inactiveSelectionBackground": "#232627", - "list.inactiveSelectionForeground": "#B9BABB", - "list.hoverBackground": "#202324", - "list.hoverForeground": "#B9BABB", - "list.dropBackground": "#007ACC1A", - "list.focusBackground": "#007ACC26", - "list.focusForeground": "#B9BABB", - "list.focusOutline": "#007ACCB3", - "list.highlightForeground": "#B9BABB", - "list.invalidItemForeground": "#434545", - "list.errorForeground": "#007ACC", - "list.warningForeground": "#007ACC", - "activityBar.background": "#191B1D", - "activityBar.foreground": "#B9BABB", - "activityBar.inactiveForeground": "#878889", - "activityBar.border": "#262C30FF", - "activityBar.activeBorder": "#262C30FF", - "activityBar.activeFocusBorder": "#007ACCB3", - "activityBarBadge.background": "#007ACC", - "activityBarBadge.foreground": "#FCFEFE", - "sideBar.background": "#191B1D", - "sideBar.foreground": "#B9BABB", - "sideBar.border": "#262C30FF", - "sideBarTitle.foreground": "#B9BABB", - "sideBarSectionHeader.background": "#191B1D", - "sideBarSectionHeader.foreground": "#B9BABB", - "sideBarSectionHeader.border": "#262C30FF", - "titleBar.activeBackground": "#191B1D", - "titleBar.activeForeground": "#B9BABB", - "titleBar.inactiveBackground": "#191B1D", - "titleBar.inactiveForeground": "#878889", - "titleBar.border": "#262C30FF", - "menubar.selectionBackground": "#232627", - "menubar.selectionForeground": "#B9BABB", - "menu.background": "#1F2223", - "menu.foreground": "#B9BABB", - "menu.selectionBackground": "#007ACC26", - "menu.selectionForeground": "#B9BABB", - "menu.separatorBackground": "#818485", - "menu.border": "#262C30FF", - "editor.background": "#151719", + "scrollbarSlider.background": "#84848433", + "scrollbarSlider.hoverBackground": "#84848466", + "scrollbarSlider.activeBackground": "#84848499", + "badge.background": "#007ABB", + "badge.foreground": "#FFFFFF", + "progressBar.background": "#888888", + "list.activeSelectionBackground": "#007ABB26", + "list.activeSelectionForeground": "#bbbbbb", + "list.inactiveSelectionBackground": "#242424", + "list.inactiveSelectionForeground": "#bbbbbb", + "list.hoverBackground": "#262626", + "list.hoverForeground": "#bbbbbb", + "list.dropBackground": "#007ABB1A", + "list.focusBackground": "#007ABB26", + "list.focusForeground": "#bbbbbb", + "list.focusOutline": "#007ABBB3", + "list.highlightForeground": "#bbbbbb", + "list.invalidItemForeground": "#444444", + "list.errorForeground": "#f48771", + "list.warningForeground": "#e5ba7d", + "activityBar.background": "#191919", + "activityBar.foreground": "#bbbbbb", + "activityBar.inactiveForeground": "#888888", + "activityBar.border": "#252627FF", + "activityBar.activeBorder": "#252627FF", + "activityBar.activeFocusBorder": "#007ABBB3", + "activityBarBadge.background": "#007ABB", + "activityBarBadge.foreground": "#FFFFFF", + "sideBar.background": "#191919", + "sideBar.foreground": "#bbbbbb", + "sideBar.border": "#252627FF", + "sideBarTitle.foreground": "#bbbbbb", + "sideBarSectionHeader.background": "#191919", + "sideBarSectionHeader.foreground": "#bbbbbb", + "sideBarSectionHeader.border": "#252627FF", + "titleBar.activeBackground": "#191919", + "titleBar.activeForeground": "#bbbbbb", + "titleBar.inactiveBackground": "#191919", + "titleBar.inactiveForeground": "#888888", + "titleBar.border": "#252627FF", + "menubar.selectionBackground": "#242424", + "menubar.selectionForeground": "#bbbbbb", + "menu.background": "#202020", + "menu.foreground": "#bbbbbb", + "menu.selectionBackground": "#007ABB26", + "menu.selectionForeground": "#bbbbbb", + "menu.separatorBackground": "#848484", + "menu.border": "#252627FF", + "commandCenter.foreground": "#bbbbbb", + "commandCenter.activeForeground": "#bbbbbb", + "commandCenter.background": "#191919", + "commandCenter.activeBackground": "#262626", + "commandCenter.border": "#252627FF", + "editor.background": "#121212", "editor.foreground": "#B7BABB", "editorLineNumber.foreground": "#858889", "editorLineNumber.activeForeground": "#B7BABB", "editorCursor.foreground": "#B7BABB", - "editor.selectionBackground": "#007ACC33", - "editor.inactiveSelectionBackground": "#007ACC80", - "editor.selectionHighlightBackground": "#007ACC1A", - "editor.wordHighlightBackground": "#007ACCB3", - "editor.wordHighlightStrongBackground": "#007ACCE6", - "editor.findMatchBackground": "#007ACC4D", - "editor.findMatchHighlightBackground": "#007ACC26", - "editor.findRangeHighlightBackground": "#232627", - "editor.hoverHighlightBackground": "#232627", - "editor.lineHighlightBackground": "#232627", - "editor.rangeHighlightBackground": "#232627", - "editorLink.activeForeground": "#007ACC", - "editorWhitespace.foreground": "#8788894D", - "editorIndentGuide.background": "#8184854D", - "editorIndentGuide.activeBackground": "#818485", - "editorRuler.foreground": "#838485", - "editorCodeLens.foreground": "#878889", - "editorBracketMatch.background": "#007ACC80", - "editorBracketMatch.border": "#262C30FF", - "editorWidget.background": "#1F2223", - "editorWidget.border": "#262C30FF", - "editorWidget.foreground": "#B9BABB", - "editorSuggestWidget.background": "#1F2223", - "editorSuggestWidget.border": "#262C30FF", - "editorSuggestWidget.foreground": "#B9BABB", - "editorSuggestWidget.highlightForeground": "#B9BABB", - "editorSuggestWidget.selectedBackground": "#007ACC26", - "editorHoverWidget.background": "#1F2223", - "editorHoverWidget.border": "#262C30FF", - "peekView.border": "#262C30FF", - "peekViewEditor.background": "#191B1D", - "peekViewEditor.matchHighlightBackground": "#007ACC33", - "peekViewResult.background": "#232627", - "peekViewResult.fileForeground": "#B9BABB", - "peekViewResult.lineForeground": "#878889", - "peekViewResult.matchHighlightBackground": "#007ACC33", - "peekViewResult.selectionBackground": "#007ACC26", - "peekViewResult.selectionForeground": "#B9BABB", - "peekViewTitle.background": "#232627", - "peekViewTitleDescription.foreground": "#878889", - "peekViewTitleLabel.foreground": "#B9BABB", - "editorGutter.background": "#151719", - "editorGutter.addedBackground": "#007ACC", - "editorGutter.deletedBackground": "#007ACC", - "diffEditor.insertedTextBackground": "#007ACC26", - "diffEditor.removedTextBackground": "#43454726", - "editorOverviewRuler.border": "#262C30FF", - "editorOverviewRuler.findMatchForeground": "#007ACC99", - "editorOverviewRuler.modifiedForeground": "#007ACC", - "editorOverviewRuler.addedForeground": "#007ACC", - "editorOverviewRuler.deletedForeground": "#007ACC", - "editorOverviewRuler.errorForeground": "#007ACC", - "editorOverviewRuler.warningForeground": "#007ACC", - "panel.background": "#191B1D", - "panel.border": "#262C30FF", - "panelTitle.activeBorder": "#007ACC", - "panelTitle.activeForeground": "#B9BABB", - "panelTitle.inactiveForeground": "#878889", - "statusBar.background": "#191B1D", - "statusBar.foreground": "#B9BABB", - "statusBar.border": "#262C30FF", - "statusBar.focusBorder": "#007ACCB3", - "statusBar.debuggingBackground": "#007ACC", - "statusBar.debuggingForeground": "#FCFEFE", - "statusBar.noFolderBackground": "#191B1D", - "statusBar.noFolderForeground": "#B9BABB", - "statusBarItem.activeBackground": "#007ACC", - "statusBarItem.hoverBackground": "#202324", - "statusBarItem.focusBorder": "#007ACCB3", - "statusBarItem.prominentBackground": "#007ACC", - "statusBarItem.prominentForeground": "#FCFEFE", - "statusBarItem.prominentHoverBackground": "#007ACC", - "tab.activeBackground": "#151719", - "tab.activeForeground": "#B9BABB", - "tab.inactiveBackground": "#191B1D", - "tab.inactiveForeground": "#878889", - "tab.border": "#262C30FF", - "tab.lastPinnedBorder": "#262C30FF", - "tab.activeBorder": "#141A1E", - "tab.hoverBackground": "#202324", - "tab.hoverForeground": "#B9BABB", - "tab.unfocusedActiveBackground": "#151719", - "tab.unfocusedActiveForeground": "#878889", - "tab.unfocusedInactiveBackground": "#191B1D", - "tab.unfocusedInactiveForeground": "#434545", - "editorGroupHeader.tabsBackground": "#191B1D", - "editorGroupHeader.tabsBorder": "#262C30FF", - "breadcrumb.foreground": "#878889", - "breadcrumb.background": "#191B1D", - "breadcrumb.focusForeground": "#B9BABB", - "breadcrumb.activeSelectionForeground": "#B9BABB", - "breadcrumbPicker.background": "#1F2223", - "notificationCenter.border": "#262C30FF", - "notificationCenterHeader.foreground": "#B9BABB", - "notificationCenterHeader.background": "#232627", - "notificationToast.border": "#262C30FF", - "notifications.foreground": "#B9BABB", - "notifications.background": "#1F2223", - "notifications.border": "#262C30FF", - "notificationLink.foreground": "#007ACC", - "extensionButton.prominentBackground": "#007ACC", - "extensionButton.prominentForeground": "#FCFEFE", - "extensionButton.prominentHoverBackground": "#0A9CFE", - "pickerGroup.border": "#262C30FF", - "pickerGroup.foreground": "#B9BABB", - "quickInput.background": "#1F2223", - "quickInput.foreground": "#B9BABB", - "quickInputList.focusBackground": "#007ACC26", - "quickInputList.focusForeground": "#B9BABB", - "quickInputList.focusIconForeground": "#B9BABB", - "terminal.foreground": "#B9BABB", - "terminal.background": "#191B1D", - "terminal.selectionBackground": "#007ACC33", - "terminalCursor.foreground": "#B9BABB", - "terminalCursor.background": "#191B1D", - "breadcrum.background": "#151719", - "quickInputTitle.background": "#1F2223" + "editor.selectionBackground": "#007ABB33", + "editor.inactiveSelectionBackground": "#007ABB80", + "editor.selectionHighlightBackground": "#007ABB1A", + "editor.wordHighlightBackground": "#007ABBB3", + "editor.wordHighlightStrongBackground": "#007ABBE6", + "editor.findMatchBackground": "#007ABB4D", + "editor.findMatchHighlightBackground": "#007ABB26", + "editor.findRangeHighlightBackground": "#242424", + "editor.hoverHighlightBackground": "#242424", + "editor.lineHighlightBackground": "#242424", + "editor.rangeHighlightBackground": "#242424", + "editorLink.activeForeground": "#007ABB", + "editorWhitespace.foreground": "#8888884D", + "editorIndentGuide.background": "#8484844D", + "editorIndentGuide.activeBackground": "#848484", + "editorRuler.foreground": "#848484", + "editorCodeLens.foreground": "#888888", + "editorBracketMatch.background": "#007ABB80", + "editorBracketMatch.border": "#252627FF", + "editorWidget.background": "#202020", + "editorWidget.border": "#252627FF", + "editorWidget.foreground": "#bbbbbb", + "editorSuggestWidget.background": "#202020", + "editorSuggestWidget.border": "#252627FF", + "editorSuggestWidget.foreground": "#bbbbbb", + "editorSuggestWidget.highlightForeground": "#bbbbbb", + "editorSuggestWidget.selectedBackground": "#007ABB26", + "editorHoverWidget.background": "#202020", + "editorHoverWidget.border": "#252627FF", + "peekView.border": "#252627FF", + "peekViewEditor.background": "#191919", + "peekViewEditor.matchHighlightBackground": "#007ABB33", + "peekViewResult.background": "#242424", + "peekViewResult.fileForeground": "#bbbbbb", + "peekViewResult.lineForeground": "#888888", + "peekViewResult.matchHighlightBackground": "#007ABB33", + "peekViewResult.selectionBackground": "#007ABB26", + "peekViewResult.selectionForeground": "#bbbbbb", + "peekViewTitle.background": "#242424", + "peekViewTitleDescription.foreground": "#888888", + "peekViewTitleLabel.foreground": "#bbbbbb", + "editorGutter.background": "#121212", + "editorGutter.addedBackground": "#73c991", + "editorGutter.deletedBackground": "#f48771", + "diffEditor.insertedTextBackground": "#73c99154", + "diffEditor.removedTextBackground": "#f4877154", + "editorOverviewRuler.border": "#252627FF", + "editorOverviewRuler.findMatchForeground": "#007ABB99", + "editorOverviewRuler.modifiedForeground": "#5ba3e0", + "editorOverviewRuler.addedForeground": "#73c991", + "editorOverviewRuler.deletedForeground": "#f48771", + "editorOverviewRuler.errorForeground": "#f48771", + "editorOverviewRuler.warningForeground": "#e5ba7d", + "panel.background": "#191919", + "panel.border": "#252627FF", + "panelTitle.activeBorder": "#007ABB", + "panelTitle.activeForeground": "#bbbbbb", + "panelTitle.inactiveForeground": "#888888", + "statusBar.background": "#191919", + "statusBar.foreground": "#bbbbbb", + "statusBar.border": "#252627FF", + "statusBar.focusBorder": "#007ABBB3", + "statusBar.debuggingBackground": "#007ABB", + "statusBar.debuggingForeground": "#FFFFFF", + "statusBar.noFolderBackground": "#191919", + "statusBar.noFolderForeground": "#bbbbbb", + "statusBarItem.activeBackground": "#007ABB", + "statusBarItem.hoverBackground": "#262626", + "statusBarItem.focusBorder": "#007ABBB3", + "statusBarItem.prominentBackground": "#007ABB", + "statusBarItem.prominentForeground": "#FFFFFF", + "statusBarItem.prominentHoverBackground": "#007ABB", + "tab.activeBackground": "#121212", + "tab.activeForeground": "#bbbbbb", + "tab.inactiveBackground": "#191919", + "tab.inactiveForeground": "#888888", + "tab.border": "#252627FF", + "tab.lastPinnedBorder": "#252627FF", + "tab.activeBorder": "#121314", + "tab.hoverBackground": "#262626", + "tab.hoverForeground": "#bbbbbb", + "tab.unfocusedActiveBackground": "#121212", + "tab.unfocusedActiveForeground": "#888888", + "tab.unfocusedInactiveBackground": "#191919", + "tab.unfocusedInactiveForeground": "#444444", + "editorGroupHeader.tabsBackground": "#191919", + "editorGroupHeader.tabsBorder": "#252627FF", + "breadcrumb.foreground": "#888888", + "breadcrumb.background": "#121212", + "breadcrumb.focusForeground": "#bbbbbb", + "breadcrumb.activeSelectionForeground": "#bbbbbb", + "breadcrumbPicker.background": "#202020", + "notificationCenter.border": "#252627FF", + "notificationCenterHeader.foreground": "#bbbbbb", + "notificationCenterHeader.background": "#242424", + "notificationToast.border": "#252627FF", + "notifications.foreground": "#bbbbbb", + "notifications.background": "#202020", + "notifications.border": "#252627FF", + "notificationLink.foreground": "#007ABB", + "extensionButton.prominentBackground": "#007ABB", + "extensionButton.prominentForeground": "#FFFFFF", + "extensionButton.prominentHoverBackground": "#0080C4", + "pickerGroup.border": "#252627FF", + "pickerGroup.foreground": "#bbbbbb", + "quickInput.background": "#202020", + "quickInput.foreground": "#bbbbbb", + "quickInputList.focusBackground": "#007ABB26", + "quickInputList.focusForeground": "#bbbbbb", + "quickInputList.focusIconForeground": "#bbbbbb", + "quickInputList.hoverBackground": "#525252", + "terminal.selectionBackground": "#007ABB33", + "terminalCursor.foreground": "#bbbbbb", + "terminalCursor.background": "#191919", + "gitDecoration.addedResourceForeground": "#73c991", + "gitDecoration.modifiedResourceForeground": "#e5ba7d", + "gitDecoration.deletedResourceForeground": "#f48771", + "gitDecoration.untrackedResourceForeground": "#73c991", + "gitDecoration.ignoredResourceForeground": "#8C8C8C", + "gitDecoration.conflictingResourceForeground": "#f48771", + "gitDecoration.stageModifiedResourceForeground": "#e5ba7d", + "gitDecoration.stageDeletedResourceForeground": "#f48771", + "quickInputTitle.background": "#202020" }, "tokenColors": [ { diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index 71739c4eee0..f0cd80705a7 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -5,7 +5,7 @@ "colors": { "foreground": "#1A1A1A", "disabledForeground": "#999999", - "errorForeground": "#0066CC", + "errorForeground": "#ad0707", "descriptionForeground": "#6B6B6B", "icon.foreground": "#6B6B6B", "focusBorder": "#D0D0D099", @@ -13,12 +13,12 @@ "textBlockQuote.border": "#D0D0D099", "textCodeBlock.background": "#F5F5F5", "textLink.foreground": "#007AF5", - "textLink.activeForeground": "#3F9FFF", + "textLink.activeForeground": "#0280FF", "textPreformat.foreground": "#6B6B6B", "textSeparator.foreground": "#D0D0D099", "button.background": "#0066CC", "button.foreground": "#FFFFFF", - "button.hoverBackground": "#0A85FF", + "button.hoverBackground": "#006BD6", "button.border": "#D0D0D099", "button.secondaryBackground": "#F5F5F5", "button.secondaryForeground": "#1A1A1A", @@ -30,26 +30,26 @@ "dropdown.border": "#D0D0D099", "dropdown.foreground": "#1A1A1A", "dropdown.listBackground": "#EEEEEE", - "input.background": "#FFFFFF", + "input.background": "#F5F5F5", "input.border": "#D0D0D099", "input.foreground": "#1A1A1A", "input.placeholderForeground": "#AAAAAA", "inputOption.activeBackground": "#0066CC33", "inputOption.activeForeground": "#1A1A1A", "inputOption.activeBorder": "#D0D0D099", - "inputValidation.errorBackground": "#FFFFFF", + "inputValidation.errorBackground": "#F5F5F5", "inputValidation.errorBorder": "#D0D0D099", "inputValidation.errorForeground": "#1A1A1A", - "inputValidation.infoBackground": "#FFFFFF", + "inputValidation.infoBackground": "#F5F5F5", "inputValidation.infoBorder": "#D0D0D099", "inputValidation.infoForeground": "#1A1A1A", - "inputValidation.warningBackground": "#FFFFFF", + "inputValidation.warningBackground": "#F5F5F5", "inputValidation.warningBorder": "#D0D0D099", "inputValidation.warningForeground": "#1A1A1A", "scrollbar.shadow": "#FFFFFF4D", - "scrollbarSlider.background": "#84848466", - "scrollbarSlider.hoverBackground": "#84848499", - "scrollbarSlider.activeBackground": "#848484CC", + "scrollbarSlider.background": "#84848433", + "scrollbarSlider.hoverBackground": "#84848466", + "scrollbarSlider.activeBackground": "#84848499", "badge.background": "#0066CC", "badge.foreground": "#FFFFFF", "progressBar.background": "#6B6B6B", @@ -65,8 +65,8 @@ "list.focusOutline": "#D0D0D099", "list.highlightForeground": "#1A1A1A", "list.invalidItemForeground": "#999999", - "list.errorForeground": "#0066CC", - "list.warningForeground": "#0066CC", + "list.errorForeground": "#ad0707", + "list.warningForeground": "#667309", "activityBar.background": "#FFFFFF", "activityBar.foreground": "#1A1A1A", "activityBar.inactiveForeground": "#6B6B6B", @@ -95,6 +95,11 @@ "menu.selectionForeground": "#1A1A1A", "menu.separatorBackground": "#848484", "menu.border": "#D0D0D099", + "commandCenter.foreground": "#1A1A1A", + "commandCenter.activeForeground": "#1A1A1A", + "commandCenter.background": "#FFFFFF", + "commandCenter.activeBackground": "#FFFFFF", + "commandCenter.border": "#D0D0D099", "editor.background": "#FFFFFF", "editor.foreground": "#1A1A1A", "editorLineNumber.foreground": "#6B6B6B", @@ -142,17 +147,17 @@ "peekViewTitleDescription.foreground": "#6B6B6B", "peekViewTitleLabel.foreground": "#1A1A1A", "editorGutter.background": "#FFFFFF", - "editorGutter.addedBackground": "#0066CC", - "editorGutter.deletedBackground": "#0066CC", - "diffEditor.insertedTextBackground": "#0066CC26", - "diffEditor.removedTextBackground": "#99999926", + "editorGutter.addedBackground": "#587c0c", + "editorGutter.deletedBackground": "#ad0707", + "diffEditor.insertedTextBackground": "#587c0c54", + "diffEditor.removedTextBackground": "#ad070754", "editorOverviewRuler.border": "#D0D0D099", "editorOverviewRuler.findMatchForeground": "#0066CC99", - "editorOverviewRuler.modifiedForeground": "#0066CC", - "editorOverviewRuler.addedForeground": "#0066CC", - "editorOverviewRuler.deletedForeground": "#0066CC", - "editorOverviewRuler.errorForeground": "#0066CC", - "editorOverviewRuler.warningForeground": "#0066CC", + "editorOverviewRuler.modifiedForeground": "#007acc", + "editorOverviewRuler.addedForeground": "#587c0c", + "editorOverviewRuler.deletedForeground": "#ad0707", + "editorOverviewRuler.errorForeground": "#ad0707", + "editorOverviewRuler.warningForeground": "#667309", "panel.background": "#F5F5F5", "panel.border": "#D0D0D099", "panelTitle.activeBorder": "#0066CC", @@ -202,19 +207,26 @@ "notificationLink.foreground": "#0066CC", "extensionButton.prominentBackground": "#0066CC", "extensionButton.prominentForeground": "#FFFFFF", - "extensionButton.prominentHoverBackground": "#0A85FF", + "extensionButton.prominentHoverBackground": "#006BD6", "pickerGroup.border": "#D0D0D099", "pickerGroup.foreground": "#1A1A1A", - "quickInput.background": "#EEEEEE", + "quickInput.background": "#F5F5F5", "quickInput.foreground": "#1A1A1A", "quickInputList.focusBackground": "#0066CC26", "quickInputList.focusForeground": "#1A1A1A", "quickInputList.focusIconForeground": "#1A1A1A", - "terminal.foreground": "#1A1A1A", - "terminal.background": "#FFFFFF", + "quickInputList.hoverBackground": "#FAFAFA", "terminal.selectionBackground": "#0066CC33", "terminalCursor.foreground": "#1A1A1A", - "terminalCursor.background": "#FFFFFF" + "terminalCursor.background": "#FFFFFF", + "gitDecoration.addedResourceForeground": "#587c0c", + "gitDecoration.modifiedResourceForeground": "#667309", + "gitDecoration.deletedResourceForeground": "#ad0707", + "gitDecoration.untrackedResourceForeground": "#587c0c", + "gitDecoration.ignoredResourceForeground": "#8E8E90", + "gitDecoration.conflictingResourceForeground": "#ad0707", + "gitDecoration.stageModifiedResourceForeground": "#667309", + "gitDecoration.stageDeletedResourceForeground": "#ad0707" }, "tokenColors": [ { From 8759dbdbf56efb6e6cdcd5016d7b0d6fc88636c4 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:40:07 -0800 Subject: [PATCH 005/387] Hide `getAllChatSessionItemProviders` We generally don't want services exposing providers in their api. Instead we can use `getChatSessionItems` for this --- .../agentSessions/agentSessionsModel.ts | 32 ++++++-------- .../chatSessions/chatSessions.contribution.ts | 43 ++++++++----------- .../chat/common/chatSessionsService.ts | 4 +- .../localAgentSessionsProvider.test.ts | 8 ++-- .../test/common/mockChatSessionsService.ts | 20 +++++---- 5 files changed, 49 insertions(+), 58 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index b579321fec1..3a95650b2db 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { coalesce } from '../../../../../base/common/arrays.js'; import { ThrottledDelayer } from '../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; @@ -22,8 +23,7 @@ import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProv //#region Interfaces, Types -export { ChatSessionStatus as AgentSessionStatus } from '../../common/chatSessionsService.js'; -export { isSessionInProgressStatus } from '../../common/chatSessionsService.js'; +export { ChatSessionStatus as AgentSessionStatus, isSessionInProgressStatus } from '../../common/chatSessionsService.js'; export interface IAgentSessionsModel { @@ -278,23 +278,19 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode mapSessionContributionToType.set(contribution.type, contribution); } + const providerFilter = providersToResolve.includes(undefined) + ? undefined + : coalesce(providersToResolve); + + const providerResults = await this.chatSessionsService.getChatSessionItems(providerFilter, token); + const resolvedProviders = new Set(); const sessions = new ResourceMap(); - for (const provider of this.chatSessionsService.getAllChatSessionItemProviders()) { - if (!providersToResolve.includes(undefined) && !providersToResolve.includes(provider.chatSessionType)) { - continue; // skip: not considered for resolving - } - let providerSessions: IChatSessionItem[]; - try { - providerSessions = await provider.provideChatSessionItems(token); - this.logService.trace(`[agent sessions] Resolved ${providerSessions.length} agent sessions for provider ${provider.chatSessionType}`); - } catch (error) { - this.logService.error(`Failed to resolve sessions for provider ${provider.chatSessionType}`, error); - continue; // skip: failed to resolve sessions for provider - } + for (const { chatSessionType, items: providerSessions } of providerResults) { + this.logService.trace(`[agent sessions] Resolved ${providerSessions.length} agent sessions for provider ${chatSessionType}`); - resolvedProviders.add(provider.chatSessionType); + resolvedProviders.add(chatSessionType); if (token.isCancellationRequested) { return; @@ -305,7 +301,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode // Icon + Label let icon: ThemeIcon; let providerLabel: string; - switch ((provider.chatSessionType)) { + switch ((chatSessionType)) { case AgentSessionProviders.Local: providerLabel = getAgentSessionProviderName(AgentSessionProviders.Local); icon = getAgentSessionProviderIcon(AgentSessionProviders.Local); @@ -319,7 +315,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode icon = getAgentSessionProviderIcon(AgentSessionProviders.Cloud); break; default: { - providerLabel = mapSessionContributionToType.get(provider.chatSessionType)?.name ?? provider.chatSessionType; + providerLabel = mapSessionContributionToType.get(chatSessionType)?.name ?? chatSessionType; icon = session.iconPath ?? Codicon.terminal; } } @@ -376,7 +372,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } sessions.set(session.resource, this.toAgentSession({ - providerType: provider.chatSessionType, + providerType: chatSessionType, providerLabel, resource: session.resource, label: session.label, diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index a9b3f78bc37..9d981f86b7f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -373,7 +373,8 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ private async updateInProgressStatus(chatSessionType: string): Promise { try { - const items = await this.getChatSessionItems(chatSessionType, CancellationToken.None); + const results = await this.getChatSessionItems([chatSessionType], CancellationToken.None); + const items = results.flatMap(r => r.items); const inProgress = items.filter(item => item.status && isSessionInProgressStatus(item.status)); this.reportInProgress(chatSessionType, inProgress.length); } catch (error) { @@ -716,7 +717,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ return this._isContributionAvailable(contribution) ? contribution : undefined; } - getAllChatSessionItemProviders(): IChatSessionItemProvider[] { + private _getAllChatSessionItemProviders(): IChatSessionItemProvider[] { return [...this._itemsProviders.values()].filter(provider => { // Check if the provider's corresponding contribution is available const contribution = this._contributions.get(provider.chatSessionType)?.contribution; @@ -761,32 +762,24 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ return this._contentProviders.has(chatSessionResource.scheme); } - async getAllChatSessionItems(token: CancellationToken): Promise> { - return Promise.all(Array.from(this.getAllChatSessionContributions(), async contrib => { - return { - chatSessionType: contrib.type, - items: await this.getChatSessionItems(contrib.type, token) - }; - })); - } + public async getChatSessionItems(providersToResolve: readonly string[] | undefined, token: CancellationToken): Promise> { + const results: Array<{ readonly chatSessionType: string; readonly items: IChatSessionItem[] }> = []; + for (const provider of this._getAllChatSessionItemProviders()) { + if (providersToResolve && !providersToResolve.includes(provider.chatSessionType)) { + continue; // skip: not considered for resolving + } - private async getChatSessionItems(chatSessionType: string, token: CancellationToken): Promise { - if (!(await this.activateChatSessionItemProvider(chatSessionType))) { - return []; + try { + const providerSessions = await raceCancellationError(provider.provideChatSessionItems(token), token); + this._logService.trace(`[ChatSessionsService] Resolved ${providerSessions.length} sessions for provider ${provider.chatSessionType}`); + results.push({ chatSessionType: provider.chatSessionType, items: providerSessions }); + } catch (error) { + // Log error but continue with other providers + continue; + } } - const resolvedType = this._resolveToPrimaryType(chatSessionType); - if (resolvedType) { - chatSessionType = resolvedType; - } - - const provider = this._itemsProviders.get(chatSessionType); - if (provider?.provideChatSessionItems) { - const sessions = await provider.provideChatSessionItems(token); - return sessions; - } - - return []; + return results; } public registerChatSessionItemProvider(provider: IChatSessionItemProvider): IDisposable { diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index 76a9b348698..3aca7692a73 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -181,7 +181,6 @@ export interface IChatSessionsService { registerChatSessionItemProvider(provider: IChatSessionItemProvider): IDisposable; activateChatSessionItemProvider(chatSessionType: string): Promise; - getAllChatSessionItemProviders(): IChatSessionItemProvider[]; getAllChatSessionContributions(): IChatSessionsExtensionPoint[]; getIconForSessionType(chatSessionType: string): ThemeIcon | URI | undefined; @@ -191,8 +190,9 @@ export interface IChatSessionsService { /** * Get the list of chat session items grouped by session type. + * @param providerTypeFilter If specified, only returns items from the given providers. If undefined, returns items from all providers. */ - getAllChatSessionItems(token: CancellationToken): Promise>; + getChatSessionItems(providerTypeFilter: readonly string[] | undefined, token: CancellationToken): Promise>; reportInProgress(chatSessionType: string, count: number): void; getInProgress(): { displayName: string; count: number }[]; diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsProvider.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsProvider.test.ts index 7be0701efe2..94b23a6fd5d 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsProvider.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsProvider.test.ts @@ -282,12 +282,12 @@ suite('LocalAgentsSessionsProvider', () => { assert.strictEqual(provider.chatSessionType, localChatSessionType); }); - test('should register itself with chat sessions service', () => { + test('should register itself with chat sessions service', async () => { const provider = createProvider(); - const providers = mockChatSessionsService.getAllChatSessionItemProviders(); - assert.strictEqual(providers.length, 1); - assert.strictEqual(providers[0], provider); + const providerResults = await mockChatSessionsService.getChatSessionItems(undefined, CancellationToken.None); + assert.strictEqual(providerResults.length, 1); + assert.strictEqual(providerResults[0].chatSessionType, provider.chatSessionType); }); test('should provide empty sessions when no live or history sessions', async () => { diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts index 0fdb89b1a55..277adff0b0d 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts @@ -87,10 +87,6 @@ export class MockChatSessionsService implements IChatSessionsService { return this.sessionItemProviders.get(chatSessionType); } - getAllChatSessionItemProviders(): IChatSessionItemProvider[] { - return Array.from(this.sessionItemProviders.values()); - } - getIconForSessionType(chatSessionType: string): ThemeIcon | URI | undefined { const contribution = this.contributions.find(c => c.type === chatSessionType); return contribution?.icon && typeof contribution.icon === 'string' ? ThemeIcon.fromId(contribution.icon) : undefined; @@ -108,13 +104,19 @@ export class MockChatSessionsService implements IChatSessionsService { return this.contributions.find(c => c.type === chatSessionType)?.inputPlaceholder; } - getAllChatSessionItems(token: CancellationToken): Promise> { - return Promise.all(Array.from(this.sessionItemProviders.values(), async provider => { - return { + getChatSessionItems(providersToResolve: readonly string[] | undefined, token: CancellationToken): Promise> { + const normalizedProviders = providersToResolve === undefined + ? undefined + : Array.isArray(providersToResolve) + ? providersToResolve + : [providersToResolve]; + + return Promise.all(Array.from(this.sessionItemProviders.values()) + .filter(provider => normalizedProviders === undefined || normalizedProviders.includes(provider.chatSessionType)) + .map(async provider => ({ chatSessionType: provider.chatSessionType, items: await provider.provideChatSessionItems(token), - }; - })); + }))); } reportInProgress(chatSessionType: string, count: number): void { From afe02521e1d0ba136574ece6307704d3f2b08a17 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:42:46 -0800 Subject: [PATCH 006/387] Add error logging --- .../chat/browser/chatSessions/chatSessions.contribution.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index 9d981f86b7f..ef91ba0eea2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -775,6 +775,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ results.push({ chatSessionType: provider.chatSessionType, items: providerSessions }); } catch (error) { // Log error but continue with other providers + this._logService.error(`[ChatSessionsService] Failed to resolve sessions for provider ${provider.chatSessionType}`, error); continue; } } From 2c4842f688a386e55566f01919ed37327c687e2d Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:31:11 -0800 Subject: [PATCH 007/387] Cleanup --- .../chatSessions/chatSessions.contribution.ts | 21 ++++++++++--------- .../test/common/mockChatSessionsService.ts | 8 +------ 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index ef91ba0eea2..c9510266cbc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -717,14 +717,6 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ return this._isContributionAvailable(contribution) ? contribution : undefined; } - private _getAllChatSessionItemProviders(): IChatSessionItemProvider[] { - return [...this._itemsProviders.values()].filter(provider => { - // Check if the provider's corresponding contribution is available - const contribution = this._contributions.get(provider.chatSessionType)?.contribution; - return !contribution || this._isContributionAvailable(contribution); - }); - } - async activateChatSessionItemProvider(chatViewType: string): Promise { await this._extensionService.whenInstalledExtensionsRegistered(); const resolvedType = this._resolveToPrimaryType(chatViewType); @@ -764,11 +756,20 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ public async getChatSessionItems(providersToResolve: readonly string[] | undefined, token: CancellationToken): Promise> { const results: Array<{ readonly chatSessionType: string; readonly items: IChatSessionItem[] }> = []; - for (const provider of this._getAllChatSessionItemProviders()) { - if (providersToResolve && !providersToResolve.includes(provider.chatSessionType)) { + for (const contrib of this.getAllChatSessionContributions()) { + if (providersToResolve && !providersToResolve.includes(contrib.type)) { continue; // skip: not considered for resolving } + const provider = await this.activateChatSessionItemProvider(contrib.type); + if (!provider) { + // We requested this provider but it is not available + if (providersToResolve?.includes(contrib.type)) { + this._logService.trace(`[ChatSessionsService] No enabled provider found for chat session type ${contrib.type}`); + } + continue; + } + try { const providerSessions = await raceCancellationError(provider.provideChatSessionItems(token), token); this._logService.trace(`[ChatSessionsService] Resolved ${providerSessions.length} sessions for provider ${provider.chatSessionType}`); diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts index 277adff0b0d..c25c2437e99 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts @@ -105,14 +105,8 @@ export class MockChatSessionsService implements IChatSessionsService { } getChatSessionItems(providersToResolve: readonly string[] | undefined, token: CancellationToken): Promise> { - const normalizedProviders = providersToResolve === undefined - ? undefined - : Array.isArray(providersToResolve) - ? providersToResolve - : [providersToResolve]; - return Promise.all(Array.from(this.sessionItemProviders.values()) - .filter(provider => normalizedProviders === undefined || normalizedProviders.includes(provider.chatSessionType)) + .filter(provider => !providersToResolve || providersToResolve.includes(provider.chatSessionType)) .map(async provider => ({ chatSessionType: provider.chatSessionType, items: await provider.provideChatSessionItems(token), From 481b411f38fc46f1798df8e96e32487d17e41305 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:37:54 -0800 Subject: [PATCH 008/387] Present cd dir separate to command Fixes #277507 --- .../chatTerminalToolConfirmationSubPart.ts | 12 +++++- .../chat/common/chatService/chatService.ts | 15 +++++++ .../commandLineCdPrefixRewriter.ts | 32 +++++++++++++++ .../browser/tools/runInTerminalTool.ts | 39 +++++++++++++++++-- .../commandLineCdPrefixRewriter.test.ts | 33 +++++++++++++++- 5 files changed, 124 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts index 1c72c6d32f4..b23a0fd06d1 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts @@ -108,6 +108,10 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS const { title, message, disclaimer, terminalCustomActions } = toolInvocation.confirmationMessages; + // Use pre-computed confirmation data from runInTerminalTool (cd prefix extraction happens there for localization) + const initialContent = terminalData.confirmation?.commandLine ?? (terminalData.commandLine.toolEdited ?? terminalData.commandLine.original).trimStart(); + const cdPrefix = terminalData.confirmation?.cdPrefix ?? ''; + const autoApproveEnabled = this.configurationService.getValue(TerminalContribSettingId.EnableAutoApprove) === true; const autoApproveWarningAccepted = this.storageService.getBoolean(TerminalToolConfirmationStorageKeys.TerminalAutoApproveWarningAccepted, StorageScope.APPLICATION, false); let moreActions: (IChatConfirmationButton | Separator)[] | undefined = undefined; @@ -149,7 +153,6 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS } }; const languageId = this.languageService.getLanguageIdByLanguageName(terminalData.language ?? 'sh') ?? 'shellscript'; - const initialContent = (terminalData.commandLine.toolEdited ?? terminalData.commandLine.original).trimStart(); const model = this._register(this.modelService.createModel( initialContent, this.languageService.createById(languageId), @@ -185,7 +188,12 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS this._register(model.onDidChangeContent(e => { const currentValue = model.getValue(); // Only set userEdited if the content actually differs from the initial value - terminalData.commandLine.userEdited = currentValue !== initialContent ? currentValue : undefined; + // Prepend cd prefix back if it was extracted for display + if (currentValue !== initialContent) { + terminalData.commandLine.userEdited = cdPrefix + currentValue; + } else { + terminalData.commandLine.userEdited = undefined; + } })); const elements = h('.chat-confirmation-message-terminal', [ h('.chat-confirmation-message-terminal-editor@editor'), diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index b4f75cc832f..77ee599c36d 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -373,6 +373,21 @@ export interface IChatTerminalToolInvocationData { userEdited?: string; toolEdited?: string; }; + /** The working directory URI for the terminal */ + cwd?: UriComponents; + /** + * Pre-computed confirmation display data (localization must happen at source). + * Contains the command line to show in confirmation (potentially without cd prefix) + * and the formatted cwd label if a cd prefix was extracted. + */ + confirmation?: { + /** The command line to display in the confirmation editor */ + commandLine: string; + /** The formatted cwd label to show in title (if cd was extracted) */ + cwdLabel?: string; + /** The cd prefix to prepend back when user edits */ + cdPrefix?: string; + }; /** Message for model recommending the use of an alternative tool */ alternativeRecommendation?: string; language: string; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineCdPrefixRewriter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineCdPrefixRewriter.ts index 9fece148ea2..4dde59106c8 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineCdPrefixRewriter.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineCdPrefixRewriter.ts @@ -8,6 +8,38 @@ import { OperatingSystem } from '../../../../../../../base/common/platform.js'; import { isPowerShell } from '../../runInTerminalHelpers.js'; import type { ICommandLineRewriter, ICommandLineRewriterOptions, ICommandLineRewriterResult } from './commandLineRewriter.js'; +export interface IExtractedCdPrefix { + /** The directory path that was extracted from the cd command */ + directory: string; + /** The command to run after the cd */ + command: string; +} + +/** + * Extracts a cd prefix from a command line, returning the directory and remaining command. + * Does not check if the directory matches the current cwd - just extracts the pattern. + */ +export function extractCdPrefix(commandLine: string, shell: string, os: OperatingSystem): IExtractedCdPrefix | undefined { + const isPwsh = isPowerShell(shell, os); + + const cdPrefixMatch = commandLine.match( + isPwsh + ? /^(?:cd(?: \/d)?|Set-Location(?: -Path)?) (?[^\s]+) ?(?:&&|;)\s+(?.+)$/i + : /^cd (?[^\s]+) &&\s+(?.+)$/ + ); + const cdDir = cdPrefixMatch?.groups?.dir; + const cdSuffix = cdPrefixMatch?.groups?.suffix; + if (cdDir && cdSuffix) { + // Remove any surrounding quotes + let cdDirPath = cdDir; + if (cdDirPath.startsWith('"') && cdDirPath.endsWith('"')) { + cdDirPath = cdDirPath.slice(1, -1); + } + return { directory: cdDirPath, command: cdSuffix }; + } + return undefined; +} + export class CommandLineCdPrefixRewriter extends Disposable implements ICommandLineRewriter { rewrite(options: ICommandLineRewriterOptions): ICommandLineRewriterResult | undefined { if (!options.cwd) { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index fdfbcb4308f..095b9cfc1a2 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -19,6 +19,7 @@ import { generateUuid } from '../../../../../../base/common/uuid.js'; import { localize } from '../../../../../../nls.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService, type ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../../../platform/label/common/label.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; import { TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; import { ITerminalLogService, ITerminalProfile } from '../../../../../../platform/terminal/common/terminal.js'; @@ -48,7 +49,7 @@ import { IPollingResult, OutputMonitorState } from './monitoring/types.js'; import { chatSessionResourceToId, LocalChatSessionUri } from '../../../../chat/common/model/chatUri.js'; import { URI } from '../../../../../../base/common/uri.js'; import type { ICommandLineRewriter } from './commandLineRewriter/commandLineRewriter.js'; -import { CommandLineCdPrefixRewriter } from './commandLineRewriter/commandLineCdPrefixRewriter.js'; +import { CommandLineCdPrefixRewriter, extractCdPrefix } from './commandLineRewriter/commandLineCdPrefixRewriter.js'; import { CommandLinePreventHistoryRewriter } from './commandLineRewriter/commandLinePreventHistoryRewriter.js'; import { CommandLinePwshChainOperatorRewriter } from './commandLineRewriter/commandLinePwshChainOperatorRewriter.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; @@ -300,6 +301,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { @IConfigurationService private readonly _configurationService: IConfigurationService, @IHistoryService private readonly _historyService: IHistoryService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILabelService private readonly _labelService: ILabelService, @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService, @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @IStorageService private readonly _storageService: IStorageService, @@ -402,6 +404,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { original: args.command, toolEdited: rewrittenCommand === args.command ? undefined : rewrittenCommand }, + cwd, language, }; @@ -498,10 +501,38 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { toolSpecificData.autoApproveInfo = commandLineAnalyzerResults.find(e => e.autoApproveInfo)?.autoApproveInfo; } - const confirmationMessages = isFinalAutoApproved ? undefined : { - title: args.isBackground + // Extract cd prefix for display - show directory in title, command suffix in editor + const commandToDisplay = (toolSpecificData.commandLine.toolEdited ?? toolSpecificData.commandLine.original).trimStart(); + const extractedCd = extractCdPrefix(commandToDisplay, shell, os); + let confirmationTitle: string; + if (extractedCd && cwd) { + // Construct the full directory path using the cwd's scheme/authority + const directoryUri = extractedCd.directory.startsWith('/') || /^[a-zA-Z]:/.test(extractedCd.directory) + ? URI.from({ scheme: cwd.scheme, authority: cwd.authority, path: extractedCd.directory }) + : URI.joinPath(cwd, extractedCd.directory); + const directoryLabel = this._labelService.getUriLabel(directoryUri); + const cdPrefix = commandToDisplay.substring(0, commandToDisplay.length - extractedCd.command.length); + + toolSpecificData.confirmation = { + commandLine: extractedCd.command, + cwdLabel: directoryLabel, + cdPrefix, + }; + + confirmationTitle = args.isBackground + ? localize('runInTerminal.background.inDirectory', "Run `{0}` command in `{1}`? (background terminal)", shellType, directoryLabel) + : localize('runInTerminal.inDirectory', "Run `{0}` command in `{1}`?", shellType, directoryLabel); + } else { + toolSpecificData.confirmation = { + commandLine: commandToDisplay, + }; + confirmationTitle = args.isBackground ? localize('runInTerminal.background', "Run `{0}` command? (background terminal)", shellType) - : localize('runInTerminal', "Run `{0}` command?", shellType), + : localize('runInTerminal', "Run `{0}` command?", shellType); + } + + const confirmationMessages = isFinalAutoApproved ? undefined : { + title: confirmationTitle, message: new MarkdownString(args.explanation), disclaimer, terminalCustomActions: customActions, diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineCdPrefixRewriter.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineCdPrefixRewriter.test.ts index d440896b361..a07d170b203 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineCdPrefixRewriter.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineCdPrefixRewriter.test.ts @@ -9,7 +9,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import type { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; -import { CommandLineCdPrefixRewriter } from '../../browser/tools/commandLineRewriter/commandLineCdPrefixRewriter.js'; +import { CommandLineCdPrefixRewriter, extractCdPrefix } from '../../browser/tools/commandLineRewriter/commandLineCdPrefixRewriter.js'; import type { ICommandLineRewriterOptions } from '../../browser/tools/commandLineRewriter/commandLineRewriter.js'; suite('CommandLineCdPrefixRewriter', () => { @@ -80,3 +80,34 @@ suite('CommandLineCdPrefixRewriter', () => { }); }); }); + +suite('extractCdPrefix', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('Posix', () => { + function t(commandLine: string, expectedDir: string | undefined, expectedCommand: string | undefined) { + const result = extractCdPrefix(commandLine, 'bash', OperatingSystem.Linux); + strictEqual(result?.directory, expectedDir); + strictEqual(result?.command, expectedCommand); + } + + test('should return undefined when no cd prefix', () => t('echo hello', undefined, undefined)); + test('should return undefined when cd has no suffix', () => t('cd /some/path', undefined, undefined)); + test('should extract cd prefix with && separator', () => t('cd /some/path && npm install', '/some/path', 'npm install')); + test('should extract quoted path', () => t('cd "/some/path" && npm install', '/some/path', 'npm install')); + test('should extract complex suffix', () => t('cd /path && npm install && npm test', '/path', 'npm install && npm test')); + }); + + suite('PowerShell', () => { + function t(commandLine: string, expectedDir: string | undefined, expectedCommand: string | undefined) { + const result = extractCdPrefix(commandLine, 'pwsh', OperatingSystem.Windows); + strictEqual(result?.directory, expectedDir); + strictEqual(result?.command, expectedCommand); + } + + test('should extract cd with ; separator', () => t('cd C:\\path; npm test', 'C:\\path', 'npm test')); + test('should extract cd /d with && separator', () => t('cd /d C:\\path && echo hello', 'C:\\path', 'echo hello')); + test('should extract Set-Location', () => t('Set-Location C:\\path; npm test', 'C:\\path', 'npm test')); + test('should extract Set-Location -Path', () => t('Set-Location -Path C:\\path; npm test', 'C:\\path', 'npm test')); + }); +}); From bbf5bf67cac6d149e54549e3a9b0d339872246ed Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 14 Jan 2026 16:24:03 -0800 Subject: [PATCH 009/387] Refactor cd prefix helper --- .../browser/runInTerminalHelpers.ts | 32 +++++++++++ .../commandLineCdPrefixRewriter.ts | 55 ++----------------- .../browser/tools/runInTerminalTool.ts | 4 +- .../test/browser/runInTerminalHelpers.test.ts | 33 ++++++++++- .../commandLineCdPrefixRewriter.test.ts | 33 +---------- 5 files changed, 72 insertions(+), 85 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts index 81acecc6588..7cbf89ca9f3 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts @@ -300,3 +300,35 @@ export function dedupeRules(rules: ICommandApprovalResultWithReason[]): ICommand return array.findIndex(r => isAutoApproveRule(r.rule) && r.rule.sourceText === sourceText) === index; }); } + +export interface IExtractedCdPrefix { + /** The directory path that was extracted from the cd command */ + directory: string; + /** The command to run after the cd */ + command: string; +} + +/** + * Extracts a cd prefix from a command line, returning the directory and remaining command. + * Does not check if the directory matches the current cwd - just extracts the pattern. + */ +export function extractCdPrefix(commandLine: string, shell: string, os: OperatingSystem): IExtractedCdPrefix | undefined { + const isPwsh = isPowerShell(shell, os); + + const cdPrefixMatch = commandLine.match( + isPwsh + ? /^(?:cd(?: \/d)?|Set-Location(?: -Path)?) (?[^\s]+) ?(?:&&|;)\s+(?.+)$/i + : /^cd (?[^\s]+) &&\s+(?.+)$/ + ); + const cdDir = cdPrefixMatch?.groups?.dir; + const cdSuffix = cdPrefixMatch?.groups?.suffix; + if (cdDir && cdSuffix) { + // Remove any surrounding quotes + let cdDirPath = cdDir; + if (cdDirPath.startsWith('"') && cdDirPath.endsWith('"')) { + cdDirPath = cdDirPath.slice(1, -1); + } + return { directory: cdDirPath, command: cdSuffix }; + } + return undefined; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineCdPrefixRewriter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineCdPrefixRewriter.ts index 4dde59106c8..c123b764b1f 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineCdPrefixRewriter.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineCdPrefixRewriter.ts @@ -5,67 +5,22 @@ import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { OperatingSystem } from '../../../../../../../base/common/platform.js'; -import { isPowerShell } from '../../runInTerminalHelpers.js'; +import { extractCdPrefix } from '../../runInTerminalHelpers.js'; import type { ICommandLineRewriter, ICommandLineRewriterOptions, ICommandLineRewriterResult } from './commandLineRewriter.js'; -export interface IExtractedCdPrefix { - /** The directory path that was extracted from the cd command */ - directory: string; - /** The command to run after the cd */ - command: string; -} - -/** - * Extracts a cd prefix from a command line, returning the directory and remaining command. - * Does not check if the directory matches the current cwd - just extracts the pattern. - */ -export function extractCdPrefix(commandLine: string, shell: string, os: OperatingSystem): IExtractedCdPrefix | undefined { - const isPwsh = isPowerShell(shell, os); - - const cdPrefixMatch = commandLine.match( - isPwsh - ? /^(?:cd(?: \/d)?|Set-Location(?: -Path)?) (?[^\s]+) ?(?:&&|;)\s+(?.+)$/i - : /^cd (?[^\s]+) &&\s+(?.+)$/ - ); - const cdDir = cdPrefixMatch?.groups?.dir; - const cdSuffix = cdPrefixMatch?.groups?.suffix; - if (cdDir && cdSuffix) { - // Remove any surrounding quotes - let cdDirPath = cdDir; - if (cdDirPath.startsWith('"') && cdDirPath.endsWith('"')) { - cdDirPath = cdDirPath.slice(1, -1); - } - return { directory: cdDirPath, command: cdSuffix }; - } - return undefined; -} - export class CommandLineCdPrefixRewriter extends Disposable implements ICommandLineRewriter { rewrite(options: ICommandLineRewriterOptions): ICommandLineRewriterResult | undefined { if (!options.cwd) { return undefined; } - const isPwsh = isPowerShell(options.shell, options.os); - // Re-write the command if it starts with `cd && ` or `cd ; ` // to just `` if the directory matches the current terminal's cwd. This simplifies // the result in the chat by removing redundancies that some models like to add. - const cdPrefixMatch = options.commandLine.match( - isPwsh - ? /^(?:cd(?: \/d)?|Set-Location(?: -Path)?) (?[^\s]+) ?(?:&&|;)\s+(?.+)$/i - : /^cd (?[^\s]+) &&\s+(?.+)$/ - ); - const cdDir = cdPrefixMatch?.groups?.dir; - const cdSuffix = cdPrefixMatch?.groups?.suffix; - if (cdDir && cdSuffix) { - // Remove any surrounding quotes - let cdDirPath = cdDir; - if (cdDirPath.startsWith('"') && cdDirPath.endsWith('"')) { - cdDirPath = cdDirPath.slice(1, -1); - } + const extracted = extractCdPrefix(options.commandLine, options.shell, options.os); + if (extracted) { // Normalize trailing slashes - cdDirPath = cdDirPath.replace(/(?:[\\\/])$/, ''); + let cdDirPath = extracted.directory.replace(/(?:[\\\/])$/, ''); let cwdFsPath = options.cwd.fsPath.replace(/(?:[\\\/])$/, ''); // Case-insensitive comparison on Windows if (options.os === OperatingSystem.Windows) { @@ -73,7 +28,7 @@ export class CommandLineCdPrefixRewriter extends Disposable implements ICommandL cwdFsPath = cwdFsPath.toLowerCase(); } if (cdDirPath === cwdFsPath) { - return { rewritten: cdSuffix, reasoning: 'Removed redundant cd command' }; + return { rewritten: extracted.command, reasoning: 'Removed redundant cd command' }; } } return undefined; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index 095b9cfc1a2..2192b823612 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -37,7 +37,7 @@ import type { ITerminalExecuteStrategy } from '../executeStrategy/executeStrateg import { NoneExecuteStrategy } from '../executeStrategy/noneExecuteStrategy.js'; import { RichExecuteStrategy } from '../executeStrategy/richExecuteStrategy.js'; import { getOutput } from '../outputHelpers.js'; -import { isFish, isPowerShell, isWindowsPowerShell, isZsh } from '../runInTerminalHelpers.js'; +import { extractCdPrefix, isFish, isPowerShell, isWindowsPowerShell, isZsh } from '../runInTerminalHelpers.js'; import { RunInTerminalToolTelemetry } from '../runInTerminalToolTelemetry.js'; import { ShellIntegrationQuality, ToolTerminalCreator, type IToolTerminal } from '../toolTerminalCreator.js'; import { TreeSitterCommandParser, TreeSitterCommandParserLanguage } from '../treeSitterCommandParser.js'; @@ -49,7 +49,7 @@ import { IPollingResult, OutputMonitorState } from './monitoring/types.js'; import { chatSessionResourceToId, LocalChatSessionUri } from '../../../../chat/common/model/chatUri.js'; import { URI } from '../../../../../../base/common/uri.js'; import type { ICommandLineRewriter } from './commandLineRewriter/commandLineRewriter.js'; -import { CommandLineCdPrefixRewriter, extractCdPrefix } from './commandLineRewriter/commandLineCdPrefixRewriter.js'; +import { CommandLineCdPrefixRewriter } from './commandLineRewriter/commandLineCdPrefixRewriter.js'; import { CommandLinePreventHistoryRewriter } from './commandLineRewriter/commandLinePreventHistoryRewriter.js'; import { CommandLinePwshChainOperatorRewriter } from './commandLineRewriter/commandLinePwshChainOperatorRewriter.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts index 8ad3ae4b9c4..c40f03d838b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ok, strictEqual } from 'assert'; -import { generateAutoApproveActions, TRUNCATION_MESSAGE, dedupeRules, isPowerShell, sanitizeTerminalOutput, truncateOutputKeepingTail } from '../../browser/runInTerminalHelpers.js'; +import { generateAutoApproveActions, TRUNCATION_MESSAGE, dedupeRules, isPowerShell, sanitizeTerminalOutput, truncateOutputKeepingTail, extractCdPrefix } from '../../browser/runInTerminalHelpers.js'; import { OperatingSystem } from '../../../../../../base/common/platform.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { ConfigurationTarget } from '../../../../../../platform/configuration/common/configuration.js'; @@ -464,3 +464,34 @@ suite('generateAutoApproveActions', () => { strictEqual(subCommandAction, undefined, 'Should not suggest approval for already approved commands'); }); }); + +suite('extractCdPrefix', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('Posix', () => { + function t(commandLine: string, expectedDir: string | undefined, expectedCommand: string | undefined) { + const result = extractCdPrefix(commandLine, 'bash', OperatingSystem.Linux); + strictEqual(result?.directory, expectedDir); + strictEqual(result?.command, expectedCommand); + } + + test('should return undefined when no cd prefix', () => t('echo hello', undefined, undefined)); + test('should return undefined when cd has no suffix', () => t('cd /some/path', undefined, undefined)); + test('should extract cd prefix with && separator', () => t('cd /some/path && npm install', '/some/path', 'npm install')); + test('should extract quoted path', () => t('cd "/some/path" && npm install', '/some/path', 'npm install')); + test('should extract complex suffix', () => t('cd /path && npm install && npm test', '/path', 'npm install && npm test')); + }); + + suite('PowerShell', () => { + function t(commandLine: string, expectedDir: string | undefined, expectedCommand: string | undefined) { + const result = extractCdPrefix(commandLine, 'pwsh', OperatingSystem.Windows); + strictEqual(result?.directory, expectedDir); + strictEqual(result?.command, expectedCommand); + } + + test('should extract cd with ; separator', () => t('cd C:\\path; npm test', 'C:\\path', 'npm test')); + test('should extract cd /d with && separator', () => t('cd /d C:\\path && echo hello', 'C:\\path', 'echo hello')); + test('should extract Set-Location', () => t('Set-Location C:\\path; npm test', 'C:\\path', 'npm test')); + test('should extract Set-Location -Path', () => t('Set-Location -Path C:\\path; npm test', 'C:\\path', 'npm test')); + }); +}); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineCdPrefixRewriter.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineCdPrefixRewriter.test.ts index a07d170b203..d440896b361 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineCdPrefixRewriter.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineCdPrefixRewriter.test.ts @@ -9,7 +9,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import type { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; -import { CommandLineCdPrefixRewriter, extractCdPrefix } from '../../browser/tools/commandLineRewriter/commandLineCdPrefixRewriter.js'; +import { CommandLineCdPrefixRewriter } from '../../browser/tools/commandLineRewriter/commandLineCdPrefixRewriter.js'; import type { ICommandLineRewriterOptions } from '../../browser/tools/commandLineRewriter/commandLineRewriter.js'; suite('CommandLineCdPrefixRewriter', () => { @@ -80,34 +80,3 @@ suite('CommandLineCdPrefixRewriter', () => { }); }); }); - -suite('extractCdPrefix', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - suite('Posix', () => { - function t(commandLine: string, expectedDir: string | undefined, expectedCommand: string | undefined) { - const result = extractCdPrefix(commandLine, 'bash', OperatingSystem.Linux); - strictEqual(result?.directory, expectedDir); - strictEqual(result?.command, expectedCommand); - } - - test('should return undefined when no cd prefix', () => t('echo hello', undefined, undefined)); - test('should return undefined when cd has no suffix', () => t('cd /some/path', undefined, undefined)); - test('should extract cd prefix with && separator', () => t('cd /some/path && npm install', '/some/path', 'npm install')); - test('should extract quoted path', () => t('cd "/some/path" && npm install', '/some/path', 'npm install')); - test('should extract complex suffix', () => t('cd /path && npm install && npm test', '/path', 'npm install && npm test')); - }); - - suite('PowerShell', () => { - function t(commandLine: string, expectedDir: string | undefined, expectedCommand: string | undefined) { - const result = extractCdPrefix(commandLine, 'pwsh', OperatingSystem.Windows); - strictEqual(result?.directory, expectedDir); - strictEqual(result?.command, expectedCommand); - } - - test('should extract cd with ; separator', () => t('cd C:\\path; npm test', 'C:\\path', 'npm test')); - test('should extract cd /d with && separator', () => t('cd /d C:\\path && echo hello', 'C:\\path', 'echo hello')); - test('should extract Set-Location', () => t('Set-Location C:\\path; npm test', 'C:\\path', 'npm test')); - test('should extract Set-Location -Path', () => t('Set-Location -Path C:\\path; npm test', 'C:\\path', 'npm test')); - }); -}); From 2652502b3c769b3e65314f3018ef11ada665db9b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 14 Jan 2026 16:38:13 -0800 Subject: [PATCH 010/387] Add test cases to ensure it doesn't fail on spaces --- .../test/browser/runInTerminalHelpers.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts index c40f03d838b..f8768465290 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts @@ -480,6 +480,10 @@ suite('extractCdPrefix', () => { test('should extract cd prefix with && separator', () => t('cd /some/path && npm install', '/some/path', 'npm install')); test('should extract quoted path', () => t('cd "/some/path" && npm install', '/some/path', 'npm install')); test('should extract complex suffix', () => t('cd /path && npm install && npm test', '/path', 'npm install && npm test')); + + suite('unsupported patterns', () => { + test('should return undefined for path with escaped space', () => t('cd /some/path\ with\ spaces && npm install', undefined, undefined)); + }); }); suite('PowerShell', () => { @@ -493,5 +497,9 @@ suite('extractCdPrefix', () => { test('should extract cd /d with && separator', () => t('cd /d C:\\path && echo hello', 'C:\\path', 'echo hello')); test('should extract Set-Location', () => t('Set-Location C:\\path; npm test', 'C:\\path', 'npm test')); test('should extract Set-Location -Path', () => t('Set-Location -Path C:\\path; npm test', 'C:\\path', 'npm test')); + + suite('unsupported patterns', () => { + test('should return undefined for quoted path with spaces', () => t('cd "C:\\path with spaces"; npm test', undefined, undefined)); + }); }); }); From 981cf9d778bbb970547adabe05c5e0ed296e0c5a Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 14 Jan 2026 18:08:49 -0800 Subject: [PATCH 011/387] Fix handling of ExtendedLanguageModelToolResult2 Looks like this broke when we finalize an API --- .../api/common/extHostTypeConverters.ts | 68 ------------------- 1 file changed, 68 deletions(-) diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 02ae97a4831..6cfbb62d07c 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -3590,74 +3590,6 @@ export namespace LanguageModelToolResult { })); } - export function from(result: vscode.ExtendedLanguageModelToolResult, extension: IExtensionDescription): Dto | SerializableObjectWithBuffers> { - if (result.toolResultMessage) { - checkProposedApiEnabled(extension, 'chatParticipantPrivate'); - } - - const checkAudienceApi = (item: LanguageModelTextPart | LanguageModelDataPart) => { - if (item.audience) { - checkProposedApiEnabled(extension, 'languageModelToolResultAudience'); - } - }; - - let hasBuffers = false; - const dto: Dto = { - content: result.content.map(item => { - if (item instanceof types.LanguageModelTextPart) { - checkAudienceApi(item); - return { - kind: 'text', - value: item.value, - audience: item.audience - }; - } else if (item instanceof types.LanguageModelPromptTsxPart) { - return { - kind: 'promptTsx', - value: item.value, - }; - } else if (item instanceof types.LanguageModelDataPart) { - checkAudienceApi(item); - hasBuffers = true; - return { - kind: 'data', - value: { - mimeType: item.mimeType, - data: VSBuffer.wrap(item.data) - }, - audience: item.audience - }; - } else { - throw new Error('Unknown LanguageModelToolResult part type'); - } - }), - toolResultMessage: MarkdownString.fromStrict(result.toolResultMessage), - toolResultDetails: result.toolResultDetails?.map(detail => URI.isUri(detail) ? detail : Location.from(detail as vscode.Location)), - }; - - return hasBuffers ? new SerializableObjectWithBuffers(dto) : dto; - } -} - -export namespace LanguageModelToolResult2 { - export function to(result: IToolResult): vscode.LanguageModelToolResult2 { - const toolResult = new types.LanguageModelToolResult2(result.content.map(item => { - if (item.kind === 'text') { - return new types.LanguageModelTextPart(item.value, item.audience); - } else if (item.kind === 'data') { - return new types.LanguageModelDataPart(item.value.data.buffer, item.value.mimeType, item.audience); - } else { - return new types.LanguageModelPromptTsxPart(item.value); - } - })); - - if (result.toolMetadata) { - (toolResult as vscode.ExtendedLanguageModelToolResult).toolMetadata = result.toolMetadata; - } - - return toolResult; - } - export function from(result: vscode.ExtendedLanguageModelToolResult2, extension: IExtensionDescription): Dto | SerializableObjectWithBuffers> { if (result.toolResultMessage) { checkProposedApiEnabled(extension, 'chatParticipantPrivate'); From d55e08ee3e12e1dac89fc9d34333469dd32a9d5c Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Wed, 14 Jan 2026 18:32:50 -0800 Subject: [PATCH 012/387] fix git diff generation in chatrepoinfo --- src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts b/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts index 61636774433..e94a89c79a9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts +++ b/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts @@ -137,6 +137,16 @@ async function generateUnifiedDiff( const originalLines = originalContent.split('\n'); const modifiedLines = modifiedContent.split('\n'); + + // Remove trailing empty element if file ends with newline + // (split('\n') on "line1\nline2\n" gives ["line1", "line2", ""]) + if (originalLines.length > 0 && originalLines[originalLines.length - 1] === '') { + originalLines.pop(); + } + if (modifiedLines.length > 0 && modifiedLines[modifiedLines.length - 1] === '') { + modifiedLines.pop(); + } + const diffLines: string[] = []; const aPath = changeType === 'added' ? '/dev/null' : `a/${relPath}`; const bPath = changeType === 'deleted' ? '/dev/null' : `b/${relPath}`; From 94cfa628c6585518e80b422e62c2cb5096edfc02 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 15 Jan 2026 11:54:32 +0000 Subject: [PATCH 013/387] Update foreground colors for improved visibility in 2026 theme files --- extensions/theme-2026/themes/2026-dark.json | 103 ++--- extensions/theme-2026/themes/2026-light.json | 384 ++++++++++--------- 2 files changed, 247 insertions(+), 240 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index e073247c68e..5a008c8437d 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -3,7 +3,7 @@ "name": "2026 Dark", "type": "dark", "colors": { - "foreground": "#bbbbbb", + "foreground": "#bebebe", "disabledForeground": "#444444", "errorForeground": "#f48771", "descriptionForeground": "#888888", @@ -21,31 +21,31 @@ "button.hoverBackground": "#0080C4", "button.border": "#252627FF", "button.secondaryBackground": "#242424", - "button.secondaryForeground": "#bbbbbb", + "button.secondaryForeground": "#bebebe", "button.secondaryHoverBackground": "#007ABB", "checkbox.background": "#242424", "checkbox.border": "#252627FF", - "checkbox.foreground": "#bbbbbb", + "checkbox.foreground": "#bebebe", "dropdown.background": "#191919", - "dropdown.border": "#252627FF", - "dropdown.foreground": "#bbbbbb", + "dropdown.border": "#323435", + "dropdown.foreground": "#bebebe", "dropdown.listBackground": "#202020", "input.background": "#191919", "input.border": "#323435FF", - "input.foreground": "#bbbbbb", + "input.foreground": "#bebebe", "input.placeholderForeground": "#777777", "inputOption.activeBackground": "#007ABB33", - "inputOption.activeForeground": "#bbbbbb", + "inputOption.activeForeground": "#bebebe", "inputOption.activeBorder": "#252627FF", "inputValidation.errorBackground": "#191919", "inputValidation.errorBorder": "#252627FF", - "inputValidation.errorForeground": "#bbbbbb", + "inputValidation.errorForeground": "#bebebe", "inputValidation.infoBackground": "#191919", "inputValidation.infoBorder": "#252627FF", - "inputValidation.infoForeground": "#bbbbbb", + "inputValidation.infoForeground": "#bebebe", "inputValidation.warningBackground": "#191919", "inputValidation.warningBorder": "#252627FF", - "inputValidation.warningForeground": "#bbbbbb", + "inputValidation.warningForeground": "#bebebe", "scrollbar.shadow": "#191B1D4D", "scrollbarSlider.background": "#84848433", "scrollbarSlider.hoverBackground": "#84848466", @@ -54,21 +54,21 @@ "badge.foreground": "#FFFFFF", "progressBar.background": "#888888", "list.activeSelectionBackground": "#007ABB26", - "list.activeSelectionForeground": "#bbbbbb", + "list.activeSelectionForeground": "#bebebe", "list.inactiveSelectionBackground": "#242424", - "list.inactiveSelectionForeground": "#bbbbbb", + "list.inactiveSelectionForeground": "#bebebe", "list.hoverBackground": "#262626", - "list.hoverForeground": "#bbbbbb", + "list.hoverForeground": "#bebebe", "list.dropBackground": "#007ABB1A", "list.focusBackground": "#007ABB26", - "list.focusForeground": "#bbbbbb", + "list.focusForeground": "#bebebe", "list.focusOutline": "#007ABBB3", - "list.highlightForeground": "#bbbbbb", + "list.highlightForeground": "#bebebe", "list.invalidItemForeground": "#444444", "list.errorForeground": "#f48771", "list.warningForeground": "#e5ba7d", "activityBar.background": "#191919", - "activityBar.foreground": "#bbbbbb", + "activityBar.foreground": "#bebebe", "activityBar.inactiveForeground": "#888888", "activityBar.border": "#252627FF", "activityBar.activeBorder": "#252627FF", @@ -76,35 +76,35 @@ "activityBarBadge.background": "#007ABB", "activityBarBadge.foreground": "#FFFFFF", "sideBar.background": "#191919", - "sideBar.foreground": "#bbbbbb", + "sideBar.foreground": "#bebebe", "sideBar.border": "#252627FF", - "sideBarTitle.foreground": "#bbbbbb", + "sideBarTitle.foreground": "#bebebe", "sideBarSectionHeader.background": "#191919", - "sideBarSectionHeader.foreground": "#bbbbbb", + "sideBarSectionHeader.foreground": "#bebebe", "sideBarSectionHeader.border": "#252627FF", "titleBar.activeBackground": "#191919", - "titleBar.activeForeground": "#bbbbbb", + "titleBar.activeForeground": "#bebebe", "titleBar.inactiveBackground": "#191919", "titleBar.inactiveForeground": "#888888", "titleBar.border": "#252627FF", "menubar.selectionBackground": "#242424", - "menubar.selectionForeground": "#bbbbbb", + "menubar.selectionForeground": "#bebebe", "menu.background": "#202020", - "menu.foreground": "#bbbbbb", + "menu.foreground": "#bebebe", "menu.selectionBackground": "#007ABB26", - "menu.selectionForeground": "#bbbbbb", + "menu.selectionForeground": "#bebebe", "menu.separatorBackground": "#848484", "menu.border": "#252627FF", - "commandCenter.foreground": "#bbbbbb", - "commandCenter.activeForeground": "#bbbbbb", + "commandCenter.foreground": "#bebebe", + "commandCenter.activeForeground": "#bebebe", "commandCenter.background": "#191919", "commandCenter.activeBackground": "#262626", - "commandCenter.border": "#252627FF", + "commandCenter.border": "#323435", "editor.background": "#121212", - "editor.foreground": "#B7BABB", + "editor.foreground": "#BABDBE", "editorLineNumber.foreground": "#858889", - "editorLineNumber.activeForeground": "#B7BABB", - "editorCursor.foreground": "#B7BABB", + "editorLineNumber.activeForeground": "#BABDBE", + "editorCursor.foreground": "#BABDBE", "editor.selectionBackground": "#007ABB33", "editor.inactiveSelectionBackground": "#007ABB80", "editor.selectionHighlightBackground": "#007ABB1A", @@ -126,11 +126,11 @@ "editorBracketMatch.border": "#252627FF", "editorWidget.background": "#202020", "editorWidget.border": "#252627FF", - "editorWidget.foreground": "#bbbbbb", + "editorWidget.foreground": "#bebebe", "editorSuggestWidget.background": "#202020", "editorSuggestWidget.border": "#252627FF", - "editorSuggestWidget.foreground": "#bbbbbb", - "editorSuggestWidget.highlightForeground": "#bbbbbb", + "editorSuggestWidget.foreground": "#bebebe", + "editorSuggestWidget.highlightForeground": "#bebebe", "editorSuggestWidget.selectedBackground": "#007ABB26", "editorHoverWidget.background": "#202020", "editorHoverWidget.border": "#252627FF", @@ -138,14 +138,14 @@ "peekViewEditor.background": "#191919", "peekViewEditor.matchHighlightBackground": "#007ABB33", "peekViewResult.background": "#242424", - "peekViewResult.fileForeground": "#bbbbbb", + "peekViewResult.fileForeground": "#bebebe", "peekViewResult.lineForeground": "#888888", "peekViewResult.matchHighlightBackground": "#007ABB33", "peekViewResult.selectionBackground": "#007ABB26", - "peekViewResult.selectionForeground": "#bbbbbb", + "peekViewResult.selectionForeground": "#bebebe", "peekViewTitle.background": "#242424", "peekViewTitleDescription.foreground": "#888888", - "peekViewTitleLabel.foreground": "#bbbbbb", + "peekViewTitleLabel.foreground": "#bebebe", "editorGutter.background": "#121212", "editorGutter.addedBackground": "#73c991", "editorGutter.deletedBackground": "#f48771", @@ -161,16 +161,16 @@ "panel.background": "#191919", "panel.border": "#252627FF", "panelTitle.activeBorder": "#007ABB", - "panelTitle.activeForeground": "#bbbbbb", + "panelTitle.activeForeground": "#bebebe", "panelTitle.inactiveForeground": "#888888", "statusBar.background": "#191919", - "statusBar.foreground": "#bbbbbb", + "statusBar.foreground": "#bebebe", "statusBar.border": "#252627FF", "statusBar.focusBorder": "#007ABBB3", "statusBar.debuggingBackground": "#007ABB", "statusBar.debuggingForeground": "#FFFFFF", "statusBar.noFolderBackground": "#191919", - "statusBar.noFolderForeground": "#bbbbbb", + "statusBar.noFolderForeground": "#bebebe", "statusBarItem.activeBackground": "#007ABB", "statusBarItem.hoverBackground": "#262626", "statusBarItem.focusBorder": "#007ABBB3", @@ -178,14 +178,14 @@ "statusBarItem.prominentForeground": "#FFFFFF", "statusBarItem.prominentHoverBackground": "#007ABB", "tab.activeBackground": "#121212", - "tab.activeForeground": "#bbbbbb", + "tab.activeForeground": "#bebebe", "tab.inactiveBackground": "#191919", "tab.inactiveForeground": "#888888", "tab.border": "#252627FF", "tab.lastPinnedBorder": "#252627FF", "tab.activeBorder": "#121314", "tab.hoverBackground": "#262626", - "tab.hoverForeground": "#bbbbbb", + "tab.hoverForeground": "#bebebe", "tab.unfocusedActiveBackground": "#121212", "tab.unfocusedActiveForeground": "#888888", "tab.unfocusedInactiveBackground": "#191919", @@ -194,14 +194,14 @@ "editorGroupHeader.tabsBorder": "#252627FF", "breadcrumb.foreground": "#888888", "breadcrumb.background": "#121212", - "breadcrumb.focusForeground": "#bbbbbb", - "breadcrumb.activeSelectionForeground": "#bbbbbb", + "breadcrumb.focusForeground": "#bebebe", + "breadcrumb.activeSelectionForeground": "#bebebe", "breadcrumbPicker.background": "#202020", "notificationCenter.border": "#252627FF", - "notificationCenterHeader.foreground": "#bbbbbb", + "notificationCenterHeader.foreground": "#bebebe", "notificationCenterHeader.background": "#242424", "notificationToast.border": "#252627FF", - "notifications.foreground": "#bbbbbb", + "notifications.foreground": "#bebebe", "notifications.background": "#202020", "notifications.border": "#252627FF", "notificationLink.foreground": "#007ABB", @@ -209,15 +209,15 @@ "extensionButton.prominentForeground": "#FFFFFF", "extensionButton.prominentHoverBackground": "#0080C4", "pickerGroup.border": "#252627FF", - "pickerGroup.foreground": "#bbbbbb", + "pickerGroup.foreground": "#bebebe", "quickInput.background": "#202020", - "quickInput.foreground": "#bbbbbb", + "quickInput.foreground": "#bebebe", "quickInputList.focusBackground": "#007ABB26", - "quickInputList.focusForeground": "#bbbbbb", - "quickInputList.focusIconForeground": "#bbbbbb", + "quickInputList.focusForeground": "#bebebe", + "quickInputList.focusIconForeground": "#bebebe", "quickInputList.hoverBackground": "#525252", "terminal.selectionBackground": "#007ABB33", - "terminalCursor.foreground": "#bbbbbb", + "terminalCursor.foreground": "#bebebe", "terminalCursor.background": "#191919", "gitDecoration.addedResourceForeground": "#73c991", "gitDecoration.modifiedResourceForeground": "#e5ba7d", @@ -227,7 +227,10 @@ "gitDecoration.conflictingResourceForeground": "#f48771", "gitDecoration.stageModifiedResourceForeground": "#e5ba7d", "gitDecoration.stageDeletedResourceForeground": "#f48771", - "quickInputTitle.background": "#202020" + "quickInputTitle.background": "#202020", + "quickInput.border": "#323435", + "chat.requestBubbleBackground": "#007ABB26", + "chat.requestBubbleHoverBackground": "#007ABB46" }, "tokenColors": [ { diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index f0cd80705a7..aff045220a3 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -3,222 +3,222 @@ "name": "2026 Light", "type": "light", "colors": { - "foreground": "#1A1A1A", + "foreground": "#202020", "disabledForeground": "#999999", "errorForeground": "#ad0707", - "descriptionForeground": "#6B6B6B", - "icon.foreground": "#6B6B6B", - "focusBorder": "#D0D0D099", - "textBlockQuote.background": "#F5F5F5", - "textBlockQuote.border": "#D0D0D099", - "textCodeBlock.background": "#F5F5F5", - "textLink.foreground": "#007AF5", - "textLink.activeForeground": "#0280FF", - "textPreformat.foreground": "#6B6B6B", - "textSeparator.foreground": "#D0D0D099", - "button.background": "#0066CC", + "descriptionForeground": "#666666", + "icon.foreground": "#666666", + "focusBorder": "#4466CCFF", + "textBlockQuote.background": "#F3F3F3", + "textBlockQuote.border": "#ECEDEEFF", + "textCodeBlock.background": "#F3F3F3", + "textLink.foreground": "#6F89D8", + "textLink.activeForeground": "#7C94DB", + "textPreformat.foreground": "#666666", + "textSeparator.foreground": "#EEEEEEFF", + "button.background": "#4466CC", "button.foreground": "#FFFFFF", - "button.hoverBackground": "#006BD6", - "button.border": "#D0D0D099", - "button.secondaryBackground": "#F5F5F5", - "button.secondaryForeground": "#1A1A1A", - "button.secondaryHoverBackground": "#0066CC", - "checkbox.background": "#F5F5F5", - "checkbox.border": "#D0D0D099", - "checkbox.foreground": "#1A1A1A", - "dropdown.background": "#FFFFFF", - "dropdown.border": "#D0D0D099", - "dropdown.foreground": "#1A1A1A", - "dropdown.listBackground": "#EEEEEE", - "input.background": "#F5F5F5", - "input.border": "#D0D0D099", - "input.foreground": "#1A1A1A", + "button.hoverBackground": "#4F6FCF", + "button.border": "#ECEDEEFF", + "button.secondaryBackground": "#F3F3F3", + "button.secondaryForeground": "#202020", + "button.secondaryHoverBackground": "#4466CC", + "checkbox.background": "#F3F3F3", + "checkbox.border": "#ECEDEEFF", + "checkbox.foreground": "#202020", + "dropdown.background": "#F9F9F9", + "dropdown.border": "#D0D1D2", + "dropdown.foreground": "#202020", + "dropdown.listBackground": "#FCFCFC", + "input.background": "#F9F9F9", + "input.border": "#D0D1D2FF", + "input.foreground": "#202020", "input.placeholderForeground": "#AAAAAA", - "inputOption.activeBackground": "#0066CC33", - "inputOption.activeForeground": "#1A1A1A", - "inputOption.activeBorder": "#D0D0D099", - "inputValidation.errorBackground": "#F5F5F5", - "inputValidation.errorBorder": "#D0D0D099", - "inputValidation.errorForeground": "#1A1A1A", - "inputValidation.infoBackground": "#F5F5F5", - "inputValidation.infoBorder": "#D0D0D099", - "inputValidation.infoForeground": "#1A1A1A", - "inputValidation.warningBackground": "#F5F5F5", - "inputValidation.warningBorder": "#D0D0D099", - "inputValidation.warningForeground": "#1A1A1A", - "scrollbar.shadow": "#FFFFFF4D", - "scrollbarSlider.background": "#84848433", - "scrollbarSlider.hoverBackground": "#84848466", - "scrollbarSlider.activeBackground": "#84848499", - "badge.background": "#0066CC", + "inputOption.activeBackground": "#4466CC33", + "inputOption.activeForeground": "#202020", + "inputOption.activeBorder": "#ECEDEEFF", + "inputValidation.errorBackground": "#F9F9F9", + "inputValidation.errorBorder": "#ECEDEEFF", + "inputValidation.errorForeground": "#202020", + "inputValidation.infoBackground": "#F9F9F9", + "inputValidation.infoBorder": "#ECEDEEFF", + "inputValidation.infoForeground": "#202020", + "inputValidation.warningBackground": "#F9F9F9", + "inputValidation.warningBorder": "#ECEDEEFF", + "inputValidation.warningForeground": "#202020", + "scrollbar.shadow": "#F5F6F84D", + "scrollbarSlider.background": "#4466CC33", + "scrollbarSlider.hoverBackground": "#4466CC66", + "scrollbarSlider.activeBackground": "#4466CC99", + "badge.background": "#4466CC", "badge.foreground": "#FFFFFF", - "progressBar.background": "#6B6B6B", - "list.activeSelectionBackground": "#0066CC26", - "list.activeSelectionForeground": "#1A1A1A", - "list.inactiveSelectionBackground": "#F5F5F5", - "list.inactiveSelectionForeground": "#1A1A1A", + "progressBar.background": "#666666", + "list.activeSelectionBackground": "#4466CC26", + "list.activeSelectionForeground": "#202020", + "list.inactiveSelectionBackground": "#F3F3F3", + "list.inactiveSelectionForeground": "#202020", "list.hoverBackground": "#FFFFFF", - "list.hoverForeground": "#1A1A1A", - "list.dropBackground": "#0066CC1A", - "list.focusBackground": "#0066CC26", - "list.focusForeground": "#1A1A1A", - "list.focusOutline": "#D0D0D099", - "list.highlightForeground": "#1A1A1A", + "list.hoverForeground": "#202020", + "list.dropBackground": "#4466CC1A", + "list.focusBackground": "#4466CC26", + "list.focusForeground": "#202020", + "list.focusOutline": "#4466CCFF", + "list.highlightForeground": "#202020", "list.invalidItemForeground": "#999999", "list.errorForeground": "#ad0707", "list.warningForeground": "#667309", - "activityBar.background": "#FFFFFF", - "activityBar.foreground": "#1A1A1A", - "activityBar.inactiveForeground": "#6B6B6B", - "activityBar.border": "#D0D0D099", - "activityBar.activeBorder": "#D0D0D099", - "activityBar.activeFocusBorder": "#0066CC99", - "activityBarBadge.background": "#0066CC", + "activityBar.background": "#F9F9F9", + "activityBar.foreground": "#202020", + "activityBar.inactiveForeground": "#666666", + "activityBar.border": "#ECEDEEFF", + "activityBar.activeBorder": "#ECEDEEFF", + "activityBar.activeFocusBorder": "#4466CCFF", + "activityBarBadge.background": "#4466CC", "activityBarBadge.foreground": "#FFFFFF", - "sideBar.background": "#FFFFFF", - "sideBar.foreground": "#1A1A1A", - "sideBar.border": "#D0D0D099", - "sideBarTitle.foreground": "#1A1A1A", - "sideBarSectionHeader.background": "#FFFFFF", - "sideBarSectionHeader.foreground": "#1A1A1A", - "sideBarSectionHeader.border": "#D0D0D099", - "titleBar.activeBackground": "#FFFFFF", - "titleBar.activeForeground": "#1A1A1A", - "titleBar.inactiveBackground": "#FFFFFF", - "titleBar.inactiveForeground": "#6B6B6B", - "titleBar.border": "#D0D0D099", - "menubar.selectionBackground": "#F5F5F5", - "menubar.selectionForeground": "#1A1A1A", - "menu.background": "#EEEEEE", - "menu.foreground": "#1A1A1A", - "menu.selectionBackground": "#0066CC26", - "menu.selectionForeground": "#1A1A1A", - "menu.separatorBackground": "#848484", - "menu.border": "#D0D0D099", - "commandCenter.foreground": "#1A1A1A", - "commandCenter.activeForeground": "#1A1A1A", - "commandCenter.background": "#FFFFFF", + "sideBar.background": "#F9F9F9", + "sideBar.foreground": "#202020", + "sideBar.border": "#ECEDEEFF", + "sideBarTitle.foreground": "#202020", + "sideBarSectionHeader.background": "#F9F9F9", + "sideBarSectionHeader.foreground": "#202020", + "sideBarSectionHeader.border": "#ECEDEEFF", + "titleBar.activeBackground": "#F9F9F9", + "titleBar.activeForeground": "#202020", + "titleBar.inactiveBackground": "#F9F9F9", + "titleBar.inactiveForeground": "#666666", + "titleBar.border": "#ECEDEEFF", + "menubar.selectionBackground": "#F3F3F3", + "menubar.selectionForeground": "#202020", + "menu.background": "#FCFCFC", + "menu.foreground": "#202020", + "menu.selectionBackground": "#4466CC26", + "menu.selectionForeground": "#202020", + "menu.separatorBackground": "#F4F4F4", + "menu.border": "#ECEDEEFF", + "commandCenter.foreground": "#202020", + "commandCenter.activeForeground": "#202020", + "commandCenter.background": "#F9F9F9", "commandCenter.activeBackground": "#FFFFFF", - "commandCenter.border": "#D0D0D099", - "editor.background": "#FFFFFF", - "editor.foreground": "#1A1A1A", - "editorLineNumber.foreground": "#6B6B6B", - "editorLineNumber.activeForeground": "#1A1A1A", - "editorCursor.foreground": "#1A1A1A", - "editor.selectionBackground": "#0066CC33", - "editor.inactiveSelectionBackground": "#0066CC80", - "editor.selectionHighlightBackground": "#0066CC1A", - "editor.wordHighlightBackground": "#0066CCB3", - "editor.wordHighlightStrongBackground": "#0066CCE6", - "editor.findMatchBackground": "#0066CC4D", - "editor.findMatchHighlightBackground": "#0066CC26", - "editor.findRangeHighlightBackground": "#F5F5F5", - "editor.hoverHighlightBackground": "#F5F5F5", - "editor.lineHighlightBackground": "#F5F5F5", - "editor.rangeHighlightBackground": "#F5F5F5", - "editorLink.activeForeground": "#0066CC", - "editorWhitespace.foreground": "#6B6B6B4D", - "editorIndentGuide.background": "#8484844D", - "editorIndentGuide.activeBackground": "#848484", - "editorRuler.foreground": "#848484", - "editorCodeLens.foreground": "#6B6B6B", - "editorBracketMatch.background": "#0066CC80", - "editorBracketMatch.border": "#D0D0D099", - "editorWidget.background": "#EEEEEE", - "editorWidget.border": "#D0D0D099", - "editorWidget.foreground": "#1A1A1A", - "editorSuggestWidget.background": "#EEEEEE", - "editorSuggestWidget.border": "#D0D0D099", - "editorSuggestWidget.foreground": "#1A1A1A", - "editorSuggestWidget.highlightForeground": "#1A1A1A", - "editorSuggestWidget.selectedBackground": "#0066CC26", - "editorHoverWidget.background": "#EEEEEE", - "editorHoverWidget.border": "#D0D0D099", - "peekView.border": "#D0D0D099", - "peekViewEditor.background": "#FFFFFF", - "peekViewEditor.matchHighlightBackground": "#0066CC33", - "peekViewResult.background": "#F5F5F5", - "peekViewResult.fileForeground": "#1A1A1A", - "peekViewResult.lineForeground": "#6B6B6B", - "peekViewResult.matchHighlightBackground": "#0066CC33", - "peekViewResult.selectionBackground": "#0066CC26", - "peekViewResult.selectionForeground": "#1A1A1A", - "peekViewTitle.background": "#F5F5F5", - "peekViewTitleDescription.foreground": "#6B6B6B", - "peekViewTitleLabel.foreground": "#1A1A1A", - "editorGutter.background": "#FFFFFF", + "commandCenter.border": "#D0D1D2", + "editor.background": "#FDFDFD", + "editor.foreground": "#202123", + "editorLineNumber.foreground": "#656668", + "editorLineNumber.activeForeground": "#202123", + "editorCursor.foreground": "#202123", + "editor.selectionBackground": "#4466CC33", + "editor.inactiveSelectionBackground": "#4466CC80", + "editor.selectionHighlightBackground": "#4466CC1A", + "editor.wordHighlightBackground": "#4466CCB3", + "editor.wordHighlightStrongBackground": "#4466CCE6", + "editor.findMatchBackground": "#4466CC4D", + "editor.findMatchHighlightBackground": "#4466CC26", + "editor.findRangeHighlightBackground": "#F3F3F3", + "editor.hoverHighlightBackground": "#F3F3F3", + "editor.lineHighlightBackground": "#F3F3F3", + "editor.rangeHighlightBackground": "#F3F3F3", + "editorLink.activeForeground": "#4466CC", + "editorWhitespace.foreground": "#6666664D", + "editorIndentGuide.background": "#F4F4F44D", + "editorIndentGuide.activeBackground": "#F4F4F4", + "editorRuler.foreground": "#F4F4F4", + "editorCodeLens.foreground": "#666666", + "editorBracketMatch.background": "#4466CC80", + "editorBracketMatch.border": "#ECEDEEFF", + "editorWidget.background": "#FCFCFC", + "editorWidget.border": "#ECEDEEFF", + "editorWidget.foreground": "#202020", + "editorSuggestWidget.background": "#FCFCFC", + "editorSuggestWidget.border": "#ECEDEEFF", + "editorSuggestWidget.foreground": "#202020", + "editorSuggestWidget.highlightForeground": "#202020", + "editorSuggestWidget.selectedBackground": "#4466CC26", + "editorHoverWidget.background": "#FCFCFC", + "editorHoverWidget.border": "#ECEDEEFF", + "peekView.border": "#ECEDEEFF", + "peekViewEditor.background": "#F9F9F9", + "peekViewEditor.matchHighlightBackground": "#4466CC33", + "peekViewResult.background": "#F3F3F3", + "peekViewResult.fileForeground": "#202020", + "peekViewResult.lineForeground": "#666666", + "peekViewResult.matchHighlightBackground": "#4466CC33", + "peekViewResult.selectionBackground": "#4466CC26", + "peekViewResult.selectionForeground": "#202020", + "peekViewTitle.background": "#F3F3F3", + "peekViewTitleDescription.foreground": "#666666", + "peekViewTitleLabel.foreground": "#202020", + "editorGutter.background": "#FDFDFD", "editorGutter.addedBackground": "#587c0c", "editorGutter.deletedBackground": "#ad0707", "diffEditor.insertedTextBackground": "#587c0c54", "diffEditor.removedTextBackground": "#ad070754", - "editorOverviewRuler.border": "#D0D0D099", - "editorOverviewRuler.findMatchForeground": "#0066CC99", + "editorOverviewRuler.border": "#ECEDEEFF", + "editorOverviewRuler.findMatchForeground": "#4466CC99", "editorOverviewRuler.modifiedForeground": "#007acc", "editorOverviewRuler.addedForeground": "#587c0c", "editorOverviewRuler.deletedForeground": "#ad0707", "editorOverviewRuler.errorForeground": "#ad0707", "editorOverviewRuler.warningForeground": "#667309", - "panel.background": "#F5F5F5", - "panel.border": "#D0D0D099", - "panelTitle.activeBorder": "#0066CC", - "panelTitle.activeForeground": "#1A1A1A", - "panelTitle.inactiveForeground": "#6B6B6B", - "statusBar.background": "#FFFFFF", - "statusBar.foreground": "#1A1A1A", - "statusBar.border": "#D0D0D099", - "statusBar.focusBorder": "#D0D0D099", - "statusBar.debuggingBackground": "#0066CC", + "panel.background": "#F9F9F9", + "panel.border": "#ECEDEEFF", + "panelTitle.activeBorder": "#4466CC", + "panelTitle.activeForeground": "#202020", + "panelTitle.inactiveForeground": "#666666", + "statusBar.background": "#F9F9F9", + "statusBar.foreground": "#202020", + "statusBar.border": "#ECEDEEFF", + "statusBar.focusBorder": "#4466CCFF", + "statusBar.debuggingBackground": "#4466CC", "statusBar.debuggingForeground": "#FFFFFF", - "statusBar.noFolderBackground": "#FFFFFF", - "statusBar.noFolderForeground": "#1A1A1A", - "statusBarItem.activeBackground": "#0066CC", + "statusBar.noFolderBackground": "#F9F9F9", + "statusBar.noFolderForeground": "#202020", + "statusBarItem.activeBackground": "#4466CC", "statusBarItem.hoverBackground": "#FFFFFF", - "statusBarItem.focusBorder": "#D0D0D099", - "statusBarItem.prominentBackground": "#0066CC", + "statusBarItem.focusBorder": "#4466CCFF", + "statusBarItem.prominentBackground": "#4466CC", "statusBarItem.prominentForeground": "#FFFFFF", - "statusBarItem.prominentHoverBackground": "#0066CC", - "tab.activeBackground": "#FFFFFF", - "tab.activeForeground": "#1A1A1A", - "tab.inactiveBackground": "#FFFFFF", - "tab.inactiveForeground": "#6B6B6B", - "tab.border": "#D0D0D099", - "tab.lastPinnedBorder": "#D0D0D099", - "tab.activeBorder": "#FFFFFF", + "statusBarItem.prominentHoverBackground": "#4466CC", + "tab.activeBackground": "#FDFDFD", + "tab.activeForeground": "#202020", + "tab.inactiveBackground": "#F9F9F9", + "tab.inactiveForeground": "#666666", + "tab.border": "#ECEDEEFF", + "tab.lastPinnedBorder": "#ECEDEEFF", + "tab.activeBorder": "#FBFBFD", "tab.hoverBackground": "#FFFFFF", - "tab.hoverForeground": "#1A1A1A", - "tab.unfocusedActiveBackground": "#FFFFFF", - "tab.unfocusedActiveForeground": "#6B6B6B", - "tab.unfocusedInactiveBackground": "#FFFFFF", + "tab.hoverForeground": "#202020", + "tab.unfocusedActiveBackground": "#FDFDFD", + "tab.unfocusedActiveForeground": "#666666", + "tab.unfocusedInactiveBackground": "#F9F9F9", "tab.unfocusedInactiveForeground": "#999999", - "editorGroupHeader.tabsBackground": "#FFFFFF", - "editorGroupHeader.tabsBorder": "#D0D0D099", - "breadcrumb.foreground": "#6B6B6B", - "breadcrumb.background": "#FFFFFF", - "breadcrumb.focusForeground": "#1A1A1A", - "breadcrumb.activeSelectionForeground": "#1A1A1A", - "breadcrumbPicker.background": "#EEEEEE", - "notificationCenter.border": "#D0D0D099", - "notificationCenterHeader.foreground": "#1A1A1A", - "notificationCenterHeader.background": "#F5F5F5", - "notificationToast.border": "#D0D0D099", - "notifications.foreground": "#1A1A1A", - "notifications.background": "#EEEEEE", - "notifications.border": "#D0D0D099", - "notificationLink.foreground": "#0066CC", - "extensionButton.prominentBackground": "#0066CC", + "editorGroupHeader.tabsBackground": "#F9F9F9", + "editorGroupHeader.tabsBorder": "#ECEDEEFF", + "breadcrumb.foreground": "#666666", + "breadcrumb.background": "#FDFDFD", + "breadcrumb.focusForeground": "#202020", + "breadcrumb.activeSelectionForeground": "#202020", + "breadcrumbPicker.background": "#FCFCFC", + "notificationCenter.border": "#ECEDEEFF", + "notificationCenterHeader.foreground": "#202020", + "notificationCenterHeader.background": "#F3F3F3", + "notificationToast.border": "#ECEDEEFF", + "notifications.foreground": "#202020", + "notifications.background": "#FCFCFC", + "notifications.border": "#ECEDEEFF", + "notificationLink.foreground": "#4466CC", + "extensionButton.prominentBackground": "#4466CC", "extensionButton.prominentForeground": "#FFFFFF", - "extensionButton.prominentHoverBackground": "#006BD6", - "pickerGroup.border": "#D0D0D099", - "pickerGroup.foreground": "#1A1A1A", - "quickInput.background": "#F5F5F5", - "quickInput.foreground": "#1A1A1A", - "quickInputList.focusBackground": "#0066CC26", - "quickInputList.focusForeground": "#1A1A1A", - "quickInputList.focusIconForeground": "#1A1A1A", + "extensionButton.prominentHoverBackground": "#4F6FCF", + "pickerGroup.border": "#ECEDEEFF", + "pickerGroup.foreground": "#202020", + "quickInput.background": "#FCFCFC", + "quickInput.foreground": "#202020", + "quickInputList.focusBackground": "#4466CC26", + "quickInputList.focusForeground": "#202020", + "quickInputList.focusIconForeground": "#202020", "quickInputList.hoverBackground": "#FAFAFA", - "terminal.selectionBackground": "#0066CC33", - "terminalCursor.foreground": "#1A1A1A", - "terminalCursor.background": "#FFFFFF", + "terminal.selectionBackground": "#4466CC33", + "terminalCursor.foreground": "#202020", + "terminalCursor.background": "#F9F9F9", "gitDecoration.addedResourceForeground": "#587c0c", "gitDecoration.modifiedResourceForeground": "#667309", "gitDecoration.deletedResourceForeground": "#ad0707", @@ -226,7 +226,11 @@ "gitDecoration.ignoredResourceForeground": "#8E8E90", "gitDecoration.conflictingResourceForeground": "#ad0707", "gitDecoration.stageModifiedResourceForeground": "#667309", - "gitDecoration.stageDeletedResourceForeground": "#ad0707" + "gitDecoration.stageDeletedResourceForeground": "#ad0707", + "quickInputTitle.background": "#FCFCFC", + "quickInput.border": "#D0D1D2", + "chat.requestBubbleBackground": "#4466CC1A", + "chat.requestBubbleHoverBackground": "#4466CC26" }, "tokenColors": [ { From fa711c84d538bef0323fd749091244b87a82e8fc Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Jan 2026 06:04:21 -0800 Subject: [PATCH 014/387] Use os in absolute check --- .../chatAgentTools/browser/tools/runInTerminalTool.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index 2192b823612..f251ee467d2 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -12,7 +12,7 @@ import { Event } from '../../../../../../base/common/event.js'; import { MarkdownString, type IMarkdownString } from '../../../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../../base/common/map.js'; -import { basename } from '../../../../../../base/common/path.js'; +import { basename, posix, win32 } from '../../../../../../base/common/path.js'; import { OperatingSystem, OS } from '../../../../../../base/common/platform.js'; import { count } from '../../../../../../base/common/strings.js'; import { generateUuid } from '../../../../../../base/common/uuid.js'; @@ -507,7 +507,10 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { let confirmationTitle: string; if (extractedCd && cwd) { // Construct the full directory path using the cwd's scheme/authority - const directoryUri = extractedCd.directory.startsWith('/') || /^[a-zA-Z]:/.test(extractedCd.directory) + const isAbsolutePath = os === OperatingSystem.Windows + ? win32.isAbsolute(extractedCd.directory) + : posix.isAbsolute(extractedCd.directory); + const directoryUri = isAbsolutePath ? URI.from({ scheme: cwd.scheme, authority: cwd.authority, path: extractedCd.directory }) : URI.joinPath(cwd, extractedCd.directory); const directoryLabel = this._labelService.getUriLabel(directoryUri); From 57b1d5f3bc01d390e8d588cff1bdcbfffa4a7fad Mon Sep 17 00:00:00 2001 From: kiofaw <1135332676@qq.com> Date: Thu, 15 Jan 2026 22:37:29 +0800 Subject: [PATCH 015/387] refactor: replace AsyncIterableObject with AsyncIterableProducer in codeBlockOperations.ts --- .../contrib/chat/browser/actions/codeBlockOperations.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index 2631365f2c3..0baba0ac0dc 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AsyncIterableObject } from '../../../../../base/common/async.js'; +import { AsyncIterableProducer } from '../../../../../base/common/async.js'; import { VSBuffer } from '../../../../../base/common/buffer.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { CharCode } from '../../../../../base/common/charCode.js'; @@ -338,7 +338,7 @@ export class ApplyCodeBlockOperation { } private getTextEdits(codeBlock: ICodeMapperCodeBlock, chatSessionResource: URI | undefined, token: CancellationToken): AsyncIterable { - return new AsyncIterableObject(async executor => { + return new AsyncIterableProducer(async executor => { const request: ICodeMapperRequest = { codeBlocks: [codeBlock], chatSessionResource, @@ -359,7 +359,7 @@ export class ApplyCodeBlockOperation { } private getNotebookEdits(codeBlock: ICodeMapperCodeBlock, chatSessionResource: URI | undefined, token: CancellationToken): AsyncIterable<[URI, TextEdit[]] | ICellEditOperation[]> { - return new AsyncIterableObject<[URI, TextEdit[]] | ICellEditOperation[]>(async executor => { + return new AsyncIterableProducer<[URI, TextEdit[]] | ICellEditOperation[]>(async executor => { const request: ICodeMapperRequest = { codeBlocks: [codeBlock], chatSessionResource, From 2cd5626d131dad82ea3fd780233edaf4d9e0a207 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 15 Jan 2026 18:17:26 +0100 Subject: [PATCH 016/387] improved chat input pickers --- .../lib/stylelint/vscode-known-variables.json | 1 + src/vs/base/browser/ui/toolbar/toolbar.css | 16 ++++- src/vs/base/browser/ui/toolbar/toolbar.ts | 68 ++++++++++++++----- .../browser/actionWidgetDropdown.ts | 5 +- .../browser/agentSessions/agentSessions.ts | 6 ++ .../agentSessions/agentSessionsFilter.ts | 9 ++- .../agentSessions/agentSessionsModel.ts | 26 +++---- .../browser/agentSessions/focusViewService.ts | 11 +-- .../browser/widget/input/chatInputPart.ts | 16 ++++- .../widget/input/chatInputPickerActionItem.ts | 60 ++++++++++++++++ .../widget/input/modePickerActionItem.ts | 28 +++++--- .../widget/input/modelPickerActionItem.ts | 13 ++-- .../input/sessionTargetPickerActionItem.ts | 22 +++--- .../chat/browser/widget/media/chat.css | 5 +- .../contrib/chat/common/chatModes.ts | 18 +++-- .../scm/browser/scmRepositoryRenderer.ts | 2 +- 16 files changed, 218 insertions(+), 88 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.ts diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 62fc5399dbe..17778581978 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -993,6 +993,7 @@ "--vscode-chat-font-size-body-xl", "--vscode-chat-font-size-body-xs", "--vscode-chat-font-size-body-xxl", + "--vscode-toolbar-action-min-width", "--comment-thread-editor-font-family", "--comment-thread-editor-font-weight", "--comment-thread-state-color", diff --git a/src/vs/base/browser/ui/toolbar/toolbar.css b/src/vs/base/browser/ui/toolbar/toolbar.css index 4c4c684755e..ea443012a3e 100644 --- a/src/vs/base/browser/ui/toolbar/toolbar.css +++ b/src/vs/base/browser/ui/toolbar/toolbar.css @@ -12,9 +12,21 @@ padding: 0; } -.monaco-toolbar.responsive { +.monaco-toolbar.responsive.responsive-all { .monaco-action-bar > .actions-container > .action-item { flex-shrink: 1; - min-width: 20px; + min-width: var(--vscode-toolbar-action-min-width, 20px); + } +} + +.monaco-toolbar.responsive.responsive-last { + .monaco-action-bar > .actions-container > .action-item { + flex-shrink: 0; + } + + .monaco-action-bar:not(.has-overflow) > .actions-container > .action-item:last-child, + .monaco-action-bar.has-overflow > .actions-container > .action-item:nth-last-child(2) { + flex-shrink: 1; + min-width: var(--vscode-toolbar-action-min-width, 20px); } } diff --git a/src/vs/base/browser/ui/toolbar/toolbar.ts b/src/vs/base/browser/ui/toolbar/toolbar.ts index 288c77f7d52..21696911bd1 100644 --- a/src/vs/base/browser/ui/toolbar/toolbar.ts +++ b/src/vs/base/browser/ui/toolbar/toolbar.ts @@ -18,7 +18,10 @@ import * as nls from '../../../../nls.js'; import { IHoverDelegate } from '../hover/hoverDelegate.js'; import { createInstantHoverDelegate } from '../hover/hoverDelegateFactory.js'; -const ACTION_MIN_WIDTH = 24; /* 20px codicon + 4px left padding*/ +const ACTION_MIN_WIDTH = 20; /* 20px codicon */ +const ACTION_PADDING = 4; /* 4px padding */ + +const ACTION_MIN_WIDTH_VAR = '--vscode-toolbar-action-min-width'; export interface IToolBarOptions { orientation?: ActionsOrientation; @@ -53,9 +56,11 @@ export interface IToolBarOptions { /** * Controls the responsive behavior of the primary group of the toolbar. * - `enabled`: Whether the responsive behavior is enabled. + * - `kind`: The kind of responsive behavior to apply. Can be either `last` to only shrink the last item, or `all` to shrink all items equally. * - `minItems`: The minimum number of items that should always be visible. + * - `actionMinWidth`: The minimum width of each action item. Defaults to `ACTION_MIN_WIDTH` (24px). */ - responsiveBehavior?: { enabled: boolean; minItems?: number }; + responsiveBehavior?: { enabled: boolean; kind: 'last' | 'all'; minItems?: number; actionMinWidth?: number }; } /** @@ -76,6 +81,7 @@ export class ToolBar extends Disposable { private originalSecondaryActions: ReadonlyArray = []; private hiddenActions: { action: IAction; size: number }[] = []; private readonly disposables = this._register(new DisposableStore()); + private readonly actionMinWidth: number; constructor(private readonly container: HTMLElement, contextMenuProvider: IContextMenuProvider, options: IToolBarOptions = { orientation: ActionsOrientation.HORIZONTAL }) { super(); @@ -155,9 +161,15 @@ export class ToolBar extends Disposable { } })); + // Store effective action min width + this.actionMinWidth = (options.responsiveBehavior?.actionMinWidth ?? ACTION_MIN_WIDTH) + ACTION_PADDING; + // Responsive support if (this.options.responsiveBehavior?.enabled) { - this.element.classList.add('responsive'); + this.element.classList.toggle('responsive', true); + this.element.classList.toggle('responsive-all', this.options.responsiveBehavior.kind === 'all'); + this.element.classList.toggle('responsive-last', this.options.responsiveBehavior.kind === 'last'); + this.element.style.setProperty(ACTION_MIN_WIDTH_VAR, `${this.actionMinWidth - ACTION_PADDING}px`); const observer = new ResizeObserver(() => { this.updateActions(this.element.getBoundingClientRect().width); @@ -239,27 +251,30 @@ export class ToolBar extends Disposable { this.actionBar.push(action, { icon: this.options.icon ?? true, label: this.options.label ?? false, keybinding: this.getKeybindingLabel(action) }); }); + this.actionBar.domNode.classList.toggle('has-overflow', this.actionBar.hasAction(this.toggleMenuAction)); + if (this.options.responsiveBehavior?.enabled) { // Reset hidden actions this.hiddenActions.length = 0; // Set the minimum width if (this.options.responsiveBehavior?.minItems !== undefined) { - let itemCount = this.options.responsiveBehavior.minItems; + const itemCount = this.options.responsiveBehavior.minItems; // Account for overflow menu + let overflowWidth = 0; if ( this.originalSecondaryActions.length > 0 || itemCount < this.originalPrimaryActions.length ) { - itemCount += 1; + overflowWidth = ACTION_MIN_WIDTH + ACTION_PADDING; } - this.container.style.minWidth = `${itemCount * ACTION_MIN_WIDTH}px`; - this.element.style.minWidth = `${itemCount * ACTION_MIN_WIDTH}px`; + this.container.style.minWidth = `${itemCount * this.actionMinWidth + overflowWidth}px`; + this.element.style.minWidth = `${itemCount * this.actionMinWidth + overflowWidth}px`; } else { - this.container.style.minWidth = `${ACTION_MIN_WIDTH}px`; - this.element.style.minWidth = `${ACTION_MIN_WIDTH}px`; + this.container.style.minWidth = `${ACTION_MIN_WIDTH + ACTION_PADDING}px`; + this.element.style.minWidth = `${ACTION_MIN_WIDTH + ACTION_PADDING}px`; } // Update toolbar actions to fit with container width @@ -290,14 +305,33 @@ export class ToolBar extends Disposable { // Each action is assumed to have a minimum width so that actions with a label // can shrink to the action's minimum width. We do this so that action visibility // takes precedence over the action label. - const actionBarWidth = () => this.actionBar.length() * ACTION_MIN_WIDTH; + const actionBarWidth = (actualWidth: boolean) => { + if (this.options.responsiveBehavior?.kind === 'last') { + const hasToggleMenuAction = this.actionBar.hasAction(this.toggleMenuAction); + const primaryActionsCount = hasToggleMenuAction + ? this.actionBar.length() - 1 + : this.actionBar.length(); + + let itemsWidth = 0; + for (let i = 0; i < primaryActionsCount - 1; i++) { + itemsWidth += this.actionBar.getWidth(i) + ACTION_PADDING; + } + + itemsWidth += actualWidth ? this.actionBar.getWidth(primaryActionsCount - 1) : this.actionMinWidth; // item to shrink + itemsWidth += hasToggleMenuAction ? ACTION_MIN_WIDTH + ACTION_PADDING : 0; // toggle menu action + + return itemsWidth; + } else { + return this.actionBar.length() * this.actionMinWidth; + } + }; // Action bar fits and there are no hidden actions to show - if (actionBarWidth() <= containerWidth && this.hiddenActions.length === 0) { + if (actionBarWidth(false) <= containerWidth && this.hiddenActions.length === 0) { return; } - if (actionBarWidth() > containerWidth) { + if (actionBarWidth(false) > containerWidth) { // Check for max items limit if (this.options.responsiveBehavior?.minItems !== undefined) { const primaryActionsCount = this.actionBar.hasAction(this.toggleMenuAction) @@ -310,14 +344,14 @@ export class ToolBar extends Disposable { } // Hide actions from the right - while (actionBarWidth() > containerWidth && this.actionBar.length() > 0) { + while (actionBarWidth(true) > containerWidth && this.actionBar.length() > 0) { const index = this.originalPrimaryActions.length - this.hiddenActions.length - 1; if (index < 0) { break; } // Store the action and its size - const size = Math.min(ACTION_MIN_WIDTH, this.getItemWidth(index)); + const size = Math.min(this.actionMinWidth, this.getItemWidth(index)); const action = this.originalPrimaryActions[index]; this.hiddenActions.unshift({ action, size }); @@ -339,7 +373,7 @@ export class ToolBar extends Disposable { // Show actions from the top of the toggle menu while (this.hiddenActions.length > 0) { const entry = this.hiddenActions.shift()!; - if (actionBarWidth() + entry.size > containerWidth) { + if (actionBarWidth(true) + entry.size > containerWidth) { // Not enough space to show the action this.hiddenActions.unshift(entry); break; @@ -355,7 +389,7 @@ export class ToolBar extends Disposable { // There are no secondary actions, and there is only one hidden item left so we // remove the overflow menu making space for the last hidden action to be shown. - if (this.originalSecondaryActions.length === 0 && this.hiddenActions.length === 1) { + if (this.originalSecondaryActions.length === 0 && this.hiddenActions.length === 0) { this.toggleMenuAction.menuActions = []; this.actionBar.pull(this.actionBar.length() - 1); } @@ -368,6 +402,8 @@ export class ToolBar extends Disposable { const secondaryActions = this.originalSecondaryActions.slice(0); this.toggleMenuAction.menuActions = Separator.join(hiddenActions, secondaryActions); } + + this.actionBar.domNode.classList.toggle('has-overflow', this.actionBar.hasAction(this.toggleMenuAction)); } private clear(): void { diff --git a/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts b/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts index a066046e271..2b5897deff6 100644 --- a/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts +++ b/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts @@ -33,6 +33,9 @@ export interface IActionWidgetDropdownOptions extends IBaseDropdownOptions { readonly actionBarActions?: IAction[]; readonly actionBarActionProvider?: IActionProvider; readonly showItemKeybindings?: boolean; + + // Function that returns the anchor element for the dropdown + getAnchor?: () => HTMLElement; } /** @@ -169,7 +172,7 @@ export class ActionWidgetDropdown extends BaseDropdown { false, actionWidgetItems, actionWidgetDelegate, - this.element, + this._options.getAnchor?.() ?? this.element, undefined, actionBarActions, accessibilityProvider diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts index 0993aa2e8c8..20fd654cbc5 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts @@ -15,6 +15,7 @@ export enum AgentSessionProviders { Local = localChatSessionType, Background = 'copilotcli', Cloud = 'copilot-cloud-agent', + ClaudeCode = 'claude-code', } export function getAgentSessionProvider(sessionResource: URI | string): AgentSessionProviders | undefined { @@ -23,6 +24,7 @@ export function getAgentSessionProvider(sessionResource: URI | string): AgentSes case AgentSessionProviders.Local: case AgentSessionProviders.Background: case AgentSessionProviders.Cloud: + case AgentSessionProviders.ClaudeCode: return type; default: return undefined; @@ -37,6 +39,8 @@ export function getAgentSessionProviderName(provider: AgentSessionProviders): st return localize('chat.session.providerLabel.background', "Background"); case AgentSessionProviders.Cloud: return localize('chat.session.providerLabel.cloud', "Cloud"); + case AgentSessionProviders.ClaudeCode: + return localize('chat.session.providerLabel.claude', "Claude"); } } @@ -48,6 +52,8 @@ export function getAgentSessionProviderIcon(provider: AgentSessionProviders): Th return Codicon.worktree; case AgentSessionProviders.Cloud: return Codicon.cloud; + case AgentSessionProviders.ClaudeCode: + return Codicon.code; } } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts index 11637008d2d..b9b71791d4f 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts @@ -109,11 +109,10 @@ export class AgentSessionsFilter extends Disposable implements Required ({ + id: provider, + label: getAgentSessionProviderName(provider) + })); for (const provider of this.chatSessionsService.getAllChatSessionContributions()) { if (providers.find(p => p.id === provider.type)) { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index 73776e50163..0a9e09cd043 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -18,7 +18,7 @@ import { ILogService } from '../../../../../platform/log/common/log.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; import { ChatSessionStatus as AgentSessionStatus, IChatSessionFileChange, IChatSessionItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus } from '../../common/chatSessionsService.js'; -import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from './agentSessions.js'; +import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName } from './agentSessions.js'; //#region Interfaces, Types @@ -305,23 +305,13 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode // Icon + Label let icon: ThemeIcon; let providerLabel: string; - switch ((provider.chatSessionType)) { - case AgentSessionProviders.Local: - providerLabel = getAgentSessionProviderName(AgentSessionProviders.Local); - icon = getAgentSessionProviderIcon(AgentSessionProviders.Local); - break; - case AgentSessionProviders.Background: - providerLabel = getAgentSessionProviderName(AgentSessionProviders.Background); - icon = getAgentSessionProviderIcon(AgentSessionProviders.Background); - break; - case AgentSessionProviders.Cloud: - providerLabel = getAgentSessionProviderName(AgentSessionProviders.Cloud); - icon = getAgentSessionProviderIcon(AgentSessionProviders.Cloud); - break; - default: { - providerLabel = mapSessionContributionToType.get(provider.chatSessionType)?.name ?? provider.chatSessionType; - icon = session.iconPath ?? Codicon.terminal; - } + const agentSessionProvider = getAgentSessionProvider(provider.chatSessionType); + if (agentSessionProvider !== undefined) { + providerLabel = getAgentSessionProviderName(agentSessionProvider); + icon = getAgentSessionProviderIcon(agentSessionProvider); + } else { + providerLabel = mapSessionContributionToType.get(provider.chatSessionType)?.name ?? provider.chatSessionType; + icon = session.iconPath ?? Codicon.terminal; } // State + Timings diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/focusViewService.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/focusViewService.ts index 3225c5f6ebd..a3a82b111cb 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/focusViewService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/focusViewService.ts @@ -30,17 +30,8 @@ import { IChatEditingService, ModifiedFileEntryState } from '../../common/editin /** * Provider types that support agent session projection mode. * Only sessions from these providers will trigger focus view. - * - * Configuration: - * - AgentSessionProviders.Local: Local chat sessions (enabled) - * - AgentSessionProviders.Background: Background CLI agents (enabled) - * - AgentSessionProviders.Cloud: Cloud agents (enabled) */ -const AGENT_SESSION_PROJECTION_ENABLED_PROVIDERS: Set = new Set([ - AgentSessionProviders.Local, - AgentSessionProviders.Background, - AgentSessionProviders.Cloud, -]); +const AGENT_SESSION_PROJECTION_ENABLED_PROVIDERS: Set = new Set(Object.values(AgentSessionProviders)); //#endregion diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index a98c158ad8c..dbf60ac07da 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -115,6 +115,7 @@ import { resizeImage } from '../../chatImageUtils.js'; import { IModelPickerDelegate, ModelPickerActionItem } from './modelPickerActionItem.js'; import { IModePickerDelegate, ModePickerActionItem } from './modePickerActionItem.js'; import { ISessionTypePickerDelegate, SessionTypePickerActionItem } from './sessionTargetPickerActionItem.js'; +import { IChatInputPickerOptions } from './chatInputPickerActionItem.js'; import { getAgentSessionProvider } from '../../agentSessions/agentSessions.js'; import { SearchableOptionPickerActionItem } from '../../chatSessions/searchableOptionPickerActionItem.js'; import { mixin } from '../../../../../../base/common/objects.js'; @@ -1754,6 +1755,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge })); const hoverDelegate = this._register(createInstantHoverDelegate()); + const pickerOptions: IChatInputPickerOptions = { + getOverflowAnchor: () => this.inputActionsToolbar.getElement(), + }; this._register(dom.addStandardDisposableListener(toolbarsContainer, dom.EventType.CLICK, e => this.inputEditor.focus())); this._register(dom.addStandardDisposableListener(this.attachmentsContainer, dom.EventType.CLICK, e => this.inputEditor.focus())); @@ -1762,6 +1766,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge menuOptions: { shouldForwardArgs: true }, hiddenItemStrategy: HiddenItemStrategy.NoHide, hoverDelegate, + responsiveBehavior: { + enabled: true, + kind: 'last', + minItems: 1, + actionMinWidth: 40 + }, actionViewItemProvider: (action, options) => { if (action.id === OpenModelPickerAction.ID && action instanceof MenuItemAction) { if (!this._currentLanguageModel) { @@ -1778,13 +1788,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge }, getModels: () => this.getModels() }; - return this.modelWidget = this.instantiationService.createInstance(ModelPickerActionItem, action, this._currentLanguageModel, undefined, itemDelegate); + return this.modelWidget = this.instantiationService.createInstance(ModelPickerActionItem, action, this._currentLanguageModel, undefined, itemDelegate, pickerOptions); } else if (action.id === OpenModePickerAction.ID && action instanceof MenuItemAction) { const delegate: IModePickerDelegate = { currentMode: this._currentModeObservable, sessionResource: () => this._widget?.viewModel?.sessionResource, }; - return this.modeWidget = this.instantiationService.createInstance(ModePickerActionItem, action, delegate); + return this.modeWidget = this.instantiationService.createInstance(ModePickerActionItem, action, delegate, pickerOptions); } else if (action.id === OpenSessionTargetPickerAction.ID && action instanceof MenuItemAction) { const delegate: ISessionTypePickerDelegate = { getActiveSessionProvider: () => { @@ -1793,7 +1803,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge }, }; const chatSessionPosition = isIChatResourceViewContext(widget.viewContext) ? 'editor' : 'sidebar'; - return this.sessionTargetWidget = this.instantiationService.createInstance(SessionTypePickerActionItem, action, chatSessionPosition, delegate); + return this.sessionTargetWidget = this.instantiationService.createInstance(SessionTypePickerActionItem, action, chatSessionPosition, delegate, pickerOptions); } else if (action.id === ChatSessionPrimaryPickerAction.ID && action instanceof MenuItemAction) { // Create all pickers and return a container action view item const widgets = this.createChatSessionPickerWidgets(action); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.ts new file mode 100644 index 00000000000..0fdc9402826 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getActiveWindow } from '../../../../../../base/browser/dom.js'; +import { IAction } from '../../../../../../base/common/actions.js'; +import { ActionWidgetDropdownActionViewItem } from '../../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js'; +import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js'; +import { IActionWidgetDropdownOptions } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; + +export interface IChatInputPickerOptions { + /** + * Provides a fallback anchor element when the picker's own element + * is not available in the DOM (e.g., when inside an overflow menu). + */ + readonly getOverflowAnchor?: () => HTMLElement | undefined; +} + +/** + * Base class for chat input picker action items (model picker, mode picker, session target picker). + * Provides common anchor resolution logic for dropdown positioning. + */ +export abstract class ChatInputPickerActionViewItem extends ActionWidgetDropdownActionViewItem { + + constructor( + action: IAction, + actionWidgetOptions: Omit, + private readonly pickerOptions: IChatInputPickerOptions, + @IActionWidgetService actionWidgetService: IActionWidgetService, + @IKeybindingService keybindingService: IKeybindingService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + // Inject the anchor getter into the options + const optionsWithAnchor: Omit = { + ...actionWidgetOptions, + getAnchor: () => this.getAnchorElement(), + }; + + super(action, optionsWithAnchor, actionWidgetService, keybindingService, contextKeyService); + } + + /** + * Returns the anchor element for the dropdown. + * Falls back to the overflow anchor if this element is not in the DOM. + */ + protected getAnchorElement(): HTMLElement { + if (this.element && getActiveWindow().document.contains(this.element)) { + return this.element; + } + return this.pickerOptions.getOverflowAnchor?.() ?? this.element!; + } + + override render(container: HTMLElement): void { + super.render(container); + container.classList.add('chat-input-picker-item'); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index 2059f7c902b..5ab6e8dc9a3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -14,7 +14,6 @@ import { autorun, IObservable } from '../../../../../../base/common/observable.j import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { URI } from '../../../../../../base/common/uri.js'; import { localize } from '../../../../../../nls.js'; -import { ActionWidgetDropdownActionViewItem } from '../../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js'; import { getFlatActionBarActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { IMenuService, MenuId, MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js'; @@ -30,16 +29,18 @@ import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../../com import { ExtensionAgentSourceType, PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js'; import { getOpenChatActionIdForMode } from '../../actions/chatActions.js'; import { IToggleChatModeArgs, ToggleAgentModeActionId } from '../../actions/chatExecuteActions.js'; +import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; export interface IModePickerDelegate { readonly currentMode: IObservable; readonly sessionResource: () => URI | undefined; } -export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem { +export class ModePickerActionItem extends ChatInputPickerActionViewItem { constructor( action: MenuItemAction, private readonly delegate: IModePickerDelegate, + pickerOptions: IChatInputPickerOptions, @IActionWidgetService actionWidgetService: IActionWidgetService, @IChatAgentService chatAgentService: IChatAgentService, @IKeybindingService keybindingService: IKeybindingService, @@ -68,7 +69,7 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem { ...action, id: getOpenChatActionIdForMode(mode), label: mode.label.get(), - icon: isDisabledViaPolicy ? ThemeIcon.fromId(Codicon.lock.id) : undefined, + icon: isDisabledViaPolicy ? ThemeIcon.fromId(Codicon.lock.id) : mode.icon.get(), class: isDisabledViaPolicy ? 'disabled-by-policy' : undefined, enabled: !isDisabledViaPolicy, checked: !isDisabledViaPolicy && currentMode.id === mode.id, @@ -94,6 +95,7 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem { return { ...makeAction(mode, currentMode), tooltip: mode.description.get() ?? chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, mode.kind)?.description ?? action.tooltip, + icon: mode.icon.get(), category: agentModeDisabledViaPolicy ? policyDisabledCategory : customCategory }; }; @@ -132,7 +134,7 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem { showItemKeybindings: true }; - super(action, modePickerActionWidgetOptions, actionWidgetService, keybindingService, contextKeyService); + super(action, modePickerActionWidgetOptions, pickerOptions, actionWidgetService, keybindingService, contextKeyService); // Listen to changes in the current mode and its properties this._register(autorun(reader => { @@ -153,13 +155,19 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem { protected override renderLabel(element: HTMLElement): IDisposable | null { this.setAriaLabelAttributes(element); + + const isDefault = this.delegate.currentMode.get().id === ChatMode.Agent.id; const state = this.delegate.currentMode.get().label.get(); - dom.reset(element, dom.$('span.chat-input-picker-label', undefined, state), ...renderLabelWithIcons(`$(chevron-down)`)); + const icon = this.delegate.currentMode.get().icon.get(); + + const labelElements = []; + labelElements.push(...renderLabelWithIcons(`$(${icon.id})`)); + if (!isDefault) { + labelElements.push(dom.$('span.chat-input-picker-label', undefined, state)); + } + labelElements.push(...renderLabelWithIcons(`$(chevron-down)`)); + + dom.reset(element, ...labelElements); return null; } - - override render(container: HTMLElement): void { - super.render(container); - container.classList.add('chat-input-picker-item'); - } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts index 4cef0ffb4ec..f0576287c7a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts @@ -10,7 +10,6 @@ import { localize } from '../../../../../../nls.js'; import * as dom from '../../../../../../base/browser/dom.js'; import { renderIcon, renderLabelWithIcons } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; -import { ActionWidgetDropdownActionViewItem } from '../../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js'; import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js'; import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider, IActionWidgetDropdownOptions } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; @@ -24,6 +23,7 @@ import { IProductService } from '../../../../../../platform/product/common/produ import { MANAGE_CHAT_COMMAND_ID } from '../../../common/constants.js'; import { TelemetryTrustedValue } from '../../../../../../platform/telemetry/common/telemetryUtils.js'; import { IManagedHoverContent } from '../../../../../../base/browser/ui/hover/hover.js'; +import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; export interface IModelPickerDelegate { readonly onDidChangeModel: Event; @@ -138,13 +138,14 @@ function getModelPickerActionBarActionProvider(commandService: ICommandService, /** * Action view item for selecting a language model in the chat interface. */ -export class ModelPickerActionItem extends ActionWidgetDropdownActionViewItem { +export class ModelPickerActionItem extends ChatInputPickerActionViewItem { constructor( action: IAction, protected currentModel: ILanguageModelChatMetadataAndIdentifier | undefined, widgetOptions: Omit | undefined, delegate: IModelPickerDelegate, + pickerOptions: IChatInputPickerOptions, @IActionWidgetService actionWidgetService: IActionWidgetService, @IContextKeyService contextKeyService: IContextKeyService, @ICommandService commandService: ICommandService, @@ -162,10 +163,10 @@ export class ModelPickerActionItem extends ActionWidgetDropdownActionViewItem { const modelPickerActionWidgetOptions: Omit = { actionProvider: modelDelegateToWidgetActionsProvider(delegate, telemetryService), - actionBarActionProvider: getModelPickerActionBarActionProvider(commandService, chatEntitlementService, productService) + actionBarActionProvider: getModelPickerActionBarActionProvider(commandService, chatEntitlementService, productService), }; - super(actionWithLabel, widgetOptions ?? modelPickerActionWidgetOptions, actionWidgetService, keybindingService, contextKeyService); + super(actionWithLabel, widgetOptions ?? modelPickerActionWidgetOptions, pickerOptions, actionWidgetService, keybindingService, contextKeyService); // Listen for model changes from the delegate this._register(delegate.onDidChangeModel(model => { @@ -200,8 +201,4 @@ export class ModelPickerActionItem extends ActionWidgetDropdownActionViewItem { return null; } - override render(container: HTMLElement): void { - super.render(container); - container.classList.add('chat-input-picker-item'); - } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts index cd5245be9db..3991359ce74 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts @@ -9,7 +9,6 @@ import { IAction } from '../../../../../../base/common/actions.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../../base/common/uri.js'; import { localize } from '../../../../../../nls.js'; -import { ActionWidgetDropdownActionViewItem } from '../../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js'; import { MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js'; import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider, IActionWidgetDropdownOptions } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; @@ -19,6 +18,7 @@ import { IKeybindingService } from '../../../../../../platform/keybinding/common import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; import { IChatSessionsService } from '../../../common/chatSessionsService.js'; import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../../agentSessions/agentSessions.js'; +import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; export interface ISessionTypePickerDelegate { getActiveSessionProvider(): AgentSessionProviders | undefined; @@ -35,13 +35,14 @@ interface ISessionTypeItem { * Action view item for selecting a session target in the chat interface. * This picker allows switching between different chat session types contributed via extensions. */ -export class SessionTypePickerActionItem extends ActionWidgetDropdownActionViewItem { +export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { private _sessionTypeItems: ISessionTypeItem[] = []; constructor( action: MenuItemAction, private readonly chatSessionPosition: 'sidebar' | 'editor', private readonly delegate: ISessionTypePickerDelegate, + pickerOptions: IChatInputPickerOptions, @IActionWidgetService actionWidgetService: IActionWidgetService, @IKeybindingService keybindingService: IKeybindingService, @IContextKeyService contextKeyService: IContextKeyService, @@ -97,7 +98,7 @@ export class SessionTypePickerActionItem extends ActionWidgetDropdownActionViewI showItemKeybindings: true, }; - super(action, sessionTargetPickerOptions, actionWidgetService, keybindingService, contextKeyService); + super(action, sessionTargetPickerOptions, pickerOptions, actionWidgetService, keybindingService, contextKeyService); this._updateAgentSessionItems(); this._register(this.chatSessionsService.onDidChangeAvailability(() => { @@ -139,12 +140,15 @@ export class SessionTypePickerActionItem extends ActionWidgetDropdownActionViewI const label = getAgentSessionProviderName(currentType ?? AgentSessionProviders.Local); const icon = getAgentSessionProviderIcon(currentType ?? AgentSessionProviders.Local); - dom.reset(element, ...renderLabelWithIcons(`$(${icon.id})`), dom.$('span.chat-input-picker-label', undefined, label), ...renderLabelWithIcons(`$(chevron-down)`)); + const labelElements = []; + labelElements.push(...renderLabelWithIcons(`$(${icon.id})`)); + if (currentType !== AgentSessionProviders.Local) { + labelElements.push(dom.$('span.chat-input-picker-label', undefined, label)); + } + labelElements.push(...renderLabelWithIcons(`$(chevron-down)`)); + + dom.reset(element, ...labelElements); + return null; } - - override render(container: HTMLElement): void { - super.render(container); - container.classList.add('chat-input-picker-item'); - } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 8f91967ab7f..6e06a7a8260 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1308,7 +1308,7 @@ have to be updated for changes to the rules above, or to support more deeply nes display: flex; } -.interactive-session .chat-input-toolbars :first-child { +.interactive-session .chat-input-toolbars :not(.responsive.chat-input-toolbar) .actions-container:first-child { margin-right: auto; } @@ -1327,12 +1327,15 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .chat-input-toolbars > .chat-input-toolbar { overflow: hidden; min-width: 0px; + width: 100%; .chat-input-picker-item { min-width: 0px; + overflow: hidden; .action-label { min-width: 0px; + overflow: hidden; .chat-input-picker-label { overflow: hidden; diff --git a/src/vs/workbench/contrib/chat/common/chatModes.ts b/src/vs/workbench/contrib/chat/common/chatModes.ts index 4d6a1cd5c59..97cbfaa63c5 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -21,6 +21,8 @@ import { ChatContextKeys } from './actions/chatContextKeys.js'; import { ChatConfiguration, ChatModeKind } from './constants.js'; import { IHandOff } from './promptSyntax/promptFileParser.js'; import { ExtensionAgentSourceType, IAgentSource, ICustomAgent, IPromptsService, PromptsStorage } from './promptSyntax/service/promptsService.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { Codicon } from '../../../../base/common/codicons.js'; export const IChatModeService = createDecorator('chatModeService'); export interface IChatModeService { @@ -248,6 +250,7 @@ export interface IChatMode { readonly id: string; readonly name: IObservable; readonly label: IObservable; + readonly icon: IObservable; readonly description: IObservable; readonly isBuiltin: boolean; readonly kind: ChatModeKind; @@ -317,6 +320,10 @@ export class CustomChatMode implements IChatMode { return this._descriptionObservable; } + get icon(): IObservable { + return constObservable(Codicon.tasklist); + } + public get isBuiltin(): boolean { return isBuiltinChatMode(this); } @@ -457,15 +464,18 @@ export class BuiltinChatMode implements IChatMode { public readonly name: IObservable; public readonly label: IObservable; public readonly description: IObservable; + public readonly icon: IObservable; constructor( public readonly kind: ChatModeKind, label: string, - description: string + description: string, + icon: ThemeIcon, ) { this.name = constObservable(kind); this.label = constObservable(label); this.description = observableValue('description', description); + this.icon = constObservable(icon); } public get isBuiltin(): boolean { @@ -495,9 +505,9 @@ export class BuiltinChatMode implements IChatMode { } export namespace ChatMode { - export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Explore and understand your code")); - export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit or refactor selected code")); - export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Describe what to build next")); + export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Explore and understand your code"), Codicon.question); + export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit or refactor selected code"), Codicon.edit); + export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Describe what to build next"), Codicon.agent); } export function isBuiltinChatMode(mode: IChatMode): boolean { diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts index 0d674e04eb3..fb552ee0e24 100644 --- a/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts @@ -94,7 +94,7 @@ export class RepositoryRenderer implements ICompressibleTreeRenderer provider.classList.toggle('active', e)); From a03c603b0491cadc8f150fb98bd12122567c2249 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Thu, 15 Jan 2026 10:43:23 -0800 Subject: [PATCH 017/387] Show todo list when there are any todos available (#288119) --- .../chat/browser/widget/chatContentParts/chatTodoListWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTodoListWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTodoListWidget.ts index fd7d48a05e3..7a5b9912b84 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTodoListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTodoListWidget.ts @@ -259,7 +259,7 @@ export class ChatTodoListWidget extends Disposable { } const todoList = this.chatTodoListService.getTodos(this._currentSessionResource); - const shouldShow = todoList.length > 2; + const shouldShow = todoList.length > 0; if (!shouldShow) { this.domNode.classList.remove('has-todos'); From 12bce8da5a931cc4b44892c45798f5e4597b1d90 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 15 Jan 2026 10:46:36 -0800 Subject: [PATCH 018/387] Agent skills management UX and file support (#287201) --- .../markdown-language-features/package.json | 31 ++--- .../src/util/file.ts | 2 +- extensions/prompt-basics/package.json | 35 ++++++ .../promptSyntax/newPromptFileActions.ts | 104 +++++++++++++++- .../pickers/askForPromptSourceFolder.ts | 10 ++ .../promptSyntax/pickers/promptFilePickers.ts | 19 ++- .../browser/promptSyntax/promptFileActions.ts | 2 + .../chat/browser/promptSyntax/skillActions.ts | 69 +++++++++++ .../languageProviders/promptHovers.ts | 117 ++++++++++-------- .../languageProviders/promptValidator.ts | 40 +++++- .../common/promptSyntax/promptFileParser.ts | 3 + .../service/promptsServiceImpl.ts | 13 +- .../languageProviders/promptHovers.test.ts | 36 ++++++ .../languageProviders/promptValidator.test.ts | 65 +++++++++- 14 files changed, 472 insertions(+), 74 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/promptSyntax/skillActions.ts diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 1df0b43d840..edffec39d74 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -20,6 +20,7 @@ "onLanguage:prompt", "onLanguage:instructions", "onLanguage:chatagent", + "onLanguage:skill", "onCommand:markdown.api.render", "onCommand:markdown.api.reloadPlugins", "onWebviewPanel:markdown.preview" @@ -181,13 +182,13 @@ "command": "markdown.editor.insertLinkFromWorkspace", "title": "%markdown.editor.insertLinkFromWorkspace%", "category": "Markdown", - "enablement": "editorLangId =~ /^(markdown|prompt|instructions|chatagent)$/ && !activeEditorIsReadonly" + "enablement": "editorLangId =~ /^(markdown|prompt|instructions|chatagent|skill)$/ && !activeEditorIsReadonly" }, { "command": "markdown.editor.insertImageFromWorkspace", "title": "%markdown.editor.insertImageFromWorkspace%", "category": "Markdown", - "enablement": "editorLangId =~ /^(markdown|prompt|instructions|chatagent)$/ && !activeEditorIsReadonly" + "enablement": "editorLangId =~ /^(markdown|prompt|instructions|chatagent|skill)$/ && !activeEditorIsReadonly" } ], "menus": { @@ -204,7 +205,7 @@ "editor/title": [ { "command": "markdown.showPreviewToSide", - "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent)$/ && !notebookEditorFocused && !hasCustomMarkdownPreview", + "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent|skill)$/ && !notebookEditorFocused && !hasCustomMarkdownPreview", "alt": "markdown.showPreview", "group": "navigation" }, @@ -232,24 +233,24 @@ "explorer/context": [ { "command": "markdown.showPreview", - "when": "resourceLangId =~ /^(markdown|prompt|instructions|chatagent)$/ && !hasCustomMarkdownPreview", + "when": "resourceLangId =~ /^(markdown|prompt|instructions|chatagent|skill)$/ && !hasCustomMarkdownPreview", "group": "navigation" }, { "command": "markdown.findAllFileReferences", - "when": "resourceLangId =~ /^(markdown|prompt|instructions|chatagent)$/", + "when": "resourceLangId =~ /^(markdown|prompt|instructions|chatagent|skill)$/", "group": "4_search" } ], "editor/title/context": [ { "command": "markdown.showPreview", - "when": "resourceLangId =~ /^(markdown|prompt|instructions|chatagent)$/ && !hasCustomMarkdownPreview", + "when": "resourceLangId =~ /^(markdown|prompt|instructions|chatagent|skill)$/ && !hasCustomMarkdownPreview", "group": "1_open" }, { "command": "markdown.findAllFileReferences", - "when": "resourceLangId =~ /^(markdown|prompt|instructions|chatagent)$/" + "when": "resourceLangId =~ /^(markdown|prompt|instructions|chatagent|skill)$/" } ], "commandPalette": [ @@ -263,17 +264,17 @@ }, { "command": "markdown.showPreview", - "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent)$/ && !notebookEditorFocused", + "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent|skill)$/ && !notebookEditorFocused", "group": "navigation" }, { "command": "markdown.showPreviewToSide", - "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent)$/ && !notebookEditorFocused", + "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent|skill)$/ && !notebookEditorFocused", "group": "navigation" }, { "command": "markdown.showLockedPreviewToSide", - "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent)$/ && !notebookEditorFocused", + "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent|skill)$/ && !notebookEditorFocused", "group": "navigation" }, { @@ -283,7 +284,7 @@ }, { "command": "markdown.showPreviewSecuritySelector", - "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent)$/ && !notebookEditorFocused" + "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent|skill)$/ && !notebookEditorFocused" }, { "command": "markdown.showPreviewSecuritySelector", @@ -295,7 +296,7 @@ }, { "command": "markdown.preview.refresh", - "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent)$/ && !notebookEditorFocused" + "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent|skill)$/ && !notebookEditorFocused" }, { "command": "markdown.preview.refresh", @@ -303,7 +304,7 @@ }, { "command": "markdown.findAllFileReferences", - "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent)$/" + "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent|skill)$/" } ] }, @@ -312,13 +313,13 @@ "command": "markdown.showPreview", "key": "shift+ctrl+v", "mac": "shift+cmd+v", - "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent)$/ && !notebookEditorFocused" + "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent|skill)$/ && !notebookEditorFocused" }, { "command": "markdown.showPreviewToSide", "key": "ctrl+k v", "mac": "cmd+k v", - "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent)$/ && !notebookEditorFocused" + "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent|skill)$/ && !notebookEditorFocused" } ], "configuration": { diff --git a/extensions/markdown-language-features/src/util/file.ts b/extensions/markdown-language-features/src/util/file.ts index aa793045278..df745296df4 100644 --- a/extensions/markdown-language-features/src/util/file.ts +++ b/extensions/markdown-language-features/src/util/file.ts @@ -19,7 +19,7 @@ export const markdownFileExtensions = Object.freeze([ 'workbook', ]); -export const markdownLanguageIds = ['markdown', 'prompt', 'instructions', 'chatagent']; +export const markdownLanguageIds = ['markdown', 'prompt', 'instructions', 'chatagent', 'skill']; export function isMarkdownFile(document: vscode.TextDocument) { return markdownLanguageIds.indexOf(document.languageId) !== -1; diff --git a/extensions/prompt-basics/package.json b/extensions/prompt-basics/package.json index f1d4ee98b29..1765ac15d8c 100644 --- a/extensions/prompt-basics/package.json +++ b/extensions/prompt-basics/package.json @@ -50,6 +50,17 @@ "**/.github/agents/*.md" ], "configuration": "./language-configuration.json" + }, + { + "id": "skill", + "aliases": [ + "Skill", + "skill" + ], + "filenames": [ + "SKILL.md" + ], + "configuration": "./language-configuration.json" } ], "grammars": [ @@ -79,6 +90,15 @@ "markup.underline.link.markdown", "punctuation.definition.list.begin.markdown" ] + }, + { + "language": "skill", + "path": "./syntaxes/prompt.tmLanguage.json", + "scopeName": "text.html.markdown.prompt", + "unbalancedBracketScopes": [ + "markup.underline.link.markdown", + "punctuation.definition.list.begin.markdown" + ] } ], "configurationDefaults": { @@ -126,6 +146,21 @@ "other": "on" }, "editor.wordBasedSuggestions": "off" + }, + "[skill]": { + "editor.insertSpaces": true, + "editor.tabSize": 2, + "editor.autoIndent": "advanced", + "editor.unicodeHighlight.ambiguousCharacters": false, + "editor.unicodeHighlight.invisibleCharacters": false, + "diffEditor.ignoreTrimWhitespace": false, + "editor.wordWrap": "on", + "editor.quickSuggestions": { + "comments": "off", + "strings": "on", + "other": "on" + }, + "editor.wordBasedSuggestions": "off" } } }, diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts index 5e9ddb4104f..71f6f880de3 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts @@ -25,6 +25,8 @@ import { CHAT_CATEGORY } from '../actions/chatActions.js'; import { askForPromptFileName } from './pickers/askForPromptName.js'; import { askForPromptSourceFolder } from './pickers/askForPromptSourceFolder.js'; import { IChatModeService } from '../../common/chatModes.js'; +import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; +import { SKILL_FILENAME } from '../../common/promptSyntax/config/promptFileLocations.js'; class AbstractNewPromptFileAction extends Action2 { @@ -165,14 +167,30 @@ function getDefaultContentSnippet(promptType: PromptsType, chatModeService: ICha `\${2:Define what this custom agent accomplishes for the user, when to use it, and the edges it won't cross. Specify its ideal inputs/outputs, the tools it may call, and how it reports progress or asks for help.}`, ].join('\n'); default: - throw new Error(`Unknown prompt type: ${promptType}`); + throw new Error(`Unsupported prompt type: ${promptType}`); } } +/** + * Generates the content snippet for a skill file with the name pre-populated. + * Per agentskills.io/specification, the name field must match the parent directory name. + */ +function getSkillContentSnippet(skillName: string): string { + return [ + `---`, + `name: ${skillName}`, + `description: '\${1:Describe what this skill does and when to use it. Include keywords that help agents identify relevant tasks.}'`, + `---`, + ``, + `\${2:Provide detailed instructions for the agent. Include step-by-step guidance, examples, and edge cases.}`, + ].join('\n'); +} + export const NEW_PROMPT_COMMAND_ID = 'workbench.command.new.prompt'; export const NEW_INSTRUCTIONS_COMMAND_ID = 'workbench.command.new.instructions'; export const NEW_AGENT_COMMAND_ID = 'workbench.command.new.agent'; +export const NEW_SKILL_COMMAND_ID = 'workbench.command.new.skill'; class NewPromptFileAction extends AbstractNewPromptFileAction { constructor() { @@ -192,6 +210,89 @@ class NewAgentFileAction extends AbstractNewPromptFileAction { } } +class NewSkillFileAction extends Action2 { + constructor() { + super({ + id: NEW_SKILL_COMMAND_ID, + title: localize('commands.new.skill.local.title', "New Skill File..."), + f1: false, + precondition: ChatContextKeys.enabled, + category: CHAT_CATEGORY, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + }, + menu: { + id: MenuId.CommandPalette, + when: ChatContextKeys.enabled + } + }); + } + + public override async run(accessor: ServicesAccessor) { + const openerService = accessor.get(IOpenerService); + const editorService = accessor.get(IEditorService); + const fileService = accessor.get(IFileService); + const instaService = accessor.get(IInstantiationService); + const quickInputService = accessor.get(IQuickInputService); + + const selectedFolder = await instaService.invokeFunction(askForPromptSourceFolder, PromptsType.skill); + if (!selectedFolder) { + return; + } + + // Ask for skill name (will be the folder name) + // Per agentskills.io/specification: name must be 1-64 chars, lowercase alphanumeric + hyphens, + // no leading/trailing hyphens, no consecutive hyphens, must match folder name + const skillName = await quickInputService.input({ + prompt: localize('commands.new.skill.name.prompt', "Enter a name for the skill (lowercase letters, numbers, and hyphens only)"), + placeHolder: localize('commands.new.skill.name.placeholder', "e.g., pdf-processing, data-analysis"), + validateInput: async (value) => { + if (!value || !value.trim()) { + return localize('commands.new.skill.name.required', "Skill name is required"); + } + const name = value.trim(); + if (name.length > 64) { + return localize('commands.new.skill.name.tooLong', "Skill name must be 64 characters or less"); + } + // Per spec: lowercase alphanumeric and hyphens only + if (!/^[a-z0-9-]+$/.test(name)) { + return localize('commands.new.skill.name.invalidChars', "Skill name may only contain lowercase letters, numbers, and hyphens"); + } + if (name.startsWith('-') || name.endsWith('-')) { + return localize('commands.new.skill.name.hyphenEdge', "Skill name must not start or end with a hyphen"); + } + if (name.includes('--')) { + return localize('commands.new.skill.name.consecutiveHyphens', "Skill name must not contain consecutive hyphens"); + } + return undefined; + } + }); + + if (!skillName) { + return; + } + + const trimmedName = skillName.trim(); + + // Create the skill folder and SKILL.md file + const skillFolder = URI.joinPath(selectedFolder.uri, trimmedName); + await fileService.createFolder(skillFolder); + + const skillFileUri = URI.joinPath(skillFolder, SKILL_FILENAME); + await fileService.createFile(skillFileUri); + + await openerService.open(skillFileUri); + + const editor = getCodeEditor(editorService.activeTextEditorControl); + if (editor && editor.hasModel() && isEqual(editor.getModel().uri, skillFileUri)) { + SnippetController2.get(editor)?.apply([{ + range: editor.getModel().getFullModelRange(), + template: getSkillContentSnippet(trimmedName), + }]); + } + } +} + class NewUntitledPromptFileAction extends Action2 { constructor() { super({ @@ -237,5 +338,6 @@ export function registerNewPromptFileActions(): void { registerAction2(NewPromptFileAction); registerAction2(NewInstructionsFileAction); registerAction2(NewAgentFileAction); + registerAction2(NewSkillFileAction); registerAction2(NewUntitledPromptFileAction); } diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/askForPromptSourceFolder.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/askForPromptSourceFolder.ts index 960490245e5..1db063db181 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/askForPromptSourceFolder.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/askForPromptSourceFolder.ts @@ -111,6 +111,8 @@ function getPlaceholderStringforNew(type: PromptsType): string { return localize('workbench.command.prompt.create.location.placeholder', "Select a location to create the prompt file"); case PromptsType.agent: return localize('workbench.command.agent.create.location.placeholder', "Select a location to create the agent file"); + case PromptsType.skill: + return localize('workbench.command.skill.create.location.placeholder', "Select a location to create the skill"); default: throw new Error('Unknown prompt type'); } @@ -125,6 +127,8 @@ function getPlaceholderStringforMove(type: PromptsType, isMove: boolean): string return localize('prompt.move.location.placeholder', "Select a location to move the prompt file to"); case PromptsType.agent: return localize('agent.move.location.placeholder', "Select a location to move the agent file to"); + case PromptsType.skill: + return localize('skill.move.location.placeholder', "Select a location to move the skill to"); default: throw new Error('Unknown prompt type'); } @@ -136,6 +140,8 @@ function getPlaceholderStringforMove(type: PromptsType, isMove: boolean): string return localize('prompt.copy.location.placeholder', "Select a location to copy the prompt file to"); case PromptsType.agent: return localize('agent.copy.location.placeholder', "Select a location to copy the agent file to"); + case PromptsType.skill: + return localize('skill.copy.location.placeholder', "Select a location to copy the skill to"); default: throw new Error('Unknown prompt type'); } @@ -179,6 +185,8 @@ function getLearnLabel(type: PromptsType): string { return localize('commands.instructions.create.ask-folder.empty.docs-label', 'Learn how to configure reusable instructions'); case PromptsType.agent: return localize('commands.agent.create.ask-folder.empty.docs-label', 'Learn how to configure custom agents'); + case PromptsType.skill: + return localize('commands.skill.create.ask-folder.empty.docs-label', 'Learn how to configure skills'); default: throw new Error('Unknown prompt type'); } @@ -192,6 +200,8 @@ function getMissingSourceFolderString(type: PromptsType): string { return localize('commands.prompts.create.ask-folder.empty.placeholder', 'No prompt source folders found.'); case PromptsType.agent: return localize('commands.agent.create.ask-folder.empty.placeholder', 'No agent source folders found.'); + case PromptsType.skill: + return localize('commands.skill.create.ask-folder.empty.placeholder', 'No skill source folders found.'); default: throw new Error('Unknown prompt type'); } diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts index 71ce3a1a959..d5de7079a34 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts @@ -16,7 +16,7 @@ import { IDialogService } from '../../../../../../platform/dialogs/common/dialog import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { getCleanPromptName } from '../../../common/promptSyntax/config/promptFileLocations.js'; import { PromptsType, INSTRUCTIONS_DOCUMENTATION_URL, AGENT_DOCUMENTATION_URL, PROMPT_DOCUMENTATION_URL, SKILL_DOCUMENTATION_URL } from '../../../common/promptSyntax/promptTypes.js'; -import { NEW_PROMPT_COMMAND_ID, NEW_INSTRUCTIONS_COMMAND_ID, NEW_AGENT_COMMAND_ID } from '../newPromptFileActions.js'; +import { NEW_PROMPT_COMMAND_ID, NEW_INSTRUCTIONS_COMMAND_ID, NEW_AGENT_COMMAND_ID, NEW_SKILL_COMMAND_ID } from '../newPromptFileActions.js'; import { IKeyMods, IQuickInputButton, IQuickInputService, IQuickPick, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator } from '../../../../../../platform/quickinput/common/quickInput.js'; import { askForPromptFileName } from './askForPromptName.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; @@ -184,6 +184,21 @@ const NEW_AGENT_FILE_OPTION: IPromptPickerQuickPickItem = { commandId: NEW_AGENT_COMMAND_ID, }; +/** + * A quick pick item that starts the 'New Skill' command. + */ +const NEW_SKILL_FILE_OPTION: IPromptPickerQuickPickItem = { + type: 'item', + label: `$(plus) ${localize( + 'commands.new-skill.select-dialog.label', + 'New skill...', + )}`, + pickable: false, + alwaysShow: true, + buttons: [newHelpButton(PromptsType.skill)], + commandId: NEW_SKILL_COMMAND_ID, +}; + /** * Button that opens a prompt file in the editor. */ @@ -419,6 +434,8 @@ export class PromptFilePickers { return [NEW_INSTRUCTIONS_FILE_OPTION, UPDATE_INSTRUCTIONS_OPTION]; case PromptsType.agent: return [NEW_AGENT_FILE_OPTION]; + case PromptsType.skill: + return [NEW_SKILL_FILE_OPTION]; default: throw new Error(`Unknown prompt type '${type}'.`); } diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptFileActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptFileActions.ts index cbeef093b90..45423904593 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptFileActions.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptFileActions.ts @@ -7,6 +7,7 @@ import { registerAttachPromptActions } from './attachInstructionsAction.js'; import { registerAgentActions } from './chatModeActions.js'; import { registerRunPromptActions } from './runPromptAction.js'; import { registerNewPromptFileActions } from './newPromptFileActions.js'; +import { registerSkillActions } from './skillActions.js'; import { registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { SaveAsAgentFileAction, SaveAsInstructionsFileAction, SaveAsPromptFileAction } from './saveAsPromptFileActions.js'; @@ -17,6 +18,7 @@ import { SaveAsAgentFileAction, SaveAsInstructionsFileAction, SaveAsPromptFileAc export function registerPromptActions(): void { registerRunPromptActions(); registerAttachPromptActions(); + registerSkillActions(); registerAction2(SaveAsPromptFileAction); registerAction2(SaveAsInstructionsFileAction); registerAction2(SaveAsAgentFileAction); diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/skillActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/skillActions.ts new file mode 100644 index 00000000000..01d802c1541 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/skillActions.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ChatViewId } from '../chat.js'; +import { CHAT_CATEGORY, CHAT_CONFIG_MENU_ID } from '../actions/chatActions.js'; +import { localize, localize2 } from '../../../../../nls.js'; +import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; +import { PromptFilePickers } from './pickers/promptFilePickers.js'; +import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; +import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; +import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; +import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; + +/** + * Action ID for the `Configure Skills` action. + */ +const CONFIGURE_SKILLS_ACTION_ID = 'workbench.action.chat.configure.skills'; + + +class ManageSkillsAction extends Action2 { + constructor() { + super({ + id: CONFIGURE_SKILLS_ACTION_ID, + title: localize2('configure-skills', "Configure Skills..."), + shortTitle: localize2('configure-skills.short', "Skills"), + icon: Codicon.lightbulb, + f1: true, + precondition: ChatContextKeys.enabled, + category: CHAT_CATEGORY, + menu: { + id: CHAT_CONFIG_MENU_ID, + when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)), + order: 9, + group: '1_level' + } + }); + } + + public override async run( + accessor: ServicesAccessor, + ): Promise { + const openerService = accessor.get(IOpenerService); + const instaService = accessor.get(IInstantiationService); + + const pickers = instaService.createInstance(PromptFilePickers); + + const placeholder = localize( + 'commands.prompt.manage-skills-dialog.placeholder', + 'Select the skill to open' + ); + + const result = await pickers.selectPromptFile({ placeholder, type: PromptsType.skill, optionEdit: false }); + if (result !== undefined) { + await openerService.open(result.promptFile); + } + } +} + +/** + * Helper to register the `Manage Skills` action. + */ +export function registerSkillActions(): void { + registerAction2(ManageSkillsAction); +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts index ecaad1af949..7cdfe0283fc 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts @@ -68,63 +68,78 @@ export class PromptHoverProvider implements HoverProvider { } private async provideHeaderHover(position: Position, promptType: PromptsType, header: PromptHeader): Promise { - if (promptType === PromptsType.instructions) { - for (const attribute of header.attributes) { - if (attribute.range.containsPosition(position)) { - switch (attribute.key) { - case PromptHeaderAttributes.name: - return this.createHover(localize('promptHeader.instructions.name', 'The name of the instruction file as shown in the UI. If not set, the name is derived from the file name.'), attribute.range); - case PromptHeaderAttributes.description: - return this.createHover(localize('promptHeader.instructions.description', 'The description of the instruction file. It can be used to provide additional context or information about the instructions and is passed to the language model as part of the prompt.'), attribute.range); - case PromptHeaderAttributes.applyTo: - return this.createHover(localize('promptHeader.instructions.applyToRange', 'One or more glob pattern (separated by comma) that describe for which files the instructions apply to. Based on these patterns, the file is automatically included in the prompt, when the context contains a file that matches one or more of these patterns. Use `**` when you want this file to always be added.\nExample: `**/*.ts`, `**/*.js`, `client/**`'), attribute.range); + switch (promptType) { + case PromptsType.instructions: + for (const attribute of header.attributes) { + if (attribute.range.containsPosition(position)) { + switch (attribute.key) { + case PromptHeaderAttributes.name: + return this.createHover(localize('promptHeader.instructions.name', 'The name of the instruction file as shown in the UI. If not set, the name is derived from the file name.'), attribute.range); + case PromptHeaderAttributes.description: + return this.createHover(localize('promptHeader.instructions.description', 'The description of the instruction file. It can be used to provide additional context or information about the instructions and is passed to the language model as part of the prompt.'), attribute.range); + case PromptHeaderAttributes.applyTo: + return this.createHover(localize('promptHeader.instructions.applyToRange', 'One or more glob pattern (separated by comma) that describe for which files the instructions apply to. Based on these patterns, the file is automatically included in the prompt, when the context contains a file that matches one or more of these patterns. Use `**` when you want this file to always be added.\nExample: `**/*.ts`, `**/*.js`, `client/**`'), attribute.range); + } } } - } - } else if (promptType === PromptsType.agent) { - const isGitHubTarget = isGithubTarget(promptType, header.target); - for (const attribute of header.attributes) { - if (attribute.range.containsPosition(position)) { - switch (attribute.key) { - case PromptHeaderAttributes.name: - return this.createHover(localize('promptHeader.agent.name', 'The name of the agent as shown in the UI.'), attribute.range); - case PromptHeaderAttributes.description: - return this.createHover(localize('promptHeader.agent.description', 'The description of the custom agent, what it does and when to use it.'), attribute.range); - case PromptHeaderAttributes.argumentHint: - return this.createHover(localize('promptHeader.agent.argumentHint', 'The argument-hint describes what inputs the custom agent expects or supports.'), attribute.range); - case PromptHeaderAttributes.model: - return this.getModelHover(attribute, attribute.range, localize('promptHeader.agent.model', 'Specify the model that runs this custom agent.'), isGitHubTarget); - case PromptHeaderAttributes.tools: - return this.getToolHover(attribute, position, localize('promptHeader.agent.tools', 'The set of tools that the custom agent has access to.')); - case PromptHeaderAttributes.handOffs: - return this.getHandsOffHover(attribute, position, isGitHubTarget); - case PromptHeaderAttributes.target: - return this.createHover(localize('promptHeader.agent.target', 'The target to which the header attributes like tools apply to. Possible values are `github-copilot` and `vscode`.'), attribute.range); - case PromptHeaderAttributes.infer: - return this.createHover(localize('promptHeader.agent.infer', 'Whether the agent can be used as a subagent.'), attribute.range); + break; + case PromptsType.skill: + for (const attribute of header.attributes) { + if (attribute.range.containsPosition(position)) { + switch (attribute.key) { + case PromptHeaderAttributes.name: + return this.createHover(localize('promptHeader.skill.name', 'The name of the skill.'), attribute.range); + case PromptHeaderAttributes.description: + return this.createHover(localize('promptHeader.skill.description', 'The description of the skill. The description is added to every request and will be used by the agent to decide when to load the skill.'), attribute.range); + } } } - } - } else { - for (const attribute of header.attributes) { - if (attribute.range.containsPosition(position)) { - switch (attribute.key) { - case PromptHeaderAttributes.name: - return this.createHover(localize('promptHeader.prompt.name', 'The name of the prompt. This is also the name of the slash command that will run this prompt.'), attribute.range); - case PromptHeaderAttributes.description: - return this.createHover(localize('promptHeader.prompt.description', 'The description of the reusable prompt, what it does and when to use it.'), attribute.range); - case PromptHeaderAttributes.argumentHint: - return this.createHover(localize('promptHeader.prompt.argumentHint', 'The argument-hint describes what inputs the prompt expects or supports.'), attribute.range); - case PromptHeaderAttributes.model: - return this.getModelHover(attribute, attribute.range, localize('promptHeader.prompt.model', 'The model to use in this prompt.'), false); - case PromptHeaderAttributes.tools: - return this.getToolHover(attribute, position, localize('promptHeader.prompt.tools', 'The tools to use in this prompt.')); - case PromptHeaderAttributes.agent: - case PromptHeaderAttributes.mode: - return this.getAgentHover(attribute, position); + break; + case PromptsType.agent: + for (const attribute of header.attributes) { + if (attribute.range.containsPosition(position)) { + switch (attribute.key) { + case PromptHeaderAttributes.name: + return this.createHover(localize('promptHeader.agent.name', 'The name of the agent as shown in the UI.'), attribute.range); + case PromptHeaderAttributes.description: + return this.createHover(localize('promptHeader.agent.description', 'The description of the custom agent, what it does and when to use it.'), attribute.range); + case PromptHeaderAttributes.argumentHint: + return this.createHover(localize('promptHeader.agent.argumentHint', 'The argument-hint describes what inputs the custom agent expects or supports.'), attribute.range); + case PromptHeaderAttributes.model: + return this.getModelHover(attribute, attribute.range, localize('promptHeader.agent.model', 'Specify the model that runs this custom agent.'), isGithubTarget(promptType, header.target)); + case PromptHeaderAttributes.tools: + return this.getToolHover(attribute, position, localize('promptHeader.agent.tools', 'The set of tools that the custom agent has access to.')); + case PromptHeaderAttributes.handOffs: + return this.getHandsOffHover(attribute, position, isGithubTarget(promptType, header.target)); + case PromptHeaderAttributes.target: + return this.createHover(localize('promptHeader.agent.target', 'The target to which the header attributes like tools apply to. Possible values are `github-copilot` and `vscode`.'), attribute.range); + case PromptHeaderAttributes.infer: + return this.createHover(localize('promptHeader.agent.infer', 'Whether the agent can be used as a subagent.'), attribute.range); + } } } - } + break; + case PromptsType.prompt: + for (const attribute of header.attributes) { + if (attribute.range.containsPosition(position)) { + switch (attribute.key) { + case PromptHeaderAttributes.name: + return this.createHover(localize('promptHeader.prompt.name', 'The name of the prompt. This is also the name of the slash command that will run this prompt.'), attribute.range); + case PromptHeaderAttributes.description: + return this.createHover(localize('promptHeader.prompt.description', 'The description of the reusable prompt, what it does and when to use it.'), attribute.range); + case PromptHeaderAttributes.argumentHint: + return this.createHover(localize('promptHeader.prompt.argumentHint', 'The argument-hint describes what inputs the prompt expects or supports.'), attribute.range); + case PromptHeaderAttributes.model: + return this.getModelHover(attribute, attribute.range, localize('promptHeader.prompt.model', 'The model to use in this prompt.'), false); + case PromptHeaderAttributes.tools: + return this.getToolHover(attribute, position, localize('promptHeader.prompt.tools', 'The tools to use in this prompt.')); + case PromptHeaderAttributes.agent: + case PromptHeaderAttributes.mode: + return this.getAgentHover(attribute, position); + } + } + } + break; } return undefined; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index 5fd5d787464..a98a73988f4 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -43,6 +43,7 @@ export class PromptValidator { this.validateHeader(promptAST, promptType, report); await this.validateBody(promptAST, promptType, report); await this.validateFileName(promptAST, promptType, report); + await this.validateSkillFolderName(promptAST, promptType, report); } private async validateFileName(promptAST: ParsedPromptFile, promptType: PromptsType, report: (markers: IMarkerData) => void): Promise { @@ -56,6 +57,36 @@ export class PromptValidator { } } + private async validateSkillFolderName(promptAST: ParsedPromptFile, promptType: PromptsType, report: (markers: IMarkerData) => void): Promise { + if (promptType !== PromptsType.skill) { + return; + } + + const nameAttribute = promptAST.header?.attributes.find(attr => attr.key === PromptHeaderAttributes.name); + if (!nameAttribute || nameAttribute.value.type !== 'string') { + return; + } + + const skillName = nameAttribute.value.value.trim(); + if (!skillName) { + return; + } + + // Extract folder name from path (e.g., .github/skills/my-skill/SKILL.md -> my-skill) + const pathParts = promptAST.uri.path.split('/'); + const skillIndex = pathParts.findIndex(part => part === 'SKILL.md'); + if (skillIndex > 0) { + const folderName = pathParts[skillIndex - 1]; + if (folderName && skillName !== folderName) { + report(toMarker( + localize('promptValidator.skillNameFolderMismatch', "The skill name '{0}' should match the folder name '{1}'.", skillName, folderName), + nameAttribute.value.range, + MarkerSeverity.Warning + )); + } + } + } + private async validateBody(promptAST: ParsedPromptFile, promptType: PromptsType, report: (markers: IMarkerData) => void): Promise { const body = promptAST.body; if (!body) { @@ -159,6 +190,10 @@ export class PromptValidator { break; } + case PromptsType.skill: + // Skill-specific validations (currently none beyond name/description) + break; + } } @@ -186,6 +221,9 @@ export class PromptValidator { case PromptsType.instructions: report(toMarker(localize('promptValidator.unknownAttribute.instructions', "Attribute '{0}' is not supported in instructions files. Supported: {1}.", attribute.key, supportedNames.value), attribute.range, MarkerSeverity.Warning)); break; + case PromptsType.skill: + report(toMarker(localize('promptValidator.unknownAttribute.skill', "Attribute '{0}' is not supported in skill files. Supported: {1}.", attribute.key, supportedNames.value), attribute.range, MarkerSeverity.Warning)); + break; } } } @@ -500,7 +538,7 @@ const allAttributeNames = { [PromptsType.prompt]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.mode, PromptHeaderAttributes.agent, PromptHeaderAttributes.argumentHint], [PromptsType.instructions]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.applyTo, PromptHeaderAttributes.excludeAgent], [PromptsType.agent]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target, PromptHeaderAttributes.infer], - [PromptsType.skill]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description], + [PromptsType.skill]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.license, PromptHeaderAttributes.compatibility, PromptHeaderAttributes.metadata], }; const githubCopilotAgentAttributeNames = [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.tools, PromptHeaderAttributes.target, GithubPromptHeaderAttributes.mcpServers, PromptHeaderAttributes.infer]; const recommendedAttributeNames = { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts index 8ca00a9dff2..59d21a4755a 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts @@ -75,6 +75,9 @@ export namespace PromptHeaderAttributes { export const excludeAgent = 'excludeAgent'; export const target = 'target'; export const infer = 'infer'; + export const license = 'license'; + export const compatibility = 'compatibility'; + export const metadata = 'metadata'; } export namespace GithubPromptHeaderAttributes { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 47b297427bb..442eae11691 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -204,6 +204,8 @@ export class PromptsService extends Disposable implements IPromptsService { } else if (type === PromptsType.prompt) { this.cachedFileLocations[PromptsType.prompt] = undefined; this.cachedSlashCommands.refresh(); + } else if (type === PromptsType.skill) { + this.cachedFileLocations[PromptsType.skill] = undefined; } })); } @@ -217,6 +219,8 @@ export class PromptsService extends Disposable implements IPromptsService { } else if (type === PromptsType.prompt) { this.cachedFileLocations[PromptsType.prompt] = undefined; this.cachedSlashCommands.refresh(); + } else if (type === PromptsType.skill) { + this.cachedFileLocations[PromptsType.skill] = undefined; } disposables.add({ @@ -232,6 +236,8 @@ export class PromptsService extends Disposable implements IPromptsService { } else if (type === PromptsType.prompt) { this.cachedFileLocations[PromptsType.prompt] = undefined; this.cachedSlashCommands.refresh(); + } else if (type === PromptsType.skill) { + this.cachedFileLocations[PromptsType.skill] = undefined; } } } @@ -339,8 +345,11 @@ export class PromptsService extends Disposable implements IPromptsService { } } - const userHome = this.userDataService.currentProfile.promptsHome; - result.push({ uri: userHome, storage: PromptsStorage.user, type }); + if (type !== PromptsType.skill) { + // no user source folders for skills + const userHome = this.userDataService.currentProfile.promptsHome; + result.push({ uri: userHome, storage: PromptsStorage.user, type }); + } return result; } diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts index c4a7e74209f..926ed459185 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts @@ -379,4 +379,40 @@ suite('PromptHoverProvider', () => { assert.strictEqual(hover, 'The name of the instruction file as shown in the UI. If not set, the name is derived from the file name.'); }); }); + + suite('skill hovers', () => { + test('hover on name attribute', async () => { + const content = [ + '---', + 'name: "My Skill"', + 'description: "Test skill"', + '---', + ].join('\n'); + const hover = await getHover(content, 2, 1, PromptsType.skill); + assert.strictEqual(hover, 'The name of the skill.'); + }); + + test('hover on description attribute', async () => { + const content = [ + '---', + 'name: "Test Skill"', + 'description: "Test skill description"', + '---', + ].join('\n'); + const hover = await getHover(content, 3, 1, PromptsType.skill); + assert.strictEqual(hover, 'The description of the skill. The description is added to every request and will be used by the agent to decide when to load the skill.'); + }); + + test('hover on file attribute', async () => { + const content = [ + '---', + 'name: "Test Skill"', + 'description: "Test skill"', + 'file: "SKILL.md"', + '---', + ].join('\n'); + const hover = await getHover(content, 4, 1, PromptsType.skill); + assert.strictEqual(hover, undefined); + }); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts index fc3f3f90363..bc0d4dded99 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts @@ -141,8 +141,10 @@ suite('PromptValidator', () => { }); }); - async function validate(code: string, promptType: PromptsType): Promise { - const uri = URI.parse('myFs://test/testFile' + getPromptFileExtension(promptType)); + async function validate(code: string, promptType: PromptsType, uri?: URI): Promise { + if (!uri) { + uri = URI.parse('myFs://test/testFile' + getPromptFileExtension(promptType)); + } const result = new PromptFileParser().parse(uri, code); const validator = instaService.createInstance(PromptValidator); const markers: IMarkerData[] = []; @@ -1122,4 +1124,63 @@ suite('PromptValidator', () => { }); + suite('skills', () => { + + test('skill name matches folder name', async () => { + const content = [ + '---', + 'name: my-skill', + 'description: Test Skill', + '---', + 'This is a skill.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///.github/skills/my-skill/SKILL.md')); + assert.deepStrictEqual(markers, [], 'Expected no validation issues when name matches folder'); + }); + + test('skill name does not match folder name', async () => { + const content = [ + '---', + 'name: different-name', + 'description: Test Skill', + '---', + 'This is a skill.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///.github/skills/my-skill/SKILL.md')); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Warning); + assert.strictEqual(markers[0].message, `The skill name 'different-name' should match the folder name 'my-skill'.`); + }); + + test('skill without name attribute does not error', async () => { + const content = [ + '---', + 'description: Test Skill', + '---', + 'This is a skill without a name.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///.github/skills/my-skill/SKILL.md')); + assert.deepStrictEqual(markers, [], 'Expected no validation issues when name is missing'); + }); + + test('skill with unknown attributes shows warning', async () => { + const content = [ + '---', + 'name: my-skill', + 'description: Test Skill', + 'unknownAttr: value', + 'anotherUnknown: 123', + '---', + 'This is a skill.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///.github/skills/my-skill/SKILL.md')); + assert.strictEqual(markers.length, 2); + assert.ok(markers.every(m => m.severity === MarkerSeverity.Warning)); + assert.ok(markers.some(m => m.message.includes('unknownAttr'))); + assert.ok(markers.some(m => m.message.includes('anotherUnknown'))); + assert.ok(markers.every(m => m.message.includes('Supported: '))); + }); + + }); + }); From fa78c64031a1067cedf074f5c389bfbd56c0ab32 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 18:47:11 +0000 Subject: [PATCH 019/387] Fix 100% CPU on Windows when watched file is deleted (#288003) * Initial plan * Fix 100% CPU on Windows when watched file is deleted When a watched file/folder is deleted on Windows, Parcel's subscribe callback may continue to receive errors from ReadDirectoryChangesW. Previously, these errors were processed by onUnexpectedError which would fire _onDidError events repeatedly. This caused 100% CPU usage as the error handling loop never stopped. The fix checks if the watcher has already failed (via watcher.failed) before processing errors. When the watcher has failed (which happens when the watched path is deleted), additional errors from Parcel are ignored. This prevents the CPU spike while still allowing the normal suspend/resume mechanism to handle watching the path when it's recreated. Fixes microsoft/vscode#263718 Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> * Update comments to better explain CPU issue prevention Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> * Add same fix to nodejs watcher for Windows CPU issue Apply the same fix to nodejsWatcherLib.ts as suggested by the reviewer. The error handler now checks if the watcher has already failed before processing additional errors, preventing the endless error loop on Windows when a watched path is deleted. Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> * lets not change parcel watcher for now * . * . --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> Co-authored-by: Benjamin Pasero --- src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts | 4 ++++ src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts index 6429554e098..4fb2322e651 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts @@ -125,6 +125,10 @@ export class NodeJSFileWatcherLibrary extends Disposable { } private notifyWatchFailed(): void { + if (this.didFail) { + return; + } + this.didFail = true; this.onDidWatchFail?.(); diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index b375d171c42..9ee123d9635 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -109,6 +109,10 @@ export class ParcelWatcherInstance extends Disposable { } notifyWatchFailed(): void { + if (this.didFail) { + return; + } + this.didFail = true; this._onDidFail.fire(); From 3e56b3a2a4002a63b82bdcfdb7ea5873dbc6957e Mon Sep 17 00:00:00 2001 From: Robo Date: Fri, 16 Jan 2026 03:49:28 +0900 Subject: [PATCH 020/387] feat: enable win11 context menu for stable (#287832) * feat: enable win11 context menu for stable * chore: update dll package * chore: codesign appx for stable * feat: support system setup * fix: allow installing appx for system setup * fix: add -SkipLicense to avoid exception during install --- build/azure-pipelines/win32/codesign.ts | 2 +- build/checksums/explorer-dll.txt | 8 ++--- build/win32/code.iss | 48 ++++++++++++++----------- build/win32/explorer-dll-fetcher.ts | 4 +-- 4 files changed, 34 insertions(+), 28 deletions(-) diff --git a/build/azure-pipelines/win32/codesign.ts b/build/azure-pipelines/win32/codesign.ts index 183bd3cc9fa..dce5e55b840 100644 --- a/build/azure-pipelines/win32/codesign.ts +++ b/build/azure-pipelines/win32/codesign.ts @@ -20,7 +20,7 @@ async function main() { // 3. Codesign context menu appx package (insiders only) const codesignTask1 = spawnCodesignProcess(esrpCliDLLPath, 'sign-windows', codeSigningFolderPath, '*.dll,*.exe,*.node'); const codesignTask2 = spawnCodesignProcess(esrpCliDLLPath, 'sign-windows-appx', codeSigningFolderPath, '*.ps1'); - const codesignTask3 = process.env['VSCODE_QUALITY'] === 'insider' + const codesignTask3 = process.env['VSCODE_QUALITY'] !== 'exploration' ? spawnCodesignProcess(esrpCliDLLPath, 'sign-windows-appx', codeSigningFolderPath, '*.appx') : undefined; diff --git a/build/checksums/explorer-dll.txt b/build/checksums/explorer-dll.txt index 4d34e265297..322522db4e1 100644 --- a/build/checksums/explorer-dll.txt +++ b/build/checksums/explorer-dll.txt @@ -1,4 +1,4 @@ -5dbdd08784067e4caf7d119f7bec05b181b155e1e9868dec5a6c5174ce59f8bd code_explorer_command_arm64.dll -c7b8dde71f62397fbcd1693e35f25d9ceab51b66e805b9f39efc78e02c6abf3c code_explorer_command_x64.dll -968a6fe75c7316d2e2176889dffed8b50e41ee3f1834751cf6387094709b00ef code_insider_explorer_command_arm64.dll -da071035467a64fabf8fc3762b52fa8cdb3f216aa2b252df5b25b8bdf96ec594 code_insider_explorer_command_x64.dll +a226d50d1b8ff584019b4fc23cfb99256c7d0abcb3d39709a7c56097946448f8 code_explorer_command_arm64.dll +f2ddd48127e26f6d311a92e5d664379624e017206a3e41f72fa6e44a440aaca9 code_explorer_command_x64.dll +6df8b42e57922cce08f1f5360bcd02e253e8369a6e8b9e41e1f9867f3715380f code_insider_explorer_command_arm64.dll +625e15bfb292ddf68c40851be0b42dbaf39a05e615228e577997491c9865c246 code_insider_explorer_command_x64.dll diff --git a/build/win32/code.iss b/build/win32/code.iss index cc11cbe80c1..f8f202f42ee 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -82,7 +82,7 @@ Type: filesandordirs; Name: "{app}\_" Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1 Name: "addcontextmenufiles"; Description: "{cm:AddContextMenuFiles,{#NameShort}}"; GroupDescription: "{cm:Other}"; Flags: unchecked -Name: "addcontextmenufolders"; Description: "{cm:AddContextMenuFolders,{#NameShort}}"; GroupDescription: "{cm:Other}"; Flags: unchecked; Check: not (IsWindows11OrLater and QualityIsInsiders) +Name: "addcontextmenufolders"; Description: "{cm:AddContextMenuFolders,{#NameShort}}"; GroupDescription: "{cm:Other}"; Flags: unchecked; Check: not IsWindows11OrLater Name: "associatewithfiles"; Description: "{cm:AssociateWithFiles,{#NameShort}}"; GroupDescription: "{cm:Other}" Name: "addtopath"; Description: "{cm:AddToPath}"; GroupDescription: "{cm:Other}" Name: "runcode"; Description: "{cm:RunAfter,{#NameShort}}"; GroupDescription: "{cm:Other}"; Check: WizardSilent @@ -95,11 +95,9 @@ Source: "*"; Excludes: "\CodeSignSummary*.md,\tools,\tools\*,\appx,\appx\*,\reso Source: "tools\*"; DestDir: "{app}\tools"; Flags: ignoreversion Source: "{#ProductJsonPath}"; DestDir: "{code:GetDestDir}\resources\app"; Flags: ignoreversion #ifdef AppxPackageName -#if "user" == InstallTarget Source: "appx\{#AppxPackage}"; DestDir: "{app}\appx"; BeforeInstall: RemoveAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater Source: "appx\{#AppxPackageDll}"; DestDir: "{app}\appx"; AfterInstall: AddAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater #endif -#endif [Icons] Name: "{group}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; AppUserModelID: "{#AppUserId}" @@ -1264,19 +1262,19 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBas Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe""" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1""" -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}ContextMenu"; ValueType: expandsz; ValueName: "Title"; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: IsWindows11OrLater and QualityIsInsiders -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufiles; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: addcontextmenufiles; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not (IsWindows11OrLater and QualityIsInsiders) +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}ContextMenu"; ValueType: expandsz; ValueName: "Title"; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufiles; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: addcontextmenufiles; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater ; Environment #if "user" == InstallTarget @@ -1501,7 +1499,11 @@ var begin if not AppxPackageInstalled(ExpandConstant('{#AppxPackageName}'), AddAppxPackageResultCode) then begin Log('Installing appx ' + AppxPackageFullname + ' ...'); +#if "user" == InstallTarget ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Add-AppxPackage -Path ''' + ExpandConstant('{app}\appx\{#AppxPackage}') + ''' -ExternalLocation ''' + ExpandConstant('{app}\appx') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); +#else + ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Add-AppxPackage -Stage ''' + ExpandConstant('{app}\appx\{#AppxPackage}') + ''' -ExternalLocation ''' + ExpandConstant('{app}\appx') + '''; Add-AppxProvisionedPackage -Online -SkipLicense -PackagePath ''' + ExpandConstant('{app}\appx\{#AppxPackage}') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); +#endif Log('Add-AppxPackage complete.'); end; end; @@ -1514,13 +1516,19 @@ begin // Following condition can be removed after two versions. if QualityIsInsiders() and AppxPackageInstalled('Microsoft.VSCodeInsiders', RemoveAppxPackageResultCode) then begin Log('Deleting old appx ' + AppxPackageFullname + ' installation...'); +#if "user" == InstallTarget ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Remove-AppxPackage -Package ''' + AppxPackageFullname + ''''), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); +#endif DeleteFile(ExpandConstant('{app}\appx\code_insiders_explorer_{#Arch}.appx')); DeleteFile(ExpandConstant('{app}\appx\code_insiders_explorer_command.dll')); end; if AppxPackageInstalled(ExpandConstant('{#AppxPackageName}'), RemoveAppxPackageResultCode) then begin Log('Removing current ' + AppxPackageFullname + ' appx installation...'); +#if "user" == InstallTarget ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Remove-AppxPackage -Package ''' + AppxPackageFullname + ''''), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); +#else + ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('$packages = Get-AppxPackage ''' + AppxPackageFullname + '''; foreach ($package in $packages) { Remove-AppxProvisionedPackage -PackageName $package.PackageFullName -Online }; foreach ($package in $packages) { Remove-AppxPackage -Package $package.PackageFullName -AllUsers }'), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); +#endif Log('Remove-AppxPackage for current appx installation complete.'); end; end; @@ -1534,8 +1542,8 @@ begin if CurStep = ssPostInstall then begin #ifdef AppxPackageName - // Remove the old context menu registry keys for insiders - if QualityIsInsiders() and WizardIsTaskSelected('addcontextmenufiles') then begin + // Remove the old context menu registry keys + if IsWindows11OrLater() and WizardIsTaskSelected('addcontextmenufiles') then begin RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\*\shell\{#RegValueName}'); RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\directory\shell\{#RegValueName}'); RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\directory\background\shell\{#RegValueName}'); @@ -1626,9 +1634,7 @@ begin exit; end; #ifdef AppxPackageName - #if "user" == InstallTarget - RemoveAppxPackage(); - #endif + RemoveAppxPackage(); #endif if not RegQueryStringValue({#EnvironmentRootKey}, '{#EnvironmentKey}', 'Path', Path) then begin diff --git a/build/win32/explorer-dll-fetcher.ts b/build/win32/explorer-dll-fetcher.ts index 09bd2691843..14dcf497aff 100644 --- a/build/win32/explorer-dll-fetcher.ts +++ b/build/win32/explorer-dll-fetcher.ts @@ -43,12 +43,12 @@ export async function downloadExplorerDll(outDir: string, quality: string = 'sta d(`downloading ${fileName}`); const artifact = await downloadArtifact({ isGeneric: true, - version: 'v5.0.0-377200', + version: 'v7.0.0-391934', artifactName: fileName, checksums, mirrorOptions: { mirror: 'https://github.com/microsoft/vscode-explorer-command/releases/download/', - customDir: 'v5.0.0-377200', + customDir: 'v7.0.0-391934', customFilename: fileName } }); From 9a53549070d717c2996927c809c870d8055c32ca Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:21:06 -0800 Subject: [PATCH 021/387] fix: add descriptions to chat session picker options (#288149) --- .../chat/browser/chatSessions/chatSessionPickerActionItem.ts | 4 ++-- .../browser/chatSessions/searchableOptionPickerActionItem.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.ts index f9576058087..9a6f9ac8a1f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.ts @@ -91,7 +91,7 @@ export class ChatSessionPickerActionItem extends ActionWidgetDropdownActionViewI icon: optionItem.icon, checked: isCurrent, class: undefined, - description: undefined, + description: optionItem.description, tooltip: optionItem.description ?? optionItem.name, label: optionItem.name, run: () => { @@ -111,7 +111,7 @@ export class ChatSessionPickerActionItem extends ActionWidgetDropdownActionViewI icon: option.icon, checked: true, class: undefined, - description: undefined, + description: option.description, tooltip: option.description ?? option.name, label: option.name, run: () => { } diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.ts index 16ef8fb0736..8c3a813b924 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.ts @@ -71,7 +71,7 @@ export class SearchableOptionPickerActionItem extends ChatSessionPickerActionIte icon: optionItem.icon, checked: isCurrent, class: undefined, - description: undefined, + description: optionItem.description, tooltip: optionItem.description ?? optionItem.name, label: optionItem.name, run: () => { From 63be45bc13db755fdc5370a8339afd92bbfa0933 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Thu, 15 Jan 2026 14:39:38 -0500 Subject: [PATCH 022/387] Fix create file not properly rendering (#288150) * Fix create file not properly rendering * Update src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../common/model/chatProgressTypes/chatToolInvocation.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts index 6a24a622c8c..8b7545f1beb 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts @@ -6,6 +6,7 @@ import { encodeBase64 } from '../../../../../../base/common/buffer.js'; import { IMarkdownString } from '../../../../../../base/common/htmlContent.js'; import { IObservable, ISettableObservable, observableValue } from '../../../../../../base/common/observable.js'; +import { localize } from '../../../../../../nls.js'; import { ConfirmedReason, IChatExtensionsContent, IChatSubagentToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind, type IChatTerminalToolInvocationData } from '../../chatService/chatService.js'; import { IPreparedToolInvocation, isToolResultOutputDetails, IToolConfirmationMessages, IToolData, IToolProgressStep, IToolResult, ToolDataSource } from '../../tools/languageModelToolsService.js'; @@ -62,7 +63,9 @@ export class ChatToolInvocation implements IChatToolInvocation { isStreaming: boolean = false, chatRequestId?: string ) { - this.invocationMessage = preparedInvocation?.invocationMessage ?? ''; + // For streaming invocations, use a default message until handleToolStream provides one + const defaultStreamingMessage = isStreaming ? localize('toolInvocationMessage', "Using \"{0}\"", toolData.displayName) : ''; + this.invocationMessage = preparedInvocation?.invocationMessage ?? defaultStreamingMessage; this.pastTenseMessage = preparedInvocation?.pastTenseMessage; this.originMessage = preparedInvocation?.originMessage; this.confirmationMessages = preparedInvocation?.confirmationMessages; From 4b8d9aa13a859d9427da3d6f538aae87e88d01cb Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:46:10 -0800 Subject: [PATCH 023/387] Agents control show `Esc` button hint (#288135) * change how we draw their attention * more * swap X to esc button --- .../browser/agentSessions/agentsControl.ts | 26 +++++++------- .../browser/agentSessions/media/focusView.css | 35 +++++++++++-------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentsControl.ts index 8bdedfbde99..d62c868d658 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentsControl.ts @@ -204,37 +204,37 @@ export class AgentsControlViewItem extends BaseActionViewItem { titleLabel.textContent = session?.label ?? localize('agentSessionProjection', "Agent Session Projection"); pill.appendChild(titleLabel); - // Close button (right side) - const closeButton = $('span.agents-control-close'); - closeButton.classList.add('codicon', 'codicon-close'); - closeButton.setAttribute('role', 'button'); - closeButton.setAttribute('aria-label', localize('exitAgentSessionProjection', "Exit Agent Session Projection")); - closeButton.tabIndex = 0; - pill.appendChild(closeButton); + // Escape button (right side) - serves as both keybinding hint and close button + const escButton = $('span.agents-control-esc-button'); + escButton.textContent = 'Esc'; + escButton.setAttribute('role', 'button'); + escButton.setAttribute('aria-label', localize('exitAgentSessionProjection', "Exit Agent Session Projection")); + escButton.tabIndex = 0; + pill.appendChild(escButton); // Setup hovers const hoverDelegate = getDefaultHoverDelegate('mouse'); - disposables.add(this.hoverService.setupManagedHover(hoverDelegate, closeButton, localize('exitAgentSessionProjectionTooltip', "Exit Agent Session Projection (Escape)"))); + disposables.add(this.hoverService.setupManagedHover(hoverDelegate, escButton, localize('exitAgentSessionProjectionTooltip', "Exit Agent Session Projection (Escape)"))); disposables.add(this.hoverService.setupManagedHover(hoverDelegate, pill, () => { const activeSession = this.focusViewService.activeSession; return activeSession ? localize('agentSessionProjectionTooltip', "Agent Session Projection: {0}", activeSession.label) : localize('agentSessionProjection', "Agent Session Projection"); })); - // Close button click handler - disposables.add(addDisposableListener(closeButton, EventType.MOUSE_DOWN, (e) => { + // Esc button click handler + disposables.add(addDisposableListener(escButton, EventType.MOUSE_DOWN, (e) => { e.preventDefault(); e.stopPropagation(); this.commandService.executeCommand(ExitFocusViewAction.ID); })); - disposables.add(addDisposableListener(closeButton, EventType.CLICK, (e) => { + disposables.add(addDisposableListener(escButton, EventType.CLICK, (e) => { e.preventDefault(); e.stopPropagation(); this.commandService.executeCommand(ExitFocusViewAction.ID); })); - // Close button keyboard handler - disposables.add(addDisposableListener(closeButton, EventType.KEY_DOWN, (e) => { + // Esc button keyboard handler + disposables.add(addDisposableListener(escButton, EventType.KEY_DOWN, (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/focusView.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/focusView.css index ebae07a1903..9f4c21d625c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/focusView.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/focusView.css @@ -216,29 +216,36 @@ Agents Control - Titlebar control white-space: nowrap; } -/* Close button (right side in session mode) */ -.agents-control-close { - display: flex; +/* Escape button (right side in session mode) - serves as keybinding hint and close button */ +.agents-control-esc-button { + display: inline-flex; align-items: center; + align-self: center; justify-content: center; - width: 20px; - height: 20px; - border-radius: 4px; + box-sizing: border-box; + border-style: solid; + border-width: 1px; + border-radius: 3px; + height: 16px; + line-height: 14px; + font-size: 10px; + padding: 0 6px; + margin: 0 2px 0 6px; + background-color: transparent; + color: var(--vscode-descriptionForeground); + border-color: color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent); cursor: pointer; - color: var(--vscode-foreground); - opacity: 0.8; - margin-left: auto; -webkit-app-region: no-drag; } -.agents-control-close:hover { - opacity: 1; - background-color: rgba(0, 0, 0, 0.1); +.agents-control-esc-button:hover { + color: var(--vscode-foreground); + border-color: color-mix(in srgb, var(--vscode-foreground) 60%, transparent); } -.agents-control-close:focus { +.agents-control-esc-button:focus { outline: 1px solid var(--vscode-focusBorder); - outline-offset: -1px; + outline-offset: 1px; } /* Search button (right of pill) */ From 601d342ddee6a3714d70f3f187084ae2473e94b2 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Fri, 16 Jan 2026 03:58:03 +0800 Subject: [PATCH 024/387] terminal in thinking dropdown (#287902) * terminals in thinking dropdown * make setting on by default:' * fix chat confirmation widget * fix spelling * fix public issue * add backup icon for terminal --- .../contrib/chat/browser/chat.contribution.ts | 6 + .../chatThinkingContentPart.ts | 20 ++- .../media/chatThinkingContent.css | 33 +++++ .../chatTerminalToolProgressPart.ts | 126 +++++++++++++++++- .../chat/browser/widget/chatListRenderer.ts | 9 +- .../contrib/chat/common/constants.ts | 1 + 6 files changed, 185 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index fef14beefad..00c48c80a54 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -837,6 +837,12 @@ configurationRegistry.registerConfiguration({ markdownDescription: nls.localize('chat.agent.thinking.collapsedTools', "Controls how tool calls are displayed in relation to thinking sections."), tags: ['experimental'], }, + [ChatConfiguration.TerminalToolsInThinking]: { + type: 'boolean', + default: true, + markdownDescription: nls.localize('chat.agent.thinking.terminalTools', "When enabled, terminal tool calls are displayed inside the thinking dropdown with a simplified view."), + tags: ['experimental'], + }, 'chat.disableAIFeatures': { type: 'boolean', description: nls.localize('chat.disableAIFeatures', "Disable and hide built-in AI features provided by GitHub Copilot, including chat and inline suggestions."), diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index 14576106a78..b3e5994ac59 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -65,6 +65,12 @@ export function getToolInvocationIcon(toolId: string): ThemeIcon { return Codicon.pencil; } + if ( + lowerToolId.includes('terminal') + ) { + return Codicon.terminal; + } + // default to generic tool icon return Codicon.tools; } @@ -522,7 +528,19 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen const itemWrapper = $('.chat-thinking-tool-wrapper'); const isMarkdownEdit = toolInvocationOrMarkdown?.kind === 'markdownContent'; - const icon = isMarkdownEdit ? Codicon.pencil : (toolInvocationId ? getToolInvocationIcon(toolInvocationId) : Codicon.tools); + const isTerminalTool = toolInvocationOrMarkdown && (toolInvocationOrMarkdown.kind === 'toolInvocation' || toolInvocationOrMarkdown.kind === 'toolInvocationSerialized') && toolInvocationOrMarkdown.toolSpecificData?.kind === 'terminal'; + + let icon: ThemeIcon; + if (isMarkdownEdit) { + icon = Codicon.pencil; + } else if (isTerminalTool) { + const terminalData = (toolInvocationOrMarkdown as IChatToolInvocation | IChatToolInvocationSerialized).toolSpecificData as { kind: 'terminal'; terminalCommandState?: { exitCode?: number } }; + const exitCode = terminalData?.terminalCommandState?.exitCode; + icon = exitCode !== undefined && exitCode !== 0 ? Codicon.error : Codicon.terminal; + } else { + icon = toolInvocationId ? getToolInvocationIcon(toolInvocationId) : Codicon.tools; + } + const iconElement = createThinkingIcon(icon); itemWrapper.appendChild(iconElement); itemWrapper.appendChild(content); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css index 9d05f50f96d..041ba90429a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css @@ -46,6 +46,35 @@ .codicon:not(.chat-thinking-icon) { display: none; } + + .chat-terminal-thinking-content { + overflow: hidden; + padding: 10px 10px 10px 2px; + + .codicon:not(.codicon-check) { + display: inline-flex; + } + } + + .chat-terminal-thinking-collapsible { + display: flex; + flex-direction: column; + width: 100%; + margin: 1px 0 0 4px; + + .chat-used-context-list.chat-terminal-thinking-content { + border: none; + padding: 0; + + .progress-container { + margin: 0; + } + } + + .chat-terminal-thinking-content .rendered-markdown [data-code] { + margin-bottom: 0px; + } + } } .chat-thinking-item.markdown-content { @@ -83,6 +112,10 @@ } } + .chat-thinking-tool-wrapper .chat-terminal-thinking-content .chat-markdown-part.rendered-markdown { + padding: 0; + } + /* chain of thought lines */ .chat-thinking-tool-wrapper, .chat-thinking-item.markdown-content { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts index b36a2b71901..6a5a55caa2a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts @@ -6,16 +6,20 @@ import { h } from '../../../../../../../base/browser/dom.js'; import { ActionBar } from '../../../../../../../base/browser/ui/actionbar/actionbar.js'; import { isMarkdownString, MarkdownString } from '../../../../../../../base/common/htmlContent.js'; +import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { ChatConfiguration } from '../../../../common/constants.js'; import { migrateLegacyTerminalToolSpecificData } from '../../../../common/chat.js'; import { IChatToolInvocation, IChatToolInvocationSerialized, type IChatMarkdownContent, type IChatTerminalToolInvocationData, type ILegacyChatTerminalToolInvocationData } from '../../../../common/chatService/chatService.js'; import { CodeBlockModelCollection } from '../../../../common/widget/codeBlockModelCollection.js'; -import { IChatCodeBlockInfo, IChatWidgetService } from '../../../chat.js'; +import { ChatTreeItem, IChatCodeBlockInfo, IChatWidgetService } from '../../../chat.js'; import { ChatQueryTitlePart } from '../chatConfirmationWidget.js'; import { IChatContentPartRenderContext } from '../chatContentParts.js'; import { ChatMarkdownContentPart, type IChatMarkdownContentPartOptions } from '../chatMarkdownContentPart.js'; import { ChatProgressSubPart } from '../chatProgressContentPart.js'; import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; +import { ChatCollapsibleContentPart } from '../chatCollapsibleContentPart.js'; +import { IChatRendererContent } from '../../../../common/model/chatViewModel.js'; import '../media/chatTerminalToolProgressPart.css'; import type { ICodeBlockRenderOptions } from '../codeBlockPart.js'; import { Action, IAction } from '../../../../../../../base/common/actions.js'; @@ -215,6 +219,8 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart private readonly _decoration: TerminalCommandDecoration; private _autoExpandTimeout: ReturnType | undefined; private _userToggledOutput: boolean = false; + private _isInThinkingContainer: boolean = false; + private _thinkingCollapsibleWrapper: ChatTerminalThinkingCollapsibleWrapper | undefined; private markdownPart: ChatMarkdownContentPart | undefined; public get codeblocks(): IChatCodeBlockInfo[] { @@ -244,6 +250,7 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IConfigurationService private readonly _configurationService: IConfigurationService, ) { super(toolInvocation); @@ -349,15 +356,54 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart elements.message.append(this.markdownPart.domNode); const progressPart = this._register(_instantiationService.createInstance(ChatProgressSubPart, elements.container, this.getIcon(), terminalData.autoApproveInfo)); - this.domNode = progressPart.domNode; this._decoration.update(); - if (expandedStateByInvocation.get(toolInvocation)) { + // wrap terminal when thinking setting enabled + const terminalToolsInThinking = this._configurationService.getValue(ChatConfiguration.TerminalToolsInThinking); + const requiresConfirmation = toolInvocation.kind === 'toolInvocation' && IChatToolInvocation.getConfirmationMessages(toolInvocation); + + if (terminalToolsInThinking && !requiresConfirmation) { + this._isInThinkingContainer = true; + this.domNode = this._createCollapsibleWrapper(progressPart.domNode, command, toolInvocation, context); + } else { + this.domNode = progressPart.domNode; + } + + if (expandedStateByInvocation.get(toolInvocation) || (this._isInThinkingContainer && IChatToolInvocation.isComplete(toolInvocation))) { void this._toggleOutput(true); } this._register(this._terminalChatService.registerProgressPart(this)); } + private _createCollapsibleWrapper(contentElement: HTMLElement, commandText: string, toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, context: IChatContentPartRenderContext): HTMLElement { + // truncate header when it's too long + const maxCommandLength = 50; + const truncatedCommand = commandText.length > maxCommandLength + ? commandText.substring(0, maxCommandLength) + '...' + : commandText; + + const isComplete = IChatToolInvocation.isComplete(toolInvocation); + const hasError = this._terminalData.terminalCommandState?.exitCode !== undefined && this._terminalData.terminalCommandState.exitCode !== 0; + const initialExpanded = !isComplete || hasError; + + const wrapper = this._register(this._instantiationService.createInstance( + ChatTerminalThinkingCollapsibleWrapper, + truncatedCommand, + contentElement, + context, + initialExpanded + )); + this._thinkingCollapsibleWrapper = wrapper; + + this._register(wrapper.onDidChangeHeight(() => this._onDidChangeHeight.fire())); + + return wrapper.domNode; + } + + public expandCollapsibleWrapper(): void { + this._thinkingCollapsibleWrapper?.expand(); + } + private async _initializeTerminalActions(): Promise { if (this._store.isDisposed) { return; @@ -445,6 +491,10 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart if (this._store.isDisposed) { return; } + // don't show dropdown when in thinking container + if (this._isInThinkingContainer) { + return; + } const resolvedCommand = command ?? this._getResolvedCommand(); const hasSnapshot = !!this._terminalData.terminalCommandOutput; if (!resolvedCommand && !hasSnapshot) { @@ -536,10 +586,14 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart this._addActions(terminalInstance, this._terminalData.terminalToolSessionId); const resolvedCommand = this._getResolvedCommand(terminalInstance); - // Auto-collapse on success - if (resolvedCommand?.exitCode === 0 && this._outputView.isExpanded && !this._userToggledOutput) { + // Auto-collapse on success (except for thinking) + if (resolvedCommand?.exitCode === 0 && this._outputView.isExpanded && !this._userToggledOutput && !this._isInThinkingContainer) { this._toggleOutput(false); } + // keep outer wrapper expanded on error + if (resolvedCommand?.exitCode !== undefined && resolvedCommand.exitCode !== 0 && this._thinkingCollapsibleWrapper) { + this.expandCollapsibleWrapper(); + } if (resolvedCommand?.endMarker) { commandDetectionListener.clear(); } @@ -549,10 +603,14 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart const resolvedImmediately = await tryResolveCommand(); if (resolvedImmediately?.endMarker) { commandDetectionListener.clear(); - // Auto-collapse on success - if (resolvedImmediately.exitCode === 0 && this._outputView.isExpanded && !this._userToggledOutput) { + // Auto-collapse on success (except for thinking) + if (resolvedImmediately.exitCode === 0 && this._outputView.isExpanded && !this._userToggledOutput && !this._isInThinkingContainer) { this._toggleOutput(false); } + // keep outer wrapper expanded on error + if (resolvedImmediately.exitCode !== undefined && resolvedImmediately.exitCode !== 0 && this._thinkingCollapsibleWrapper) { + this.expandCollapsibleWrapper(); + } return; } }; @@ -1356,3 +1414,57 @@ export class FocusChatInstanceAction extends Action implements IAction { this.tooltip = this._keybindingService.appendKeybinding(this.label, TerminalContribCommandId.FocusMostRecentChatTerminal); } } + +class ChatTerminalThinkingCollapsibleWrapper extends ChatCollapsibleContentPart { + private readonly _contentElement: HTMLElement; + private readonly _commandText: string; + + constructor( + commandText: string, + contentElement: HTMLElement, + context: IChatContentPartRenderContext, + initialExpanded: boolean, + @IHoverService hoverService: IHoverService, + ) { + const title = `Ran \`${commandText}\``; + super(title, context, undefined, hoverService); + + this._contentElement = contentElement; + this._commandText = commandText; + + this.domNode.classList.add('chat-terminal-thinking-collapsible'); + + this._setCodeFormattedTitle(); + this.setExpanded(initialExpanded); + } + + private _setCodeFormattedTitle(): void { + if (!this._collapseButton) { + return; + } + + const labelElement = this._collapseButton.labelElement; + labelElement.textContent = ''; + + const ranText = document.createTextNode(localize('chat.terminal.ran.prefix', "Ran ")); + const codeElement = document.createElement('code'); + codeElement.textContent = this._commandText; + + labelElement.appendChild(ranText); + labelElement.appendChild(codeElement); + } + + protected override initContent(): HTMLElement { + const listWrapper = dom.$('.chat-used-context-list.chat-terminal-thinking-content'); + listWrapper.appendChild(this._contentElement); + return listWrapper; + } + + public expand(): void { + this.setExpanded(true); + } + + hasSameContent(_other: IChatRendererContent, _followingContent: IChatRendererContent[], _element: ChatTreeItem): boolean { + return false; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index ef2be62a646..2f964a2b26b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -1294,13 +1294,18 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer(ChatConfiguration.TerminalToolsInThinking); + return !!terminalToolsInThinking; } if (part.kind === 'toolInvocation') { diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 5c212aba616..ee1e23c2dff 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -23,6 +23,7 @@ export enum ChatConfiguration { CheckpointsEnabled = 'chat.checkpoints.enabled', ThinkingStyle = 'chat.agent.thinkingStyle', ThinkingGenerateTitles = 'chat.agent.thinking.generateTitles', + TerminalToolsInThinking = 'chat.agent.thinking.terminalTools', TodosShowWidget = 'chat.tools.todos.showWidget', NotifyWindowOnResponseReceived = 'chat.notifyWindowOnResponseReceived', ChatViewSessionsEnabled = 'chat.viewSessions.enabled', From ffd0ae8c67655e70ed6301db8cf2a416afe197be Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 15 Jan 2026 21:04:34 +0100 Subject: [PATCH 025/387] Button - fix overflow wrapping (#288160) --- src/vs/base/browser/ui/button/button.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/base/browser/ui/button/button.css b/src/vs/base/browser/ui/button/button.css index b641c7fc50c..8496f1b2284 100644 --- a/src/vs/base/browser/ui/button/button.css +++ b/src/vs/base/browser/ui/button/button.css @@ -16,6 +16,7 @@ border: 1px solid var(--vscode-button-border, transparent); line-height: 16px; font-size: 12px; + overflow-wrap: normal; } .monaco-text-button.small { From 8e70e1e590fd93ed39877a769bf976e8fef67233 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 15 Jan 2026 12:09:00 -0800 Subject: [PATCH 026/387] chat: fix last response state stored as active in the session index (#288161) --- .../contrib/chat/common/model/chatSessionStore.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts b/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts index 1465a8d5c54..4363f93ea7c 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts @@ -674,6 +674,14 @@ async function getSessionMetadata(session: ChatModel | ISerializableChatData): P lastRequestEnded: lastMessageDate, }; + let lastResponseState = session instanceof ChatModel ? + (session.lastRequest?.response?.state ?? ResponseModelState.Complete) : + ResponseModelState.Complete; + + if (lastResponseState === ResponseModelState.Pending || lastResponseState === ResponseModelState.NeedsInput) { + lastResponseState = ResponseModelState.Cancelled; + } + return { sessionId: session.sessionId, title: title || localize('newChat', "New Chat"), @@ -684,9 +692,7 @@ async function getSessionMetadata(session: ChatModel | ISerializableChatData): P isEmpty: session instanceof ChatModel ? session.getRequests().length === 0 : session.requests.length === 0, stats, isExternal: session instanceof ChatModel && !LocalChatSessionUri.parseLocalSessionId(session.sessionResource), - lastResponseState: session instanceof ChatModel ? - (session.lastRequest?.response?.state ?? ResponseModelState.Complete) : - ResponseModelState.Complete + lastResponseState, }; } From 88091dc760ba59009a6ef5cc030a3a26940efed8 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 15 Jan 2026 21:47:58 +0100 Subject: [PATCH 027/387] Chat - only render working set when there are session files (#288164) --- .../chat/browser/widget/input/chatInputPart.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index fc7036977b4..add94becac8 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -2284,7 +2284,19 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge })) ); - const shouldRender = derived(reader => editSessionEntries.read(reader).length > 0 || sessionFiles.read(reader).length > 0); + const shouldRender = derived(reader => { + const sessionFilesLength = sessionFiles.read(reader).length; + const editSessionEntriesLength = editSessionEntries.read(reader).length; + + const sessionResource = chatEditingSession?.chatSessionResource ?? this._widget?.viewModel?.model.sessionResource; + if (sessionResource && getChatSessionType(sessionResource) === localChatSessionType) { + return sessionFilesLength > 0 || editSessionEntriesLength > 0; + } + + // For background sessions, only render the + // working set when there are session files + return sessionFilesLength > 0; + }); this._renderingChatEdits.value = autorun(reader => { if (this.options.renderWorkingSet && shouldRender.read(reader)) { From bebef40b5a810169dd017e9c6dbb07df264616da Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 15 Jan 2026 13:34:23 -0800 Subject: [PATCH 028/387] chat: fix uri compare failing in chat session storage (#288169) Fixes a data loss issue where we'd fail to store the session --- .../contrib/chat/common/model/chatSessionOperationLog.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts b/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts index 4fdb96b6dbd..d5ca52b3b9b 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts @@ -6,10 +6,11 @@ import { assertNever } from '../../../../../base/common/assert.js'; import { isMarkdownString } from '../../../../../base/common/htmlContent.js'; import { equals as objectsEqual } from '../../../../../base/common/objects.js'; -import { isEqual as urisEqual } from '../../../../../base/common/resources.js'; +import { isEqual as _urisEqual } from '../../../../../base/common/resources.js'; import { hasKey } from '../../../../../base/common/types.js'; -import { ModifiedFileEntryState } from '../editing/chatEditingService.js'; +import { URI, UriComponents } from '../../../../../base/common/uri.js'; import { IChatMarkdownContent, ResponseModelState } from '../chatService/chatService.js'; +import { ModifiedFileEntryState } from '../editing/chatEditingService.js'; import { IParsedChatRequest } from '../requestParser/chatParserTypes.js'; import { IChatAgentEditedFileEvent, IChatDataSerializerLog, IChatModel, IChatProgressResponseContent, IChatRequestModel, IChatRequestVariableData, ISerializableChatData, ISerializableChatModelInputState, ISerializableChatRequestData, SerializedChatResponsePart } from './chatModel.js'; import * as Adapt from './objectMutationLog.js'; @@ -94,6 +95,10 @@ const responsePartSchema = Adapt.v { + return _urisEqual(URI.from(a), URI.from(b)); +}; + const messageSchema = Adapt.object({ text: Adapt.v(m => m.text), parts: Adapt.v(m => m.parts, (a, b) => a.length === b.length && a.every((part, i) => part.text === b[i].text)), From ccdcaa24078da3f784b2905d964f2531357be78b Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:36:13 -0800 Subject: [PATCH 029/387] refactor agent command center and projection code (#288163) * change how we draw their attention * more * swap X to esc button * refactor * remove unused * fix placeholder --- .../chat/browser/actions/chatActions.ts | 2 +- .../chat/browser/actions/chatNewActions.ts | 10 +- ...ns.ts => agentSessionProjectionActions.ts} | 53 ++++--- ...ce.ts => agentSessionProjectionService.ts} | 120 ++++++++------- .../agentSessions.contribution.ts | 47 +++--- .../agentSessions/agentSessionsOpener.ts | 8 +- .../agentSessions/agentStatusService.ts | 126 +++++++++++++++ ...{agentsControl.ts => agentStatusWidget.ts} | 144 +++++++++++++----- .../media/agentSessionProjection.css | 62 ++++++++ .../{focusView.css => agentStatusWidget.css} | 120 ++++----------- .../contrib/chat/browser/chat.contribution.ts | 6 + .../chat/common/actions/chatContextKeys.ts | 3 +- .../contrib/chat/common/constants.ts | 1 + 13 files changed, 472 insertions(+), 230 deletions(-) rename src/vs/workbench/contrib/chat/browser/agentSessions/{focusViewActions.ts => agentSessionProjectionActions.ts} (72%) rename src/vs/workbench/contrib/chat/browser/agentSessions/{focusViewService.ts => agentSessionProjectionService.ts} (67%) create mode 100644 src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusService.ts rename src/vs/workbench/contrib/chat/browser/agentSessions/{agentsControl.ts => agentStatusWidget.ts} (64%) create mode 100644 src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionProjection.css rename src/vs/workbench/contrib/chat/browser/agentSessions/media/{focusView.css => agentStatusWidget.css} (54%) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index a34abcfa1ff..2e30ae24cec 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -948,7 +948,7 @@ MenuRegistry.appendMenuItem(MenuId.CommandCenter, { ChatContextKeys.Setup.disabled.negate() ), ContextKeyExpr.has('config.chat.commandCenter.enabled'), - ContextKeyExpr.has(`config.${ChatConfiguration.AgentSessionProjectionEnabled}`).negate() // Hide when agent controls are shown + ContextKeyExpr.has(`config.${ChatConfiguration.AgentStatusEnabled}`).negate() // Hide when agent status is shown ), order: 10001 // to the right of command center }); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts index 9daecba598b..7c67a0de0d5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts @@ -30,7 +30,7 @@ import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; import { ACTION_ID_NEW_CHAT, ACTION_ID_NEW_EDIT_SESSION, CHAT_CATEGORY, handleCurrentEditingSession } from './chatActions.js'; import { clearChatEditor } from './chatClear.js'; import { AgentSessionsViewerOrientation } from '../agentSessions/agentSessions.js'; -import { IFocusViewService } from '../agentSessions/focusViewService.js'; +import { IAgentSessionProjectionService } from '../agentSessions/agentSessionProjectionService.js'; export interface INewEditSessionActionContext { @@ -121,11 +121,11 @@ export function registerNewChatActions() { async run(accessor: ServicesAccessor, ...args: unknown[]) { const accessibilityService = accessor.get(IAccessibilityService); - const focusViewService = accessor.get(IFocusViewService); + const projectionService = accessor.get(IAgentSessionProjectionService); - // Exit focus view mode if active (back button behavior) - if (focusViewService.isActive) { - await focusViewService.exitFocusView(); + // Exit projection mode if active (back button behavior) + if (projectionService.isActive) { + await projectionService.exitProjection(); return; } const viewsService = accessor.get(IViewsService); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/focusViewActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionActions.ts similarity index 72% rename from src/vs/workbench/contrib/chat/browser/agentSessions/focusViewActions.ts rename to src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionActions.ts index d76b5c2c967..0be275274f0 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/focusViewActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionActions.ts @@ -11,7 +11,7 @@ import { KeyCode } from '../../../../../base/common/keyCodes.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { ChatConfiguration } from '../../common/constants.js'; -import { IFocusViewService } from './focusViewService.js'; +import { IAgentSessionProjectionService } from './agentSessionProjectionService.js'; import { IAgentSession, isMarshalledAgentSessionContext, IMarshalledAgentSessionContext } from './agentSessionsModel.js'; import { IAgentSessionsService } from './agentSessionsService.js'; import { CHAT_CATEGORY } from '../actions/chatActions.js'; @@ -21,25 +21,25 @@ import { IsCompactTitleBarContext } from '../../../../common/contextkeys.js'; //#region Enter Agent Session Projection -export class EnterFocusViewAction extends Action2 { +export class EnterAgentSessionProjectionAction extends Action2 { static readonly ID = 'agentSession.enterAgentSessionProjection'; constructor() { super({ - id: EnterFocusViewAction.ID, + id: EnterAgentSessionProjectionAction.ID, title: localize2('enterAgentSessionProjection', "Enter Agent Session Projection"), category: CHAT_CATEGORY, f1: false, precondition: ContextKeyExpr.and( ChatContextKeys.enabled, ContextKeyExpr.has(`config.${ChatConfiguration.AgentSessionProjectionEnabled}`), - ChatContextKeys.inFocusViewMode.negate() + ChatContextKeys.inAgentSessionProjection.negate() ), }); } override async run(accessor: ServicesAccessor, context?: IAgentSession | IMarshalledAgentSessionContext): Promise { - const focusViewService = accessor.get(IFocusViewService); + const projectionService = accessor.get(IAgentSessionProjectionService); const agentSessionsService = accessor.get(IAgentSessionsService); let session: IAgentSession | undefined; @@ -52,7 +52,7 @@ export class EnterFocusViewAction extends Action2 { } if (session) { - await focusViewService.enterFocusView(session); + await projectionService.enterProjection(session); } } } @@ -61,30 +61,30 @@ export class EnterFocusViewAction extends Action2 { //#region Exit Agent Session Projection -export class ExitFocusViewAction extends Action2 { +export class ExitAgentSessionProjectionAction extends Action2 { static readonly ID = 'agentSession.exitAgentSessionProjection'; constructor() { super({ - id: ExitFocusViewAction.ID, + id: ExitAgentSessionProjectionAction.ID, title: localize2('exitAgentSessionProjection', "Exit Agent Session Projection"), category: CHAT_CATEGORY, f1: true, precondition: ContextKeyExpr.and( ChatContextKeys.enabled, - ChatContextKeys.inFocusViewMode + ChatContextKeys.inAgentSessionProjection ), keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.Escape, - when: ChatContextKeys.inFocusViewMode, + when: ChatContextKeys.inAgentSessionProjection, }, }); } override async run(accessor: ServicesAccessor): Promise { - const focusViewService = accessor.get(IFocusViewService); - await focusViewService.exitFocusView(); + const projectionService = accessor.get(IAgentSessionProjectionService); + await projectionService.exitProjection(); } } @@ -129,14 +129,33 @@ export class OpenInChatPanelAction extends Action2 { //#endregion -//#region Toggle Agents Control +//#region Toggle Agent Status -export class ToggleAgentsControl extends ToggleTitleBarConfigAction { +export class ToggleAgentStatusAction extends ToggleTitleBarConfigAction { constructor() { super( - ChatConfiguration.AgentSessionProjectionEnabled, - localize('toggle.agentsControl', 'Agents Controls'), - localize('toggle.agentsControlDescription', "Toggle visibility of the Agents Controls in title bar"), 6, + ChatConfiguration.AgentStatusEnabled, + localize('toggle.agentStatus', 'Agent Status'), + localize('toggle.agentStatusDescription', "Toggle visibility of the Agent Status in title bar"), 6, + ContextKeyExpr.and( + ChatContextKeys.enabled, + IsCompactTitleBarContext.negate(), + ChatContextKeys.supported + ) + ); + } +} + +//#endregion + +//#region Toggle Agent Session Projection + +export class ToggleAgentSessionProjectionAction extends ToggleTitleBarConfigAction { + constructor() { + super( + ChatConfiguration.AgentSessionProjectionEnabled, + localize('toggle.agentSessionProjection', 'Agent Session Projection'), + localize('toggle.agentSessionProjectionDescription', "Toggle Agent Session Projection mode for focused workspace review of agent sessions"), 7, ContextKeyExpr.and( ChatContextKeys.enabled, IsCompactTitleBarContext.negate(), diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/focusViewService.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionService.ts similarity index 67% rename from src/vs/workbench/contrib/chat/browser/agentSessions/focusViewService.ts rename to src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionService.ts index a3a82b111cb..db02aad380e 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/focusViewService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './media/focusView.css'; +import './media/agentSessionProjection.css'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; @@ -24,36 +24,37 @@ import { ChatConfiguration } from '../../common/constants.js'; import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; import { ACTION_ID_NEW_CHAT } from '../actions/chatActions.js'; import { IChatEditingService, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js'; +import { IAgentStatusService } from './agentStatusService.js'; //#region Configuration /** * Provider types that support agent session projection mode. - * Only sessions from these providers will trigger focus view. + * Only sessions from these providers will trigger projection mode. */ const AGENT_SESSION_PROJECTION_ENABLED_PROVIDERS: Set = new Set(Object.values(AgentSessionProviders)); //#endregion -//#region Focus View Service Interface +//#region Agent Session Projection Service Interface -export interface IFocusViewService { +export interface IAgentSessionProjectionService { readonly _serviceBrand: undefined; /** - * Whether focus view mode is active. + * Whether projection mode is active. */ readonly isActive: boolean; /** - * The currently active session in focus view, if any. + * The currently active session in projection mode, if any. */ readonly activeSession: IAgentSession | undefined; /** - * Event fired when focus view mode changes. + * Event fired when projection mode changes. */ - readonly onDidChangeFocusViewMode: Event; + readonly onDidChangeProjectionMode: Event; /** * Event fired when the active session changes (including when switching between sessions). @@ -61,23 +62,23 @@ export interface IFocusViewService { readonly onDidChangeActiveSession: Event; /** - * Enter focus view mode for the given session. + * Enter projection mode for the given session. */ - enterFocusView(session: IAgentSession): Promise; + enterProjection(session: IAgentSession): Promise; /** - * Exit focus view mode. + * Exit projection mode. */ - exitFocusView(): Promise; + exitProjection(): Promise; } -export const IFocusViewService = createDecorator('focusViewService'); +export const IAgentSessionProjectionService = createDecorator('agentSessionProjectionService'); //#endregion -//#region Focus View Service Implementation +//#region Agent Session Projection Service Implementation -export class FocusViewService extends Disposable implements IFocusViewService { +export class AgentSessionProjectionService extends Disposable implements IAgentSessionProjectionService { declare readonly _serviceBrand: undefined; @@ -87,16 +88,16 @@ export class FocusViewService extends Disposable implements IFocusViewService { private _activeSession: IAgentSession | undefined; get activeSession(): IAgentSession | undefined { return this._activeSession; } - private readonly _onDidChangeFocusViewMode = this._register(new Emitter()); - readonly onDidChangeFocusViewMode = this._onDidChangeFocusViewMode.event; + private readonly _onDidChangeProjectionMode = this._register(new Emitter()); + readonly onDidChangeProjectionMode = this._onDidChangeProjectionMode.event; private readonly _onDidChangeActiveSession = this._register(new Emitter()); readonly onDidChangeActiveSession = this._onDidChangeActiveSession.event; - private readonly _inFocusViewModeContextKey: IContextKey; + private readonly _inProjectionModeContextKey: IContextKey; - /** Working set saved when entering focus view (to restore on exit) */ - private _nonFocusViewWorkingSet: IEditorWorkingSet | undefined; + /** Working set saved when entering projection mode (to restore on exit) */ + private _preProjectionWorkingSet: IEditorWorkingSet | undefined; /** Working sets per session, keyed by session resource URI string */ private readonly _sessionWorkingSets = new Map(); @@ -112,12 +113,13 @@ export class FocusViewService extends Disposable implements IFocusViewService { @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @ICommandService private readonly commandService: ICommandService, @IChatEditingService private readonly chatEditingService: IChatEditingService, + @IAgentStatusService private readonly agentStatusService: IAgentStatusService, ) { super(); - this._inFocusViewModeContextKey = ChatContextKeys.inFocusViewMode.bindTo(contextKeyService); + this._inProjectionModeContextKey = ChatContextKeys.inAgentSessionProjection.bindTo(contextKeyService); - // Listen for editor close events to exit focus view when all editors are closed + // Listen for editor close events to exit projection mode when all editors are closed this._register(this.editorService.onDidCloseEditor(() => this._checkForEmptyEditors())); } @@ -126,7 +128,7 @@ export class FocusViewService extends Disposable implements IFocusViewService { } private _checkForEmptyEditors(): void { - // Only check if we're in focus view mode + // Only check if we're in projection mode if (!this._isActive) { return; } @@ -135,8 +137,8 @@ export class FocusViewService extends Disposable implements IFocusViewService { const hasVisibleEditors = this.editorService.visibleEditors.length > 0; if (!hasVisibleEditors) { - this.logService.trace('[FocusView] All editors closed, exiting focus view mode'); - this.exitFocusView(); + this.logService.trace('[AgentSessionProjection] All editors closed, exiting projection mode'); + this.exitProjection(); } } @@ -144,7 +146,7 @@ export class FocusViewService extends Disposable implements IFocusViewService { // Clear editors first await this.editorGroupsService.applyWorkingSet('empty', { preserveFocus: true }); - this.logService.trace(`[FocusView] Opening files for session '${session.label}'`, { + this.logService.trace(`[AgentSessionProjection] Opening files for session '${session.label}'`, { hasChanges: !!session.changes, isArray: Array.isArray(session.changes), changeCount: Array.isArray(session.changes) ? session.changes.length : 0 @@ -160,7 +162,7 @@ export class FocusViewService extends Disposable implements IFocusViewService { modifiedUri: change.modifiedUri })); - this.logService.trace(`[FocusView] Found ${diffResources.length} files with diffs to display`); + this.logService.trace(`[AgentSessionProjection] Found ${diffResources.length} files with diffs to display`); if (diffResources.length > 0) { // Open multi-diff editor showing all changes @@ -170,53 +172,53 @@ export class FocusViewService extends Disposable implements IFocusViewService { resources: diffResources, }); - this.logService.trace(`[FocusView] Multi-diff editor opened successfully`); + this.logService.trace(`[AgentSessionProjection] Multi-diff editor opened successfully`); // Save this as the session's working set const sessionKey = session.resource.toString(); - const newWorkingSet = this.editorGroupsService.saveWorkingSet(`focus-view-session-${sessionKey}`); + const newWorkingSet = this.editorGroupsService.saveWorkingSet(`agent-session-projection-${sessionKey}`); this._sessionWorkingSets.set(sessionKey, newWorkingSet); } else { - this.logService.trace(`[FocusView] No files with diffs to display (all changes missing originalUri)`); + this.logService.trace(`[AgentSessionProjection] No files with diffs to display (all changes missing originalUri)`); } } else { - this.logService.trace(`[FocusView] Session has no changes to display`); + this.logService.trace(`[AgentSessionProjection] Session has no changes to display`); } } - async enterFocusView(session: IAgentSession): Promise { + async enterProjection(session: IAgentSession): Promise { // Check if the feature is enabled if (!this._isEnabled()) { - this.logService.trace('[FocusView] Agent Session Projection is disabled'); + this.logService.trace('[AgentSessionProjection] Agent Session Projection is disabled'); return; } // Check if this session's provider type supports agent session projection if (!AGENT_SESSION_PROJECTION_ENABLED_PROVIDERS.has(session.providerType)) { - this.logService.trace(`[FocusView] Provider type '${session.providerType}' does not support agent session projection`); + this.logService.trace(`[AgentSessionProjection] Provider type '${session.providerType}' does not support agent session projection`); return; } // For local sessions, check if there are pending edits to show - // If there's nothing to focus, just open the chat without entering focus view mode + // If there's nothing to focus, just open the chat without entering projection mode let hasUndecidedChanges = true; if (session.providerType === AgentSessionProviders.Local) { const editingSession = this.chatEditingService.getEditingSession(session.resource); hasUndecidedChanges = editingSession?.entries.get().some(e => e.state.get() === ModifiedFileEntryState.Modified) ?? false; if (!hasUndecidedChanges) { - this.logService.trace('[FocusView] Local session has no undecided changes, opening chat without focus view'); + this.logService.trace('[AgentSessionProjection] Local session has no undecided changes, opening chat without projection mode'); } } - // Only enter focus view mode if there are changes to show + // Only enter projection mode if there are changes to show if (hasUndecidedChanges) { if (!this._isActive) { - // First time entering focus view - save the current working set as our "non-focus-view" backup - this._nonFocusViewWorkingSet = this.editorGroupsService.saveWorkingSet('focus-view-backup'); + // First time entering projection mode - save the current working set as our backup + this._preProjectionWorkingSet = this.editorGroupsService.saveWorkingSet('agent-session-projection-backup'); } else if (this._activeSession) { - // Already in focus view, switching sessions - save the current session's working set + // Already in projection mode, switching sessions - save the current session's working set const previousSessionKey = this._activeSession.resource.toString(); - const previousWorkingSet = this.editorGroupsService.saveWorkingSet(`focus-view-session-${previousSessionKey}`); + const previousWorkingSet = this.editorGroupsService.saveWorkingSet(`agent-session-projection-${previousSessionKey}`); this._sessionWorkingSets.set(previousSessionKey, previousWorkingSet); } @@ -227,10 +229,14 @@ export class FocusViewService extends Disposable implements IFocusViewService { const wasActive = this._isActive; this._isActive = true; this._activeSession = session; - this._inFocusViewModeContextKey.set(true); - this.layoutService.mainContainer.classList.add('focus-view-active'); + this._inProjectionModeContextKey.set(true); + this.layoutService.mainContainer.classList.add('agent-session-projection-active'); + + // Update the agent status to show session mode + this.agentStatusService.enterSessionMode(session.resource.toString(), session.label); + if (!wasActive) { - this._onDidChangeFocusViewMode.fire(true); + this._onDidChangeProjectionMode.fire(true); } // Always fire session change event (for title updates when switching sessions) this._onDidChangeActiveSession.fire(session); @@ -251,7 +257,7 @@ export class FocusViewService extends Disposable implements IFocusViewService { } } - async exitFocusView(): Promise { + async exitProjection(): Promise { if (!this._isActive) { return; } @@ -259,28 +265,32 @@ export class FocusViewService extends Disposable implements IFocusViewService { // Save the current session's working set before exiting if (this._activeSession) { const sessionKey = this._activeSession.resource.toString(); - const workingSet = this.editorGroupsService.saveWorkingSet(`focus-view-session-${sessionKey}`); + const workingSet = this.editorGroupsService.saveWorkingSet(`agent-session-projection-${sessionKey}`); this._sessionWorkingSets.set(sessionKey, workingSet); } - // Restore the non-focus-view working set - if (this._nonFocusViewWorkingSet) { + // Restore the pre-projection working set + if (this._preProjectionWorkingSet) { const existingWorkingSets = this.editorGroupsService.getWorkingSets(); - const exists = existingWorkingSets.some(ws => ws.id === this._nonFocusViewWorkingSet!.id); + const exists = existingWorkingSets.some(ws => ws.id === this._preProjectionWorkingSet!.id); if (exists) { - await this.editorGroupsService.applyWorkingSet(this._nonFocusViewWorkingSet); - this.editorGroupsService.deleteWorkingSet(this._nonFocusViewWorkingSet); + await this.editorGroupsService.applyWorkingSet(this._preProjectionWorkingSet); + this.editorGroupsService.deleteWorkingSet(this._preProjectionWorkingSet); } else { await this.editorGroupsService.applyWorkingSet('empty', { preserveFocus: true }); } - this._nonFocusViewWorkingSet = undefined; + this._preProjectionWorkingSet = undefined; } this._isActive = false; this._activeSession = undefined; - this._inFocusViewModeContextKey.set(false); - this.layoutService.mainContainer.classList.remove('focus-view-active'); - this._onDidChangeFocusViewMode.fire(false); + this._inProjectionModeContextKey.set(false); + this.layoutService.mainContainer.classList.remove('agent-session-projection-active'); + + // Update the agent status to exit session mode + this.agentStatusService.exitSessionMode(); + + this._onDidChangeProjectionMode.fire(false); this._onDidChangeActiveSession.fire(undefined); // Start a new chat to clear the sidebar diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts index 9c0d2e6a662..34aef21310e 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts @@ -19,9 +19,10 @@ import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase import { ISubmenuItem, MenuId, MenuRegistry, registerAction2, SubmenuItemAction } from '../../../../../platform/actions/common/actions.js'; import { ArchiveAgentSessionAction, ArchiveAgentSessionSectionAction, UnarchiveAgentSessionSectionAction, UnarchiveAgentSessionAction, OpenAgentSessionInEditorGroupAction, OpenAgentSessionInNewEditorGroupAction, OpenAgentSessionInNewWindowAction, ShowAgentSessionsSidebar, HideAgentSessionsSidebar, ToggleAgentSessionsSidebar, RefreshAgentSessionsViewerAction, FindAgentSessionInViewerAction, MarkAgentSessionUnreadAction, MarkAgentSessionReadAction, FocusAgentSessionsAction, SetAgentSessionsOrientationStackedAction, SetAgentSessionsOrientationSideBySideAction, ToggleChatViewSessionsAction, PickAgentSessionAction, ArchiveAllAgentSessionsAction, RenameAgentSessionAction, DeleteAgentSessionAction, DeleteAllLocalSessionsAction } from './agentSessionsActions.js'; import { AgentSessionsQuickAccessProvider, AGENT_SESSIONS_QUICK_ACCESS_PREFIX } from './agentSessionsQuickAccess.js'; -import { IFocusViewService, FocusViewService } from './focusViewService.js'; -import { EnterFocusViewAction, ExitFocusViewAction, OpenInChatPanelAction, ToggleAgentsControl } from './focusViewActions.js'; -import { AgentsControlViewItem } from './agentsControl.js'; +import { IAgentSessionProjectionService, AgentSessionProjectionService } from './agentSessionProjectionService.js'; +import { EnterAgentSessionProjectionAction, ExitAgentSessionProjectionAction, OpenInChatPanelAction, ToggleAgentStatusAction, ToggleAgentSessionProjectionAction } from './agentSessionProjectionActions.js'; +import { IAgentStatusService, AgentStatusService } from './agentStatusService.js'; +import { AgentStatusWidget } from './agentStatusWidget.js'; import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; @@ -54,11 +55,12 @@ registerAction2(ToggleChatViewSessionsAction); registerAction2(SetAgentSessionsOrientationStackedAction); registerAction2(SetAgentSessionsOrientationSideBySideAction); -// Focus View -registerAction2(EnterFocusViewAction); -registerAction2(ExitFocusViewAction); +// Agent Session Projection +registerAction2(EnterAgentSessionProjectionAction); +registerAction2(ExitAgentSessionProjectionAction); registerAction2(OpenInChatPanelAction); -registerAction2(ToggleAgentsControl); +registerAction2(ToggleAgentStatusAction); +registerAction2(ToggleAgentSessionProjectionAction); // --- Agent Sessions Toolbar @@ -189,14 +191,15 @@ Registry.as(QuickAccessExtensions.Quickaccess).registerQui registerWorkbenchContribution2(LocalAgentsSessionsProvider.ID, LocalAgentsSessionsProvider, WorkbenchPhase.AfterRestored); registerSingleton(IAgentSessionsService, AgentSessionsService, InstantiationType.Delayed); -registerSingleton(IFocusViewService, FocusViewService, InstantiationType.Delayed); +registerSingleton(IAgentStatusService, AgentStatusService, InstantiationType.Delayed); +registerSingleton(IAgentSessionProjectionService, AgentSessionProjectionService, InstantiationType.Delayed); -// Register Agents Control as a menu item in the command center (alongside the search box, not replacing it) +// Register Agent Status as a menu item in the command center (alongside the search box, not replacing it) MenuRegistry.appendMenuItem(MenuId.CommandCenter, { submenu: MenuId.AgentsControlMenu, title: localize('agentsControl', "Agents"), icon: Codicon.chatSparkle, - when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentSessionProjectionEnabled}`), + when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentStatusEnabled}`), order: 10002 // to the right of the chat button }); @@ -206,18 +209,18 @@ MenuRegistry.appendMenuItem(MenuId.AgentsControlMenu, { id: 'workbench.action.chat.toggle', title: localize('openChat', "Open Chat"), }, - when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentSessionProjectionEnabled}`), + when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentStatusEnabled}`), }); /** - * Provides custom rendering for the agents control in the command center. - * Uses IActionViewItemService to render a custom AgentsControlViewItem + * Provides custom rendering for the agent status in the command center. + * Uses IActionViewItemService to render a custom AgentStatusWidget * for the AgentsControlMenu submenu. - * Also adds a CSS class to the workbench when agents control is enabled. + * Also adds a CSS class to the workbench when agent status is enabled. */ -class AgentsControlRendering extends Disposable implements IWorkbenchContribution { +class AgentStatusRendering extends Disposable implements IWorkbenchContribution { - static readonly ID = 'workbench.contrib.agentsControl.rendering'; + static readonly ID = 'workbench.contrib.agentStatus.rendering'; constructor( @IActionViewItemService actionViewItemService: IActionViewItemService, @@ -230,24 +233,24 @@ class AgentsControlRendering extends Disposable implements IWorkbenchContributio if (!(action instanceof SubmenuItemAction)) { return undefined; } - return instantiationService.createInstance(AgentsControlViewItem, action, options); + return instantiationService.createInstance(AgentStatusWidget, action, options); }, undefined)); // Add/remove CSS class on workbench based on setting const updateClass = () => { - const enabled = configurationService.getValue(ChatConfiguration.AgentSessionProjectionEnabled) === true; - mainWindow.document.body.classList.toggle('agents-control-enabled', enabled); + const enabled = configurationService.getValue(ChatConfiguration.AgentStatusEnabled) === true; + mainWindow.document.body.classList.toggle('agent-status-enabled', enabled); }; updateClass(); this._register(configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(ChatConfiguration.AgentSessionProjectionEnabled)) { + if (e.affectsConfiguration(ChatConfiguration.AgentStatusEnabled)) { updateClass(); } })); } } -// Register the workbench contribution that provides custom rendering for the agents control -registerWorkbenchContribution2(AgentsControlRendering.ID, AgentsControlRendering, WorkbenchPhase.AfterRestored); +// Register the workbench contribution that provides custom rendering for the agent status +registerWorkbenchContribution2(AgentStatusRendering.ID, AgentStatusRendering, WorkbenchPhase.AfterRestored); //#endregion diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts index ba320e5e29f..7fe2e56a977 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts @@ -11,22 +11,20 @@ import { ACTIVE_GROUP, SIDE_GROUP } from '../../../../services/editor/common/edi import { IEditorOptions } from '../../../../../platform/editor/common/editor.js'; import { IChatSessionsService } from '../../common/chatSessionsService.js'; import { Schemas } from '../../../../../base/common/network.js'; -import { IFocusViewService } from './focusViewService.js'; +import { IAgentSessionProjectionService } from './agentSessionProjectionService.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ChatConfiguration } from '../../common/constants.js'; export async function openSession(accessor: ServicesAccessor, session: IAgentSession, openOptions?: { sideBySide?: boolean; editorOptions?: IEditorOptions }): Promise { const configurationService = accessor.get(IConfigurationService); - const focusViewService = accessor.get(IFocusViewService); + const projectionService = accessor.get(IAgentSessionProjectionService); session.setRead(true); // mark as read when opened - // Check if Agent Session Projection is enabled const agentSessionProjectionEnabled = configurationService.getValue(ChatConfiguration.AgentSessionProjectionEnabled) === true; - if (agentSessionProjectionEnabled) { // Enter Agent Session Projection mode for the session - await focusViewService.enterFocusView(session); + await projectionService.enterProjection(session); } else { // Fall back to opening in chat widget when Agent Session Projection is disabled await openSessionInChatWidget(accessor, session, openOptions); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusService.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusService.ts new file mode 100644 index 00000000000..a6607e468f5 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusService.ts @@ -0,0 +1,126 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; + +//#region Agent Status Mode + +export enum AgentStatusMode { + /** Default mode showing workspace name + session stats */ + Default = 'default', + /** Session mode showing session title + Esc button */ + Session = 'session', +} + +export interface IAgentStatusSessionInfo { + readonly sessionId: string; + readonly title: string; +} + +//#endregion + +//#region Agent Status Service Interface + +export interface IAgentStatusService { + readonly _serviceBrand: undefined; + + /** + * The current mode of the agent status widget. + */ + readonly mode: AgentStatusMode; + + /** + * The current session info when in session mode, undefined otherwise. + */ + readonly sessionInfo: IAgentStatusSessionInfo | undefined; + + /** + * Event fired when the control mode changes. + */ + readonly onDidChangeMode: Event; + + /** + * Event fired when the session info changes (including when entering/exiting session mode). + */ + readonly onDidChangeSessionInfo: Event; + + /** + * Enter session mode, showing the session title and escape button. + * Used by Agent Session Projection when entering a focused session view. + */ + enterSessionMode(sessionId: string, title: string): void; + + /** + * Exit session mode, returning to the default mode with workspace name and stats. + * Used by Agent Session Projection when exiting a focused session view. + */ + exitSessionMode(): void; + + /** + * Update the session title while in session mode. + */ + updateSessionTitle(title: string): void; +} + +export const IAgentStatusService = createDecorator('agentStatusService'); + +//#endregion + +//#region Agent Status Service Implementation + +export class AgentStatusService extends Disposable implements IAgentStatusService { + + declare readonly _serviceBrand: undefined; + + private _mode: AgentStatusMode = AgentStatusMode.Default; + get mode(): AgentStatusMode { return this._mode; } + + private _sessionInfo: IAgentStatusSessionInfo | undefined; + get sessionInfo(): IAgentStatusSessionInfo | undefined { return this._sessionInfo; } + + private readonly _onDidChangeMode = this._register(new Emitter()); + readonly onDidChangeMode = this._onDidChangeMode.event; + + private readonly _onDidChangeSessionInfo = this._register(new Emitter()); + readonly onDidChangeSessionInfo = this._onDidChangeSessionInfo.event; + + enterSessionMode(sessionId: string, title: string): void { + const newInfo: IAgentStatusSessionInfo = { sessionId, title }; + const modeChanged = this._mode !== AgentStatusMode.Session; + + this._mode = AgentStatusMode.Session; + this._sessionInfo = newInfo; + + if (modeChanged) { + this._onDidChangeMode.fire(this._mode); + } + this._onDidChangeSessionInfo.fire(this._sessionInfo); + } + + exitSessionMode(): void { + if (this._mode === AgentStatusMode.Default) { + return; + } + + this._mode = AgentStatusMode.Default; + this._sessionInfo = undefined; + + this._onDidChangeMode.fire(this._mode); + this._onDidChangeSessionInfo.fire(undefined); + } + + updateSessionTitle(title: string): void { + if (this._mode !== AgentStatusMode.Session || !this._sessionInfo) { + return; + } + + this._sessionInfo = { ...this._sessionInfo, title }; + this._onDidChangeSessionInfo.fire(this._sessionInfo); + } +} + +//#endregion diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts similarity index 64% rename from src/vs/workbench/contrib/chat/browser/agentSessions/agentsControl.ts rename to src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts index d62c868d658..8368ce9ff6f 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './media/focusView.css'; +import './media/agentStatusWidget.css'; import { $, addDisposableListener, EventType, reset } from '../../../../../base/browser/dom.js'; import { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; @@ -12,23 +12,31 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { localize } from '../../../../../nls.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; -import { IFocusViewService } from './focusViewService.js'; +import { AgentStatusMode, IAgentStatusService } from './agentStatusService.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; -import { ExitFocusViewAction } from './focusViewActions.js'; +import { ExitAgentSessionProjectionAction } from './agentSessionProjectionActions.js'; import { IAgentSessionsService } from './agentSessionsService.js'; import { isSessionInProgressStatus } from './agentSessionsModel.js'; import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IAction } from '../../../../../base/common/actions.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { IBrowserWorkbenchEnvironmentService } from '../../../../services/environment/browser/environmentService.js'; +import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { Verbosity } from '../../../../common/editor.js'; +import { Schemas } from '../../../../../base/common/network.js'; const OPEN_CHAT_ACTION_ID = 'workbench.action.chat.open'; const QUICK_CHAT_ACTION_ID = 'workbench.action.quickchat.toggle'; const QUICK_OPEN_ACTION_ID = 'workbench.action.quickOpenWithModes'; +const NLS_EXTENSION_HOST = localize('devExtensionWindowTitlePrefix', "[Extension Development Host]"); +const TITLE_DIRTY = '\u25cf '; + /** - * Agents Control View Item - renders agent status in the command center when agent session projection is enabled. + * Agent Status Widget - renders agent status in the command center. * * Shows two different states: * 1. Default state: Copilot icon pill (turns blue with in-progress count when agents are running) @@ -36,7 +44,7 @@ const QUICK_OPEN_ACTION_ID = 'workbench.action.quickOpenWithModes'; * * The command center search box and navigation controls remain visible alongside this control. */ -export class AgentsControlViewItem extends BaseActionViewItem { +export class AgentStatusWidget extends BaseActionViewItem { private _container: HTMLElement | undefined; private readonly _dynamicDisposables = this._register(new DisposableStore()); @@ -44,22 +52,25 @@ export class AgentsControlViewItem extends BaseActionViewItem { constructor( action: IAction, options: IBaseActionViewItemOptions | undefined, - @IFocusViewService private readonly focusViewService: IFocusViewService, + @IAgentStatusService private readonly agentStatusService: IAgentStatusService, @IHoverService private readonly hoverService: IHoverService, @ICommandService private readonly commandService: ICommandService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @ILabelService private readonly labelService: ILabelService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService, + @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, + @IEditorService private readonly editorService: IEditorService, ) { super(undefined, action, options); - // Re-render when session changes - this._register(this.focusViewService.onDidChangeActiveSession(() => { + // Re-render when control mode or session info changes + this._register(this.agentStatusService.onDidChangeMode(() => { this._render(); })); - this._register(this.focusViewService.onDidChangeFocusViewMode(() => { + this._register(this.agentStatusService.onDidChangeSessionInfo(() => { this._render(); })); @@ -67,12 +78,24 @@ export class AgentsControlViewItem extends BaseActionViewItem { this._register(this.agentSessionsService.model.onDidChangeSessions(() => { this._render(); })); + + // Re-render when active editor changes (for file name display when tabs are hidden) + this._register(this.editorService.onDidActiveEditorChange(() => { + this._render(); + })); + + // Re-render when tabs visibility changes + this._register(this.editorGroupsService.onDidChangeEditorPartOptions(({ newPartOptions, oldPartOptions }) => { + if (newPartOptions.showTabs !== oldPartOptions.showTabs) { + this._render(); + } + })); } override render(container: HTMLElement): void { super.render(container); this._container = container; - container.classList.add('agents-control-container'); + container.classList.add('agent-status-container'); // Initial render this._render(); @@ -89,7 +112,7 @@ export class AgentsControlViewItem extends BaseActionViewItem { // Clear previous disposables for dynamic content this._dynamicDisposables.clear(); - if (this.focusViewService.isActive && this.focusViewService.activeSession) { + if (this.agentStatusService.mode === AgentStatusMode.Session) { // Agent Session Projection mode - show session title + close button this._renderSessionMode(this._dynamicDisposables); } else { @@ -111,7 +134,7 @@ export class AgentsControlViewItem extends BaseActionViewItem { const hasUnreadSessions = unreadSessions.length > 0; // Create pill - add 'has-active' class when sessions are in progress - const pill = $('div.agents-control-pill.chat-input-mode'); + const pill = $('div.agent-status-pill.chat-input-mode'); if (hasActiveSessions) { pill.classList.add('has-active'); } else if (hasUnreadSessions) { @@ -123,42 +146,41 @@ export class AgentsControlViewItem extends BaseActionViewItem { this._container.appendChild(pill); // Left side indicator (status) - const leftIndicator = $('span.agents-control-status'); + const leftIndicator = $('span.agent-status-indicator'); if (hasActiveSessions) { // Running indicator when there are active sessions - const runningIcon = $('span.agents-control-status-icon'); + const runningIcon = $('span.agent-status-icon'); reset(runningIcon, renderIcon(Codicon.sessionInProgress)); leftIndicator.appendChild(runningIcon); - const runningCount = $('span.agents-control-status-text'); + const runningCount = $('span.agent-status-text'); runningCount.textContent = String(activeSessions.length); leftIndicator.appendChild(runningCount); } else if (hasUnreadSessions) { // Unread indicator when there are unread sessions - const unreadIcon = $('span.agents-control-status-icon'); + const unreadIcon = $('span.agent-status-icon'); reset(unreadIcon, renderIcon(Codicon.circleFilled)); leftIndicator.appendChild(unreadIcon); - const unreadCount = $('span.agents-control-status-text'); + const unreadCount = $('span.agent-status-text'); unreadCount.textContent = String(unreadSessions.length); leftIndicator.appendChild(unreadCount); } else { // Keyboard shortcut when idle (show open chat keybinding) const kb = this.keybindingService.lookupKeybinding(OPEN_CHAT_ACTION_ID)?.getLabel(); if (kb) { - const kbLabel = $('span.agents-control-keybinding'); + const kbLabel = $('span.agent-status-keybinding'); kbLabel.textContent = kb; leftIndicator.appendChild(kbLabel); } } pill.appendChild(leftIndicator); - // Show workspace name (centered) - const label = $('span.agents-control-label'); - const workspaceName = this.labelService.getWorkspaceLabel(this.workspaceContextService.getWorkspace()); - label.textContent = workspaceName; + // Show label (matching command center behavior - includes prefix/suffix decorations) + const label = $('span.agent-status-label'); + label.textContent = this._getLabel(); pill.appendChild(label); // Send icon (right side) - const sendIcon = $('span.agents-control-send'); + const sendIcon = $('span.agent-status-send'); reset(sendIcon, renderIcon(Codicon.send)); pill.appendChild(sendIcon); @@ -195,17 +217,17 @@ export class AgentsControlViewItem extends BaseActionViewItem { return; } - const pill = $('div.agents-control-pill.session-mode'); + const pill = $('div.agent-status-pill.session-mode'); this._container.appendChild(pill); // Session title (left/center) - const titleLabel = $('span.agents-control-title'); - const session = this.focusViewService.activeSession; - titleLabel.textContent = session?.label ?? localize('agentSessionProjection', "Agent Session Projection"); + const titleLabel = $('span.agent-status-title'); + const sessionInfo = this.agentStatusService.sessionInfo; + titleLabel.textContent = sessionInfo?.title ?? localize('agentSessionProjection', "Agent Session Projection"); pill.appendChild(titleLabel); // Escape button (right side) - serves as both keybinding hint and close button - const escButton = $('span.agents-control-esc-button'); + const escButton = $('span.agent-status-esc-button'); escButton.textContent = 'Esc'; escButton.setAttribute('role', 'button'); escButton.setAttribute('aria-label', localize('exitAgentSessionProjection', "Exit Agent Session Projection")); @@ -216,21 +238,21 @@ export class AgentsControlViewItem extends BaseActionViewItem { const hoverDelegate = getDefaultHoverDelegate('mouse'); disposables.add(this.hoverService.setupManagedHover(hoverDelegate, escButton, localize('exitAgentSessionProjectionTooltip', "Exit Agent Session Projection (Escape)"))); disposables.add(this.hoverService.setupManagedHover(hoverDelegate, pill, () => { - const activeSession = this.focusViewService.activeSession; - return activeSession ? localize('agentSessionProjectionTooltip', "Agent Session Projection: {0}", activeSession.label) : localize('agentSessionProjection', "Agent Session Projection"); + const sessionInfo = this.agentStatusService.sessionInfo; + return sessionInfo ? localize('agentSessionProjectionTooltip', "Agent Session Projection: {0}", sessionInfo.title) : localize('agentSessionProjection', "Agent Session Projection"); })); // Esc button click handler disposables.add(addDisposableListener(escButton, EventType.MOUSE_DOWN, (e) => { e.preventDefault(); e.stopPropagation(); - this.commandService.executeCommand(ExitFocusViewAction.ID); + this.commandService.executeCommand(ExitAgentSessionProjectionAction.ID); })); disposables.add(addDisposableListener(escButton, EventType.CLICK, (e) => { e.preventDefault(); e.stopPropagation(); - this.commandService.executeCommand(ExitFocusViewAction.ID); + this.commandService.executeCommand(ExitAgentSessionProjectionAction.ID); })); // Esc button keyboard handler @@ -238,7 +260,7 @@ export class AgentsControlViewItem extends BaseActionViewItem { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); - this.commandService.executeCommand(ExitFocusViewAction.ID); + this.commandService.executeCommand(ExitAgentSessionProjectionAction.ID); } })); @@ -251,7 +273,7 @@ export class AgentsControlViewItem extends BaseActionViewItem { return; } - const searchButton = $('span.agents-control-search'); + const searchButton = $('span.agent-status-search'); reset(searchButton, renderIcon(Codicon.search)); searchButton.setAttribute('role', 'button'); searchButton.setAttribute('aria-label', localize('openQuickOpen', "Open Quick Open")); @@ -282,4 +304,58 @@ export class AgentsControlViewItem extends BaseActionViewItem { } })); } + + /** + * Compute the label to display, matching the command center behavior. + * Includes prefix and suffix decorations (remote host, extension dev host, etc.) + */ + private _getLabel(): string { + const { prefix, suffix } = this._getTitleDecorations(); + + // Base label: workspace name or file name (when tabs are hidden) + let label = this.labelService.getWorkspaceLabel(this.workspaceContextService.getWorkspace()); + if (this.editorGroupsService.partOptions.showTabs === 'none') { + const activeEditor = this.editorService.activeEditor; + if (activeEditor) { + const dirty = activeEditor.isDirty() && !activeEditor.isSaving() ? TITLE_DIRTY : ''; + label = `${dirty}${activeEditor.getTitle(Verbosity.SHORT)}`; + } + } + + if (!label) { + label = localize('agentStatusWidget.askAnything', "Ask anything..."); + } + + // Apply prefix and suffix decorations + if (prefix) { + label = localize('label1', "{0} {1}", prefix, label); + } + if (suffix) { + label = localize('label2', "{0} {1}", label, suffix); + } + + return label.replaceAll(/\r\n|\r|\n/g, '\u23CE'); + } + + /** + * Get prefix and suffix decorations for the title (matching WindowTitle behavior) + */ + private _getTitleDecorations(): { prefix: string | undefined; suffix: string | undefined } { + let prefix: string | undefined; + const suffix: string | undefined = undefined; + + // Add remote host label if connected to a remote + if (this.environmentService.remoteAuthority) { + prefix = this.labelService.getHostLabel(Schemas.vscodeRemote, this.environmentService.remoteAuthority); + } + + // Add extension development host prefix + if (this.environmentService.isExtensionDevelopment) { + prefix = !prefix + ? NLS_EXTENSION_HOST + : `${NLS_EXTENSION_HOST} - ${prefix}`; + } + + return { prefix, suffix }; + } } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionProjection.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionProjection.css new file mode 100644 index 00000000000..d6d3b3c3694 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionProjection.css @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* ======================================== +Agent Session Projection Mode - Tab and Editor styling +======================================== */ + +/* Style all tabs with the same background as the agent status */ +.monaco-workbench.agent-session-projection-active .part.editor > .content .editor-group-container > .title .tabs-container > .tab { + background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent) !important; +} + +/* Active tab gets slightly stronger tint */ +.monaco-workbench.agent-session-projection-active .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { + background-color: color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent) !important; +} + +.hc-black .monaco-workbench.agent-session-projection-active .part.editor > .content .editor-group-container > .title .tabs-container > .tab, +.hc-light .monaco-workbench.agent-session-projection-active .part.editor > .content .editor-group-container > .title .tabs-container > .tab { + background-color: transparent !important; + border: 1px solid var(--vscode-contrastBorder); +} + +/* Border around entire editor area using pseudo-element overlay */ +.monaco-workbench.agent-session-projection-active .part.editor { + position: relative; +} + +@keyframes agent-session-projection-glow-pulse { + 0%, 100% { + box-shadow: + 0 0 8px 2px color-mix(in srgb, var(--vscode-progressBar-background) 50%, transparent), + 0 0 20px 4px color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent), + inset 0 0 15px 2px color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent); + } + 50% { + box-shadow: + 0 0 15px 4px color-mix(in srgb, var(--vscode-progressBar-background) 70%, transparent), + 0 0 35px 8px color-mix(in srgb, var(--vscode-progressBar-background) 35%, transparent), + inset 0 0 25px 4px color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent); + } +} + +.monaco-workbench.agent-session-projection-active .part.editor::after { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + z-index: 1000; + border: 2px solid var(--vscode-progressBar-background); + border-radius: 4px; + animation: agent-session-projection-glow-pulse 2s ease-in-out infinite; +} + +.hc-black .monaco-workbench.agent-session-projection-active .part.editor::after, +.hc-light .monaco-workbench.agent-session-projection-active .part.editor::after { + border-color: var(--vscode-contrastBorder); + animation: none; + box-shadow: none; +} diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/focusView.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css similarity index 54% rename from src/vs/workbench/contrib/chat/browser/agentSessions/media/focusView.css rename to src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css index 9f4c21d625c..f62f059fa8c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/focusView.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css @@ -4,74 +4,16 @@ *--------------------------------------------------------------------------------------------*/ /* ======================================== -Focus View Mode - Tab styling to match agents control +Agent Status Widget - Titlebar control ======================================== */ -/* Style all tabs with the same background as the agents control */ -.monaco-workbench.focus-view-active .part.editor > .content .editor-group-container > .title .tabs-container > .tab { - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent) !important; -} - -/* Active tab gets slightly stronger tint */ -.monaco-workbench.focus-view-active .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent) !important; -} - -.hc-black .monaco-workbench.focus-view-active .part.editor > .content .editor-group-container > .title .tabs-container > .tab, -.hc-light .monaco-workbench.focus-view-active .part.editor > .content .editor-group-container > .title .tabs-container > .tab { - background-color: transparent !important; - border: 1px solid var(--vscode-contrastBorder); -} - -/* Border around entire editor area using pseudo-element overlay */ -.monaco-workbench.focus-view-active .part.editor { - position: relative; -} - -@keyframes focus-view-glow-pulse { - 0%, 100% { - box-shadow: - 0 0 8px 2px color-mix(in srgb, var(--vscode-progressBar-background) 50%, transparent), - 0 0 20px 4px color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent), - inset 0 0 15px 2px color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent); - } - 50% { - box-shadow: - 0 0 15px 4px color-mix(in srgb, var(--vscode-progressBar-background) 70%, transparent), - 0 0 35px 8px color-mix(in srgb, var(--vscode-progressBar-background) 35%, transparent), - inset 0 0 25px 4px color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent); - } -} - -.monaco-workbench.focus-view-active .part.editor::after { - content: ''; - position: absolute; - inset: 0; - pointer-events: none; - z-index: 1000; - border: 2px solid var(--vscode-progressBar-background); - border-radius: 4px; - animation: focus-view-glow-pulse 2s ease-in-out infinite; -} - -.hc-black .monaco-workbench.focus-view-active .part.editor::after, -.hc-light .monaco-workbench.focus-view-active .part.editor::after { - border-color: var(--vscode-contrastBorder); - animation: none; - box-shadow: none; -} - -/* ======================================== -Agents Control - Titlebar control -======================================== */ - -/* Hide command center search box when agents control enabled */ -.agents-control-enabled .command-center .action-item.command-center-center { +/* Hide command center search box when agent status enabled */ +.agent-status-enabled .command-center .action-item.command-center-center { display: none !important; } -/* Give agents control same width as search box */ -.agents-control-enabled .command-center .action-item.agents-control-container { +/* Give agent status same width as search box */ +.agent-status-enabled .command-center .action-item.agent-status-container { width: 38vw; max-width: 600px; display: flex; @@ -81,7 +23,7 @@ Agents Control - Titlebar control justify-content: center; } -.agents-control-container { +.agent-status-container { display: flex; flex-direction: row; flex-wrap: nowrap; @@ -92,7 +34,7 @@ Agents Control - Titlebar control } /* Pill - shared styles */ -.agents-control-pill { +.agent-status-pill { display: flex; align-items: center; gap: 6px; @@ -106,57 +48,57 @@ Agents Control - Titlebar control } /* Chat input mode (default state) */ -.agents-control-pill.chat-input-mode { +.agent-status-pill.chat-input-mode { background-color: var(--vscode-commandCenter-background, rgba(0, 0, 0, 0.05)); border: 1px solid var(--vscode-commandCenter-border, transparent); cursor: pointer; } -.agents-control-pill.chat-input-mode:hover { +.agent-status-pill.chat-input-mode:hover { background-color: var(--vscode-commandCenter-activeBackground, rgba(0, 0, 0, 0.1)); border-color: var(--vscode-commandCenter-activeBorder, rgba(0, 0, 0, 0.2)); } -.agents-control-pill.chat-input-mode:focus { +.agent-status-pill.chat-input-mode:focus { outline: 1px solid var(--vscode-focusBorder); outline-offset: -1px; } /* Active state - has running sessions */ -.agents-control-pill.chat-input-mode.has-active { +.agent-status-pill.chat-input-mode.has-active { background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent); border: 1px solid color-mix(in srgb, var(--vscode-progressBar-background) 50%, transparent); } -.agents-control-pill.chat-input-mode.has-active:hover { +.agent-status-pill.chat-input-mode.has-active:hover { background-color: color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent); border-color: color-mix(in srgb, var(--vscode-progressBar-background) 70%, transparent); } -.agents-control-pill.chat-input-mode.has-active .agents-control-label { +.agent-status-pill.chat-input-mode.has-active .agent-status-label { color: var(--vscode-progressBar-background); opacity: 1; } /* Unread state - has unread sessions (no background change, just indicator) */ -.agents-control-pill.chat-input-mode.has-unread .agents-control-status-icon { +.agent-status-pill.chat-input-mode.has-unread .agent-status-icon { font-size: 8px; } /* Session mode (viewing a session) */ -.agents-control-pill.session-mode { +.agent-status-pill.session-mode { background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent); border: 1px solid color-mix(in srgb, var(--vscode-progressBar-background) 50%, transparent); padding: 0 12px; } -.agents-control-pill.session-mode:hover { +.agent-status-pill.session-mode:hover { background-color: color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent); border-color: color-mix(in srgb, var(--vscode-progressBar-background) 70%, transparent); } /* Label (workspace name, centered) */ -.agents-control-label { +.agent-status-label { flex: 1; text-align: center; color: var(--vscode-foreground); @@ -167,47 +109,47 @@ Agents Control - Titlebar control } /* Left side status indicator */ -.agents-control-status { +.agent-status-indicator { display: flex; align-items: center; gap: 4px; color: var(--vscode-descriptionForeground); } -.agents-control-pill.has-active .agents-control-status { +.agent-status-pill.has-active .agent-status-indicator { color: var(--vscode-progressBar-background); } -.agents-control-status-icon { +.agent-status-icon { display: flex; align-items: center; } -.agents-control-status-text { +.agent-status-text { font-size: 11px; font-weight: 500; } -.agents-control-keybinding { +.agent-status-keybinding { font-size: 11px; opacity: 0.7; } /* Send icon (right side) */ -.agents-control-send { +.agent-status-send { display: flex; align-items: center; color: var(--vscode-foreground); opacity: 0.7; } -.agents-control-pill.has-active .agents-control-send { +.agent-status-pill.has-active .agent-status-send { color: var(--vscode-textLink-foreground); opacity: 1; } /* Session title */ -.agents-control-title { +.agent-status-title { flex: 1; font-weight: 500; color: var(--vscode-foreground); @@ -217,7 +159,7 @@ Agents Control - Titlebar control } /* Escape button (right side in session mode) - serves as keybinding hint and close button */ -.agents-control-esc-button { +.agent-status-esc-button { display: inline-flex; align-items: center; align-self: center; @@ -238,18 +180,18 @@ Agents Control - Titlebar control -webkit-app-region: no-drag; } -.agents-control-esc-button:hover { +.agent-status-esc-button:hover { color: var(--vscode-foreground); border-color: color-mix(in srgb, var(--vscode-foreground) 60%, transparent); } -.agents-control-esc-button:focus { +.agent-status-esc-button:focus { outline: 1px solid var(--vscode-focusBorder); outline-offset: 1px; } /* Search button (right of pill) */ -.agents-control-search { +.agent-status-search { display: flex; align-items: center; justify-content: center; @@ -262,12 +204,12 @@ Agents Control - Titlebar control -webkit-app-region: no-drag; } -.agents-control-search:hover { +.agent-status-search:hover { opacity: 1; background-color: var(--vscode-commandCenter-activeBackground, rgba(0, 0, 0, 0.1)); } -.agents-control-search:focus { +.agent-status-search:focus { outline: 1px solid var(--vscode-focusBorder); outline-offset: -1px; } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 00c48c80a54..968eacb5dea 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -193,6 +193,12 @@ configurationRegistry.registerConfiguration({ markdownDescription: nls.localize('chat.commandCenter.enabled', "Controls whether the command center shows a menu for actions to control chat (requires {0}).", '`#window.commandCenter#`'), default: true }, + [ChatConfiguration.AgentStatusEnabled]: { + type: 'boolean', + markdownDescription: nls.localize('chat.agentsControl.enabled', "Controls whether the Agent Status is shown in the title bar command center, replacing the search box with quick access to chat sessions."), + default: true, + tags: ['experimental'] + }, [ChatConfiguration.AgentSessionProjectionEnabled]: { type: 'boolean', markdownDescription: nls.localize('chat.agentSessionProjection.enabled', "Controls whether Agent Session Projection mode is enabled for reviewing agent sessions in a focused workspace."), diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index d9033dd9f6f..8a651031c41 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -106,8 +106,7 @@ export namespace ChatContextKeys { export const isKatexMathElement = new RawContextKey('chatIsKatexMathElement', false, { type: 'boolean', description: localize('chatIsKatexMathElement', "True when focusing a KaTeX math element.") }); - // Focus View mode - export const inFocusViewMode = new RawContextKey('chatInFocusViewMode', false, { type: 'boolean', description: localize('chatInFocusViewMode', "True when the workbench is in focus view mode for an agent session.") }); + export const inAgentSessionProjection = new RawContextKey('chatInAgentSessionProjection', false, { type: 'boolean', description: localize('chatInAgentSessionProjection', "True when the workbench is in agent session projection mode for reviewing an agent session.") }); } export namespace ChatContextKeyExprs { diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index ee1e23c2dff..0487a189c3d 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -10,6 +10,7 @@ import { RawContextKey } from '../../../../platform/contextkey/common/contextkey export enum ChatConfiguration { AgentEnabled = 'chat.agent.enabled', + AgentStatusEnabled = 'chat.agentsControl.enabled', AgentSessionProjectionEnabled = 'chat.agentSessionProjection.enabled', Edits2Enabled = 'chat.edits2.enabled', ExtensionToolsEnabled = 'chat.extensionTools.enabled', From 1656700d1db06e50d22e733b890f2d29c23f70d9 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:03:48 -0800 Subject: [PATCH 030/387] streaming latest session to agentSessionWidget (#288176) * stream progress * tidy --- .../agentSessions/agentStatusWidget.ts | 109 ++++++++++++++---- .../agentSessions/media/agentStatusWidget.css | 18 +++ 2 files changed, 103 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts index 8368ce9ff6f..9bdcdd03e02 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts @@ -17,7 +17,7 @@ import { ICommandService } from '../../../../../platform/commands/common/command import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { ExitAgentSessionProjectionAction } from './agentSessionProjectionActions.js'; import { IAgentSessionsService } from './agentSessionsService.js'; -import { isSessionInProgressStatus } from './agentSessionsModel.js'; +import { IAgentSession, isSessionInProgressStatus } from './agentSessionsModel.js'; import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IAction } from '../../../../../base/common/actions.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; @@ -27,10 +27,13 @@ import { IEditorGroupsService } from '../../../../services/editor/common/editorG import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { Verbosity } from '../../../../common/editor.js'; import { Schemas } from '../../../../../base/common/network.js'; +import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; +import { openSession } from './agentSessionsOpener.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -const OPEN_CHAT_ACTION_ID = 'workbench.action.chat.open'; -const QUICK_CHAT_ACTION_ID = 'workbench.action.quickchat.toggle'; -const QUICK_OPEN_ACTION_ID = 'workbench.action.quickOpenWithModes'; +// Action triggered when clicking the main pill - change this to modify the primary action +const ACTION_ID = 'workbench.action.quickchat.toggle'; +const SEARCH_BUTTON_ACITON_ID = 'workbench.action.quickOpenWithModes'; const NLS_EXTENSION_HOST = localize('devExtensionWindowTitlePrefix', "[Extension Development Host]"); const TITLE_DIRTY = '\u25cf '; @@ -49,9 +52,13 @@ export class AgentStatusWidget extends BaseActionViewItem { private _container: HTMLElement | undefined; private readonly _dynamicDisposables = this._register(new DisposableStore()); + /** The currently displayed in-progress session (if any) - clicking pill opens this */ + private _displayedSession: IAgentSession | undefined; + constructor( action: IAction, options: IBaseActionViewItemOptions | undefined, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IAgentStatusService private readonly agentStatusService: IAgentStatusService, @IHoverService private readonly hoverService: IHoverService, @ICommandService private readonly commandService: ICommandService, @@ -164,8 +171,8 @@ export class AgentStatusWidget extends BaseActionViewItem { unreadCount.textContent = String(unreadSessions.length); leftIndicator.appendChild(unreadCount); } else { - // Keyboard shortcut when idle (show open chat keybinding) - const kb = this.keybindingService.lookupKeybinding(OPEN_CHAT_ACTION_ID)?.getLabel(); + // Keyboard shortcut when idle (show quick chat keybinding - matches click action) + const kb = this.keybindingService.lookupKeybinding(ACTION_ID)?.getLabel(); if (kb) { const kbLabel = $('span.agent-status-keybinding'); kbLabel.textContent = kb; @@ -174,29 +181,43 @@ export class AgentStatusWidget extends BaseActionViewItem { } pill.appendChild(leftIndicator); - // Show label (matching command center behavior - includes prefix/suffix decorations) + // Show label - either progress from most recent active session, or workspace name const label = $('span.agent-status-label'); - label.textContent = this._getLabel(); + const { session: activeSession, progress: progressText } = this._getMostRecentActiveSession(activeSessions); + this._displayedSession = activeSession; + if (progressText) { + // Show progress with fade-in animation + label.classList.add('has-progress'); + label.textContent = progressText; + } else { + label.textContent = this._getLabel(); + } pill.appendChild(label); - // Send icon (right side) - const sendIcon = $('span.agent-status-send'); - reset(sendIcon, renderIcon(Codicon.send)); - pill.appendChild(sendIcon); + // Send icon (right side) - only show when not streaming progress + if (!progressText) { + const sendIcon = $('span.agent-status-send'); + reset(sendIcon, renderIcon(Codicon.send)); + pill.appendChild(sendIcon); + } - // Setup hover with keyboard shortcut + // Setup hover - show session name when displaying progress, otherwise show keybinding const hoverDelegate = getDefaultHoverDelegate('mouse'); - const kbForTooltip = this.keybindingService.lookupKeybinding(QUICK_CHAT_ACTION_ID)?.getLabel(); - const tooltip = kbForTooltip - ? localize('askTooltip', "Open Quick Chat ({0})", kbForTooltip) - : localize('askTooltip2', "Open Quick Chat"); - disposables.add(this.hoverService.setupManagedHover(hoverDelegate, pill, tooltip)); + disposables.add(this.hoverService.setupManagedHover(hoverDelegate, pill, () => { + if (this._displayedSession) { + return localize('openSessionTooltip', "Open session: {0}", this._displayedSession.label); + } + const kbForTooltip = this.keybindingService.lookupKeybinding(ACTION_ID)?.getLabel(); + return kbForTooltip + ? localize('askTooltip', "Open Quick Chat ({0})", kbForTooltip) + : localize('askTooltip2', "Open Quick Chat"); + })); - // Click handler - open quick chat + // Click handler - open displayed session if showing progress, otherwise open quick chat disposables.add(addDisposableListener(pill, EventType.CLICK, (e) => { e.preventDefault(); e.stopPropagation(); - this.commandService.executeCommand(QUICK_CHAT_ACTION_ID); + this._handlePillClick(); })); // Keyboard handler @@ -204,7 +225,7 @@ export class AgentStatusWidget extends BaseActionViewItem { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); - this.commandService.executeCommand(QUICK_CHAT_ACTION_ID); + this._handlePillClick(); } })); @@ -282,7 +303,7 @@ export class AgentStatusWidget extends BaseActionViewItem { // Setup hover const hoverDelegate = getDefaultHoverDelegate('mouse'); - const searchKb = this.keybindingService.lookupKeybinding(QUICK_OPEN_ACTION_ID)?.getLabel(); + const searchKb = this.keybindingService.lookupKeybinding(SEARCH_BUTTON_ACITON_ID)?.getLabel(); const searchTooltip = searchKb ? localize('openQuickOpenTooltip', "Go to File ({0})", searchKb) : localize('openQuickOpenTooltip2', "Go to File"); @@ -292,7 +313,7 @@ export class AgentStatusWidget extends BaseActionViewItem { disposables.add(addDisposableListener(searchButton, EventType.CLICK, (e) => { e.preventDefault(); e.stopPropagation(); - this.commandService.executeCommand(QUICK_OPEN_ACTION_ID); + this.commandService.executeCommand(SEARCH_BUTTON_ACITON_ID); })); // Keyboard handler @@ -300,11 +321,51 @@ export class AgentStatusWidget extends BaseActionViewItem { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); - this.commandService.executeCommand(QUICK_OPEN_ACTION_ID); + this.commandService.executeCommand(SEARCH_BUTTON_ACITON_ID); } })); } + /** + * Handle pill click - opens the displayed session if showing progress, otherwise executes default action + */ + private _handlePillClick(): void { + if (this._displayedSession) { + this.instantiationService.invokeFunction(openSession, this._displayedSession); + } else { + this.commandService.executeCommand(ACTION_ID); + } + } + + /** + * Get the most recently interacted active session and its progress text. + * Returns undefined session if no active sessions. + */ + private _getMostRecentActiveSession(activeSessions: IAgentSession[]): { session: IAgentSession | undefined; progress: string | undefined } { + if (activeSessions.length === 0) { + return { session: undefined, progress: undefined }; + } + + // Sort by most recently started request + const sorted = [...activeSessions].sort((a, b) => { + const timeA = a.timing.lastRequestStarted ?? a.timing.created; + const timeB = b.timing.lastRequestStarted ?? b.timing.created; + return timeB - timeA; + }); + + const mostRecent = sorted[0]; + if (!mostRecent.description) { + return { session: mostRecent, progress: undefined }; + } + + // Convert markdown to plain text if needed + const progress = typeof mostRecent.description === 'string' + ? mostRecent.description + : renderAsPlaintext(mostRecent.description); + + return { session: mostRecent, progress }; + } + /** * Compute the label to display, matching the command center behavior. * Includes prefix and suffix decorations (remote host, extension dev host, etc.) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css index f62f059fa8c..541e3bf02a5 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css @@ -108,6 +108,24 @@ Agent Status Widget - Titlebar control text-overflow: ellipsis; } +/* Progress label - fade in animation when showing session progress */ +.agent-status-label.has-progress { + animation: agentStatusFadeIn 0.3s ease-out; + color: var(--vscode-progressBar-background); + opacity: 1; +} + +@keyframes agentStatusFadeIn { + from { + opacity: 0; + transform: translateY(2px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + /* Left side status indicator */ .agent-status-indicator { display: flex; From eec05c584c0258260f036cdd17fe60faca240107 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Thu, 15 Jan 2026 14:13:08 -0800 Subject: [PATCH 031/387] enhance newline handling of diff export in chatRepoIno --- .../contrib/chat/browser/chatRepoInfo.ts | 84 +++++++++++++++++-- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts b/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts index e94a89c79a9..695e5151d6b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts +++ b/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts @@ -103,6 +103,10 @@ function determineChangeType(resource: ISCMResource, groupId: string): 'added' | /** * Generates a unified diff string compatible with `git apply`. + * + * Note: This implementation has a known limitation - if the only change between + * files is the presence/absence of a trailing newline (content otherwise identical), + * no diff will be generated because VS Code's diff algorithm treats the lines as equal. */ async function generateUnifiedDiff( fileService: IFileService, @@ -138,12 +142,17 @@ async function generateUnifiedDiff( const originalLines = originalContent.split('\n'); const modifiedLines = modifiedContent.split('\n'); + // Track whether files end with newline for git apply compatibility + // split('\n') on "line1\nline2\n" gives ["line1", "line2", ""] + // split('\n') on "line1\nline2" gives ["line1", "line2"] + const originalEndsWithNewline = originalContent.length > 0 && originalContent.endsWith('\n'); + const modifiedEndsWithNewline = modifiedContent.length > 0 && modifiedContent.endsWith('\n'); + // Remove trailing empty element if file ends with newline - // (split('\n') on "line1\nline2\n" gives ["line1", "line2", ""]) - if (originalLines.length > 0 && originalLines[originalLines.length - 1] === '') { + if (originalEndsWithNewline && originalLines.length > 0 && originalLines[originalLines.length - 1] === '') { originalLines.pop(); } - if (modifiedLines.length > 0 && modifiedLines[modifiedLines.length - 1] === '') { + if (modifiedEndsWithNewline && modifiedLines.length > 0 && modifiedLines[modifiedLines.length - 1] === '') { modifiedLines.pop(); } @@ -160,6 +169,9 @@ async function generateUnifiedDiff( for (const line of modifiedLines) { diffLines.push(`+${line}`); } + if (!modifiedEndsWithNewline) { + diffLines.push('\\ No newline at end of file'); + } } } else if (changeType === 'deleted') { if (originalLines.length > 0) { @@ -167,9 +179,12 @@ async function generateUnifiedDiff( for (const line of originalLines) { diffLines.push(`-${line}`); } + if (!originalEndsWithNewline) { + diffLines.push('\\ No newline at end of file'); + } } } else { - const hunks = computeDiffHunks(originalLines, modifiedLines); + const hunks = computeDiffHunks(originalLines, modifiedLines, originalEndsWithNewline, modifiedEndsWithNewline); for (const hunk of hunks) { diffLines.push(hunk); } @@ -185,7 +200,12 @@ async function generateUnifiedDiff( * Computes unified diff hunks using VS Code's diff algorithm. * Merges adjacent/overlapping hunks to produce a valid patch. */ -function computeDiffHunks(originalLines: string[], modifiedLines: string[]): string[] { +function computeDiffHunks( + originalLines: string[], + modifiedLines: string[], + originalEndsWithNewline: boolean, + modifiedEndsWithNewline: boolean +): string[] { const contextSize = 3; const result: string[] = []; @@ -237,6 +257,10 @@ function computeDiffHunks(originalLines: string[], modifiedLines: string[]): str const hunkModStart = Math.max(1, firstChange.modified.startLineNumber - contextSize); const hunkLines: string[] = []; + // Track which line in hunkLines corresponds to the last line of each file + let lastOriginalLineIndex = -1; + let lastModifiedLineIndex = -1; + let origLineNum = hunkOrigStart; let origCount = 0; let modCount = 0; @@ -250,7 +274,16 @@ function computeDiffHunks(originalLines: string[], modifiedLines: string[]): str // Emit context lines before this change while (origLineNum < origStart) { + const idx = hunkLines.length; hunkLines.push(` ${originalLines[origLineNum - 1]}`); + // Context lines are in both files + if (origLineNum === originalLines.length) { + lastOriginalLineIndex = idx; + } + const modLineNum = hunkModStart + modCount; + if (modLineNum === modifiedLines.length) { + lastModifiedLineIndex = idx; + } origLineNum++; origCount++; modCount++; @@ -258,28 +291,67 @@ function computeDiffHunks(originalLines: string[], modifiedLines: string[]): str // Emit deleted lines for (let i = origStart; i < origEnd; i++) { + const idx = hunkLines.length; hunkLines.push(`-${originalLines[i - 1]}`); + if (i === originalLines.length) { + lastOriginalLineIndex = idx; + } origLineNum++; origCount++; } // Emit added lines for (let i = modStart; i < modEnd; i++) { + const idx = hunkLines.length; hunkLines.push(`+${modifiedLines[i - 1]}`); + if (i === modifiedLines.length) { + lastModifiedLineIndex = idx; + } modCount++; } } // Emit trailing context lines while (origLineNum <= hunkOrigEnd) { + const idx = hunkLines.length; hunkLines.push(` ${originalLines[origLineNum - 1]}`); + // Context lines are in both files + if (origLineNum === originalLines.length) { + lastOriginalLineIndex = idx; + } + const modLineNum = hunkModStart + modCount; + if (modLineNum === modifiedLines.length) { + lastModifiedLineIndex = idx; + } origLineNum++; origCount++; modCount++; } result.push(`@@ -${hunkOrigStart},${origCount} +${hunkModStart},${modCount} @@`); - result.push(...hunkLines); + + // Add "No newline at end of file" markers for git apply compatibility + // The marker must appear immediately after the line that lacks a newline + for (let i = 0; i < hunkLines.length; i++) { + result.push(hunkLines[i]); + + const isLastOriginal = i === lastOriginalLineIndex; + const isLastModified = i === lastModifiedLineIndex; + + if (isLastOriginal && isLastModified) { + // Context line is the last line of both files + // If either lacks newline, we need a marker (but only one) + if (!originalEndsWithNewline || !modifiedEndsWithNewline) { + result.push('\\ No newline at end of file'); + } + } else if (isLastOriginal && !originalEndsWithNewline) { + // Deletion or context line that's only the last of original + result.push('\\ No newline at end of file'); + } else if (isLastModified && !modifiedEndsWithNewline) { + // Addition or context line that's only the last of modified + result.push('\\ No newline at end of file'); + } + } } return result; From ad012358c47072dc7ab24d16fe481aa8c40e7a4d Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 15 Jan 2026 14:23:42 -0800 Subject: [PATCH 032/387] WIP --- .../browser/actions/chatExecuteActions.ts | 8 +- src/vs/workbench/contrib/chat/browser/chat.ts | 29 ++ .../contrib/chat/browser/widget/chatWidget.ts | 3 +- .../browser/widget/input/chatInputPart.ts | 198 ++++++-- .../input/sessionTargetPickerActionItem.ts | 13 +- .../chat/common/actions/chatContextKeys.ts | 2 + .../agentSessionsWelcome.contribution.ts | 122 +++++ .../browser/agentSessionsWelcome.ts | 438 ++++++++++++++++++ .../browser/agentSessionsWelcomeInput.ts | 72 +++ .../browser/gettingStarted.contribution.ts | 3 +- .../browser/media/agentSessionsWelcome.css | 360 ++++++++++++++ src/vs/workbench/workbench.common.main.ts | 1 + 12 files changed, 1205 insertions(+), 44 deletions(-) create mode 100644 src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcome.contribution.ts create mode 100644 src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcome.ts create mode 100644 src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcomeInput.ts create mode 100644 src/vs/workbench/contrib/welcomeGettingStarted/browser/media/agentSessionsWelcome.css diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 884cc180f4c..0d297efe2ff 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -479,9 +479,13 @@ export class ChatSessionPrimaryPickerAction extends Action2 { order: 4, group: 'navigation', when: - ContextKeyExpr.and( + ContextKeyExpr.or( + ChatContextKeys.chatSessionHasModels, ChatContextKeys.lockedToCodingAgent, - ChatContextKeys.chatSessionHasModels + ContextKeyExpr.and( + ChatContextKeys.inAgentSessionsWelcome, + ChatContextKeys.chatSessionType.notEqualsTo('local') + ) ) } }); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 2edca9a2966..d4ac8d70efa 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -27,6 +27,27 @@ import { IChatEditorOptions } from './widgetHosts/editor/chatEditor.js'; import { ChatInputPart } from './widget/input/chatInputPart.js'; import { ChatWidget, IChatWidgetContrib } from './widget/chatWidget.js'; import { ICodeBlockActionContext } from './widget/chatContentParts/codeBlockPart.js'; +import { AgentSessionProviders } from './agentSessions/agentSessions.js'; + +/** + * Delegate interface for the session target picker. + * Allows consumers to get and optionally set the active session provider. + */ +export interface ISessionTypePickerDelegate { + getActiveSessionProvider(): AgentSessionProviders | undefined; + /** + * Optional setter for the active session provider. + * When provided, the picker will call this instead of executing the openNewChatSessionInPlace command. + * This allows the welcome view to maintain independent state from the main chat panel. + */ + setActiveSessionProvider?(provider: AgentSessionProviders): void; + /** + * Optional event that fires when the active session provider changes. + * When provided, listeners (like chatInputPart) can react to session type changes + * and update pickers accordingly. + */ + onDidChangeActiveSessionProvider?: Event; +} export const IChatWidgetService = createDecorator('chatWidgetService'); @@ -183,6 +204,13 @@ export interface IChatWidgetViewOptions { supportsChangingModes?: boolean; dndContainer?: HTMLElement; defaultMode?: IChatMode; + /** + * Optional delegate for the session target picker. + * When provided, allows the widget to maintain independent state for the selected session type. + * This is useful for contexts like the welcome view where target selection should not + * immediately open a new session. + */ + sessionTypePickerDelegate?: ISessionTypePickerDelegate; } export interface IChatViewViewContext { @@ -276,6 +304,7 @@ export interface IChatWidget { clear(): Promise; getViewState(): IChatModelInputState | undefined; lockToCodingAgent(name: string, displayName: string, agentId?: string): void; + unlockFromCodingAgent(): void; handleDelegationExitIfNeeded(sourceAgent: Pick | undefined, targetAgent: IChatAgentData | undefined): Promise; delegateScrollFromMouseWheelEvent(event: IMouseWheelEvent): void; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 42f602ace9f..2298702f2b7 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -1851,7 +1851,8 @@ export class ChatWidget extends Disposable implements IChatWidget { supportsChangingModes: this.viewOptions.supportsChangingModes, dndContainer: this.viewOptions.dndContainer, widgetViewKindTag: this.getWidgetViewKindTag(), - defaultMode: this.viewOptions.defaultMode + defaultMode: this.viewOptions.defaultMode, + sessionTypePickerDelegate: this.viewOptions.sessionTypePickerDelegate }; if (this.viewModel?.editing) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index a98c158ad8c..f6d695bb280 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -96,7 +96,7 @@ import { ActionLocation, ChatContinueInSessionActionItem, ContinueChatInSessionA import { ChatSessionPrimaryPickerAction, ChatSubmitAction, IChatExecuteActionContext, OpenModelPickerAction, OpenModePickerAction, OpenSessionTargetPickerAction } from '../../actions/chatExecuteActions.js'; import { IAgentSessionsService } from '../../agentSessions/agentSessionsService.js'; import { ImplicitContextAttachmentWidget } from '../../attachments/implicitContextAttachment.js'; -import { IChatWidget, isIChatResourceViewContext } from '../../chat.js'; +import { IChatWidget, ISessionTypePickerDelegate, isIChatResourceViewContext } from '../../chat.js'; import { ChatAttachmentModel } from '../../attachments/chatAttachmentModel.js'; import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, SCMHistoryItemChangeRangeAttachmentWidget, TerminalCommandAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../../attachments/chatAttachmentWidgets.js'; import { IDisposableReference } from '../chatContentParts/chatCollections.js'; @@ -114,7 +114,7 @@ import { ChatRelatedFiles } from '../../attachments/chatInputRelatedFilesContrib import { resizeImage } from '../../chatImageUtils.js'; import { IModelPickerDelegate, ModelPickerActionItem } from './modelPickerActionItem.js'; import { IModePickerDelegate, ModePickerActionItem } from './modePickerActionItem.js'; -import { ISessionTypePickerDelegate, SessionTypePickerActionItem } from './sessionTargetPickerActionItem.js'; +import { SessionTypePickerActionItem } from './sessionTargetPickerActionItem.js'; import { getAgentSessionProvider } from '../../agentSessions/agentSessions.js'; import { SearchableOptionPickerActionItem } from '../../chatSessions/searchableOptionPickerActionItem.js'; import { mixin } from '../../../../../../base/common/objects.js'; @@ -146,6 +146,11 @@ export interface IChatInputPartOptions { supportsChangingModes?: boolean; dndContainer?: HTMLElement; widgetViewKindTag: string; + /** + * Optional delegate for the session target picker. + * When provided, allows the input part to maintain independent state for the selected session type. + */ + sessionTypePickerDelegate?: ISessionTypePickerDelegate; } export interface IWorkingSetEntry { @@ -333,6 +338,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private filePartOfEditSessionKey: IContextKey; private chatSessionHasOptions: IContextKey; private chatSessionOptionsValid: IContextKey; + private agentSessionTypeKey: IContextKey; private modelWidget: ModelPickerActionItem | undefined; private modeWidget: ModePickerActionItem | undefined; private sessionTargetWidget: SessionTypePickerActionItem | undefined; @@ -502,12 +508,33 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const sessionResource = this._widget?.viewModel?.model.sessionResource; if (sessionResource) { const ctx = this.chatService.getChatSessionFromInternalUri(sessionResource); - if (ctx?.chatSessionType === chatSessionType) { + // const delegateSessionType = delegate?.setActiveSessionProvider && delegate?.getActiveSessionProvider?.(); + const delegateSessionType = this.options.sessionTypePickerDelegate?.getActiveSessionProvider?.(); + if (ctx?.chatSessionType === chatSessionType || delegateSessionType === chatSessionType) { this.refreshChatSessionPickers(); } } })); + // Listen for session type changes from the delegate (e.g., welcome page session picker) + if (this.options.sessionTypePickerDelegate?.onDidChangeActiveSessionProvider) { + this._register(this.options.sessionTypePickerDelegate.onDidChangeActiveSessionProvider((newSessionType) => { + // Update the context key so menu items can react + this.agentSessionTypeKey.set(newSessionType); + // When the session type changes via the delegate, ensure the provider is activated + // so contributed option groups are available before refreshing pickers. + void this.chatSessionsService.activateChatSessionItemProvider(newSessionType).then(() => { + // Update the lock state based on the new session type after activation + // Non-local session types (e.g., cloud/remote) should lock to coding agent mode + // This must be done after activation so the contribution is available + this.updateWidgetLockStateFromSessionType(newSessionType); + // The pickers will be determined based on the delegate's active session provider + this.refreshChatSessionPickers(); + this.tryUpdateWidgetController(); + }); + })); + } + this._attachmentModel = this._register(this.instantiationService.createInstance(ChatAttachmentModel)); this._register(this._attachmentModel.onDidChange(() => this._syncInputStateToModel())); this.selectedToolsModel = this._register(this.instantiationService.createInstance(ChatSelectedTools, this.currentModeObs)); @@ -523,6 +550,15 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.filePartOfEditSessionKey = ChatContextKeys.filePartOfEditSession.bindTo(contextKeyService); this.chatSessionHasOptions = ChatContextKeys.chatSessionHasModels.bindTo(contextKeyService); this.chatSessionOptionsValid = ChatContextKeys.chatSessionOptionsValid.bindTo(contextKeyService); + this.agentSessionTypeKey = ChatContextKeys.agentSessionType.bindTo(contextKeyService); + + // Initialize agentSessionType from delegate if available + if (this.options.sessionTypePickerDelegate?.getActiveSessionProvider) { + const initialSessionType = this.options.sessionTypePickerDelegate.getActiveSessionProvider(); + if (initialSessionType) { + this.agentSessionTypeKey.set(initialSessionType); + } + } const chatToolCount = ChatContextKeys.chatToolCount.bindTo(contextKeyService); @@ -737,9 +773,19 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this.chatService.getChatSessionFromInternalUri(sessionResource); }; - // Get all option groups for the current session type + // Determine the effective session type: + // - If we have a delegate with a setter (e.g., welcome page), use the delegate's session type + // - Otherwise, use the actual session's type const ctx = resolveChatSessionContext(); - const optionGroups = ctx ? this.chatSessionsService.getOptionGroupsForSessionType(ctx.chatSessionType) : undefined; + const delegate = this.options.sessionTypePickerDelegate; + const delegateSessionType = delegate?.setActiveSessionProvider && delegate?.getActiveSessionProvider?.(); + const effectiveSessionType = delegateSessionType || ctx?.chatSessionType; + + // Check if we're using a delegate-provided session type different from the actual session + const usingDelegateSessionType = delegateSessionType && delegateSessionType !== ctx?.chatSessionType; + + // Get all option groups for the effective session type + const optionGroups = effectiveSessionType ? this.chatSessionsService.getOptionGroupsForSessionType(effectiveSessionType) : undefined; if (!optionGroups || optionGroups.length === 0) { return []; } @@ -749,10 +795,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Init option group context keys for (const optionGroup of optionGroups) { - if (!ctx) { - continue; - } - const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id); + // For delegate session types, use the first item or default; otherwise get from session + const currentOption = usingDelegateSessionType + ? (optionGroup.items.find(item => item.default) || optionGroup.items[0]) + : (ctx ? this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id) : undefined); if (currentOption) { const optionId = typeof currentOption === 'string' ? currentOption : currentOption.id; this.updateOptionContextKey(optionGroup.id, optionId); @@ -761,16 +807,21 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const widgets: (ChatSessionPickerActionItem | SearchableOptionPickerActionItem)[] = []; for (const optionGroup of optionGroups) { - if (!ctx) { + // For delegate session types, we don't require ctx or session values + if (!usingDelegateSessionType && !ctx) { continue; } - const hasSessionValue = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id); + const hasSessionValue = ctx ? this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id) : undefined; const hasItems = optionGroup.items.length > 0; - if (!hasSessionValue && !hasItems) { + // For delegate session types, only check if items exist; otherwise check session value or items + if (!usingDelegateSessionType && !hasSessionValue && !hasItems) { // This session does not have a value to contribute for this option group continue; } + if (usingDelegateSessionType && !hasItems) { + continue; + } if (!this.evaluateOptionGroupVisibility(optionGroup)) { continue; @@ -784,28 +835,27 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge getCurrentOption: () => this.getCurrentOptionForGroup(optionGroup.id), onDidChangeOption: this.getOrCreateOptionEmitter(optionGroup.id).event, setOption: (option: IChatSessionProviderOptionItem) => { - const ctx = resolveChatSessionContext(); - if (!ctx) { - return; - } // Update context key for this option group this.updateOptionContextKey(optionGroup.id, option.id); - this.getOrCreateOptionEmitter(optionGroup.id).fire(option); - this.chatSessionsService.notifySessionOptionsChange( - ctx.chatSessionResource, - [{ optionId: optionGroup.id, value: option }] - ).catch(err => this.logService.error(`Failed to notify extension of ${optionGroup.id} change:`, err)); + + // Only notify session options change if we have an actual session (not delegate-only) + const ctx = resolveChatSessionContext(); + if (ctx && !usingDelegateSessionType) { + this.chatSessionsService.notifySessionOptionsChange( + ctx.chatSessionResource, + [{ optionId: optionGroup.id, value: option }] + ).catch(err => this.logService.error(`Failed to notify extension of ${optionGroup.id} change:`, err)); + } // Refresh pickers to re-evaluate visibility of other option groups this.refreshChatSessionPickers(); }, getOptionGroup: () => { - const ctx = resolveChatSessionContext(); - if (!ctx) { - return undefined; - } - const groups = this.chatSessionsService.getOptionGroupsForSessionType(ctx.chatSessionType); + // Use the effective session type (delegate's type takes precedence) + // effectiveSessionType is guaranteed to be defined here since we've already + // validated optionGroups exist at this point + const groups = effectiveSessionType ? this.chatSessionsService.getOptionGroupsForSessionType(effectiveSessionType) : undefined; return groups?.find(g => g.id === optionGroup.id); } }; @@ -1382,18 +1432,34 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (!ctx) { return hideAll(); } - const optionGroups = this.chatSessionsService.getOptionGroupsForSessionType(ctx.chatSessionType); + + // Determine the effective session type: + // - If we have a delegate with a setter (e.g., welcome page), use the delegate's session type + // - Otherwise, use the actual session's type + const delegate = this.options.sessionTypePickerDelegate; + const delegateSessionType = delegate?.setActiveSessionProvider && delegate?.getActiveSessionProvider?.(); + const effectiveSessionType = delegateSessionType || ctx.chatSessionType; + + // Check if we're using a delegate-provided session type different from the actual session + const usingDelegateSessionType = delegateSessionType && delegateSessionType !== ctx.chatSessionType; + + const optionGroups = this.chatSessionsService.getOptionGroupsForSessionType(effectiveSessionType); if (!optionGroups || optionGroups.length === 0) { return hideAll(); } - if (!this.chatSessionsService.hasAnySessionOptions(ctx.chatSessionResource)) { + // For delegate-provided session types, we don't require the actual session to have options + // because the actual session might be local while the delegate selects a different type + if (!usingDelegateSessionType && !this.chatSessionsService.hasAnySessionOptions(ctx.chatSessionResource)) { return hideAll(); } // First update all context keys with current values (before evaluating visibility) for (const optionGroup of optionGroups) { - const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id); + // For delegate session types, use the first item as default; otherwise get from session + const currentOption = usingDelegateSessionType + ? optionGroup.items[0] + : this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id); if (currentOption) { const optionId = typeof currentOption === 'string' ? currentOption : currentOption.id; this.updateOptionContextKey(optionGroup.id, optionId); @@ -1405,7 +1471,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Compute which option groups should be visible based on when expressions const visibleGroupIds = new Set(); for (const optionGroup of optionGroups) { - if (!this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id)) { + // For delegate session types, show groups that have items; otherwise check session value + const hasValue = usingDelegateSessionType + ? optionGroup.items.length > 0 + : !!this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id); + if (!hasValue) { continue; } if (this.evaluateOptionGroupVisibility(optionGroup)) { @@ -1421,7 +1491,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Validate that all selected options exist in their respective option group items let allOptionsValid = true; for (const optionGroup of optionGroups) { - const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id); + const currentOption = usingDelegateSessionType + ? optionGroup.items[0] + : this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id); if (currentOption) { const currentOptionId = typeof currentOption === 'string' ? currentOption : currentOption.id; const isValidOption = optionGroup.items.some(item => item.id === currentOptionId); @@ -1456,7 +1528,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } for (const [optionGroupId] of this.chatSessionPickerWidgets.entries()) { - const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroupId); + const currentOption = usingDelegateSessionType + ? optionGroups.find(g => g.id === optionGroupId)?.items[0] + : this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroupId); if (currentOption) { const optionGroup = optionGroups.find(g => g.id === optionGroupId); if (optionGroup) { @@ -1502,12 +1576,24 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (!ctx) { return; } - const optionGroups = this.chatSessionsService.getOptionGroupsForSessionType(ctx.chatSessionType); + + // Determine the effective session type (delegate's type takes precedence) + const delegate = this.options.sessionTypePickerDelegate; + const delegateSessionType = delegate?.setActiveSessionProvider && delegate?.getActiveSessionProvider?.(); + const effectiveSessionType = delegateSessionType || ctx.chatSessionType; + const usingDelegateSessionType = delegateSessionType && delegateSessionType !== ctx.chatSessionType; + + const optionGroups = this.chatSessionsService.getOptionGroupsForSessionType(effectiveSessionType); const optionGroup = optionGroups?.find(g => g.id === optionGroupId); if (!optionGroup || optionGroup.items.length === 0) { return; } + // For delegate session types, return the default or first item + if (usingDelegateSessionType) { + return optionGroup.items.find(item => item.default) || optionGroup.items[0]; + } + const currentOptionValue = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroupId); if (!currentOptionValue) { const defaultItem = optionGroup.items.find(item => item.default); @@ -1523,6 +1609,40 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } + /** + * Updates the agentSessionType context key based on delegate or actual session. + */ + private updateAgentSessionTypeContextKey(): void { + const sessionResource = this._widget?.viewModel?.model.sessionResource; + + // Determine effective session type: + // - If we have a delegate with a setter (e.g., welcome page), use the delegate's session type + // - Otherwise, use the actual session's type + const delegate = this.options.sessionTypePickerDelegate; + const delegateSessionType = delegate?.setActiveSessionProvider && delegate?.getActiveSessionProvider?.(); + const sessionType = delegateSessionType || (sessionResource ? getChatSessionType(sessionResource) : ''); + + this.agentSessionTypeKey.set(sessionType); + } + + /** + * Updates the widget lock state based on a session type. + * Local sessions unlock from coding agent mode, while remote/cloud sessions lock to coding agent mode. + */ + private updateWidgetLockStateFromSessionType(sessionType: string): void { + if (sessionType === localChatSessionType) { + this._widget?.unlockFromCodingAgent(); + return; + } + + const contribution = this.chatSessionsService.getChatSessionContribution(sessionType); + if (contribution) { + this._widget?.lockToCodingAgent(contribution.name, contribution.displayName, contribution.type); + } else { + this._widget?.unlockFromCodingAgent(); + } + } + /** * Updates the widget controller based on session type. */ @@ -1532,7 +1652,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return; } - const sessionType = getChatSessionType(sessionResource); + // Determine effective session type: + // - If we have a delegate with a setter (e.g., welcome page), use the delegate's session type + // - Otherwise, use the actual session's type + const delegate = this.options.sessionTypePickerDelegate; + const delegateSessionType = delegate?.setActiveSessionProvider && delegate?.getActiveSessionProvider?.(); + const sessionType = delegateSessionType || getChatSessionType(sessionResource); const isLocalSession = sessionType === localChatSessionType; if (!isLocalSession) { @@ -1550,6 +1675,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._widget = widget; this._register(widget.onDidChangeViewModel(() => { + // Update agentSessionType when view model changes + this.updateAgentSessionTypeContextKey(); this.refreshChatSessionPickers(); this.tryUpdateWidgetController(); })); @@ -1786,7 +1913,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge }; return this.modeWidget = this.instantiationService.createInstance(ModePickerActionItem, action, delegate); } else if (action.id === OpenSessionTargetPickerAction.ID && action instanceof MenuItemAction) { - const delegate: ISessionTypePickerDelegate = { + // Use provided delegate if available, otherwise create default delegate + const delegate: ISessionTypePickerDelegate = this.options.sessionTypePickerDelegate ?? { getActiveSessionProvider: () => { const sessionResource = this._widget?.viewModel?.sessionResource; return sessionResource ? getAgentSessionProvider(sessionResource) : undefined; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts index cd5245be9db..2aca776cca6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts @@ -19,10 +19,7 @@ import { IKeybindingService } from '../../../../../../platform/keybinding/common import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; import { IChatSessionsService } from '../../../common/chatSessionsService.js'; import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../../agentSessions/agentSessions.js'; - -export interface ISessionTypePickerDelegate { - getActiveSessionProvider(): AgentSessionProviders | undefined; -} +import { ISessionTypePickerDelegate } from '../../chat.js'; interface ISessionTypeItem { type: AgentSessionProviders; @@ -64,7 +61,13 @@ export class SessionTypePickerActionItem extends ActionWidgetDropdownActionViewI icon: getAgentSessionProviderIcon(sessionTypeItem.type), enabled: true, run: async () => { - this.commandService.executeCommand(sessionTypeItem.commandId, this.chatSessionPosition); + // If delegate provides a setter, use it for local state management + // Otherwise execute the command to open a new session + if (this.delegate.setActiveSessionProvider) { + this.delegate.setActiveSessionProvider(sessionTypeItem.type); + } else { + this.commandService.executeCommand(sessionTypeItem.commandId, this.chatSessionPosition); + } if (this.element) { this.renderLabel(this.element); } diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index d9033dd9f6f..346c9920958 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -61,6 +61,8 @@ export namespace ChatContextKeys { export const inputHasAgent = new RawContextKey('chatInputHasAgent', false); export const location = new RawContextKey('chatLocation', undefined); export const inQuickChat = new RawContextKey('quickChatHasFocus', false, { type: 'boolean', description: localize('inQuickChat', "True when the quick chat UI has focus, false otherwise.") }); + export const inAgentSessionsWelcome = new RawContextKey('inAgentSessionsWelcome', false, { type: 'boolean', description: localize('inAgentSessionsWelcome', "True when the chat input is within the agent sessions welcome page.") }); + export const chatSessionType = new RawContextKey('chatSessionType', '', { type: 'string', description: localize('chatSessionType', "The type of the current chat session.") }); export const hasFileAttachments = new RawContextKey('chatHasFileAttachments', false, { type: 'boolean', description: localize('chatHasFileAttachments', "True when the chat has file attachments.") }); export const chatSessionIsEmpty = new RawContextKey('chatSessionIsEmpty', true, { type: 'boolean', description: localize('chatSessionIsEmpty', "True when the current chat session has no requests.") }); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcome.contribution.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcome.contribution.ts new file mode 100644 index 00000000000..1a39d62ed66 --- /dev/null +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcome.contribution.ts @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from '../../../../nls.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; +import { registerWorkbenchContribution2, WorkbenchPhase, IWorkbenchContribution } from '../../../common/contributions.js'; +import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js'; +import { EditorExtensions, IEditorFactoryRegistry } from '../../../common/editor.js'; +import { IEditorResolverService, RegisteredEditorPriority } from '../../../services/editor/common/editorResolverService.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; +import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; +import { AgentSessionsWelcomeInput } from './agentSessionsWelcomeInput.js'; +import { AgentSessionsWelcomePage, AgentSessionsWelcomeInputSerializer } from './agentSessionsWelcome.js'; + +// Registration priority +const agentSessionsWelcomeInputTypeId = 'workbench.editors.agentSessionsWelcomeInput'; + +// Register editor serializer +Registry.as(EditorExtensions.EditorFactory) + .registerEditorSerializer(agentSessionsWelcomeInputTypeId, AgentSessionsWelcomeInputSerializer); + +// Register editor pane +Registry.as(EditorExtensions.EditorPane).registerEditorPane( + EditorPaneDescriptor.create( + AgentSessionsWelcomePage, + AgentSessionsWelcomePage.ID, + localize('agentSessionsWelcome', "Agent Sessions Welcome") + ), + [ + new SyncDescriptor(AgentSessionsWelcomeInput) + ] +); + +// Register resolver contribution +class AgentSessionsWelcomeEditorResolverContribution extends Disposable implements IWorkbenchContribution { + static readonly ID = 'workbench.contrib.agentSessionsWelcomeEditorResolver'; + + constructor( + @IEditorResolverService editorResolverService: IEditorResolverService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + this._register(editorResolverService.registerEditor( + `${AgentSessionsWelcomeInput.RESOURCE.scheme}:${AgentSessionsWelcomeInput.RESOURCE.authority}/**`, + { + id: AgentSessionsWelcomePage.ID, + label: localize('agentSessionsWelcome.displayName', "Agent Sessions Welcome"), + priority: RegisteredEditorPriority.builtin, + }, + { + singlePerResource: true, + canSupportResource: resource => + resource.scheme === AgentSessionsWelcomeInput.RESOURCE.scheme && + resource.authority === AgentSessionsWelcomeInput.RESOURCE.authority + }, + { + createEditorInput: () => { + return { + editor: instantiationService.createInstance(AgentSessionsWelcomeInput, {}), + }; + } + } + )); + } +} + +// Register command to open agent sessions welcome page +CommandsRegistry.registerCommand('workbench.action.openAgentSessionsWelcome', (accessor) => { + const editorService = accessor.get(IEditorService); + const instantiationService = accessor.get(IInstantiationService); + const input = instantiationService.createInstance(AgentSessionsWelcomeInput, {}); + return editorService.openEditor(input, { pinned: true }); +}); + +// Runner contribution - handles opening on startup +class AgentSessionsWelcomeRunnerContribution extends Disposable implements IWorkbenchContribution { + static readonly ID = 'workbench.contrib.agentSessionsWelcomeRunner'; + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @IEditorService private readonly editorService: IEditorService, + @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + this.run(); + } + + private async run(): Promise { + // Get startup editor configuration + const startupEditor = this.configurationService.getValue('workbench.startupEditor'); + + // Only proceed if configured to show agent sessions welcome page + if (startupEditor !== 'agentSessionsWelcomePage') { + return; + } + + // Wait for editors to restore + await this.editorGroupsService.whenReady; + + // Don't open if there are already editors open + if (this.editorService.activeEditor) { + return; + } + + // Open the agent sessions welcome page + const input = this.instantiationService.createInstance(AgentSessionsWelcomeInput, {}); + await this.editorService.openEditor(input, { pinned: false }); + } +} + +// Register contributions +registerWorkbenchContribution2(AgentSessionsWelcomeEditorResolverContribution.ID, AgentSessionsWelcomeEditorResolverContribution, WorkbenchPhase.BlockStartup); +registerWorkbenchContribution2(AgentSessionsWelcomeRunnerContribution.ID, AgentSessionsWelcomeRunnerContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcome.ts new file mode 100644 index 00000000000..fac4ffeb984 --- /dev/null +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcome.ts @@ -0,0 +1,438 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/agentSessionsWelcome.css'; +import { $, addDisposableListener, append, clearNode, Dimension, getWindow, scheduleAtNextAnimationFrame } from '../../../../base/browser/dom.js'; +import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js'; +import { Toggle } from '../../../../base/browser/ui/toggle/toggle.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { DisposableStore, IReference, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { ScrollbarVisibility } from '../../../../base/common/scrollable.js'; +import { localize } from '../../../../nls.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IStorageService } from '../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js'; +import { defaultToggleStyles, getListStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { EditorPane } from '../../../browser/parts/editor/editorPane.js'; +import { IEditorOpenContext, IEditorSerializer } from '../../../common/editor.js'; +import { SIDE_BAR_FOREGROUND } from '../../../common/theme.js'; +import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; +import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; +import { ChatAgentLocation, ChatModeKind } from '../../chat/common/constants.js'; +import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; +import { ChatWidget } from '../../chat/browser/widget/chatWidget.js'; +import { IAgentSessionsService } from '../../chat/browser/agentSessions/agentSessionsService.js'; +import { AgentSessionProviders } from '../../chat/browser/agentSessions/agentSessions.js'; +import { IAgentSession } from '../../chat/browser/agentSessions/agentSessionsModel.js'; +import { AgentSessionsWelcomeEditorOptions, AgentSessionsWelcomeInput } from './agentSessionsWelcomeInput.js'; +import { IWalkthroughsService, IResolvedWalkthrough } from './gettingStartedService.js'; +import { IChatService } from '../../chat/common/chatService/chatService.js'; +import { IChatModel } from '../../chat/common/model/chatModel.js'; +import { ISessionTypePickerDelegate } from '../../chat/browser/chat.js'; +import { AgentSessionsControl, IAgentSessionsControlOptions } from '../../chat/browser/agentSessions/agentSessionsControl.js'; +import { IAgentSessionsFilter } from '../../chat/browser/agentSessions/agentSessionsViewer.js'; +import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; + +const configurationKey = 'workbench.startupEditor'; +const MAX_SESSIONS = 6; + +export class AgentSessionsWelcomePage extends EditorPane { + + static readonly ID = 'agentSessionsWelcomePage'; + + private container!: HTMLElement; + private contentContainer!: HTMLElement; + private scrollableElement: DomScrollableElement | undefined; + private chatWidget: ChatWidget | undefined; + private chatModelRef: IReference | undefined; + private sessionsControl: AgentSessionsControl | undefined; + private sessionsControlContainer: HTMLElement | undefined; + private readonly sessionsControlDisposables = this._register(new DisposableStore()); + private readonly contentDisposables = this._register(new DisposableStore()); + private contextService: IContextKeyService; + private walkthroughs: IResolvedWalkthrough[] = []; + private _selectedSessionProvider: AgentSessionProviders = AgentSessionProviders.Local; + + constructor( + group: IEditorGroup, + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService, + @IStorageService storageService: IStorageService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, + @ICommandService private readonly commandService: ICommandService, + @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IProductService private readonly productService: IProductService, + @IWalkthroughsService private readonly walkthroughsService: IWalkthroughsService, + @IChatService private readonly chatService: IChatService, + ) { + super(AgentSessionsWelcomePage.ID, group, telemetryService, themeService, storageService); + + this.container = $('.agentSessionsWelcome', { + role: 'document', + tabindex: 0, + 'aria-label': localize('agentSessionsWelcomeAriaLabel', "Overview of agent sessions and how to get started.") + }); + + this.contextService = this._register(contextKeyService.createScoped(this.container)); + ChatContextKeys.inAgentSessionsWelcome.bindTo(this.contextService).set(true); + } + + protected createEditor(parent: HTMLElement): void { + parent.appendChild(this.container); + + // Create scrollable content + this.contentContainer = $('.agentSessionsWelcome-content'); + this.scrollableElement = this._register(new DomScrollableElement(this.contentContainer, { + className: 'agentSessionsWelcome-scrollable', + vertical: ScrollbarVisibility.Auto + })); + this.container.appendChild(this.scrollableElement.getDomNode()); + } + + override async setInput(input: AgentSessionsWelcomeInput, options: AgentSessionsWelcomeEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); + await this.buildContent(); + } + + private async buildContent(): Promise { + this.contentDisposables.clear(); + this.sessionsControlDisposables.clear(); + this.sessionsControl = undefined; + clearNode(this.contentContainer); + + // Get walkthroughs + this.walkthroughs = this.walkthroughsService.getWalkthroughs(); + + // Header + const header = append(this.contentContainer, $('.agentSessionsWelcome-header')); + append(header, $('h1.product-name', {}, this.productService.nameLong)); + + const startEntries = append(header, $('.agentSessionsWelcome-startEntries')); + this.buildStartEntries(startEntries); + + // Chat input section + const chatSection = append(this.contentContainer, $('.agentSessionsWelcome-chatSection')); + this.buildChatWidget(chatSection); + + // Sessions or walkthroughs + const sessions = this.agentSessionsService.model.sessions; + const sessionsSection = append(this.contentContainer, $('.agentSessionsWelcome-sessionsSection')); + if (sessions.length > 0) { + this.buildSessionsGrid(sessionsSection, sessions); + } else { + const walkthroughsSection = append(this.contentContainer, $('.agentSessionsWelcome-walkthroughsSection')); + this.buildWalkthroughs(walkthroughsSection); + } + + // Footer + const footer = append(this.contentContainer, $('.agentSessionsWelcome-footer')); + this.buildFooter(footer); + + // Listen for session changes - store reference to avoid querySelector + this.contentDisposables.add(this.agentSessionsService.model.onDidChangeSessions(() => { + clearNode(sessionsSection); + this.buildSessionsOrPrompts(sessionsSection); + })); + + this.scrollableElement?.scanDomNode(); + } + + private buildStartEntries(container: HTMLElement): void { + const entries = [ + { icon: Codicon.folderOpened, label: localize('openRecent', "Open Recent..."), command: 'workbench.action.openRecent' }, + { icon: Codicon.newFile, label: localize('newFile', "New file..."), command: 'workbench.action.files.newUntitledFile' }, + { icon: Codicon.repoClone, label: localize('cloneRepo', "Clone Git Repository..."), command: 'git.clone' }, + ]; + + for (const entry of entries) { + const button = append(container, $('button.agentSessionsWelcome-startEntry')); + button.appendChild(renderIcon(entry.icon)); + button.appendChild(document.createTextNode(entry.label)); + button.onclick = () => this.commandService.executeCommand(entry.command); + } + } + + private buildChatWidget(container: HTMLElement): void { + const chatWidgetContainer = append(container, $('.agentSessionsWelcome-chatWidget')); + + // Create editor overflow widgets container + const editorOverflowWidgetsDomNode = this.layoutService.getContainer(getWindow(chatWidgetContainer)).appendChild($('.chat-editor-overflow.monaco-editor')); + this.contentDisposables.add(toDisposable(() => editorOverflowWidgetsDomNode.remove())); + + // Create ChatWidget with scoped services + const scopedContextKeyService = this.contentDisposables.add(this.contextService.createScoped(chatWidgetContainer)); + const scopedInstantiationService = this.contentDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService]))); + + // Create a delegate for the session target picker with independent local state + const onDidChangeActiveSessionProvider = this.contentDisposables.add(new Emitter()); + const sessionTypePickerDelegate: ISessionTypePickerDelegate = { + getActiveSessionProvider: () => this._selectedSessionProvider, + setActiveSessionProvider: (provider: AgentSessionProviders) => { + this._selectedSessionProvider = provider; + onDidChangeActiveSessionProvider.fire(provider); + }, + onDidChangeActiveSessionProvider: onDidChangeActiveSessionProvider.event + }; + + this.chatWidget = this.contentDisposables.add(scopedInstantiationService.createInstance( + ChatWidget, + ChatAgentLocation.Chat, + {}, // Empty resource view context + { + autoScroll: mode => mode !== ChatModeKind.Ask, + renderFollowups: false, + supportsFileReferences: true, + renderInputOnTop: true, + rendererOptions: { + renderTextEditsAsSummary: () => true, + referencesExpandedWhenEmptyResponse: false, + progressMessageAtBottomOfResponse: mode => mode !== ChatModeKind.Ask, + }, + editorOverflowWidgetsDomNode, + enableImplicitContext: true, + enableWorkingSet: 'explicit', + supportsChangingModes: true, + sessionTypePickerDelegate, + }, + { + listForeground: SIDE_BAR_FOREGROUND, + listBackground: editorBackground, + overlayBackground: editorBackground, + inputEditorBackground: editorBackground, + resultEditorBackground: editorBackground, + } + )); + + this.chatWidget.render(chatWidgetContainer); + this.chatWidget.setVisible(true); + + // Start a chat session so the widget has a viewModel + // This is necessary for actions like mode switching to work properly + this.chatModelRef = this.chatService.startSession(ChatAgentLocation.Chat); + this.contentDisposables.add(this.chatModelRef); + if (this.chatModelRef.object) { + this.chatWidget.setModel(this.chatModelRef.object); + } + + // Focus the input when clicking anywhere in the chat widget area + // This ensures our widget becomes lastFocusedWidget for the chatWidgetService + this.contentDisposables.add(addDisposableListener(chatWidgetContainer, 'mousedown', () => { + this.chatWidget?.focusInput(); + })); + } + + private buildSessionsOrPrompts(container: HTMLElement): void { + // Clear previous sessions control + this.sessionsControlDisposables.clear(); + this.sessionsControl = undefined; + + const sessions = this.agentSessionsService.model.sessions; + + if (sessions.length > 0) { + this.buildSessionsGrid(container, sessions); + } + } + + + private buildSessionsGrid(container: HTMLElement, _sessions: IAgentSession[]): void { + this.sessionsControlContainer = append(container, $('.agentSessionsWelcome-sessionsGrid')); + + // Create a filter that limits results and excludes archived sessions + const onDidChangeEmitter = this.sessionsControlDisposables.add(new Emitter()); + const filter: IAgentSessionsFilter = { + onDidChange: onDidChangeEmitter.event, + limitResults: () => MAX_SESSIONS, + groupResults: () => false, + exclude: (session: IAgentSession) => session.isArchived(), + getExcludes: () => ({ + providers: [], + states: [], + archived: true, + read: false, + }), + }; + + const options: IAgentSessionsControlOptions = { + overrideStyles: getListStyles({ + listBackground: editorBackground, + }), + filter, + getHoverPosition: () => HoverPosition.BELOW, + trackActiveEditorSession: () => false, + }; + + this.sessionsControl = this.sessionsControlDisposables.add(this.instantiationService.createInstance( + AgentSessionsControl, + this.sessionsControlContainer, + options + )); + + // Schedule layout at next animation frame to ensure proper rendering + this.sessionsControlDisposables.add(scheduleAtNextAnimationFrame(getWindow(this.sessionsControlContainer), () => { + this.layoutSessionsControl(); + })); + + // "Open Agent Sessions" link + const openButton = append(container, $('button.agentSessionsWelcome-openSessionsButton')); + openButton.textContent = localize('openAgentSessions', "Open Agent Sessions"); + openButton.onclick = () => this.commandService.executeCommand('workbench.action.chat.open'); + } + + private buildWalkthroughs(container: HTMLElement): void { + const activeWalkthroughs = this.walkthroughs.filter(w => + !w.when || this.contextService.contextMatchesRules(w.when) + ).slice(0, 3); + + if (activeWalkthroughs.length === 0) { + return; + } + + for (const walkthrough of activeWalkthroughs) { + const card = append(container, $('.agentSessionsWelcome-walkthroughCard')); + card.onclick = () => { + this.commandService.executeCommand('workbench.action.openWalkthrough', walkthrough.id); + }; + + // Icon + const iconContainer = append(card, $('.agentSessionsWelcome-walkthroughCard-icon')); + if (walkthrough.icon.type === 'icon') { + iconContainer.appendChild(renderIcon(walkthrough.icon.icon)); + } + + // Content + const content = append(card, $('.agentSessionsWelcome-walkthroughCard-content')); + const title = append(content, $('.agentSessionsWelcome-walkthroughCard-title')); + title.textContent = walkthrough.title; + + if (walkthrough.description) { + const desc = append(content, $('.agentSessionsWelcome-walkthroughCard-description')); + desc.textContent = walkthrough.description; + } + + // Navigation arrows container + const navContainer = append(card, $('.agentSessionsWelcome-walkthroughCard-nav')); + const prevButton = append(navContainer, $('button.nav-button')); + prevButton.appendChild(renderIcon(Codicon.chevronLeft)); + prevButton.onclick = (e) => { e.stopPropagation(); }; + + const nextButton = append(navContainer, $('button.nav-button')); + nextButton.appendChild(renderIcon(Codicon.chevronRight)); + nextButton.onclick = (e) => { e.stopPropagation(); }; + } + } + + private buildFooter(container: HTMLElement): void { + // Learning link + const learningLink = append(container, $('button.agentSessionsWelcome-footerLink')); + learningLink.appendChild(renderIcon(Codicon.mortarBoard)); + learningLink.appendChild(document.createTextNode(localize('exploreHelp', "Explore Learning & Help Resources"))); + learningLink.onclick = () => this.commandService.executeCommand('workbench.action.openWalkthrough'); + + // Show on startup checkbox + const showOnStartupContainer = append(container, $('.agentSessionsWelcome-showOnStartup')); + const showOnStartupCheckbox = this.contentDisposables.add(new Toggle({ + icon: Codicon.check, + actionClassName: 'agentSessionsWelcome-checkbox', + isChecked: this.configurationService.getValue(configurationKey) === 'agentSessionsWelcomePage', + title: localize('checkboxTitle', "When checked, this page will be shown on startup."), + ...defaultToggleStyles + })); + showOnStartupCheckbox.domNode.id = 'showOnStartup'; + const showOnStartupLabel = $('label.caption', { for: 'showOnStartup' }, localize('showOnStartup', "Show welcome page on startup")); + + const onShowOnStartupChanged = () => { + if (showOnStartupCheckbox.checked) { + this.configurationService.updateValue(configurationKey, 'agentSessionsWelcomePage'); + } else { + this.configurationService.updateValue(configurationKey, 'none'); + } + }; + + this.contentDisposables.add(showOnStartupCheckbox.onChange(() => onShowOnStartupChanged())); + this.contentDisposables.add(addDisposableListener(showOnStartupLabel, 'click', () => { + showOnStartupCheckbox.checked = !showOnStartupCheckbox.checked; + onShowOnStartupChanged(); + })); + + showOnStartupContainer.appendChild(showOnStartupCheckbox.domNode); + showOnStartupContainer.appendChild(showOnStartupLabel); + } + + private lastDimension: Dimension | undefined; + + override layout(dimension: Dimension): void { + this.lastDimension = dimension; + this.container.style.height = `${dimension.height}px`; + this.container.style.width = `${dimension.width}px`; + + // Layout chat widget with height for input area + if (this.chatWidget) { + const chatWidth = Math.min(800, dimension.width - 80); + // Use a reasonable height for the input part - the CSS will hide the list area + const inputHeight = 150; + this.chatWidget.layout(inputHeight, chatWidth); + } + + // Layout sessions control + this.layoutSessionsControl(); + + this.scrollableElement?.scanDomNode(); + } + + private layoutSessionsControl(): void { + if (!this.sessionsControl || !this.sessionsControlContainer || !this.lastDimension) { + return; + } + + const sessionsWidth = Math.min(800, this.lastDimension.width - 80); + // Calculate height based on actual visible sessions (capped at MAX_SESSIONS) + // Use 52px per item from AgentSessionsListDelegate.ITEM_HEIGHT + // Give the list FULL height so virtualization renders all items + // CSS transforms handle the 2-column visual layout + const visibleSessions = Math.min( + this.agentSessionsService.model.sessions.filter(s => !s.isArchived()).length, + MAX_SESSIONS + ); + const sessionsHeight = visibleSessions * 52; + this.sessionsControl.layout(sessionsHeight, sessionsWidth); + + // Set margin offset for 2-column layout: actual height - visual height + // Visual height = ceil(n/2) * 52, so offset = floor(n/2) * 52 + const marginOffset = Math.floor(visibleSessions / 2) * 52; + this.sessionsControlContainer.style.setProperty('--sessions-grid-margin-offset', `-${marginOffset}px`); + } + + override focus(): void { + super.focus(); + this.chatWidget?.focusInput(); + } +} + +export class AgentSessionsWelcomeInputSerializer implements IEditorSerializer { + canSerialize(editorInput: AgentSessionsWelcomeInput): boolean { + return true; + } + + serialize(editorInput: AgentSessionsWelcomeInput): string { + return JSON.stringify({}); + } + + deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): AgentSessionsWelcomeInput { + return new AgentSessionsWelcomeInput({}); + } +} diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcomeInput.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcomeInput.ts new file mode 100644 index 00000000000..5b5cdf097c3 --- /dev/null +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcomeInput.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from '../../../../nls.js'; +import { EditorInput } from '../../../common/editor/editorInput.js'; +import { URI } from '../../../../base/common/uri.js'; +import { Schemas } from '../../../../base/common/network.js'; +import { IUntypedEditorInput } from '../../../common/editor.js'; +import { IEditorOptions } from '../../../../platform/editor/common/editor.js'; + +export const agentSessionsWelcomeInputTypeId = 'workbench.editors.agentSessionsWelcomeInput'; + +export interface AgentSessionsWelcomeEditorOptions extends IEditorOptions { + showTelemetryNotice?: boolean; +} + +export class AgentSessionsWelcomeInput extends EditorInput { + + static readonly ID = agentSessionsWelcomeInputTypeId; + static readonly RESOURCE = URI.from({ scheme: Schemas.walkThrough, authority: 'vscode_agent_sessions_welcome' }); + + private _showTelemetryNotice: boolean; + + override get typeId(): string { + return AgentSessionsWelcomeInput.ID; + } + + override get editorId(): string | undefined { + return this.typeId; + } + + override toUntyped(): IUntypedEditorInput { + return { + resource: AgentSessionsWelcomeInput.RESOURCE, + options: { + override: AgentSessionsWelcomeInput.ID, + pinned: false + } + }; + } + + get resource(): URI | undefined { + return AgentSessionsWelcomeInput.RESOURCE; + } + + override matches(other: EditorInput | IUntypedEditorInput): boolean { + if (super.matches(other)) { + return true; + } + + return other instanceof AgentSessionsWelcomeInput; + } + + constructor(options: AgentSessionsWelcomeEditorOptions = {}) { + super(); + this._showTelemetryNotice = !!options.showTelemetryNotice; + } + + override getName() { + return localize('agentSessionsWelcome', "Welcome"); + } + + get showTelemetryNotice(): boolean { + return this._showTelemetryNotice; + } + + set showTelemetryNotice(value: boolean) { + this._showTelemetryNotice = value; + } +} diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts index b63e894b1ea..0158c0614d8 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts @@ -306,7 +306,7 @@ configurationRegistry.registerConfiguration({ 'workbench.startupEditor': { 'scope': ConfigurationScope.RESOURCE, 'type': 'string', - 'enum': ['none', 'welcomePage', 'readme', 'newUntitledFile', 'welcomePageInEmptyWorkbench', 'terminal'], + 'enum': ['none', 'welcomePage', 'readme', 'newUntitledFile', 'welcomePageInEmptyWorkbench', 'terminal', 'agentSessionsWelcomePage'], 'enumDescriptions': [ localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.none' }, "Start without an editor."), localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePage' }, "Open the Welcome page, with content to aid in getting started with VS Code and extensions."), @@ -314,6 +314,7 @@ configurationRegistry.registerConfiguration({ localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.newUntitledFile' }, "Open a new untitled text file (only applies when opening an empty window)."), localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePageInEmptyWorkbench' }, "Open the Welcome page when opening an empty workbench."), localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.terminal' }, "Open a new terminal in the editor area."), + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.agentSessionsWelcomePage' }, "Open the Agent Sessions Welcome page."), ], 'default': 'welcomePage', 'description': localize('workbench.startupEditor', "Controls which editor is shown at startup, if none are restored from the previous session.") diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/agentSessionsWelcome.css b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/agentSessionsWelcome.css new file mode 100644 index 00000000000..f2b27b08900 --- /dev/null +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/agentSessionsWelcome.css @@ -0,0 +1,360 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.agentSessionsWelcome { + display: flex; + flex-direction: column; + height: 100%; + background-color: var(--vscode-welcomePage-background); + overflow: hidden; +} + +.agentSessionsWelcome-scrollable { + height: 100%; +} + +.agentSessionsWelcome-content { + display: flex; + flex-direction: column; + align-items: center; + padding: 40px 40px 80px; + max-width: 900px; + margin: 0 auto; + width: 100%; + box-sizing: border-box; +} + +/* Header */ +.agentSessionsWelcome-header { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 24px; + width: 100%; +} + +.agentSessionsWelcome-header h1.product-name { + font-size: 32px; + font-weight: 400; + margin: 0 0 12px 0; + color: var(--vscode-foreground); +} + +.agentSessionsWelcome-startEntries { + display: flex; + gap: 16px; + flex-wrap: wrap; + justify-content: center; +} + +.agentSessionsWelcome-startEntry { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border: none; + border-radius: 4px; + background-color: transparent; + color: var(--vscode-foreground); + font-size: 13px; + cursor: pointer; +} + +.agentSessionsWelcome-startEntry:hover { + color: var(--vscode-textLink-foreground); +} + +.agentSessionsWelcome-startEntry .codicon { + color: var(--vscode-descriptionForeground); +} + +/* Chat widget section */ +.agentSessionsWelcome-chatSection { + width: 100%; + max-width: 800px; + margin-bottom: 24px; +} + +/* Hide the chat tree/list - we only want the input */ +.agentSessionsWelcome-chatWidget .interactive-list { + display: none !important; +} + +/* Hide the welcome message containers */ +.agentSessionsWelcome-chatWidget .chat-welcome-view, +.agentSessionsWelcome-chatWidget .chat-welcome-view-container { + display: none !important; +} + +/* Constrain the chat container height - let it size to content */ +.agentSessionsWelcome-chatWidget .interactive-session { + height: auto !important; +} + +/* Input part styling - match chat panel */ +.agentSessionsWelcome-chatWidget .interactive-input-part { + margin: 0; + padding: 16px 0; +} + +/* Suggested prompts */ +.agentSessionsWelcome-sessionsSection { + width: 100%; + max-width: 800px; + margin-bottom: 24px; +} + +.agentSessionsWelcome-suggestedPrompts { + display: flex; + gap: 12px; + justify-content: center; + flex-wrap: wrap; +} + +.agentSessionsWelcome-suggestedPrompt { + padding: 8px 16px; + border: 1px solid var(--vscode-button-secondaryBorder, var(--vscode-contrastBorder, transparent)); + border-radius: 20px; + background-color: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); + font-size: 13px; + cursor: pointer; + transition: background-color 0.1s; +} + +.agentSessionsWelcome-suggestedPrompt:hover { + background-color: var(--vscode-button-secondaryHoverBackground); +} + +/* Sessions grid */ +.agentSessionsWelcome-sessionsGrid { + margin-bottom: 12px; + width: 100%; + overflow: hidden; +} + +/* Style the agent sessions control within welcome page - 2 column layout */ +.agentSessionsWelcome-sessionsGrid .agent-sessions-viewer { + height: auto; + min-height: 0; +} + +.agentSessionsWelcome-sessionsGrid .agent-sessions-viewer .monaco-list { + background: transparent !important; +} + +.agentSessionsWelcome-sessionsGrid .agent-sessions-viewer .monaco-list-rows { + background: transparent !important; +} + +/* Hide scrollbars in welcome page sessions list */ +.agentSessionsWelcome-sessionsGrid .agent-sessions-viewer .monaco-scrollable-element > .scrollbar { + display: none !important; +} + +/* 2-column grid layout using CSS transforms on virtualized list */ +.agentSessionsWelcome-sessionsGrid .monaco-list-row { + width: 50% !important; +} + +/* + * Transform items into 2-column layout: + * - Items 0,1 form visual row 1 (top: 0) + * - Items 2,3 form visual row 2 (top: 52) + * - Items 4,5 form visual row 3 (top: 104) + * Left column (even): items stay in place or move up + * Right column (odd): items move right and up + */ + +/* Item 1 (index 1): move to right column of row 1 */ +.agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(2) { + transform: translateX(100%) translateY(-52px); +} + +/* Item 2 (index 2): move up to row 2 left column */ +.agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(3) { + transform: translateY(-52px); +} + +/* Item 3 (index 3): move to right column of row 2 */ +.agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(4) { + transform: translateX(100%) translateY(-104px); +} + +/* Item 4 (index 4): move up to row 3 left column */ +.agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(5) { + transform: translateY(-104px); +} + +/* Item 5 (index 5): move to right column of row 3 */ +.agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(6) { + transform: translateX(100%) translateY(-156px); +} + +/* Clip the extra space caused by transforms - uses CSS variable set by JS */ +.agentSessionsWelcome-sessionsGrid .agent-sessions-viewer .monaco-scrollable-element { + margin-bottom: var(--sessions-grid-margin-offset, 0px); +} + +/* Style individual session items in the welcome page */ +.agentSessionsWelcome-sessionsGrid .agent-session-item { + border-radius: 4px; +} + +.agentSessionsWelcome-sessionsGrid .agent-session-item:hover { + background-color: var(--vscode-list-hoverBackground); +} + +/* Hide toolbar on items in welcome page for cleaner look */ +.agentSessionsWelcome-sessionsGrid .agent-session-title-toolbar { + display: none !important; +} + +.agentSessionsWelcome-openSessionsButton { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + width: 100%; + padding: 8px 16px; + border: none; + border-radius: 4px; + background-color: transparent; + color: var(--vscode-foreground); + font-size: 13px; + cursor: pointer; +} + +.agentSessionsWelcome-openSessionsButton:hover { + color: var(--vscode-textLink-foreground); +} + +/* Walkthroughs section */ +.agentSessionsWelcome-walkthroughsSection { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; + max-width: 800px; + margin-bottom: 32px; +} + +.agentSessionsWelcome-walkthroughCard { + display: flex; + align-items: center; + padding: 16px; + border: 1px solid var(--vscode-welcomePage-tileBorder, var(--vscode-contrastBorder, transparent)); + border-radius: 8px; + background-color: var(--vscode-welcomePage-tileBackground); + cursor: pointer; + transition: background-color 0.1s; + gap: 16px; +} + +.agentSessionsWelcome-walkthroughCard:hover { + background-color: var(--vscode-welcomePage-tileHoverBackground); +} + +.agentSessionsWelcome-walkthroughCard-icon { + flex-shrink: 0; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; +} + +.agentSessionsWelcome-walkthroughCard-icon .codicon { + font-size: 28px; + color: var(--vscode-welcomePage-progress-foreground, var(--vscode-foreground)); +} + +.agentSessionsWelcome-walkthroughCard-content { + flex: 1; + min-width: 0; +} + +.agentSessionsWelcome-walkthroughCard-title { + font-size: 14px; + font-weight: 600; + color: var(--vscode-foreground); + margin-bottom: 4px; +} + +.agentSessionsWelcome-walkthroughCard-description { + font-size: 12px; + color: var(--vscode-descriptionForeground); +} + +.agentSessionsWelcome-walkthroughCard-nav { + display: flex; + gap: 4px; +} + +.agentSessionsWelcome-walkthroughCard-nav .nav-button { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border: none; + border-radius: 4px; + background-color: transparent; + color: var(--vscode-descriptionForeground); + cursor: pointer; +} + +.agentSessionsWelcome-walkthroughCard-nav .nav-button:hover { + background-color: var(--vscode-toolbar-hoverBackground); +} + +/* Footer */ +.agentSessionsWelcome-footer { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + width: 100%; + max-width: 800px; + margin-top: 16px; +} + +.agentSessionsWelcome-footerLink { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + border: none; + border-radius: 4px; + background-color: transparent; + color: var(--vscode-foreground); + font-size: 13px; + cursor: pointer; +} + +.agentSessionsWelcome-footerLink:hover { + color: var(--vscode-textLink-foreground); +} + +.agentSessionsWelcome-footerLink .codicon { + color: var(--vscode-descriptionForeground); +} + +.agentSessionsWelcome-showOnStartup { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: var(--vscode-descriptionForeground); +} + +.agentSessionsWelcome-showOnStartup label { + cursor: pointer; +} + +.agentSessionsWelcome-checkbox { + width: 16px; + height: 16px; +} diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index e7c16a7de53..82c60b5949c 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -350,6 +350,7 @@ import './contrib/surveys/browser/languageSurveys.contribution.js'; // Welcome import './contrib/welcomeGettingStarted/browser/gettingStarted.contribution.js'; +import './contrib/welcomeGettingStarted/browser/agentSessionsWelcome.contribution.js'; import './contrib/welcomeWalkthrough/browser/walkThrough.contribution.js'; import './contrib/welcomeViews/common/viewsWelcome.contribution.js'; import './contrib/welcomeViews/common/newFile.contribution.js'; From 71aa128e5a830ad2d95834e314be7a0a9d49e80d Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 15 Jan 2026 14:55:16 -0800 Subject: [PATCH 033/387] chat: refactor chatlistwidget out from the chatwidget for better reusability --- .../agentSessions/agentSessionsViewer.ts | 76 +- .../chat/browser/widget/chatListRenderer.ts | 5 +- .../chat/browser/widget/chatListWidget.ts | 831 ++++++++++++++++++ .../contrib/chat/browser/widget/chatWidget.ts | 403 ++------- 4 files changed, 992 insertions(+), 323 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 17c8d9f3a5a..f39bfe4b357 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import './media/agentsessionsviewer.css'; +import * as dom from '../../../../../base/browser/dom.js'; import { h } from '../../../../../base/browser/dom.js'; import { localize } from '../../../../../nls.js'; import { IIdentityProvider, IListVirtualDelegate } from '../../../../../base/browser/ui/list/list.js'; @@ -39,6 +40,12 @@ import { ServiceCollection } from '../../../../../platform/instantiation/common/ import { Event } from '../../../../../base/common/event.js'; import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; import { MarkdownString, IMarkdownString } from '../../../../../base/common/htmlContent.js'; +import { ChatListWidget } from '../widget/chatListWidget.js'; +import { IChatService } from '../../common/chatService/chatService.js'; +import { IChatWidgetService } from '../chat.js'; +import { ChatViewModel } from '../../common/model/chatViewModel.js'; +import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; +import { ChatModeKind } from '../../common/constants.js'; export type AgentSessionListItem = IAgentSession | IAgentSessionSection; @@ -89,6 +96,8 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer, template: IAgentSessionItemTemplate): void { template.elementDisposable.add( - this.hoverService.setupDelayedHover(template.element, () => ({ - content: this.buildTooltip(session.element), - style: HoverStyle.Pointer, - position: { - hoverPosition: this.options.getHoverPosition() - } - }), { groupId: 'agent.sessions' }) + this.hoverService.setupDelayedHover(template.element, () => this.buildHoverContent(session.element), { groupId: 'agent.sessions' }) ); } + private buildHoverContent(session: IAgentSession): { content: HTMLElement | IMarkdownString; style: HoverStyle; position: { hoverPosition: HoverPosition } } { + // Create container for the hover + const container = dom.$('.agent-session-hover'); + container.style.width = '500px'; + container.style.height = '300px'; + container.style.overflow = 'hidden'; + + // Try to load the chat session + const sessionResource = session.resource; + this.chatService.getOrRestoreSession(sessionResource).then(modelRef => { + if (!modelRef) { + // Show fallback tooltip text + const tooltip = this.buildTooltip(session); + container.textContent = typeof tooltip === 'string' ? tooltip : tooltip.value; + return; + } + + // Create view model + const codeBlockCollection = this.instantiationService.createInstance(CodeBlockModelCollection, 'agentSessionHover'); + const viewModel = this.instantiationService.createInstance( + ChatViewModel, + modelRef.object, + codeBlockCollection + ); + + // Create the chat list widget + const listWidget = this.instantiationService.createInstance( + ChatListWidget, + container, + { + rendererOptions: { + renderStyle: 'minimal', + noHeader: true, + editableCodeBlock: false, + }, + currentChatMode: () => ChatModeKind.Ask, + } + ); + listWidget.setViewModel(viewModel); + listWidget.layout(300, 500); + + // Handle followup clicks - open the session and accept input + listWidget.onDidClickFollowup(async (followup) => { + const widget = await this.chatWidgetService.openSession(sessionResource); + if (widget) { + widget.acceptInput(followup.message); + } + }); + }); + + return { + content: container, + style: HoverStyle.Pointer, + position: { + hoverPosition: this.options.getHoverPosition() + } + }; + } + private buildTooltip(session: IAgentSession): IMarkdownString { const lines: string[] = []; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 2f964a2b26b..a2ac602c0b1 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -25,6 +25,7 @@ import { Iterable } from '../../../../../base/common/iterator.js'; import { KeyCode } from '../../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, IDisposable, dispose, thenIfNotDisposed, toDisposable } from '../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../base/common/map.js'; +import { ScrollEvent } from '../../../../../base/common/scrollable.js'; import { FileAccess, Schemas } from '../../../../../base/common/network.js'; import { clamp } from '../../../../../base/common/numbers.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; @@ -150,7 +151,7 @@ export interface IChatRendererDelegate { getListLength(): number; currentChatMode(): ChatModeKind; - readonly onDidScroll?: Event; + readonly onDidScroll?: Event; } const mostRecentResponseClassName = 'chat-most-recent-response'; @@ -435,7 +436,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer ChatModeKind; + + /** + * View ID for editor options (used in ChatWidget context). + */ + readonly viewId?: string; + + /** + * Input editor background color key. + */ + readonly inputEditorBackground?: string; + + /** + * Result editor background color key. + */ + readonly resultEditorBackground?: string; + + /** + * Optional filter for the tree. + */ + readonly filter?: ITreeFilter; + + /** + * Optional code block model collection to use. + * If not provided, one will be created. + */ + readonly codeBlockModelCollection?: CodeBlockModelCollection; + + /** + * Initial view model. + */ + readonly viewModel?: IChatViewModel; + + /** + * Optional pre-created editor options. + * If provided, these will be used instead of creating new ones. + */ + readonly editorOptions?: ChatEditorOptions; + + /** + * The chat location (for rerun requests). + */ + readonly location?: ChatAgentLocation; + + /** + * Callback to get current language model ID (for rerun requests). + */ + readonly getCurrentLanguageModelId?: () => string | undefined; + + /** + * Callback to get current mode info (for rerun requests). + */ + readonly getCurrentModeInfo?: () => IChatRequestModeInfo | undefined; + + /** + * The render style for the chat widget. Affects minimum height behavior. + */ + readonly renderStyle?: 'compact' | 'minimal'; +} + +/** + * A reusable widget that encapsulates chat list/tree rendering. + * This can be used in various contexts such as the main chat widget, + * hover previews, etc. + */ +export class ChatListWidget extends Disposable { + + //#region Events + + private readonly _onDidScroll = this._register(new Emitter()); + readonly onDidScroll: Event = this._onDidScroll.event; + + private readonly _onDidChangeContentHeight = this._register(new Emitter()); + readonly onDidChangeContentHeight: Event = this._onDidChangeContentHeight.event; + + private readonly _onDidClickFollowup = this._register(new Emitter()); + readonly onDidClickFollowup: Event = this._onDidClickFollowup.event; + + private readonly _onDidFocus = this._register(new Emitter()); + readonly onDidFocus: Event = this._onDidFocus.event; + + private readonly _onDidChangeItemHeight = this._register(new Emitter<{ element: ChatTreeItem; height: number }>()); + /** Event fired when an item's height changes. Used for dynamic layout mode. */ + readonly onDidChangeItemHeight: Event<{ element: ChatTreeItem; height: number }> = this._onDidChangeItemHeight.event; + + /** + * Event fired when a request item is clicked. + */ + get onDidClickRequest(): Event { + return this._renderer.onDidClickRequest; + } + + /** + * Event fired when an item is re-rendered. + */ + get onDidRerender(): Event { + return this._renderer.onDidRerender; + } + + /** + * Event fired when a template is disposed. + */ + get onDidDispose(): Event { + return this._renderer.onDidDispose; + } + + /** + * Event fired when focus moves outside the editing area. + */ + get onDidFocusOutside(): Event { + return this._renderer.onDidFocusOutside; + } + + //#endregion + + //#region Private fields + + private readonly _tree: WorkbenchObjectTree; + private readonly _renderer: ChatListItemRenderer; + private readonly _codeBlockModelCollection: CodeBlockModelCollection; + + private _viewModel: IChatViewModel | undefined; + private _visible = true; + private _lastItem: ChatTreeItem | undefined; + private _previousScrollHeight: number = 0; + private _mostRecentlyFocusedItemIndex: number = -1; + private _scrollLock: boolean = true; + private _settingChangeCounter: number = 0; + private _visibleChangeCount: number = 0; + + private readonly _container: HTMLElement; + private readonly _scrollDownButton: Button; + private readonly _scrollAnimationFrameDisposable = this._register(new MutableDisposable()); + private readonly _lastItemIdContextKey: IContextKey; + + private readonly _location: ChatAgentLocation | undefined; + private readonly _getCurrentLanguageModelId: (() => string | undefined) | undefined; + private readonly _getCurrentModeInfo: (() => IChatRequestModeInfo | undefined) | undefined; + private readonly _renderStyle: 'compact' | 'minimal' | undefined; + + //#endregion + + //#region Properties + + get domNode(): HTMLElement { + return this._container; + } + + get scrollTop(): number { + return this._tree.scrollTop; + } + + set scrollTop(value: number) { + this._tree.scrollTop = value; + } + + get scrollHeight(): number { + return this._tree.scrollHeight; + } + + get renderHeight(): number { + return this._tree.renderHeight; + } + + get contentHeight(): number { + return this._tree.contentHeight; + } + + /** + * Whether the list is scrolled to the bottom. + */ + get isScrolledToBottom(): boolean { + return this._tree.scrollTop + this._tree.renderHeight >= this._tree.scrollHeight - 2; + } + + /** + * The last item in the list. + */ + get lastItem(): ChatTreeItem | undefined { + return this._lastItem; + } + + + + //#endregion + + constructor( + container: HTMLElement, + options: IChatListWidgetOptions, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IChatService private readonly chatService: IChatService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @ILogService private readonly logService: ILogService, + ) { + super(); + + this._viewModel = options.viewModel; + this._codeBlockModelCollection = options.codeBlockModelCollection ?? this._register(this.instantiationService.createInstance(CodeBlockModelCollection, 'chatListWidget')); + this._location = options.location; + this._getCurrentLanguageModelId = options.getCurrentLanguageModelId; + this._getCurrentModeInfo = options.getCurrentModeInfo; + this._lastItemIdContextKey = ChatContextKeys.lastItemId.bindTo(this.contextKeyService); + this._container = container; + + const scopedInstantiationService = this._register(this.instantiationService.createChild( + new ServiceCollection([IContextKeyService, this.contextKeyService]) + )); + this._renderStyle = options.renderStyle; + + // Create overflow widgets container + const overflowWidgetsContainer = options.overflowWidgetsDomNode ?? document.createElement('div'); + if (!options.overflowWidgetsDomNode) { + overflowWidgetsContainer.classList.add('chat-overflow-widget-container', 'monaco-editor'); + this._container.append(overflowWidgetsContainer); + this._register(toDisposable(() => overflowWidgetsContainer.remove())); + } + + // Create editor options (use provided or create new) + const editorOptions = options.editorOptions ?? this._register(scopedInstantiationService.createInstance( + ChatEditorOptions, + options.viewId, + 'foreground', + options.inputEditorBackground ?? 'chat.requestEditor.background', + options.resultEditorBackground ?? 'chat.responseEditor.background' + )); + + // Create delegate + const delegate = scopedInstantiationService.createInstance( + ChatListDelegate, + options.defaultElementHeight ?? 200 + ); + + // Create renderer delegate + const rendererDelegate: IChatRendererDelegate = { + getListLength: () => this._tree.getNode(null).visibleChildrenCount, + onDidScroll: this.onDidScroll, + container: this._container, + currentChatMode: options.currentChatMode ?? (() => ChatModeKind.Ask), + }; + + // Create renderer + this._renderer = this._register(scopedInstantiationService.createInstance( + ChatListItemRenderer, + editorOptions, + options.rendererOptions ?? {}, + rendererDelegate, + this._codeBlockModelCollection, + overflowWidgetsContainer, + this._viewModel, + )); + + // Wire up renderer events + this._register(this._renderer.onDidClickFollowup(item => { + this._onDidClickFollowup.fire(item); + })); + + this._register(this._renderer.onDidChangeItemHeight(e => { + this._onDidChangeItemHeight.fire(e); + if (this._tree.hasElement(e.element) && this._visible) { + this._tree.updateElementHeight(e.element, e.height); + } + })); + + // Handle rerun with agent or command detection internally + this._register(this._renderer.onDidClickRerunWithAgentOrCommandDetection(e => { + const request = this.chatService.getSession(e.sessionResource)?.getRequests().find(candidate => candidate.id === e.requestId); + if (request) { + const sendOptions: IChatSendRequestOptions = { + noCommandDetection: true, + attempt: request.attempt + 1, + location: this._location, + userSelectedModelId: this._getCurrentLanguageModelId?.(), + modeInfo: this._getCurrentModeInfo?.(), + }; + this.chatService.resendRequest(request, sendOptions).catch(e => this.logService.error('FAILED to rerun request', e)); + } + })); + + // Create tree + const styles = options.styles ?? {}; + this._tree = this._register(scopedInstantiationService.createInstance( + WorkbenchObjectTree, + 'ChatList', + this._container, + delegate, + [this._renderer], + { + identityProvider: { getId: (e: ChatTreeItem) => e.id }, + horizontalScrolling: false, + alwaysConsumeMouseWheel: false, + supportDynamicHeights: true, + hideTwistiesOfChildlessElements: true, + accessibilityProvider: this.instantiationService.createInstance(ChatAccessibilityProvider), + keyboardNavigationLabelProvider: { + getKeyboardNavigationLabel: (e: ChatTreeItem) => + isRequestVM(e) ? e.message : isResponseVM(e) ? e.response.value : '' + }, + setRowLineHeight: false, + scrollToActiveElement: true, + filter: options.filter, + overrideStyles: { + listFocusBackground: styles.listBackground, + listInactiveFocusBackground: styles.listBackground, + listActiveSelectionBackground: styles.listBackground, + listFocusAndSelectionBackground: styles.listBackground, + listInactiveSelectionBackground: styles.listBackground, + listHoverBackground: styles.listBackground, + listBackground: styles.listBackground, + listFocusForeground: styles.listForeground, + listHoverForeground: styles.listForeground, + listInactiveFocusForeground: styles.listForeground, + listInactiveSelectionForeground: styles.listForeground, + listActiveSelectionForeground: styles.listForeground, + listFocusAndSelectionForeground: styles.listForeground, + listActiveSelectionIconForeground: undefined, + listInactiveSelectionIconForeground: undefined, + } + } + )); + + // Create scroll-down button + this._scrollDownButton = this._register(new Button(this._container, { + buttonBackground: asCssVariable(buttonSecondaryBackground), + buttonForeground: asCssVariable(buttonSecondaryForeground), + buttonHoverBackground: asCssVariable(buttonSecondaryHoverBackground), + buttonSecondaryBackground: undefined, + buttonSecondaryForeground: undefined, + buttonSecondaryHoverBackground: undefined, + buttonSeparator: undefined, + supportIcons: true, + })); + this._scrollDownButton.element.classList.add('chat-scroll-down'); + this._scrollDownButton.label = `$(${Codicon.arrowDown.id}) ${localize('scrollDown', "Scroll down")}`; + this._scrollDownButton.element.style.display = 'none'; // Hidden by default + + this._register(this._scrollDownButton.onDidClick(() => { + this.setScrollLock(true); + this.scrollToEnd(); + })); + + // Wire up tree events + + // Handle content height changes (fires high-level event, internal scroll handling) + this._register(this._tree.onDidChangeContentHeight(() => { + this.handleContentHeightChange(); + this._onDidChangeContentHeight.fire(); + })); + + this._register(this._tree.onDidFocus(() => { + this._onDidFocus.fire(); + })); + + // Handle focus changes internally (update mostRecentlyFocusedItemIndex) + this._register(this._tree.onDidChangeFocus(() => { + const focused = this.getFocus(); + if (focused && focused.length > 0) { + const focusedItem = focused[0]; + const items = this.getItems(); + const idx = items.findIndex(i => i === focusedItem); + if (idx !== -1) { + this._mostRecentlyFocusedItemIndex = idx; + } + } + })); + + // Handle scroll events (fire public event and manage scroll-down button) + this._register(this._tree.onDidScroll((e) => { + this._onDidScroll.fire(e); + this.updateScrollDownButtonVisibility(); + })); + + // Handle context menu internally + this._register(this._tree.onContextMenu(e => { + this.handleContextMenu(e); + })); + } + + //#region Internal event handlers + + /** + * Handle content height changes - auto-scroll if needed. + */ + private handleContentHeightChange(): void { + if (!this.hasScrollHeightChanged()) { + return; + } + const rendering = this._lastItem && isResponseVM(this._lastItem) && this._lastItem.renderData; + if (!rendering || this.scrollLock) { + if (this.wasLastElementVisible()) { + this._scrollAnimationFrameDisposable.value = dom.scheduleAtNextAnimationFrame(dom.getWindow(this._container), () => { + this.scrollToEnd(); + }, 0); + } + } + + this.updatePreviousScrollHeight(); + } + + /** + * Update scroll-down button visibility based on scroll position and scroll lock. + */ + private updateScrollDownButtonVisibility(): void { + const show = !this.isScrolledToBottom && !this._scrollLock; + this._scrollDownButton.element.style.display = show ? '' : 'none'; + } + + /** + * Handle context menu events. + */ + private handleContextMenu(e: ITreeContextMenuEvent): void { + e.browserEvent.preventDefault(); + e.browserEvent.stopPropagation(); + + const selected = e.element; + + // Check if the context menu was opened on a KaTeX element + const target = e.browserEvent.target as HTMLElement; + const isKatexElement = target.closest(`.${katexContainerClassName}`) !== null; + + const scopedContextKeyService = this.contextKeyService.createOverlay([ + [ChatContextKeys.responseIsFiltered.key, isResponseVM(selected) && !!selected.errorDetails?.responseIsFiltered], + [ChatContextKeys.isKatexMathElement.key, isKatexElement] + ]); + this.contextMenuService.showContextMenu({ + menuId: MenuId.ChatContext, + menuActionOptions: { shouldForwardArgs: true }, + contextKeyService: scopedContextKeyService, + getAnchor: () => e.anchor, + getActionsContext: () => selected, + }); + } + + //#endregion + + //#region ViewModel methods + + /** + * Set the view model for the list to render. + */ + setViewModel(viewModel: IChatViewModel | undefined): void { + this._viewModel = viewModel; + this._renderer.updateViewModel(viewModel); + this.refresh(); + } + + /** + * Refresh the list from the current view model. + * Uses internal state for diff identity calculation. + */ + refresh(): void { + if (!this._viewModel) { + this._tree.setChildren(null, []); + this._lastItem = undefined; + this._lastItemIdContextKey.set([]); + return; + } + + const items = this._viewModel.getItems(); + this._lastItem = items.at(-1); + this._lastItemIdContextKey.set(this._lastItem ? [this._lastItem.id] : []); + + const treeItems: ITreeElement[] = items.map(item => ({ + element: item, + collapsed: false, + collapsible: false, + })); + + const editing = this._viewModel.editing; + const checkpoint = this._viewModel.model?.checkpoint; + + this._tree.setChildren(null, treeItems, { + diffIdentityProvider: { + getId: (element) => { + return element.dataId + + // If a response is in the process of progressive rendering, we need to ensure that it will + // be re-rendered so progressive rendering is restarted, even if the model wasn't updated. + `${isResponseVM(element) && element.renderData ? `_${this._visibleChangeCount}` : ''}` + + // Re-render once content references are loaded + (isResponseVM(element) ? `_${element.contentReferences.length}` : '') + + // Re-render if element becomes hidden due to undo/redo + `_${element.shouldBeRemovedOnSend ? `${element.shouldBeRemovedOnSend.afterUndoStop || '1'}` : '0'}` + + // Re-render if we have an element currently being edited + `_${editing ? '1' : '0'}` + + // Re-render if we have an element currently being checkpointed + `_${checkpoint ? '1' : '0'}` + + // Re-render all if invoked by setting change + `_setting${this._settingChangeCounter}` + + // Rerender request if we got new content references in the response + // since this may change how we render the corresponding attachments in the request + (isRequestVM(element) && element.contentReferences ? `_${element.contentReferences?.length}` : ''); + }, + } + }); + } + + /** + * Set scroll lock state. + */ + setScrollLock(value: boolean): void { + this._scrollLock = value; + this.updateScrollDownButtonVisibility(); + } + + /** + * Get scroll lock state. + */ + get scrollLock(): boolean { + return this._scrollLock; + } + + /** + * Set the setting change counter (forces refresh). + */ + setSettingChangeCounter(value: number): void { + this._settingChangeCounter = value; + } + + /** + * Set the visible change count (for diff identity). + */ + setVisibleChangeCount(value: number): void { + this._visibleChangeCount = value; + } + + /** + * Scroll to reveal an element if editing. + */ + scrollToCurrentItem(currentElement: IChatRequestViewModel): void { + if (!this._viewModel?.editing || !currentElement) { + return; + } + if (!this._tree.hasElement(currentElement)) { + return; + } + const relativeTop = this._tree.getRelativeTop(currentElement); + if (relativeTop === null || relativeTop < 0 || relativeTop > 1) { + this._tree.reveal(currentElement, 0); + } + } + + //#endregion + + //#region Tree methods + + /** + * Rerender the tree. + */ + rerender(): void { + this._tree.rerender(); + } + + private getItems(): ChatTreeItem[] { + const items: ChatTreeItem[] = []; + const root = this._tree.getNode(null); + for (const child of root.children) { + if (child.element) { + items.push(child.element); + } + } + return items; + } + + + /** + * Delegate scroll events from a mouse wheel event to the tree. + */ + delegateScrollFromMouseWheelEvent(event: IMouseWheelEvent): void { + this._tree.delegateScrollFromMouseWheelEvent(event); + } + + /** + * Whether the tree has a specific element. + */ + hasElement(element: ChatTreeItem): boolean { + return this._tree.hasElement(element); + } + + /** + * Update the height of an element. + */ + updateElementHeight(element: ChatTreeItem, height?: number): void { + if (this._tree.hasElement(element) && this._visible) { + this._tree.updateElementHeight(element, height); + } + } + + /** + * Scroll to reveal an element. + */ + reveal(element: ChatTreeItem, relativeTop?: number): void { + this._tree.reveal(element, relativeTop); + } + + /** + * Get the focused elements. + */ + getFocus(): ChatTreeItem[] { + return this._tree.getFocus().filter((e): e is ChatTreeItem => e !== null); + } + + /** + * Set the focused elements. + */ + setFocus(elements: ChatTreeItem[]): void { + this._tree.setFocus(elements); + } + + focusItem(item: ChatTreeItem): void { + if (!this.hasElement(item)) { + return; + } + this._tree.setFocus([item]); + this._tree.domFocus(); + } + + /** + * Focus the last item in the list. Returns the index of the focused item. + * @param useMostRecentlyFocusedIndex If true, use the mostRecentlyFocusedIndex if valid + */ + focusLastItem(useMostRecentlyFocusedIndex?: boolean): number { + const items = this.getItems(); + if (items.length === 0) { + return -1; + } + + let focusIndex: number; + if (useMostRecentlyFocusedIndex && this._mostRecentlyFocusedItemIndex >= 0 && this._mostRecentlyFocusedItemIndex < items.length) { + focusIndex = this._mostRecentlyFocusedItemIndex; + } else { + focusIndex = items.length - 1; + } + + this._tree.setFocus([items[focusIndex]]); + this._tree.domFocus(); + return focusIndex; + } + + /** + * Scroll the list to reveal the last item. + */ + scrollToEnd(): void { + if (this._lastItem) { + const offset = Math.max(this._lastItem.currentRenderedHeight ?? 0, 1e6); + if (this._tree.hasElement(this._lastItem)) { + this._tree.reveal(this._lastItem, offset); + } + } + } + + private hasScrollHeightChanged(): boolean { + return this._tree.scrollHeight !== this._previousScrollHeight; + } + + private updatePreviousScrollHeight(): void { + this._previousScrollHeight = this._tree.scrollHeight; + } + + private wasLastElementVisible(): boolean { + // Due to rounding, the scrollTop + renderHeight will not exactly match the scrollHeight. + // Consider the tree to be scrolled all the way down if it is within 2px of the bottom. + return this._tree.scrollTop + this._tree.renderHeight >= this._previousScrollHeight - 2; + } + + /** + * Focus the list. + */ + focus(): void { + this._tree.domFocus(); + } + + /** + * Get the DOM focus state. + */ + isDOMFocused(): boolean { + return this._tree.isDOMFocused(); + } + + //#endregion + + //#region Renderer methods + + /** + * Get code block info for a response. + */ + getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] { + return this._renderer.getCodeBlockInfosForResponse(response); + } + + /** + * Get code block info by URI. + */ + getCodeBlockInfoForEditor(uri: URI): IChatCodeBlockInfo | undefined { + return this._renderer.getCodeBlockInfoForEditor(uri); + } + + /** + * Get file tree info for a response. + */ + getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[] { + return this._renderer.getFileTreeInfosForResponse(response); + } + + /** + * Get the last focused file tree for a response. + */ + getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined { + return this._renderer.getLastFocusedFileTreeForResponse(response); + } + + /** + * Get editors currently in use. + */ + editorsInUse(): Iterable { + return this._renderer.editorsInUse(); + } + + /** + * Get template data for a request ID. + */ + getTemplateDataForRequestId(requestId: string | undefined): IChatListItemTemplate | undefined { + if (!requestId) { + return undefined; + } + return this._renderer.getTemplateDataForRequestId(requestId); + } + + /** + * Update item height after rendering. + */ + updateItemHeightOnRender(element: ChatTreeItem, template: IChatListItemTemplate): void { + this._renderer.updateItemHeightOnRender(element, template); + } + + /** + * Update renderer options. + */ + updateRendererOptions(options: IChatListItemRendererOptions): void { + this._renderer.updateOptions(options); + } + + /** + * Set the visibility of the list. + */ + setVisible(visible: boolean): void { + this._visible = visible; + this._renderer.setVisible(visible); + } + + /** + * Layout the list. + */ + layout(height: number, width?: number): void { + // Set CSS variable for minimum response height + if (this._renderStyle === 'compact' || this._renderStyle === 'minimal') { + this._container.style.removeProperty('--chat-current-response-min-height'); + } else { + this._container.style.setProperty('--chat-current-response-min-height', height * .75 + 'px'); + } + this._tree.layout(height, width); + this._renderer.layout(width ?? this._container.clientWidth); + } + + //#endregion +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 42f602ace9f..754306fa18e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -8,14 +8,11 @@ import './media/chatAgentHover.css'; import './media/chatViewWelcome.css'; import * as dom from '../../../../../base/browser/dom.js'; import { IMouseWheelEvent } from '../../../../../base/browser/mouseEvent.js'; -import { Button } from '../../../../../base/browser/ui/button/button.js'; -import { ITreeContextMenuEvent, ITreeElement } from '../../../../../base/browser/ui/tree/tree.js'; import { disposableTimeout, timeout } from '../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { toErrorMessage } from '../../../../../base/common/errorMessage.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; -import { FuzzyScore } from '../../../../../base/common/filters.js'; import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../../base/common/iterator.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, thenIfNotDisposed } from '../../../../../base/common/lifecycle.js'; @@ -34,24 +31,20 @@ import { localize } from '../../../../../nls.js'; import { MenuId } from '../../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; + import { ITextResourceEditorInput } from '../../../../../platform/editor/common/editor.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; -import { WorkbenchObjectTree } from '../../../../../platform/list/browser/listService.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { bindContextKey } from '../../../../../platform/observable/common/platformObservableUtils.js'; import product from '../../../../../platform/product/common/product.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; -import { buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground } from '../../../../../platform/theme/common/colorRegistry.js'; -import { asCssVariable } from '../../../../../platform/theme/common/colorUtils.js'; import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; import { IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js'; import { EditorResourceAccessor } from '../../../../common/editor.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; -import { katexContainerClassName } from '../../../markdown/common/markedKatexExtension.js'; import { checkModeOption } from '../../common/chat.js'; import { IChatAgentAttachmentCapabilities, IChatAgentCommand, IChatAgentData, IChatAgentService } from '../../common/participants/chatAgents.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; @@ -66,7 +59,7 @@ import { IChatSessionsService } from '../../common/chatSessionsService.js'; import { IChatSlashCommandService } from '../../common/participants/chatSlashCommands.js'; import { IChatTodoListService } from '../../common/tools/chatTodoListService.js'; import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isWorkspaceVariableEntry, PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/attachments/chatVariableEntries.js'; -import { ChatViewModel, IChatRequestViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js'; +import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js'; import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; import { ILanguageModelToolsService, ToolSet } from '../../common/tools/languageModelToolsService.js'; @@ -76,11 +69,11 @@ import { IHandOff, PromptHeader, Target } from '../../common/promptSyntax/prompt import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js'; import { handleModeSwitch } from '../actions/chatActions.js'; import { ChatTreeItem, IChatAcceptInputOptions, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewModelChangeEvent, IChatWidgetViewOptions, isIChatResourceViewContext, isIChatViewViewContext } from '../chat.js'; -import { ChatAccessibilityProvider } from '../accessibility/chatAccessibilityProvider.js'; import { ChatAttachmentModel } from '../attachments/chatAttachmentModel.js'; import { ChatSuggestNextWidget } from './chatContentParts/chatSuggestNextWidget.js'; import { ChatInputPart, IChatInputPartOptions, IChatInputStyles } from './input/chatInputPart.js'; -import { ChatListDelegate, ChatListItemRenderer, IChatListItemTemplate, IChatRendererDelegate } from './chatListRenderer.js'; +import { IChatListItemTemplate } from './chatListRenderer.js'; +import { ChatListWidget } from './chatListWidget.js'; import { ChatEditorOptions } from './chatOptions.js'; import { ChatViewWelcomePart, IChatSuggestedPrompts, IChatViewWelcomeContent } from '../viewsWelcome/chatViewWelcomeController.js'; import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js'; @@ -221,14 +214,11 @@ export class ChatWidget extends Disposable implements IChatWidget { get domNode() { return this.container; } - private tree!: WorkbenchObjectTree; - private renderer!: ChatListItemRenderer; + private listWidget!: ChatListWidget; private readonly _codeBlockModelCollection: CodeBlockModelCollection; - private lastItem: ChatTreeItem | undefined; private readonly visibilityTimeoutDisposable: MutableDisposable = this._register(new MutableDisposable()); private readonly visibilityAnimationFrameDisposable: MutableDisposable = this._register(new MutableDisposable()); - private readonly scrollAnimationFrameDisposable: MutableDisposable = this._register(new MutableDisposable()); private readonly inputPartDisposable: MutableDisposable = this._register(new MutableDisposable()); private readonly inlineInputPartDisposable: MutableDisposable = this._register(new MutableDisposable()); @@ -254,14 +244,6 @@ export class ChatWidget extends Disposable implements IChatWidget { private _visible = false; get visible() { return this._visible; } - private previousTreeScrollHeight: number = 0; - - /** - * Whether the list is scroll-locked to the bottom. Initialize to true so that we can scroll to the bottom on first render. - * The initial render leads to a lot of `onDidChangeTreeContentHeight` as the renderer works out the real heights of rows. - */ - private scrollLock = true; - private _instructionFilesCheckPromise: Promise | undefined; private _instructionFilesExist: boolean | undefined; @@ -284,8 +266,6 @@ export class ChatWidget extends Disposable implements IChatWidget { private readonly promptUriCache = new Map(); private _isLoadingPromptDescriptions = false; - private _mostRecentlyFocusedItemIndex: number = -1; - private readonly viewModelDisposables = this._register(new DisposableStore()); private _viewModel: ChatViewModel | undefined; @@ -371,7 +351,6 @@ export class ChatWidget extends Disposable implements IChatWidget { @IChatService private readonly chatService: IChatService, @IChatAgentService private readonly chatAgentService: IChatAgentService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IContextMenuService private readonly contextMenuService: IContextMenuService, @IChatAccessibilityService private readonly chatAccessibilityService: IChatAccessibilityService, @ILogService private readonly logService: ILogService, @IThemeService private readonly themeService: IThemeService, @@ -523,7 +502,7 @@ export class ChatWidget extends Disposable implements IChatWidget { await timeout(0); // wait for list to actually render - for (const codeBlockPart of this.renderer.editorsInUse()) { + for (const codeBlockPart of this.listWidget.editorsInUse()) { if (extUri.isEqual(codeBlockPart.uri, resource, true)) { const editor = codeBlockPart.editor; @@ -612,7 +591,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } get contentHeight(): number { - return this.input.contentHeight + this.tree.contentHeight + this.chatSuggestNextWidget.height; + return this.input.contentHeight + this.listWidget.contentHeight + this.chatSuggestNextWidget.height; } get attachmentModel(): ChatAttachmentModel { @@ -652,20 +631,6 @@ export class ChatWidget extends Disposable implements IChatWidget { this.renderWelcomeViewContentIfNeeded(); this.createList(this.listContainer, { editable: !isInlineChat(this) && !isQuickChat(this), ...this.viewOptions.rendererOptions, renderStyle }); - const scrollDownButton = this._register(new Button(this.listContainer, { - supportIcons: true, - buttonBackground: asCssVariable(buttonSecondaryBackground), - buttonForeground: asCssVariable(buttonSecondaryForeground), - buttonHoverBackground: asCssVariable(buttonSecondaryHoverBackground), - })); - scrollDownButton.element.classList.add('chat-scroll-down'); - scrollDownButton.label = `$(${Codicon.chevronDown.id})`; - scrollDownButton.setTitle(localize('scrollDownButtonLabel', "Scroll down")); - this._register(scrollDownButton.onDidClick(() => { - this.scrollLock = true; - this.scrollToEnd(); - })); - // Update the font family and size this._register(autorun(reader => { const fontFamily = this.chatLayoutService.fontFamily.read(reader); @@ -675,7 +640,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.container.style.fontSize = `${fontSize}px`; if (this.visible) { - this.tree.rerender(); + this.listWidget.rerender(); } })); @@ -684,7 +649,7 @@ export class ChatWidget extends Disposable implements IChatWidget { // Do initial render if (this.viewModel) { this.onDidChangeItems(); - this.scrollToEnd(); + this.listWidget.scrollToEnd(); } this.contribs = ChatWidget.CONTRIBS.map(contrib => { @@ -729,15 +694,6 @@ export class ChatWidget extends Disposable implements IChatWidget { } } - private scrollToEnd() { - if (this.lastItem) { - const offset = Math.max(this.lastItem.currentRenderedHeight ?? 0, 1e6); - if (this.tree.hasElement(this.lastItem)) { - this.tree.reveal(this.lastItem, offset); - } - } - } - focusInput(): void { this.input.focus(); @@ -809,17 +765,9 @@ export class ChatWidget extends Disposable implements IChatWidget { private onDidChangeItems(skipDynamicLayout?: boolean) { if (this._visible || !this.viewModel) { - const treeItems = (this.viewModel?.getItems() ?? []) - .map((item): ITreeElement => { - return { - element: item, - collapsed: false, - collapsible: false - }; - }); + const items = this.viewModel?.getItems() ?? []; - - if (treeItems.length > 0) { + if (items.length > 0) { this.updateChatViewVisibility(); } else { this.renderWelcomeViewContentIfNeeded(); @@ -827,33 +775,10 @@ export class ChatWidget extends Disposable implements IChatWidget { this._onWillMaybeChangeHeight.fire(); - this.lastItem = treeItems.at(-1)?.element; - ChatContextKeys.lastItemId.bindTo(this.contextKeyService).set(this.lastItem ? [this.lastItem.id] : []); - this.tree.setChildren(null, treeItems, { - diffIdentityProvider: { - getId: (element) => { - return element.dataId + - // Ensure re-rendering an element once slash commands are loaded, so the colorization can be applied. - `${(isRequestVM(element)) /* && !!this.lastSlashCommands ? '_scLoaded' : '' */}` + - // If a response is in the process of progressive rendering, we need to ensure that it will - // be re-rendered so progressive rendering is restarted, even if the model wasn't updated. - `${isResponseVM(element) && element.renderData ? `_${this.visibleChangeCount}` : ''}` + - // Re-render once content references are loaded - (isResponseVM(element) ? `_${element.contentReferences.length}` : '') + - // Re-render if element becomes hidden due to undo/redo - `_${element.shouldBeRemovedOnSend ? `${element.shouldBeRemovedOnSend.afterUndoStop || '1'}` : '0'}` + - // Re-render if we have an element currently being edited - `_${this.viewModel?.editing ? '1' : '0'}` + - // Re-render if we have an element currently being checkpointed - `_${this.viewModel?.model.checkpoint ? '1' : '0'}` + - // Re-render all if invoked by setting change - `_setting${this.settingChangeCounter || '0'}` + - // Rerender request if we got new content references in the response - // since this may change how we render the corresponding attachments in the request - (isRequestVM(element) && element.contentReferences ? `_${element.contentReferences?.length}` : ''); - }, - } - }); + // Update list widget state and refresh + this.listWidget.setVisibleChangeCount(this.visibleChangeCount); + this.listWidget.setSettingChangeCounter(this.settingChangeCounter); + this.listWidget.refresh(); if (!skipDynamicLayout && this._dynamicMessageLayoutData) { this.layoutDynamicChatTreeItemMode(); @@ -1212,8 +1137,9 @@ export class ChatWidget extends Disposable implements IChatWidget { } private async renderFollowups(): Promise { - if (this.lastItem && isResponseVM(this.lastItem) && this.lastItem.isComplete) { - this.input.renderFollowups(this.lastItem.replyFollowups, this.lastItem); + const lastItem = this.listWidget.lastItem; + if (lastItem && isResponseVM(lastItem) && lastItem.isComplete) { + this.input.renderFollowups(lastItem.replyFollowups, lastItem); } else { this.input.renderFollowups(undefined, undefined); } @@ -1427,7 +1353,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const wasVisible = this._visible; this._visible = visible; this.visibleChangeCount++; - this.renderer.setVisible(visible); + this.listWidget.setVisible(visible); this.input.setVisible(visible); if (visible) { @@ -1450,35 +1376,41 @@ export class ChatWidget extends Disposable implements IChatWidget { } private createList(listContainer: HTMLElement, options: IChatListItemRendererOptions): void { - const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService]))); - const delegate = scopedInstantiationService.createInstance(ChatListDelegate, this.viewOptions.defaultElementHeight ?? 200); - const rendererDelegate: IChatRendererDelegate = { - getListLength: () => this.tree.getNode(null).visibleChildrenCount, - onDidScroll: this.onDidScroll, - container: listContainer, - currentChatMode: () => this.input.currentModeKind, - }; - // Create a dom element to hold UI from editor widgets embedded in chat messages const overflowWidgetsContainer = document.createElement('div'); overflowWidgetsContainer.classList.add('chat-overflow-widget-container', 'monaco-editor'); listContainer.append(overflowWidgetsContainer); - this.renderer = this._register(scopedInstantiationService.createInstance( - ChatListItemRenderer, - this.editorOptions, - options, - rendererDelegate, - this._codeBlockModelCollection, - overflowWidgetsContainer, - this.viewModel, + // Create chat list widget + this.listWidget = this._register(this.instantiationService.createInstance( + ChatListWidget, + listContainer, + { + rendererOptions: options, + renderStyle: this.viewOptions.renderStyle, + defaultElementHeight: this.viewOptions.defaultElementHeight ?? 200, + overflowWidgetsDomNode: overflowWidgetsContainer, + styles: { + listForeground: this.styles.listForeground, + listBackground: this.styles.listBackground, + }, + currentChatMode: () => this.input.currentModeKind, + filter: this.viewOptions.filter ? { filter: this.viewOptions.filter.bind(this.viewOptions) } : undefined, + codeBlockModelCollection: this._codeBlockModelCollection, + viewModel: this.viewModel, + editorOptions: this.editorOptions, + location: this.location, + getCurrentLanguageModelId: () => this.input.currentLanguageModel, + getCurrentModeInfo: () => this.input.currentModeInfo, + } )); - this._register(this.renderer.onDidClickRequest(async item => { + // Wire up ChatWidget-specific list widget events + this._register(this.listWidget.onDidClickRequest(async item => { this.clickedRequest(item); })); - this._register(this.renderer.onDidRerender(item => { + this._register(this.listWidget.onDidRerender(item => { if (isRequestVM(item.currentElement) && this.configurationService.getValue('chat.editRequests') !== 'input') { if (!item.rowContainer.contains(this.inputContainer)) { item.rowContainer.appendChild(this.inputContainer); @@ -1487,103 +1419,33 @@ export class ChatWidget extends Disposable implements IChatWidget { } })); - this._register(this.renderer.onDidDispose((item) => { + this._register(this.listWidget.onDidDispose(() => { this.focusedInputDOM.appendChild(this.inputContainer); this.input.focus(); })); - this._register(this.renderer.onDidFocusOutside(() => { + this._register(this.listWidget.onDidFocusOutside(() => { this.finishedEditing(); })); - this._register(this.renderer.onDidClickFollowup(item => { + this._register(this.listWidget.onDidClickFollowup(item => { // is this used anymore? this.acceptInput(item.message); })); - this._register(this.renderer.onDidClickRerunWithAgentOrCommandDetection(e => { - const request = this.chatService.getSession(e.sessionResource)?.getRequests().find(candidate => candidate.id === e.requestId); - if (request) { - const options: IChatSendRequestOptions = { - noCommandDetection: true, - attempt: request.attempt + 1, - location: this.location, - userSelectedModelId: this.input.currentLanguageModel, - modeInfo: this.input.currentModeInfo, - }; - this.chatService.resendRequest(request, options).catch(e => this.logService.error('FAILED to rerun request', e)); - } - })); - this.tree = this._register(scopedInstantiationService.createInstance( - WorkbenchObjectTree, - 'Chat', - listContainer, - delegate, - [this.renderer], - { - identityProvider: { getId: (e: ChatTreeItem) => e.id }, - horizontalScrolling: false, - alwaysConsumeMouseWheel: false, - supportDynamicHeights: true, - hideTwistiesOfChildlessElements: true, - accessibilityProvider: this.instantiationService.createInstance(ChatAccessibilityProvider), - keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: ChatTreeItem) => isRequestVM(e) ? e.message : isResponseVM(e) ? e.response.value : '' }, // TODO - setRowLineHeight: false, - filter: this.viewOptions.filter ? { filter: this.viewOptions.filter.bind(this.viewOptions), } : undefined, - scrollToActiveElement: true, - overrideStyles: { - listFocusBackground: this.styles.listBackground, - listInactiveFocusBackground: this.styles.listBackground, - listActiveSelectionBackground: this.styles.listBackground, - listFocusAndSelectionBackground: this.styles.listBackground, - listInactiveSelectionBackground: this.styles.listBackground, - listHoverBackground: this.styles.listBackground, - listBackground: this.styles.listBackground, - listFocusForeground: this.styles.listForeground, - listHoverForeground: this.styles.listForeground, - listInactiveFocusForeground: this.styles.listForeground, - listInactiveSelectionForeground: this.styles.listForeground, - listActiveSelectionForeground: this.styles.listForeground, - listFocusAndSelectionForeground: this.styles.listForeground, - listActiveSelectionIconForeground: undefined, - listInactiveSelectionIconForeground: undefined, - } - })); - - this._register(this.tree.onDidChangeFocus(() => { - const focused = this.tree.getFocus(); - if (focused && focused.length > 0) { - const focusedItem = focused[0]; - const items = this.tree.getNode(null).children; - const idx = items.findIndex(i => i.element === focusedItem); - if (idx !== -1) { - this._mostRecentlyFocusedItemIndex = idx; - } - } + this._register(this.listWidget.onDidChangeContentHeight(() => { + this._onDidChangeContentHeight.fire(); })); - this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); - - this._register(this.tree.onDidChangeContentHeight(() => { - this.onDidChangeTreeContentHeight(); - })); - this._register(this.renderer.onDidChangeItemHeight(e => { - if (this.tree.hasElement(e.element) && this.visible) { - this.tree.updateElementHeight(e.element, e.height); - } - })); - this._register(this.tree.onDidFocus(() => { + this._register(this.listWidget.onDidFocus(() => { this._onDidFocus.fire(); })); - this._register(this.tree.onDidScroll(() => { + this._register(this.listWidget.onDidScroll(() => { this._onDidScroll.fire(); - - const isScrolledDown = this.tree.scrollTop >= this.tree.scrollHeight - this.tree.renderHeight - 2; - this.container.classList.toggle('show-scroll-down', !isScrolledDown && !this.scrollLock); })); } startEditing(requestId: string): void { - const editedRequest = this.renderer.getTemplateDataForRequestId(requestId); + const editedRequest = this.listWidget.getTemplateDataForRequestId(requestId); if (editedRequest) { this.clickedRequest(editedRequest); } @@ -1657,7 +1519,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.inputPart.dnd.setDisabledOverlay(!isInput); this.input.renderAttachedContext(); this.input.setValue(currentElement.messageText, false); - this.renderer.updateItemHeightOnRender(currentElement, item); + this.listWidget.updateItemHeightOnRender(currentElement, item); this.onDidChangeItems(); this.input.inputEditor.focus(); @@ -1670,11 +1532,11 @@ export class ChatWidget extends Disposable implements IChatWidget { // listeners if (!isInput) { this._register(this.inlineInputPart.inputEditor.onDidChangeModelContent(() => { - this.scrollToCurrentItem(currentElement); + this.listWidget.scrollToCurrentItem(currentElement); })); this._register(this.inlineInputPart.inputEditor.onDidChangeCursorSelection((e) => { - this.scrollToCurrentItem(currentElement); + this.listWidget.scrollToCurrentItem(currentElement); })); } } @@ -1694,7 +1556,7 @@ export class ChatWidget extends Disposable implements IChatWidget { finishedEditing(completedEdit?: boolean): void { // reset states - const editedRequest = this.renderer.getTemplateDataForRequestId(this.viewModel?.editing?.id); + const editedRequest = this.listWidget.getTemplateDataForRequestId(this.viewModel?.editing?.id); if (this.recentlyRestoredCheckpoint) { this.recentlyRestoredCheckpoint = false; } else { @@ -1739,7 +1601,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.onDidChangeItems(); if (editedRequest?.currentElement) { - this.renderer.updateItemHeightOnRender(editedRequest.currentElement, editedRequest); + this.listWidget.updateItemHeightOnRender(editedRequest.currentElement, editedRequest); } type CancelRequestEditEvent = { @@ -1762,69 +1624,6 @@ export class ChatWidget extends Disposable implements IChatWidget { this.inputPart.focus(); } - private scrollToCurrentItem(currentElement: IChatRequestViewModel): void { - if (this.viewModel?.editing && currentElement) { - const element = currentElement; - if (!this.tree.hasElement(element)) { - return; - } - const relativeTop = this.tree.getRelativeTop(element); - if (relativeTop === null || relativeTop < 0 || relativeTop > 1) { - this.tree.reveal(element, 0); - } - } - } - - private onContextMenu(e: ITreeContextMenuEvent): void { - e.browserEvent.preventDefault(); - e.browserEvent.stopPropagation(); - - const selected = e.element; - - // Check if the context menu was opened on a KaTeX element - const target = e.browserEvent.target as HTMLElement; - const isKatexElement = target.closest(`.${katexContainerClassName}`) !== null; - - const scopedContextKeyService = this.contextKeyService.createOverlay([ - [ChatContextKeys.responseIsFiltered.key, isResponseVM(selected) && !!selected.errorDetails?.responseIsFiltered], - [ChatContextKeys.isKatexMathElement.key, isKatexElement] - ]); - this.contextMenuService.showContextMenu({ - menuId: MenuId.ChatContext, - menuActionOptions: { shouldForwardArgs: true }, - contextKeyService: scopedContextKeyService, - getAnchor: () => e.anchor, - getActionsContext: () => selected, - }); - } - - private onDidChangeTreeContentHeight(): void { - // If the list was previously scrolled all the way down, ensure it stays scrolled down, if scroll lock is on - if (this.tree.scrollHeight !== this.previousTreeScrollHeight) { - const lastItem = this.viewModel?.getItems().at(-1); - const lastResponseIsRendering = isResponseVM(lastItem) && lastItem.renderData; - if (!lastResponseIsRendering || this.scrollLock) { - // Due to rounding, the scrollTop + renderHeight will not exactly match the scrollHeight. - // Consider the tree to be scrolled all the way down if it is within 2px of the bottom. - const lastElementWasVisible = this.tree.scrollTop + this.tree.renderHeight >= this.previousTreeScrollHeight - 2; - if (lastElementWasVisible) { - this.scrollAnimationFrameDisposable.value = dom.scheduleAtNextAnimationFrame(dom.getWindow(this.listContainer), () => { - // Can't set scrollTop during this event listener, the list might overwrite the change - - this.scrollToEnd(); - }, 0); - } - } - } - - // TODO@roblourens add `show-scroll-down` class when button should show - // Show the button when content height changes, the list is not fully scrolled down, and (the latest response is currently rendering OR I haven't yet scrolled all the way down since the last response) - // So for example it would not reappear if I scroll up and delete a message - - this.previousTreeScrollHeight = this.tree.scrollHeight; - this._onDidChangeContentHeight.fire(); - } - private getWidgetViewKindTag(): string { if (!this.viewContext) { return 'editor'; @@ -1855,7 +1654,7 @@ export class ChatWidget extends Disposable implements IChatWidget { }; if (this.viewModel?.editing) { - const editedRequest = this.renderer.getTemplateDataForRequestId(this.viewModel?.editing?.id); + const editedRequest = this.listWidget.getTemplateDataForRequestId(this.viewModel?.editing?.id); const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, editedRequest?.contextKeyService]))); this.inlineInputPartDisposable.value = scopedInstantiationService.createInstance(ChatInputPart, this.location, @@ -1921,9 +1720,9 @@ export class ChatWidget extends Disposable implements IChatWidget { }); })); this._register(this.input.onDidChangeHeight(() => { - const editedRequest = this.renderer.getTemplateDataForRequestId(this.viewModel?.editing?.id); + const editedRequest = this.listWidget.getTemplateDataForRequestId(this.viewModel?.editing?.id); if (isRequestVM(editedRequest?.currentElement) && this.viewModel?.editing) { - this.renderer.updateItemHeightOnRender(editedRequest?.currentElement, editedRequest); + this.listWidget.updateItemHeightOnRender(editedRequest?.currentElement, editedRequest); } if (this.bodyDimension) { @@ -2036,7 +1835,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.onDidChangeItems(); if (events?.some(e => e?.kind === 'addRequest') && this.visible) { - this.scrollToEnd(); + this.listWidget.scrollToEnd(); } }))); this.viewModelDisposables.add(this.viewModel.onDidDisposeModel(() => { @@ -2080,34 +1879,30 @@ export class ChatWidget extends Disposable implements IChatWidget { } })); - if (this.tree && this.visible) { + if (this.listWidget && this.visible) { this.onDidChangeItems(); - this.scrollToEnd(); + this.listWidget.scrollToEnd(); } - this.renderer.updateViewModel(this.viewModel); + this.listWidget.setViewModel(this.viewModel); this.updateChatInputContext(); this.input.renderChatTodoListWidget(this.viewModel.sessionResource); } getFocus(): ChatTreeItem | undefined { - return this.tree.getFocus()[0] ?? undefined; + return this.listWidget.getFocus()[0] ?? undefined; } reveal(item: ChatTreeItem, relativeTop?: number): void { - this.tree.reveal(item, relativeTop); + this.listWidget.reveal(item, relativeTop); } focus(item: ChatTreeItem): void { - const items = this.tree.getNode(null).children; - const node = items.find(i => i.element?.id === item.id); - if (!node) { + if (!this.listWidget.hasElement(item)) { return; } - this._mostRecentlyFocusedItemIndex = items.indexOf(node); - this.tree.setFocus([node.element]); - this.tree.domFocus(); + this.listWidget.focusItem(item); } setInputPlaceholder(placeholder: string): void { @@ -2144,9 +1939,9 @@ export class ChatWidget extends Disposable implements IChatWidget { // Update capabilities for the locked agent const agent = this.chatAgentService.getAgent(agentId); this._updateAgentCapabilitiesContextKeys(agent); - this.renderer.updateOptions({ restorable: false, editable: false, noFooter: true, progressMessageAtBottomOfResponse: true }); + this.listWidget.updateRendererOptions({ restorable: false, editable: false, noFooter: true, progressMessageAtBottomOfResponse: true }); if (this.visible) { - this.tree.rerender(); + this.listWidget.rerender(); } } @@ -2164,9 +1959,9 @@ export class ChatWidget extends Disposable implements IChatWidget { this.viewModel.resetInputPlaceholder(); } this.inputEditor.updateOptions({ placeholder: undefined }); - this.renderer.updateOptions({ restorable: true, editable: true, noFooter: false, progressMessageAtBottomOfResponse: mode => mode !== ChatModeKind.Ask }); + this.listWidget.updateRendererOptions({ restorable: true, editable: true, noFooter: false, progressMessageAtBottomOfResponse: mode => mode !== ChatModeKind.Ask }); if (this.visible) { - this.tree.rerender(); + this.listWidget.rerender(); } } @@ -2261,7 +2056,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } this._onDidAcceptInput.fire(); - this.scrollLock = this.isLockedToCodingAgent || !!checkModeOption(this.input.currentModeKind, this.viewOptions.autoScroll); + this.listWidget.setScrollLock(this.isLockedToCodingAgent || !!checkModeOption(this.input.currentModeKind, this.viewOptions.autoScroll)); const editorValue = this.getInput(); const requestInputs: IChatRequestInputOptions = { @@ -2381,38 +2176,23 @@ export class ChatWidget extends Disposable implements IChatWidget { } getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] { - return this.renderer.getCodeBlockInfosForResponse(response); + return this.listWidget.getCodeBlockInfosForResponse(response); } getCodeBlockInfoForEditor(uri: URI): IChatCodeBlockInfo | undefined { - return this.renderer.getCodeBlockInfoForEditor(uri); + return this.listWidget.getCodeBlockInfoForEditor(uri); } getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[] { - return this.renderer.getFileTreeInfosForResponse(response); + return this.listWidget.getFileTreeInfosForResponse(response); } getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined { - return this.renderer.getLastFocusedFileTreeForResponse(response); + return this.listWidget.getLastFocusedFileTreeForResponse(response); } focusResponseItem(lastFocused?: boolean): void { - if (!this.viewModel) { - return; - } - const items = this.tree.getNode(null).children; - let item; - if (lastFocused) { - item = items[this._mostRecentlyFocusedItemIndex] ?? items[items.length - 1]; - } else { - item = items[items.length - 1]; - } - if (!item) { - return; - } - - this.tree.setFocus([item.element]); - this.tree.domFocus(); + this.listWidget.focusLastItem(lastFocused); } layout(height: number, width: number): void { @@ -2430,27 +2210,22 @@ export class ChatWidget extends Disposable implements IChatWidget { const inputHeight = this.inputPart.inputPartHeight; const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height; - const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight - 2; - const lastItem = this.viewModel?.getItems().at(-1); + const lastElementVisible = this.listWidget.isScrolledToBottom; + const lastItem = this.listWidget.lastItem; const contentHeight = Math.max(0, height - inputHeight - chatSuggestNextWidgetHeight); - if (this.viewOptions.renderStyle === 'compact' || this.viewOptions.renderStyle === 'minimal') { - this.listContainer.style.removeProperty('--chat-current-response-min-height'); - } else { - this.listContainer.style.setProperty('--chat-current-response-min-height', contentHeight * .75 + 'px'); - if (heightUpdated && lastItem && this.visible && this.tree.hasElement(lastItem)) { - this.tree.updateElementHeight(lastItem, undefined); + if (this.viewOptions.renderStyle !== 'compact' && this.viewOptions.renderStyle !== 'minimal') { + if (heightUpdated && lastItem && this.visible && this.listWidget.hasElement(lastItem)) { + this.listWidget.updateElementHeight(lastItem, undefined); } } - this.tree.layout(contentHeight, width); + this.listWidget.layout(contentHeight, width); this.welcomeMessageContainer.style.height = `${contentHeight}px`; - this.renderer.layout(width); - const lastResponseIsRendering = isResponseVM(lastItem) && lastItem.renderData; if (lastElementVisible && (!lastResponseIsRendering || checkModeOption(this.input.currentModeKind, this.viewOptions.autoScroll))) { - this.scrollToEnd(); + this.listWidget.scrollToEnd(); } this.listContainer.style.height = `${contentHeight}px`; @@ -2465,10 +2240,10 @@ export class ChatWidget extends Disposable implements IChatWidget { // TODO@TylerLeonhardt: This could use some refactoring to make it clear which layout strategy is being used setDynamicChatTreeItemLayout(numOfChatTreeItems: number, maxHeight: number) { this._dynamicMessageLayoutData = { numOfMessages: numOfChatTreeItems, maxHeight, enabled: true }; - this._register(this.renderer.onDidChangeItemHeight(() => this.layoutDynamicChatTreeItemMode())); + this._register(this.listWidget.onDidChangeItemHeight(() => this.layoutDynamicChatTreeItemMode())); const mutableDisposable = this._register(new MutableDisposable()); - this._register(this.tree.onDidScroll((e) => { + this._register(this.listWidget.onDidScroll((e) => { // TODO@TylerLeonhardt this should probably just be disposed when this is disabled // and then set up again when it is enabled again if (!this._dynamicMessageLayoutData?.enabled) { @@ -2554,7 +2329,7 @@ export class ChatWidget extends Disposable implements IChatWidget { ); if (needsRerender || !listHeight) { - this.scrollToEnd(); + this.listWidget.scrollToEnd(); } } @@ -2630,6 +2405,6 @@ export class ChatWidget extends Disposable implements IChatWidget { } delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent): void { - this.tree.delegateScrollFromMouseWheelEvent(browserEvent); + this.listWidget.delegateScrollFromMouseWheelEvent(browserEvent); } } From dee9a8da3c98844d3892b8327491c27caf9fc5d1 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 15 Jan 2026 14:55:32 -0800 Subject: [PATCH 034/387] chat: show chat contents when hovering agent sessions --- src/vs/base/browser/ui/hover/hover.ts | 6 + src/vs/platform/hover/browser/hoverService.ts | 1 + .../agentSessions/agentSessionHoverWidget.ts | 196 ++++++++++++++++++ .../agentSessions/agentSessionsControl.ts | 2 +- .../agentSessions/agentSessionsViewer.ts | 153 ++------------ .../chat/browser/widget/media/chat.css | 4 + 6 files changed, 224 insertions(+), 138 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts diff --git a/src/vs/base/browser/ui/hover/hover.ts b/src/vs/base/browser/ui/hover/hover.ts index 50510f6e9a6..0926751505b 100644 --- a/src/vs/base/browser/ui/hover/hover.ts +++ b/src/vs/base/browser/ui/hover/hover.ts @@ -235,6 +235,12 @@ export interface IHoverOptions { * Options that define how the hover looks. */ appearance?: IHoverAppearanceOptions; + + /** + * An optional callback that is called when the hover is shown. This is called + * later for delayed hovers. + */ + onDidShow?(): void; } // `target` is ignored for delayed hover methods as it's included in the method and added diff --git a/src/vs/platform/hover/browser/hoverService.ts b/src/vs/platform/hover/browser/hoverService.ts index 8759f177d08..8a1e4fa9690 100644 --- a/src/vs/platform/hover/browser/hoverService.ts +++ b/src/vs/platform/hover/browser/hoverService.ts @@ -300,6 +300,7 @@ export class HoverService extends Disposable implements IHoverService { new HoverContextViewDelegate(hover, focus), options.container ); + options.onDidShow?.(); } hideHover(force?: boolean): void { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts new file mode 100644 index 00000000000..efe969bde8d --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts @@ -0,0 +1,196 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../base/browser/dom.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { IAgentSession, getAgentChangesSummary, hasValidDiff, AgentSessionStatus } from './agentSessionsModel.js'; +import { IChatService } from '../../common/chatService/chatService.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ChatListWidget } from '../widget/chatListWidget.js'; +import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; +import { ChatViewModel } from '../../common/model/chatViewModel.js'; +import { ChatModeKind } from '../../common/constants.js'; +import { IChatWidgetService } from '../chat.js'; +import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { localize } from '../../../../../nls.js'; +import { fromNow, getDurationString } from '../../../../../base/common/date.js'; +import { IChatModel } from '../../common/model/chatModel.js'; + +export class AgentSessionHoverWidget extends Disposable { + + public readonly domNode: HTMLElement; + private modelRef: Promise; + + constructor( + private readonly session: IAgentSession, + @IChatService private readonly chatService: IChatService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + ) { + super(); + this.domNode = dom.$('.agent-session-hover.interactive-session'); + this.domNode.style.width = '500px'; + this.domNode.style.height = '300px'; + this.domNode.style.overflow = 'hidden'; + + this.modelRef = this.chatService.getOrRestoreSession(session.resource).then(modelRef => { + if (this._store.isDisposed) { + modelRef?.dispose(); + return; + } + + if (!modelRef) { + // Show fallback tooltip text + const tooltip = this.buildFallbackTooltip(this.session); + this.domNode.textContent = typeof tooltip === 'string' ? tooltip : tooltip.value; + return; + } + + this._register(modelRef); + return modelRef.object; + }); + } + + public async onRendered() { + const model = await this.modelRef; + if (!model || this._store.isDisposed) { + return; + } + + // Create view model + const codeBlockCollection = this._register(this.instantiationService.createInstance(CodeBlockModelCollection, 'agentSessionHover')); + const viewModel = this._register(this.instantiationService.createInstance( + ChatViewModel, + model, + codeBlockCollection + )); + + // Create the chat list widget + const container = dom.append(this.domNode, dom.$('.interactive-list')); + const listWidget = this._register(this.instantiationService.createInstance( + ChatListWidget, + container, + { + rendererOptions: { + renderStyle: 'compact', + noHeader: true, + editableCodeBlock: false, + }, + currentChatMode: () => ChatModeKind.Ask, + } + )); + listWidget.setViewModel(viewModel); + listWidget.layout(300, 500); + + // Handle followup clicks - open the session and accept input + this._register(listWidget.onDidClickFollowup(async (followup) => { + const widget = await this.chatWidgetService.openSession(model.sessionResource); + if (widget) { + widget.acceptInput(followup.message); + } + })); + } + + private buildFallbackTooltip(session: IAgentSession): IMarkdownString { + const lines: string[] = []; + + // Title + lines.push(`**${session.label}**`); + + // Tooltip (from provider) + if (session.tooltip) { + const tooltip = typeof session.tooltip === 'string' ? session.tooltip : session.tooltip.value; + lines.push(tooltip); + } else { + + // Description + if (session.description) { + const description = typeof session.description === 'string' ? session.description : session.description.value; + lines.push(description); + } + + // Badge + if (session.badge) { + const badge = typeof session.badge === 'string' ? session.badge : session.badge.value; + lines.push(badge); + } + } + + // Details line: Status • Provider • Duration/Time + const details: string[] = []; + + // Status + details.push(this.toStatusLabel(session.status)); + + // Provider + details.push(session.providerLabel); + + // Duration or start time + if (session.timing.finishedOrFailedTime && session.timing.inProgressTime) { + const duration = this.toDuration(session.timing.inProgressTime, session.timing.finishedOrFailedTime, true); + if (duration) { + details.push(duration); + } + } else { + const startTime = session.timing.lastRequestStarted ?? session.timing.created; + details.push(fromNow(startTime, true, true)); + } + + lines.push(details.join(' • ')); + + // Diff information + const diff = getAgentChangesSummary(session.changes); + if (diff && hasValidDiff(session.changes)) { + const diffParts: string[] = []; + if (diff.files > 0) { + diffParts.push(diff.files === 1 ? localize('tooltip.file', "1 file") : localize('tooltip.files', "{0} files", diff.files)); + } + if (diff.insertions > 0) { + diffParts.push(`+${diff.insertions}`); + } + if (diff.deletions > 0) { + diffParts.push(`-${diff.deletions}`); + } + if (diffParts.length > 0) { + lines.push(`$(diff) ${diffParts.join(', ')}`); + } + } + + // Archived status + if (session.isArchived()) { + lines.push(`$(archive) ${localize('tooltip.archived', "Archived")}`); + } + + return new MarkdownString(lines.join('\n\n'), { supportThemeIcons: true }); + } + + private toDuration(startTime: number, endTime: number, useFullTimeWords: boolean): string | undefined { + const elapsed = Math.round((endTime - startTime) / 1000) * 1000; + if (elapsed < 1000) { + return undefined; + } + + return getDurationString(elapsed, useFullTimeWords); + } + + private toStatusLabel(status: AgentSessionStatus): string { + let statusLabel: string; + switch (status) { + case AgentSessionStatus.NeedsInput: + statusLabel = localize('agentSessionNeedsInput', "Needs Input"); + break; + case AgentSessionStatus.InProgress: + statusLabel = localize('agentSessionInProgress', "In Progress"); + break; + case AgentSessionStatus.Failed: + statusLabel = localize('agentSessionFailed', "Failed"); + break; + default: + statusLabel = localize('agentSessionCompleted', "Completed"); + } + + return statusLabel; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 0777b28e33c..16787222ab6 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -125,7 +125,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo new AgentSessionsListDelegate(), new AgentSessionsCompressionDelegate(), [ - this.instantiationService.createInstance(AgentSessionRenderer, this.options), + this._register(this.instantiationService.createInstance(AgentSessionRenderer, this.options)), this.instantiationService.createInstance(AgentSessionSectionRenderer), ], new AgentSessionsDataSource(this.options.filter, sorter), diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index f39bfe4b357..44e476a7444 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import './media/agentsessionsviewer.css'; -import * as dom from '../../../../../base/browser/dom.js'; import { h } from '../../../../../base/browser/dom.js'; import { localize } from '../../../../../nls.js'; import { IIdentityProvider, IListVirtualDelegate } from '../../../../../base/browser/ui/list/list.js'; @@ -13,7 +12,7 @@ import { ITreeCompressionDelegate } from '../../../../../base/browser/ui/tree/as import { ICompressedTreeNode } from '../../../../../base/browser/ui/tree/compressedObjectTreeModel.js'; import { ICompressibleKeyboardNavigationLabelProvider, ICompressibleTreeRenderer } from '../../../../../base/browser/ui/tree/objectTree.js'; import { ITreeNode, ITreeElementRenderDetails, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction } from '../../../../../base/browser/ui/tree/tree.js'; -import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { AgentSessionSection, AgentSessionStatus, getAgentChangesSummary, hasValidDiff, IAgentSession, IAgentSessionSection, IAgentSessionsModel, isAgentSession, isAgentSessionSection, isAgentSessionsModel, isLocalAgentSessionItem, isSessionInProgressStatus } from './agentSessionsModel.js'; import { IconLabel } from '../../../../../base/browser/ui/iconLabel/iconLabel.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; @@ -28,7 +27,7 @@ import { ListViewTargetSector } from '../../../../../base/browser/ui/list/listVi import { coalesce } from '../../../../../base/common/arrays.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { fillEditorsDragData } from '../../../../browser/dnd.js'; -import { HoverStyle } from '../../../../../base/browser/ui/hover/hover.js'; +import { HoverStyle, IDelayedHoverOptions } from '../../../../../base/browser/ui/hover/hover.js'; import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { IntervalTimer } from '../../../../../base/common/async.js'; @@ -40,12 +39,7 @@ import { ServiceCollection } from '../../../../../platform/instantiation/common/ import { Event } from '../../../../../base/common/event.js'; import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; import { MarkdownString, IMarkdownString } from '../../../../../base/common/htmlContent.js'; -import { ChatListWidget } from '../widget/chatListWidget.js'; -import { IChatService } from '../../common/chatService/chatService.js'; -import { IChatWidgetService } from '../chat.js'; -import { ChatViewModel } from '../../common/model/chatViewModel.js'; -import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; -import { ChatModeKind } from '../../common/constants.js'; +import { AgentSessionHoverWidget } from './agentSessionHoverWidget.js'; export type AgentSessionListItem = IAgentSession | IAgentSessionSection; @@ -83,12 +77,14 @@ export interface IAgentSessionRendererOptions { getHoverPosition(): HoverPosition; } -export class AgentSessionRenderer implements ICompressibleTreeRenderer { +export class AgentSessionRenderer extends Disposable implements ICompressibleTreeRenderer { static readonly TEMPLATE_ID = 'agent-session'; readonly templateId = AgentSessionRenderer.TEMPLATE_ID; + private readonly _sessionHover = this._register(new MutableDisposable()); + constructor( private readonly options: IAgentSessionRendererOptions, @IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService, @@ -96,9 +92,9 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer { - if (!modelRef) { - // Show fallback tooltip text - const tooltip = this.buildTooltip(session); - container.textContent = typeof tooltip === 'string' ? tooltip : tooltip.value; - return; - } - - // Create view model - const codeBlockCollection = this.instantiationService.createInstance(CodeBlockModelCollection, 'agentSessionHover'); - const viewModel = this.instantiationService.createInstance( - ChatViewModel, - modelRef.object, - codeBlockCollection - ); - - // Create the chat list widget - const listWidget = this.instantiationService.createInstance( - ChatListWidget, - container, - { - rendererOptions: { - renderStyle: 'minimal', - noHeader: true, - editableCodeBlock: false, - }, - currentChatMode: () => ChatModeKind.Ask, - } - ); - listWidget.setViewModel(viewModel); - listWidget.layout(300, 500); - - // Handle followup clicks - open the session and accept input - listWidget.onDidClickFollowup(async (followup) => { - const widget = await this.chatWidgetService.openSession(sessionResource); - if (widget) { - widget.acceptInput(followup.message); - } - }); - }); + private buildHoverContent(session: IAgentSession): IDelayedHoverOptions { + const widget = this._sessionHover.value = this.instantiationService.createInstance(AgentSessionHoverWidget, session); return { - content: container, + content: widget.domNode, style: HoverStyle.Pointer, + onDidShow: () => { + widget.onRendered(); + }, position: { hoverPosition: this.options.getHoverPosition() } }; } - private buildTooltip(session: IAgentSession): IMarkdownString { - const lines: string[] = []; - - // Title - lines.push(`**${session.label}**`); - - // Tooltip (from provider) - if (session.tooltip) { - const tooltip = typeof session.tooltip === 'string' ? session.tooltip : session.tooltip.value; - lines.push(tooltip); - } else { - - // Description - if (session.description) { - const description = typeof session.description === 'string' ? session.description : session.description.value; - lines.push(description); - } - - // Badge - if (session.badge) { - const badge = typeof session.badge === 'string' ? session.badge : session.badge.value; - lines.push(badge); - } - } - - // Details line: Status • Provider • Duration/Time - const details: string[] = []; - - // Status - details.push(toStatusLabel(session.status)); - - // Provider - details.push(session.providerLabel); - - // Duration or start time - if (session.timing.finishedOrFailedTime && session.timing.inProgressTime) { - const duration = this.toDuration(session.timing.inProgressTime, session.timing.finishedOrFailedTime, true); - if (duration) { - details.push(duration); - } - } else { - const startTime = session.timing.lastRequestStarted ?? session.timing.created; - details.push(fromNow(startTime, true, true)); - } - - lines.push(details.join(' • ')); - - // Diff information - const diff = getAgentChangesSummary(session.changes); - if (diff && hasValidDiff(session.changes)) { - const diffParts: string[] = []; - if (diff.files > 0) { - diffParts.push(diff.files === 1 ? localize('tooltip.file', "1 file") : localize('tooltip.files', "{0} files", diff.files)); - } - if (diff.insertions > 0) { - diffParts.push(`+${diff.insertions}`); - } - if (diff.deletions > 0) { - diffParts.push(`-${diff.deletions}`); - } - if (diffParts.length > 0) { - lines.push(`$(diff) ${diffParts.join(', ')}`); - } - } - - // Archived status - if (session.isArchived()) { - lines.push(`$(archive) ${localize('tooltip.archived', "Archived")}`); - } - - return new MarkdownString(lines.join('\n\n'), { supportThemeIcons: true }); - } - renderCompressedElements(node: ITreeNode, FuzzyScore>, index: number, templateData: IAgentSessionItemTemplate, details?: ITreeElementRenderDetails): void { throw new Error('Should never happen since session is incompressible'); } @@ -502,7 +381,7 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer Date: Thu, 15 Jan 2026 17:56:53 -0500 Subject: [PATCH 035/387] use f2 for mac, windows (#288175) fixes #288174 --- .../contrib/chat/browser/actions/chatAccessibilityActions.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts index 51badfa9692..118e7128b4e 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts @@ -81,7 +81,10 @@ class OpenThinkingAccessibleViewAction extends Action2 { f1: true, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F3, + primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F2, + linux: { + primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F3, + }, when: ChatContextKeys.inChatSession } }); From 5606ec9727822cdccf62090f118dd1d101419678 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 16 Jan 2026 00:01:29 +0100 Subject: [PATCH 036/387] Simplify code --- .../common/abstractExtensionService.ts | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index b207618675d..e61278a6153 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -401,20 +401,17 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } private async _activateAddedExtensionIfNeeded(extensionDescription: IExtensionDescription): Promise { - let shouldActivate = false; let shouldActivateReason: string | null = null; let hasWorkspaceContains = false; const activationEvents = this._activationEventReader.readActivationEvents(extensionDescription); for (const activationEvent of activationEvents) { if (this._allRequestedActivateEvents.has(activationEvent)) { // This activation event was fired before the extension was added - shouldActivate = true; shouldActivateReason = activationEvent; break; } if (activationEvent === '*') { - shouldActivate = true; shouldActivateReason = activationEvent; break; } @@ -424,17 +421,12 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } if (activationEvent === 'onStartupFinished') { - shouldActivate = true; shouldActivateReason = activationEvent; break; } } - if (shouldActivate) { - await Promise.all( - this._extensionHostManagers.map(extHostManager => extHostManager.activate(extensionDescription.identifier, { startup: false, extensionId: extensionDescription.identifier, activationEvent: shouldActivateReason! })) - ).then(() => { }); - } else if (hasWorkspaceContains) { + if (!shouldActivateReason && hasWorkspaceContains) { const workspace = await this._contextService.getCompleteWorkspace(); const forceUsingSearch = !!this._environmentService.remoteAuthority; const host: IWorkspaceContainsActivationHost = { @@ -446,13 +438,15 @@ export abstract class AbstractExtensionService extends Disposable implements IEx }; const result = await checkActivateWorkspaceContainsExtension(host, extensionDescription); - if (!result) { - return; + if (result) { + shouldActivateReason = result.activationEvent; } + } + if (shouldActivateReason) { await Promise.all( - this._extensionHostManagers.map(extHostManager => extHostManager.activate(extensionDescription.identifier, { startup: false, extensionId: extensionDescription.identifier, activationEvent: result.activationEvent })) - ).then(() => { }); + this._extensionHostManagers.map(extHostManager => extHostManager.activate(extensionDescription.identifier, { startup: false, extensionId: extensionDescription.identifier, activationEvent: shouldActivateReason })) + ); } } From 8a2adc0d0acef78b153a407bb266240e90c16aa2 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 15 Jan 2026 15:21:55 -0800 Subject: [PATCH 037/387] Fix missing user prompt files (#288182) --- .../promptSyntax/utils/promptFilesLocator.ts | 13 ++ .../service/promptsService.test.ts | 214 ++++++++++++++++++ 2 files changed, 227 insertions(+) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts index 015c6c54040..73848fc2fd9 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts @@ -64,6 +64,8 @@ export class PromptFilesLocator { : this.toAbsoluteLocations(configuredLocations, userHome); const paths = new ResourceSet(); + + // Search in config-based user locations (e.g., tilde paths like ~/.copilot/skills) for (const { uri, storage } of absoluteLocations) { if (storage !== PromptsStorage.user) { continue; @@ -79,6 +81,17 @@ export class PromptFilesLocator { } } + // Also search in the VS Code user data prompts folder (for all types except skills) + if (type !== PromptsType.skill) { + const userDataPromptsHome = this.userDataService.currentProfile.promptsHome; + const files = await this.resolveFilesAtLocation(userDataPromptsHome, type, token); + for (const file of files) { + if (getPromptFileType(file) === type) { + paths.add(file); + } + } + } + return [...paths]; } 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 230ace54a1d..705865d9d65 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 @@ -6,6 +6,7 @@ import assert from 'assert'; import * as sinon from 'sinon'; import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; +import { Event } from '../../../../../../../base/common/event.js'; import { match } from '../../../../../../../base/common/glob.js'; import { ResourceSet } from '../../../../../../../base/common/map.js'; import { Schemas } from '../../../../../../../base/common/network.js'; @@ -32,6 +33,7 @@ import { testWorkspace } from '../../../../../../../platform/workspace/test/comm import { IWorkbenchEnvironmentService } from '../../../../../../services/environment/common/environmentService.js'; import { IFilesConfigurationService } from '../../../../../../services/filesConfiguration/common/filesConfigurationService.js'; import { IUserDataProfileService } from '../../../../../../services/userDataProfile/common/userDataProfile.js'; +import { toUserDataProfile } from '../../../../../../../platform/userDataProfile/common/userDataProfile.js'; import { TestContextService, TestUserDataProfileService } from '../../../../../../test/common/workbenchTestServices.js'; import { ChatRequestVariableSet, isPromptFileVariableEntry, toFileVariableEntry } from '../../../../common/attachments/chatVariableEntries.js'; import { ComputeAutomaticInstructions, newInstructionsCollectionEvent } from '../../../../common/promptSyntax/computeAutomaticInstructions.js'; @@ -1091,6 +1093,218 @@ suite('PromptsService', () => { 'Must get custom agents with .md extension from .github/agents/ folder.', ); }); + + test('agents from user data folder', async () => { + const rootFolderName = 'custom-agents-user-data'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + const userPromptsFolder = '/user-data/prompts'; + const userPromptsFolderUri = URI.file(userPromptsFolder); + + // Override the user data profile service to use a file:// URI that the InMemoryFileSystemProvider supports + const customUserDataProfileService = { + _serviceBrand: undefined, + onDidChangeCurrentProfile: Event.None, + currentProfile: { + ...toUserDataProfile('test', 'test', URI.file(userPromptsFolder).with({ path: '/user-data' }), URI.file('/cache')), + promptsHome: userPromptsFolderUri, + }, + updateCurrentProfile: async () => { } + }; + instaService.stub(IUserDataProfileService, customUserDataProfileService); + + // Recreate the service with the new stub + const testService = disposables.add(instaService.createInstance(PromptsService)); + + // Create agent files in both workspace and user data folder + await mockFiles(fileService, [ + // Workspace agent + { + path: `${rootFolder}/.github/agents/workspace-agent.agent.md`, + contents: [ + '---', + 'description: \'Workspace agent.\'', + '---', + 'I am a workspace agent.', + ] + }, + // User data agent + { + path: `${userPromptsFolder}/user-agent.agent.md`, + contents: [ + '---', + 'description: \'User data agent.\'', + 'tools: [ user-tool ]', + '---', + 'I am a user data agent.', + ] + }, + // Another user data agent without header + { + path: `${userPromptsFolder}/simple-user-agent.agent.md`, + contents: [ + 'A simple user agent without header.', + ] + } + ]); + + const result = (await testService.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + + // Should find agents from both workspace and user data + assert.strictEqual(result.length, 3, 'Should find 3 agents (1 workspace + 2 user data)'); + + const workspaceAgent = result.find(a => a.source.storage === PromptsStorage.local); + assert.ok(workspaceAgent, 'Should find workspace agent'); + assert.strictEqual(workspaceAgent.name, 'workspace-agent'); + assert.strictEqual(workspaceAgent.description, 'Workspace agent.'); + + const userAgents = result.filter(a => a.source.storage === PromptsStorage.user); + assert.strictEqual(userAgents.length, 2, 'Should find 2 user data agents'); + + const userAgentWithHeader = userAgents.find(a => a.name === 'user-agent'); + assert.ok(userAgentWithHeader, 'Should find user agent with header'); + assert.strictEqual(userAgentWithHeader.description, 'User data agent.'); + assert.deepStrictEqual(userAgentWithHeader.tools, ['user-tool']); + + const simpleUserAgent = userAgents.find(a => a.name === 'simple-user-agent'); + assert.ok(simpleUserAgent, 'Should find simple user agent'); + assert.strictEqual(simpleUserAgent.agentInstructions.content, 'A simple user agent without header.'); + }); + }); + + suite('listPromptFiles - prompts', () => { + test('prompts from user data folder', async () => { + const rootFolderName = 'prompts-user-data'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + const userPromptsFolder = '/user-data/prompts'; + const userPromptsFolderUri = URI.file(userPromptsFolder); + + // Override the user data profile service + const customUserDataProfileService = { + _serviceBrand: undefined, + onDidChangeCurrentProfile: Event.None, + currentProfile: { + ...toUserDataProfile('test', 'test', URI.file(userPromptsFolder).with({ path: '/user-data' }), URI.file('/cache')), + promptsHome: userPromptsFolderUri, + }, + updateCurrentProfile: async () => { } + }; + instaService.stub(IUserDataProfileService, customUserDataProfileService); + + // Recreate the service with the new stub + const testService = disposables.add(instaService.createInstance(PromptsService)); + + // Create prompt files in both workspace and user data folder + await mockFiles(fileService, [ + // Workspace prompt + { + path: `${rootFolder}/.github/prompts/workspace-prompt.prompt.md`, + contents: [ + '---', + 'description: \'Workspace prompt.\'', + '---', + 'I am a workspace prompt.', + ] + }, + // User data prompt + { + path: `${userPromptsFolder}/user-prompt.prompt.md`, + contents: [ + '---', + 'description: \'User data prompt.\'', + '---', + 'I am a user data prompt.', + ] + } + ]); + + const result = await testService.listPromptFiles(PromptsType.prompt, CancellationToken.None); + + // Should find prompts from both workspace and user data + assert.strictEqual(result.length, 2, 'Should find 2 prompts (1 workspace + 1 user data)'); + + const workspacePrompt = result.find(p => p.storage === PromptsStorage.local); + assert.ok(workspacePrompt, 'Should find workspace prompt'); + assert.ok(workspacePrompt.uri.path.includes('workspace-prompt.prompt.md')); + + const userPrompt = result.find(p => p.storage === PromptsStorage.user); + assert.ok(userPrompt, 'Should find user data prompt'); + assert.ok(userPrompt.uri.path.includes('user-prompt.prompt.md')); + }); + }); + + suite('listPromptFiles - instructions', () => { + test('instructions from user data folder', async () => { + const rootFolderName = 'instructions-user-data'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + const userPromptsFolder = '/user-data/prompts'; + const userPromptsFolderUri = URI.file(userPromptsFolder); + + // Override the user data profile service + const customUserDataProfileService = { + _serviceBrand: undefined, + onDidChangeCurrentProfile: Event.None, + currentProfile: { + ...toUserDataProfile('test', 'test', URI.file(userPromptsFolder).with({ path: '/user-data' }), URI.file('/cache')), + promptsHome: userPromptsFolderUri, + }, + updateCurrentProfile: async () => { } + }; + instaService.stub(IUserDataProfileService, customUserDataProfileService); + + // Recreate the service with the new stub + const testService = disposables.add(instaService.createInstance(PromptsService)); + + // Create instructions files in both workspace and user data folder + await mockFiles(fileService, [ + // Workspace instructions + { + path: `${rootFolder}/.github/instructions/workspace-instructions.instructions.md`, + contents: [ + '---', + 'description: \'Workspace instructions.\'', + 'applyTo: "**/*.ts"', + '---', + 'I am workspace instructions.', + ] + }, + // User data instructions + { + path: `${userPromptsFolder}/user-instructions.instructions.md`, + contents: [ + '---', + 'description: \'User data instructions.\'', + 'applyTo: "**/*.tsx"', + '---', + 'I am user data instructions.', + ] + } + ]); + + const result = await testService.listPromptFiles(PromptsType.instructions, CancellationToken.None); + + // Should find instructions from both workspace and user data + assert.strictEqual(result.length, 2, 'Should find 2 instructions (1 workspace + 1 user data)'); + + const workspaceInstructions = result.find(p => p.storage === PromptsStorage.local); + assert.ok(workspaceInstructions, 'Should find workspace instructions'); + assert.ok(workspaceInstructions.uri.path.includes('workspace-instructions.instructions.md')); + + const userInstructions = result.find(p => p.storage === PromptsStorage.user); + assert.ok(userInstructions, 'Should find user data instructions'); + assert.ok(userInstructions.uri.path.includes('user-instructions.instructions.md')); + }); }); suite('listPromptFiles - skills', () => { From d71906bc726c0be875b8a37f58bffe9b2c798eeb Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 15 Jan 2026 15:40:18 -0800 Subject: [PATCH 038/387] Agents welcome view --- .../browser/widget/input/chatInputPart.ts | 90 +++++-------------- .../input/sessionTargetPickerActionItem.ts | 2 - .../agentSessionsWelcome.contribution.ts | 0 .../browser/agentSessionsWelcome.ts | 4 +- .../browser/agentSessionsWelcomeInput.ts | 0 .../browser/media/agentSessionsWelcome.css | 0 src/vs/workbench/workbench.common.main.ts | 2 +- 7 files changed, 27 insertions(+), 71 deletions(-) rename src/vs/workbench/contrib/{welcomeGettingStarted => welcomeAgentSessions}/browser/agentSessionsWelcome.contribution.ts (100%) rename src/vs/workbench/contrib/{welcomeGettingStarted => welcomeAgentSessions}/browser/agentSessionsWelcome.ts (98%) rename src/vs/workbench/contrib/{welcomeGettingStarted => welcomeAgentSessions}/browser/agentSessionsWelcomeInput.ts (100%) rename src/vs/workbench/contrib/{welcomeGettingStarted => welcomeAgentSessions}/browser/media/agentSessionsWelcome.css (100%) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index f6d695bb280..c761b0818a1 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -83,7 +83,7 @@ import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; import { IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; import { IChatModelInputState, IChatRequestModeInfo, IInputModel } from '../../../common/model/chatModel.js'; import { ChatMode, IChatMode, IChatModeService } from '../../../common/chatModes.js'; -import { IChatFollowup, IChatService } from '../../../common/chatService/chatService.js'; +import { IChatFollowup, IChatService, IChatSessionContext } from '../../../common/chatService/chatService.js'; import { IChatSessionFileChange, IChatSessionProviderOptionItem, IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; import { getChatSessionType } from '../../../common/model/chatUri.js'; import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isStringVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; @@ -508,7 +508,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const sessionResource = this._widget?.viewModel?.model.sessionResource; if (sessionResource) { const ctx = this.chatService.getChatSessionFromInternalUri(sessionResource); - // const delegateSessionType = delegate?.setActiveSessionProvider && delegate?.getActiveSessionProvider?.(); const delegateSessionType = this.options.sessionTypePickerDelegate?.getActiveSessionProvider?.(); if (ctx?.chatSessionType === chatSessionType || delegateSessionType === chatSessionType) { this.refreshChatSessionPickers(); @@ -516,22 +515,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } })); - // Listen for session type changes from the delegate (e.g., welcome page session picker) + // Listen for session type changes from the welcome page delegate if (this.options.sessionTypePickerDelegate?.onDidChangeActiveSessionProvider) { - this._register(this.options.sessionTypePickerDelegate.onDidChangeActiveSessionProvider((newSessionType) => { - // Update the context key so menu items can react + this._register(this.options.sessionTypePickerDelegate.onDidChangeActiveSessionProvider(async (newSessionType) => { this.agentSessionTypeKey.set(newSessionType); - // When the session type changes via the delegate, ensure the provider is activated - // so contributed option groups are available before refreshing pickers. - void this.chatSessionsService.activateChatSessionItemProvider(newSessionType).then(() => { - // Update the lock state based on the new session type after activation - // Non-local session types (e.g., cloud/remote) should lock to coding agent mode - // This must be done after activation so the contribution is available - this.updateWidgetLockStateFromSessionType(newSessionType); - // The pickers will be determined based on the delegate's active session provider - this.refreshChatSessionPickers(); - this.tryUpdateWidgetController(); - }); + this.updateWidgetLockStateFromSessionType(newSessionType); + this.refreshChatSessionPickers(); })); } @@ -773,18 +762,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this.chatService.getChatSessionFromInternalUri(sessionResource); }; - // Determine the effective session type: - // - If we have a delegate with a setter (e.g., welcome page), use the delegate's session type - // - Otherwise, use the actual session's type + // Get all option groups for the current session type const ctx = resolveChatSessionContext(); - const delegate = this.options.sessionTypePickerDelegate; - const delegateSessionType = delegate?.setActiveSessionProvider && delegate?.getActiveSessionProvider?.(); - const effectiveSessionType = delegateSessionType || ctx?.chatSessionType; - - // Check if we're using a delegate-provided session type different from the actual session - const usingDelegateSessionType = delegateSessionType && delegateSessionType !== ctx?.chatSessionType; - - // Get all option groups for the effective session type + const effectiveSessionType = this.getEffectiveSessionType(ctx, this.options.sessionTypePickerDelegate); + const usingDelegateSessionType = effectiveSessionType !== ctx?.chatSessionType; const optionGroups = effectiveSessionType ? this.chatSessionsService.getOptionGroupsForSessionType(effectiveSessionType) : undefined; if (!optionGroups || optionGroups.length === 0) { return []; @@ -795,10 +776,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Init option group context keys for (const optionGroup of optionGroups) { - // For delegate session types, use the first item or default; otherwise get from session - const currentOption = usingDelegateSessionType - ? (optionGroup.items.find(item => item.default) || optionGroup.items[0]) - : (ctx ? this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id) : undefined); + if (!ctx) { + continue; + } + const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id); if (currentOption) { const optionId = typeof currentOption === 'string' ? currentOption : currentOption.id; this.updateOptionContextKey(optionGroup.id, optionId); @@ -1433,16 +1414,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return hideAll(); } - // Determine the effective session type: - // - If we have a delegate with a setter (e.g., welcome page), use the delegate's session type - // - Otherwise, use the actual session's type - const delegate = this.options.sessionTypePickerDelegate; - const delegateSessionType = delegate?.setActiveSessionProvider && delegate?.getActiveSessionProvider?.(); - const effectiveSessionType = delegateSessionType || ctx.chatSessionType; - - // Check if we're using a delegate-provided session type different from the actual session - const usingDelegateSessionType = delegateSessionType && delegateSessionType !== ctx.chatSessionType; - + const effectiveSessionType = this.getEffectiveSessionType(ctx, this.options.sessionTypePickerDelegate); + const usingDelegateSessionType = effectiveSessionType !== ctx.chatSessionType; const optionGroups = this.chatSessionsService.getOptionGroupsForSessionType(effectiveSessionType); if (!optionGroups || optionGroups.length === 0) { return hideAll(); @@ -1456,10 +1429,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // First update all context keys with current values (before evaluating visibility) for (const optionGroup of optionGroups) { - // For delegate session types, use the first item as default; otherwise get from session - const currentOption = usingDelegateSessionType - ? optionGroup.items[0] - : this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id); + const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id); if (currentOption) { const optionId = typeof currentOption === 'string' ? currentOption : currentOption.id; this.updateOptionContextKey(optionGroup.id, optionId); @@ -1471,11 +1441,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Compute which option groups should be visible based on when expressions const visibleGroupIds = new Set(); for (const optionGroup of optionGroups) { - // For delegate session types, show groups that have items; otherwise check session value - const hasValue = usingDelegateSessionType - ? optionGroup.items.length > 0 - : !!this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id); - if (!hasValue) { + if (!this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id)) { continue; } if (this.evaluateOptionGroupVisibility(optionGroup)) { @@ -1491,9 +1457,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Validate that all selected options exist in their respective option group items let allOptionsValid = true; for (const optionGroup of optionGroups) { - const currentOption = usingDelegateSessionType - ? optionGroup.items[0] - : this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id); + const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id); if (currentOption) { const currentOptionId = typeof currentOption === 'string' ? currentOption : currentOption.id; const isValidOption = optionGroup.items.some(item => item.id === currentOptionId); @@ -1528,9 +1492,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } for (const [optionGroupId] of this.chatSessionPickerWidgets.entries()) { - const currentOption = usingDelegateSessionType - ? optionGroups.find(g => g.id === optionGroupId)?.items[0] - : this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroupId); + const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroupId); if (currentOption) { const optionGroup = optionGroups.find(g => g.id === optionGroupId); if (optionGroup) { @@ -1577,23 +1539,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return; } - // Determine the effective session type (delegate's type takes precedence) - const delegate = this.options.sessionTypePickerDelegate; - const delegateSessionType = delegate?.setActiveSessionProvider && delegate?.getActiveSessionProvider?.(); - const effectiveSessionType = delegateSessionType || ctx.chatSessionType; - const usingDelegateSessionType = delegateSessionType && delegateSessionType !== ctx.chatSessionType; - + const effectiveSessionType = this.getEffectiveSessionType(ctx, this.options.sessionTypePickerDelegate); const optionGroups = this.chatSessionsService.getOptionGroupsForSessionType(effectiveSessionType); const optionGroup = optionGroups?.find(g => g.id === optionGroupId); if (!optionGroup || optionGroup.items.length === 0) { return; } - // For delegate session types, return the default or first item - if (usingDelegateSessionType) { - return optionGroup.items.find(item => item.default) || optionGroup.items[0]; - } - const currentOptionValue = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroupId); if (!currentOptionValue) { const defaultItem = optionGroup.items.find(item => item.default); @@ -1609,6 +1561,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } + private getEffectiveSessionType(ctx: IChatSessionContext | undefined, delegate: ISessionTypePickerDelegate | undefined): string { + return this.options.sessionTypePickerDelegate?.getActiveSessionProvider?.() || ctx?.chatSessionType || ''; + } + /** * Updates the agentSessionType context key based on delegate or actual session. */ diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts index 2aca776cca6..8a18ff54f1e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts @@ -61,8 +61,6 @@ export class SessionTypePickerActionItem extends ActionWidgetDropdownActionViewI icon: getAgentSessionProviderIcon(sessionTypeItem.type), enabled: true, run: async () => { - // If delegate provides a setter, use it for local state management - // Otherwise execute the command to open a new session if (this.delegate.setActiveSessionProvider) { this.delegate.setActiveSessionProvider(sessionTypeItem.type); } else { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcome.contribution.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.contribution.ts similarity index 100% rename from src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcome.contribution.ts rename to src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.contribution.ts diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts similarity index 98% rename from src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcome.ts rename to src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index fac4ffeb984..92222d2552a 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -37,13 +37,13 @@ import { IAgentSessionsService } from '../../chat/browser/agentSessions/agentSes import { AgentSessionProviders } from '../../chat/browser/agentSessions/agentSessions.js'; import { IAgentSession } from '../../chat/browser/agentSessions/agentSessionsModel.js'; import { AgentSessionsWelcomeEditorOptions, AgentSessionsWelcomeInput } from './agentSessionsWelcomeInput.js'; -import { IWalkthroughsService, IResolvedWalkthrough } from './gettingStartedService.js'; import { IChatService } from '../../chat/common/chatService/chatService.js'; import { IChatModel } from '../../chat/common/model/chatModel.js'; import { ISessionTypePickerDelegate } from '../../chat/browser/chat.js'; import { AgentSessionsControl, IAgentSessionsControlOptions } from '../../chat/browser/agentSessions/agentSessionsControl.js'; import { IAgentSessionsFilter } from '../../chat/browser/agentSessions/agentSessionsViewer.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; +import { IResolvedWalkthrough, IWalkthroughsService } from '../../welcomeGettingStarted/browser/gettingStartedService.js'; const configurationKey = 'workbench.startupEditor'; const MAX_SESSIONS = 6; @@ -192,6 +192,7 @@ export class AgentSessionsWelcomePage extends EditorPane { this.chatWidget = this.contentDisposables.add(scopedInstantiationService.createInstance( ChatWidget, ChatAgentLocation.Chat, + // TODO: @osortega should we have a completely different ID and check that context instead in chatInputPart? {}, // Empty resource view context { autoScroll: mode => mode !== ChatModeKind.Ask, @@ -399,6 +400,7 @@ export class AgentSessionsWelcomePage extends EditorPane { return; } + // TODO: @osortega this is a weird way of doing this, maybe we handle the 2-colum layout in the control itself? const sessionsWidth = Math.min(800, this.lastDimension.width - 80); // Calculate height based on actual visible sessions (capped at MAX_SESSIONS) // Use 52px per item from AgentSessionsListDelegate.ITEM_HEIGHT diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcomeInput.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcomeInput.ts similarity index 100% rename from src/vs/workbench/contrib/welcomeGettingStarted/browser/agentSessionsWelcomeInput.ts rename to src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcomeInput.ts diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/agentSessionsWelcome.css b/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css similarity index 100% rename from src/vs/workbench/contrib/welcomeGettingStarted/browser/media/agentSessionsWelcome.css rename to src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 82c60b5949c..3159df0f6ce 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -350,7 +350,7 @@ import './contrib/surveys/browser/languageSurveys.contribution.js'; // Welcome import './contrib/welcomeGettingStarted/browser/gettingStarted.contribution.js'; -import './contrib/welcomeGettingStarted/browser/agentSessionsWelcome.contribution.js'; +import './contrib/welcomeAgentSessions/browser/agentSessionsWelcome.contribution.js'; import './contrib/welcomeWalkthrough/browser/walkThrough.contribution.js'; import './contrib/welcomeViews/common/viewsWelcome.contribution.js'; import './contrib/welcomeViews/common/newFile.contribution.js'; From 5f0d68e0dfee9bc3fdfc1335d40227d97118909d Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 15 Jan 2026 16:09:36 -0800 Subject: [PATCH 039/387] Hygiene --- .../contrib/chat/browser/agentSessions/agentSessions.ts | 1 + .../chat/browser/agentSessions/agentSessionsControl.ts | 6 ++++++ .../welcomeAgentSessions/browser/agentSessionsWelcome.ts | 2 +- .../browser/media/agentSessionsWelcome.css | 6 +++--- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts index 20fd654cbc5..a72ab1cd2e6 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts @@ -71,6 +71,7 @@ export interface IAgentSessionsControl { refresh(): void; openFind(): void; reveal(sessionResource: URI): void; + setGridMarginOffset(offset: number): void; } export const agentSessionReadIndicatorForeground = registerColor( diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 0777b28e33c..1022548399a 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -313,4 +313,10 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo this.sessionsList.setFocus([session]); this.sessionsList.setSelection([session]); } + + setGridMarginOffset(offset: number): void { + if (this.sessionsContainer) { + this.sessionsContainer.style.marginBottom = `-${offset}px`; + } + } } diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index 92222d2552a..b820d539d1f 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -416,7 +416,7 @@ export class AgentSessionsWelcomePage extends EditorPane { // Set margin offset for 2-column layout: actual height - visual height // Visual height = ceil(n/2) * 52, so offset = floor(n/2) * 52 const marginOffset = Math.floor(visibleSessions / 2) * 52; - this.sessionsControlContainer.style.setProperty('--sessions-grid-margin-offset', `-${marginOffset}px`); + this.sessionsControl.setGridMarginOffset(marginOffset); } override focus(): void { diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css b/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css index f2b27b08900..35d56a9feab 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css @@ -115,7 +115,7 @@ .agentSessionsWelcome-suggestedPrompt { padding: 8px 16px; - border: 1px solid var(--vscode-button-secondaryBorder, var(--vscode-contrastBorder, transparent)); + border: 1px solid var(--vscode-button-border, var(--vscode-contrastBorder, transparent)); border-radius: 20px; background-color: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); @@ -193,9 +193,9 @@ transform: translateX(100%) translateY(-156px); } -/* Clip the extra space caused by transforms - uses CSS variable set by JS */ +/* Clip the extra space caused by transforms - margin set directly by JS */ .agentSessionsWelcome-sessionsGrid .agent-sessions-viewer .monaco-scrollable-element { - margin-bottom: var(--sessions-grid-margin-offset, 0px); + /* margin-bottom is set programmatically in layoutSessionsControl() */ } /* Style individual session items in the welcome page */ From 2baec299205e6c9c74383b942da299164934e6fa Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 15 Jan 2026 19:11:43 -0500 Subject: [PATCH 040/387] prevent flicker in expanding chat output (#287865) --- .../chatTerminalToolProgressPart.ts | 73 ++--- .../terminalToolAutoExpand.ts | 121 ++++++++ .../chatTerminalToolProgressPart.test.ts | 267 ++++++++++++++++++ .../terminalToolAutoExpand.test.ts | 267 ++++++++++++++++++ 4 files changed, 685 insertions(+), 43 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/terminalToolAutoExpand.ts create mode 100644 src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatTerminalToolProgressPart.test.ts create mode 100644 src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/terminalToolAutoExpand.test.ts diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts index 6a5a55caa2a..752a69d67fe 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts @@ -18,6 +18,7 @@ import { IChatContentPartRenderContext } from '../chatContentParts.js'; import { ChatMarkdownContentPart, type IChatMarkdownContentPartOptions } from '../chatMarkdownContentPart.js'; import { ChatProgressSubPart } from '../chatProgressContentPart.js'; import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; +import { TerminalToolAutoExpand } from './terminalToolAutoExpand.js'; import { ChatCollapsibleContentPart } from '../chatCollapsibleContentPart.js'; import { IChatRendererContent } from '../../../../common/model/chatViewModel.js'; import '../media/chatTerminalToolProgressPart.css'; @@ -217,7 +218,6 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart private readonly _isSerializedInvocation: boolean; private _terminalInstance: ITerminalInstance | undefined; private readonly _decoration: TerminalCommandDecoration; - private _autoExpandTimeout: ReturnType | undefined; private _userToggledOutput: boolean = false; private _isInThinkingContainer: boolean = false; private _thinkingCollapsibleWrapper: ChatTerminalThinkingCollapsibleWrapper | undefined; @@ -559,29 +559,38 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart } const store = new DisposableStore(); + + const hasRealOutput = (): boolean => { + // Check for snapshot output + if (this._terminalData.terminalCommandOutput?.text?.trim()) { + return true; + } + // Check for live output (cursor moved past executed marker) + const command = this._getResolvedCommand(terminalInstance); + if (!command?.executedMarker || terminalInstance.isDisposed) { + return false; + } + const buffer = terminalInstance.xterm?.raw.buffer.active; + if (!buffer) { + return false; + } + const cursorLine = buffer.baseY + buffer.cursorY; + return cursorLine > command.executedMarker.line; + }; + + // Use the extracted auto-expand logic + const autoExpand = store.add(new TerminalToolAutoExpand({ + commandDetection, + onWillData: terminalInstance.onWillData, + shouldAutoExpand: () => !this._outputView.isExpanded && !this._userToggledOutput && !this._store.isDisposed, + hasRealOutput, + })); + store.add(autoExpand.onDidRequestExpand(() => this._toggleOutput(true))); + store.add(commandDetection.onCommandExecuted(() => { this._addActions(terminalInstance, this._terminalData.terminalToolSessionId); - // Auto-expand if there's output, checking periodically for up to 1 second - if (!this._outputView.isExpanded && !this._userToggledOutput && !this._autoExpandTimeout) { - let attempts = 0; - const maxAttempts = 5; - const checkForOutput = () => { - this._autoExpandTimeout = undefined; - if (this._store.isDisposed || this._outputView.isExpanded || this._userToggledOutput) { - return; - } - if (this._hasOutput(terminalInstance)) { - this._toggleOutput(true); - return; - } - attempts++; - if (attempts < maxAttempts) { - this._autoExpandTimeout = setTimeout(checkForOutput, 200); - } - }; - this._autoExpandTimeout = setTimeout(checkForOutput, 200); - } })); + store.add(commandDetection.onCommandFinished(() => { this._addActions(terminalInstance, this._terminalData.terminalToolSessionId); const resolvedCommand = this._getResolvedCommand(terminalInstance); @@ -690,10 +699,6 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart } private _handleDispose(): void { - if (this._autoExpandTimeout) { - clearTimeout(this._autoExpandTimeout); - this._autoExpandTimeout = undefined; - } this._terminalOutputContextKey.reset(); this._terminalChatService.clearFocusedProgressPart(this); } @@ -747,24 +752,6 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart this._focusChatInput(); } - private _hasOutput(terminalInstance: ITerminalInstance): boolean { - // Check for snapshot - if (this._terminalData.terminalCommandOutput?.text?.trim()) { - return true; - } - // Check for live output (cursor moved past executed marker) - const command = this._getResolvedCommand(terminalInstance); - if (!command?.executedMarker || terminalInstance.isDisposed) { - return false; - } - const buffer = terminalInstance.xterm?.raw.buffer.active; - if (!buffer) { - return false; - } - const cursorLine = buffer.baseY + buffer.cursorY; - return cursorLine > command.executedMarker.line; - } - private _resolveCommand(instance: ITerminalInstance): ITerminalCommand | undefined { if (instance.isDisposed) { return undefined; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/terminalToolAutoExpand.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/terminalToolAutoExpand.ts new file mode 100644 index 00000000000..bf23d8519c1 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/terminalToolAutoExpand.ts @@ -0,0 +1,121 @@ +/*--------------------------------------------------------------------------------------------- + * 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, IDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { Emitter, Event } from '../../../../../../../base/common/event.js'; +import type { ICommandDetectionCapability } from '../../../../../../../platform/terminal/common/capabilities/capabilities.js'; +import { disposableTimeout } from '../../../../../../../base/common/async.js'; + +/** + * The auto-expand algorithm for terminal tool progress parts. + * + * The algorithm is: + * 1. When command executes, kick off 500ms timeout - if hit without data events, expand only if there's real output + * 2. On first data event, wait 50ms and expand if command not yet finished + * 3. Fast commands (finishing quickly) should NOT auto-expand to prevent flickering + */ +export interface ITerminalToolAutoExpandOptions { + /** + * The command detection capability to listen for command events. + */ + readonly commandDetection: ICommandDetectionCapability; + + /** + * Event fired when data is received from the terminal. + */ + readonly onWillData: Event; + + /** + * Check if the output should auto-expand (e.g. not already expanded, user hasn't toggled). + */ + shouldAutoExpand(): boolean; + + /** + * Check if there is real output (not just shell integration sequences). + */ + hasRealOutput(): boolean; +} + +/** + * Timeout constants for the auto-expand algorithm. + */ +export const enum TerminalToolAutoExpandTimeout { + /** + * Timeout in milliseconds to wait when no data events are received before checking for auto-expand. + */ + NoData = 500, + /** + * Timeout in milliseconds to wait after first data event before checking for auto-expand. + * This prevents flickering for fast commands like `ls` that finish quickly. + */ + DataEvent = 50, +} + +export class TerminalToolAutoExpand extends Disposable { + private _commandFinished = false; + private _receivedData = false; + private _dataEventTimeout: IDisposable | undefined; + private _noDataTimeout: IDisposable | undefined; + + private readonly _onDidRequestExpand = this._register(new Emitter()); + readonly onDidRequestExpand: Event = this._onDidRequestExpand.event; + + constructor( + private readonly _options: ITerminalToolAutoExpandOptions, + ) { + super(); + this._setupListeners(); + } + + private _setupListeners(): void { + const store = this._register(new DisposableStore()); + + const commandDetection = this._options.commandDetection; + + store.add(commandDetection.onCommandExecuted(() => { + // Auto-expand for long-running commands: + if (this._options.shouldAutoExpand() && !this._noDataTimeout) { + this._noDataTimeout = disposableTimeout(() => { + this._noDataTimeout = undefined; + if (!this._receivedData && this._options.shouldAutoExpand() && this._options.hasRealOutput()) { + this._onDidRequestExpand.fire(); + } + }, TerminalToolAutoExpandTimeout.NoData, store); + } + })); + + // 2. Wait for first data event - when hit, wait 50ms and expand if command not yet finished + // Also checks for real output since shell integration sequences trigger onWillData + store.add(this._options.onWillData(() => { + if (this._receivedData) { + return; + } + this._receivedData = true; + this._noDataTimeout?.dispose(); + this._noDataTimeout = undefined; + // Wait 50ms and expand if command hasn't finished yet and has real output + if (this._options.shouldAutoExpand() && !this._dataEventTimeout) { + this._dataEventTimeout = disposableTimeout(() => { + this._dataEventTimeout = undefined; + if (!this._commandFinished && this._options.shouldAutoExpand() && this._options.hasRealOutput()) { + this._onDidRequestExpand.fire(); + } + }, TerminalToolAutoExpandTimeout.DataEvent, store); + } + })); + + store.add(commandDetection.onCommandFinished(() => { + this._commandFinished = true; + this._clearAutoExpandTimeouts(); + })); + } + + private _clearAutoExpandTimeouts(): void { + this._dataEventTimeout?.dispose(); + this._dataEventTimeout = undefined; + this._noDataTimeout?.dispose(); + this._noDataTimeout = undefined; + } +} diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatTerminalToolProgressPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatTerminalToolProgressPart.test.ts new file mode 100644 index 00000000000..ba9914a7d04 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatTerminalToolProgressPart.test.ts @@ -0,0 +1,267 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { Emitter } from '../../../../../../../base/common/event.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { runWithFakedTimers } from '../../../../../../../base/test/common/timeTravelScheduler.js'; +import { timeout } from '../../../../../../../base/common/async.js'; +import { TerminalToolAutoExpand, TerminalToolAutoExpandTimeout } from '../../../../browser/widget/chatContentParts/toolInvocationParts/terminalToolAutoExpand.js'; +import type { ICommandDetectionCapability } from '../../../../../../../platform/terminal/common/capabilities/capabilities.js'; + +suite('ChatTerminalToolProgressPart Auto-Expand Logic', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + // Mocked events + let onCommandExecuted: Emitter; + let onCommandFinished: Emitter; + let onWillData: Emitter; + + // State tracking + let isExpanded: boolean; + let userToggledOutput: boolean; + let hasRealOutputValue: boolean; + + function shouldAutoExpand(): boolean { + return !isExpanded && !userToggledOutput; + } + + function hasRealOutput(): boolean { + return hasRealOutputValue; + } + + function setupAutoExpandLogic(): void { + // Create a mock command detection capability + const mockCommandDetection = { + onCommandExecuted: onCommandExecuted.event, + onCommandFinished: onCommandFinished.event, + } as Pick as ICommandDetectionCapability; + + // Use the real TerminalToolAutoExpand class + const autoExpand = store.add(new TerminalToolAutoExpand({ + commandDetection: mockCommandDetection, + onWillData: onWillData.event, + shouldAutoExpand, + hasRealOutput, + })); + store.add(autoExpand.onDidRequestExpand(() => { + isExpanded = true; + })); + } + + setup(() => { + onCommandExecuted = store.add(new Emitter()); + onCommandFinished = store.add(new Emitter()); + onWillData = store.add(new Emitter()); + + isExpanded = false; + userToggledOutput = false; + hasRealOutputValue = false; + }); + + test('fast command without data should not auto-expand (finishes before timeout)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Command finishes quickly (before timeout) + onCommandFinished.fire(undefined); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand for fast command without data'); + })); + + test('fast command with quick data should not auto-expand (data + finish before timeout)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives + onWillData.fire('output'); + + // Command finishes quickly (before timeout) + onCommandFinished.fire(undefined); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when command finishes within timeout of first data'); + })); + + test('long-running command with data should auto-expand (data received, command still running after timeout)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = true; // Has real output + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives + onWillData.fire('output'); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 100); + + assert.strictEqual(isExpanded, true, 'Should expand when command still running after first data timeout'); + + onCommandFinished.fire(undefined); + })); + + test('long-running command with data but no real output should NOT auto-expand (like sleep with shell sequences)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = false; // Shell integration sequences, not real output + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Shell integration data arrives (not real output) + onWillData.fire('shell-sequence'); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when data is shell sequences, not real output'); + + onCommandFinished.fire(undefined); + })); + + test('long-running command without data should NOT auto-expand if no real output (like sleep)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = false; // No real output like `sleep 1` + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when no real output even after timeout'); + + onCommandFinished.fire(undefined); + })); + + test('long-running command without data SHOULD auto-expand if real output exists', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = true; // Has real output in buffer + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, true, 'Should expand when real output exists after timeout'); + + onCommandFinished.fire(undefined); + })); + + test('data arriving after command finish should not trigger expand', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + setupAutoExpandLogic(); + + // Command executes and finishes immediately + onCommandExecuted.fire(undefined); + onCommandFinished.fire(undefined); + + // Data arrives after command finished + onWillData.fire('late output'); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when data arrives after command finished'); + })); + + test('user toggled output prevents auto-expand', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + userToggledOutput = true; + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives + onWillData.fire('output'); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when user has manually toggled output'); + onCommandFinished.fire(undefined); + })); + + test('already expanded output prevents additional auto-expand', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + isExpanded = true; + + // Create a mock command detection capability + const mockCommandDetection = { + onCommandExecuted: onCommandExecuted.event, + onCommandFinished: onCommandFinished.event, + } as Pick as ICommandDetectionCapability; + + // Track if event was fired + let eventFired = false; + const autoExpand = store.add(new TerminalToolAutoExpand({ + commandDetection: mockCommandDetection, + onWillData: onWillData.event, + shouldAutoExpand: () => !isExpanded && !userToggledOutput, + hasRealOutput: () => hasRealOutputValue, + })); + store.add(autoExpand.onDidRequestExpand(() => { + eventFired = true; + })); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives + onWillData.fire('output'); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(eventFired, false, 'Should NOT fire expand event when already expanded'); + onCommandFinished.fire(undefined); + })); + + test('data arriving cancels no-data timeout', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = true; // Would have expanded if no-data timeout fired + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives (cancels no-data timeout) + onWillData.fire('output'); + + // Command finishes immediately after data (before data timeout would fire) + onCommandFinished.fire(undefined); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'No-data timeout should be cancelled when data arrives'); + })); + + test('multiple data events only trigger one timeout', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = true; // Has real output + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Multiple data events + onWillData.fire('output 1'); + onWillData.fire('output 2'); + onWillData.fire('output 3'); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 100); + + assert.strictEqual(isExpanded, true, 'Should expand exactly once after first data'); + onCommandFinished.fire(undefined); + })); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/terminalToolAutoExpand.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/terminalToolAutoExpand.test.ts new file mode 100644 index 00000000000..32361b27dfc --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/terminalToolAutoExpand.test.ts @@ -0,0 +1,267 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { Emitter } from '../../../../../../../base/common/event.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { runWithFakedTimers } from '../../../../../../../base/test/common/timeTravelScheduler.js'; +import { timeout } from '../../../../../../../base/common/async.js'; +import { TerminalToolAutoExpand, TerminalToolAutoExpandTimeout } from '../../../../browser/widget/chatContentParts/toolInvocationParts/terminalToolAutoExpand.js'; +import type { ICommandDetectionCapability } from '../../../../../../../platform/terminal/common/capabilities/capabilities.js'; + +suite('TerminalToolAutoExpand', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + // Mocked events + let onCommandExecuted: Emitter; + let onCommandFinished: Emitter; + let onWillData: Emitter; + + // State tracking + let isExpanded: boolean; + let userToggledOutput: boolean; + let hasRealOutputValue: boolean; + + function shouldAutoExpand(): boolean { + return !isExpanded && !userToggledOutput; + } + + function hasRealOutput(): boolean { + return hasRealOutputValue; + } + + function setupAutoExpandLogic(): void { + // Create a mock command detection capability + const mockCommandDetection = { + onCommandExecuted: onCommandExecuted.event, + onCommandFinished: onCommandFinished.event, + } as Pick as ICommandDetectionCapability; + + // Use the real TerminalToolAutoExpand class + const autoExpand = store.add(new TerminalToolAutoExpand({ + commandDetection: mockCommandDetection, + onWillData: onWillData.event, + shouldAutoExpand, + hasRealOutput, + })); + store.add(autoExpand.onDidRequestExpand(() => { + isExpanded = true; + })); + } + + setup(() => { + onCommandExecuted = store.add(new Emitter()); + onCommandFinished = store.add(new Emitter()); + onWillData = store.add(new Emitter()); + + isExpanded = false; + userToggledOutput = false; + hasRealOutputValue = false; + }); + + test('fast command without data should not auto-expand (finishes before timeout)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Command finishes quickly (before timeout) + onCommandFinished.fire(undefined); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand for fast command without data'); + })); + + test('fast command with quick data should not auto-expand (data + finish before timeout)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives + onWillData.fire('output'); + + // Command finishes quickly (before timeout) + onCommandFinished.fire(undefined); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when command finishes within timeout of first data'); + })); + + test('long-running command with data should auto-expand (data received, command still running after timeout)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = true; // Has real output + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives + onWillData.fire('output'); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 100); + + assert.strictEqual(isExpanded, true, 'Should expand when command still running after first data timeout'); + + onCommandFinished.fire(undefined); + })); + + test('long-running command with data but no real output should NOT auto-expand (like sleep with shell sequences)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = false; // Shell integration sequences, not real output + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Shell integration data arrives (not real output) + onWillData.fire('shell-sequence'); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when data is shell sequences, not real output'); + + onCommandFinished.fire(undefined); + })); + + test('long-running command without data should NOT auto-expand if no real output (like sleep)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = false; // No real output like `sleep 1` + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when no real output even after timeout'); + + onCommandFinished.fire(undefined); + })); + + test('long-running command without data SHOULD auto-expand if real output exists', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = true; // Has real output in buffer + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, true, 'Should expand when real output exists after timeout'); + + onCommandFinished.fire(undefined); + })); + + test('data arriving after command finish should not trigger expand', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + setupAutoExpandLogic(); + + // Command executes and finishes immediately + onCommandExecuted.fire(undefined); + onCommandFinished.fire(undefined); + + // Data arrives after command finished + onWillData.fire('late output'); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when data arrives after command finished'); + })); + + test('user toggled output prevents auto-expand', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + userToggledOutput = true; + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives + onWillData.fire('output'); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when user has manually toggled output'); + onCommandFinished.fire(undefined); + })); + + test('already expanded output prevents additional auto-expand', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + isExpanded = true; + + // Create a mock command detection capability + const mockCommandDetection = { + onCommandExecuted: onCommandExecuted.event, + onCommandFinished: onCommandFinished.event, + } as Pick as ICommandDetectionCapability; + + // Track if event was fired + let eventFired = false; + const autoExpand = store.add(new TerminalToolAutoExpand({ + commandDetection: mockCommandDetection, + onWillData: onWillData.event, + shouldAutoExpand: () => !isExpanded && !userToggledOutput, + hasRealOutput: () => hasRealOutputValue, + })); + store.add(autoExpand.onDidRequestExpand(() => { + eventFired = true; + })); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives + onWillData.fire('output'); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(eventFired, false, 'Should NOT fire expand event when already expanded'); + onCommandFinished.fire(undefined); + })); + + test('data arriving cancels no-data timeout', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = true; // Would have expanded if no-data timeout fired + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives (cancels no-data timeout) + onWillData.fire('output'); + + // Command finishes immediately after data (before data timeout would fire) + onCommandFinished.fire(undefined); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'No-data timeout should be cancelled when data arrives'); + })); + + test('multiple data events only trigger one timeout', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = true; // Has real output + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Multiple data events + onWillData.fire('output 1'); + onWillData.fire('output 2'); + onWillData.fire('output 3'); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 100); + + assert.strictEqual(isExpanded, true, 'Should expand exactly once after first data'); + onCommandFinished.fire(undefined); + })); +}); From 94bd7d47f97ff4b77650ce5799ddd6b86e468cd9 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:30:02 -0800 Subject: [PATCH 041/387] rebuild agentStatusWidget (#288192) * rebuild agentStatusWidget * Update src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../agentSessions/agentStatusWidget.ts | 366 +++++++++++++----- .../agentSessions/media/agentStatusWidget.css | 111 +++++- .../chatSessions/chatSessions.contribution.ts | 24 ++ 3 files changed, 397 insertions(+), 104 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts index 9bdcdd03e02..4c3887db7ba 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts @@ -17,7 +17,7 @@ import { ICommandService } from '../../../../../platform/commands/common/command import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { ExitAgentSessionProjectionAction } from './agentSessionProjectionActions.js'; import { IAgentSessionsService } from './agentSessionsService.js'; -import { IAgentSession, isSessionInProgressStatus } from './agentSessionsModel.js'; +import { AgentSessionStatus, IAgentSession, isSessionInProgressStatus } from './agentSessionsModel.js'; import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IAction } from '../../../../../base/common/actions.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; @@ -55,6 +55,9 @@ export class AgentStatusWidget extends BaseActionViewItem { /** The currently displayed in-progress session (if any) - clicking pill opens this */ private _displayedSession: IAgentSession | undefined; + /** Cached render state to avoid unnecessary DOM rebuilds */ + private _lastRenderState: string | undefined; + constructor( action: IAction, options: IBaseActionViewItemOptions | undefined, @@ -113,6 +116,45 @@ export class AgentStatusWidget extends BaseActionViewItem { return; } + // Compute current render state to avoid unnecessary DOM rebuilds + const mode = this.agentStatusService.mode; + const sessionInfo = this.agentStatusService.sessionInfo; + const { activeSessions, unreadSessions, attentionNeededSessions } = this._getSessionStats(); + + // Get attention session info for state computation + const attentionSession = attentionNeededSessions.length > 0 + ? [...attentionNeededSessions].sort((a, b) => { + const timeA = a.timing.lastRequestStarted ?? a.timing.created; + const timeB = b.timing.lastRequestStarted ?? b.timing.created; + return timeB - timeA; + })[0] + : undefined; + + const attentionText = attentionSession?.description + ? (typeof attentionSession.description === 'string' + ? attentionSession.description + : renderAsPlaintext(attentionSession.description)) + : attentionSession?.label; + + const label = this._getLabel(); + + // Build state key for comparison + const stateKey = JSON.stringify({ + mode, + sessionTitle: sessionInfo?.title, + activeCount: activeSessions.length, + unreadCount: unreadSessions.length, + attentionCount: attentionNeededSessions.length, + attentionText, + label, + }); + + // Skip re-render if state hasn't changed + if (this._lastRenderState === stateKey) { + return; + } + this._lastRenderState = stateKey; + // Clear existing content reset(this._container); @@ -128,80 +170,113 @@ export class AgentStatusWidget extends BaseActionViewItem { } } + // #region Session Statistics + + /** + * Get computed session statistics for rendering. + */ + private _getSessionStats(): { + activeSessions: IAgentSession[]; + unreadSessions: IAgentSession[]; + attentionNeededSessions: IAgentSession[]; + hasActiveSessions: boolean; + hasUnreadSessions: boolean; + hasAttentionNeeded: boolean; + } { + const sessions = this.agentSessionsService.model.sessions; + const activeSessions = sessions.filter(s => isSessionInProgressStatus(s.status)); + const unreadSessions = sessions.filter(s => !s.isRead()); + // Sessions that need user attention (approval/confirmation/input) + const attentionNeededSessions = sessions.filter(s => s.status === AgentSessionStatus.NeedsInput); + + return { + activeSessions, + unreadSessions, + attentionNeededSessions, + hasActiveSessions: activeSessions.length > 0, + hasUnreadSessions: unreadSessions.length > 0, + hasAttentionNeeded: attentionNeededSessions.length > 0, + }; + } + + // #endregion + + // #region Mode Renderers + private _renderChatInputMode(disposables: DisposableStore): void { if (!this._container) { return; } - // Get agent session statistics - const sessions = this.agentSessionsService.model.sessions; - const activeSessions = sessions.filter(s => isSessionInProgressStatus(s.status)); - const unreadSessions = sessions.filter(s => !s.isRead()); - const hasActiveSessions = activeSessions.length > 0; - const hasUnreadSessions = unreadSessions.length > 0; + const { activeSessions, unreadSessions, attentionNeededSessions, hasAttentionNeeded } = this._getSessionStats(); - // Create pill - add 'has-active' class when sessions are in progress + // Create pill const pill = $('div.agent-status-pill.chat-input-mode'); - if (hasActiveSessions) { - pill.classList.add('has-active'); - } else if (hasUnreadSessions) { - pill.classList.add('has-unread'); + if (hasAttentionNeeded) { + pill.classList.add('needs-attention'); } pill.setAttribute('role', 'button'); pill.setAttribute('aria-label', localize('openQuickChat', "Open Quick Chat")); pill.tabIndex = 0; this._container.appendChild(pill); - // Left side indicator (status) - const leftIndicator = $('span.agent-status-indicator'); - if (hasActiveSessions) { - // Running indicator when there are active sessions - const runningIcon = $('span.agent-status-icon'); - reset(runningIcon, renderIcon(Codicon.sessionInProgress)); - leftIndicator.appendChild(runningIcon); - const runningCount = $('span.agent-status-text'); - runningCount.textContent = String(activeSessions.length); - leftIndicator.appendChild(runningCount); - } else if (hasUnreadSessions) { - // Unread indicator when there are unread sessions - const unreadIcon = $('span.agent-status-icon'); - reset(unreadIcon, renderIcon(Codicon.circleFilled)); - leftIndicator.appendChild(unreadIcon); - const unreadCount = $('span.agent-status-text'); - unreadCount.textContent = String(unreadSessions.length); - leftIndicator.appendChild(unreadCount); + // Left icon container (sparkle by default, report+count when attention needed, search on hover) + const leftIcon = $('span.agent-status-left-icon'); + if (hasAttentionNeeded) { + // Show report icon + count when sessions need attention + const reportIcon = renderIcon(Codicon.report); + const countSpan = $('span.agent-status-attention-count'); + countSpan.textContent = String(attentionNeededSessions.length); + reset(leftIcon, reportIcon, countSpan); + leftIcon.classList.add('has-attention'); } else { - // Keyboard shortcut when idle (show quick chat keybinding - matches click action) - const kb = this.keybindingService.lookupKeybinding(ACTION_ID)?.getLabel(); - if (kb) { - const kbLabel = $('span.agent-status-keybinding'); - kbLabel.textContent = kb; - leftIndicator.appendChild(kbLabel); - } + reset(leftIcon, renderIcon(Codicon.searchSparkle)); } - pill.appendChild(leftIndicator); + pill.appendChild(leftIcon); - // Show label - either progress from most recent active session, or workspace name + // Label (workspace name by default, placeholder on hover) + // Show attention progress or default label const label = $('span.agent-status-label'); - const { session: activeSession, progress: progressText } = this._getMostRecentActiveSession(activeSessions); - this._displayedSession = activeSession; + const { session: attentionSession, progress: progressText } = this._getSessionNeedingAttention(attentionNeededSessions); + this._displayedSession = attentionSession; + + const defaultLabel = progressText ?? this._getLabel(); + if (progressText) { - // Show progress with fade-in animation label.classList.add('has-progress'); - label.textContent = progressText; - } else { - label.textContent = this._getLabel(); } + + const hoverLabel = localize('askAnythingPlaceholder', "Ask anything or describe what to build next"); + + label.textContent = defaultLabel; pill.appendChild(label); - // Send icon (right side) - only show when not streaming progress + // Send icon (hidden by default, shown on hover - only when not showing attention message) + const sendIcon = $('span.agent-status-send'); + reset(sendIcon, renderIcon(Codicon.send)); + sendIcon.classList.add('hidden'); + pill.appendChild(sendIcon); + + // Hover behavior - swap icon and label (only when showing default state). + // When progressText is defined (e.g. sessions need attention), keep the attention/progress + // message visible and do not replace it with the generic placeholder on hover. if (!progressText) { - const sendIcon = $('span.agent-status-send'); - reset(sendIcon, renderIcon(Codicon.send)); - pill.appendChild(sendIcon); + disposables.add(addDisposableListener(pill, EventType.MOUSE_ENTER, () => { + reset(leftIcon, renderIcon(Codicon.searchSparkle)); + leftIcon.classList.remove('has-attention'); + label.textContent = hoverLabel; + label.classList.remove('has-progress'); + sendIcon.classList.remove('hidden'); + })); + + disposables.add(addDisposableListener(pill, EventType.MOUSE_LEAVE, () => { + reset(leftIcon, renderIcon(Codicon.searchSparkle)); + label.textContent = defaultLabel; + sendIcon.classList.add('hidden'); + })); } - // Setup hover - show session name when displaying progress, otherwise show keybinding + // Setup hover tooltip const hoverDelegate = getDefaultHoverDelegate('mouse'); disposables.add(this.hoverService.setupManagedHover(hoverDelegate, pill, () => { if (this._displayedSession) { @@ -229,8 +304,8 @@ export class AgentStatusWidget extends BaseActionViewItem { } })); - // Search button (right of pill) - this._renderSearchButton(disposables); + // Status badge (separate rectangle on right) - always rendered for smooth transitions + this._renderStatusBadge(disposables, activeSessions, unreadSessions); } private _renderSessionMode(disposables: DisposableStore): void { @@ -238,68 +313,53 @@ export class AgentStatusWidget extends BaseActionViewItem { return; } + const { activeSessions, unreadSessions } = this._getSessionStats(); + const pill = $('div.agent-status-pill.session-mode'); this._container.appendChild(pill); - // Session title (left/center) + // Search button (left side, inside pill) + this._renderSearchButton(disposables, pill); + + // Session title (center) const titleLabel = $('span.agent-status-title'); const sessionInfo = this.agentStatusService.sessionInfo; titleLabel.textContent = sessionInfo?.title ?? localize('agentSessionProjection', "Agent Session Projection"); pill.appendChild(titleLabel); - // Escape button (right side) - serves as both keybinding hint and close button - const escButton = $('span.agent-status-esc-button'); - escButton.textContent = 'Esc'; - escButton.setAttribute('role', 'button'); - escButton.setAttribute('aria-label', localize('exitAgentSessionProjection', "Exit Agent Session Projection")); - escButton.tabIndex = 0; - pill.appendChild(escButton); + // Escape button (right side) + this._renderEscapeButton(disposables, pill); - // Setup hovers + // Setup pill hover const hoverDelegate = getDefaultHoverDelegate('mouse'); - disposables.add(this.hoverService.setupManagedHover(hoverDelegate, escButton, localize('exitAgentSessionProjectionTooltip', "Exit Agent Session Projection (Escape)"))); disposables.add(this.hoverService.setupManagedHover(hoverDelegate, pill, () => { const sessionInfo = this.agentStatusService.sessionInfo; return sessionInfo ? localize('agentSessionProjectionTooltip', "Agent Session Projection: {0}", sessionInfo.title) : localize('agentSessionProjection', "Agent Session Projection"); })); - // Esc button click handler - disposables.add(addDisposableListener(escButton, EventType.MOUSE_DOWN, (e) => { - e.preventDefault(); - e.stopPropagation(); - this.commandService.executeCommand(ExitAgentSessionProjectionAction.ID); - })); - - disposables.add(addDisposableListener(escButton, EventType.CLICK, (e) => { - e.preventDefault(); - e.stopPropagation(); - this.commandService.executeCommand(ExitAgentSessionProjectionAction.ID); - })); - - // Esc button keyboard handler - disposables.add(addDisposableListener(escButton, EventType.KEY_DOWN, (e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - e.stopPropagation(); - this.commandService.executeCommand(ExitAgentSessionProjectionAction.ID); - } - })); - - // Search button (right of pill) - this._renderSearchButton(disposables); + // Status badge (separate rectangle on right) - always rendered for smooth transitions + this._renderStatusBadge(disposables, activeSessions, unreadSessions); } - private _renderSearchButton(disposables: DisposableStore): void { - if (!this._container) { + // #endregion + + // #region Reusable Components + + /** + * Render the search button. If parent is provided, appends to parent; otherwise appends to container. + */ + private _renderSearchButton(disposables: DisposableStore, parent?: HTMLElement): void { + const container = parent ?? this._container; + if (!container) { return; } const searchButton = $('span.agent-status-search'); - reset(searchButton, renderIcon(Codicon.search)); + reset(searchButton, renderIcon(Codicon.searchSparkle)); searchButton.setAttribute('role', 'button'); searchButton.setAttribute('aria-label', localize('openQuickOpen', "Open Quick Open")); searchButton.tabIndex = 0; - this._container.appendChild(searchButton); + container.appendChild(searchButton); // Setup hover const hoverDelegate = getDefaultHoverDelegate('mouse'); @@ -326,6 +386,110 @@ export class AgentStatusWidget extends BaseActionViewItem { })); } + /** + * Render the status badge showing in-progress and/or unread session counts. + * Shows split UI with both indicators when both types exist. + * Always renders for smooth fade transitions - uses visibility classes. + */ + private _renderStatusBadge(disposables: DisposableStore, activeSessions: IAgentSession[], unreadSessions: IAgentSession[]): void { + if (!this._container) { + return; + } + + const hasActiveSessions = activeSessions.length > 0; + const hasUnreadSessions = unreadSessions.length > 0; + const hasContent = hasActiveSessions || hasUnreadSessions; + + const badge = $('div.agent-status-badge'); + if (!hasContent) { + badge.classList.add('empty'); + } + this._container.appendChild(badge); + + // Unread section (blue dot + count) + if (hasUnreadSessions) { + const unreadSection = $('span.agent-status-badge-section.unread'); + const unreadIcon = $('span.agent-status-icon'); + reset(unreadIcon, renderIcon(Codicon.circleFilled)); + unreadSection.appendChild(unreadIcon); + const unreadCount = $('span.agent-status-text'); + unreadCount.textContent = String(unreadSessions.length); + unreadSection.appendChild(unreadCount); + badge.appendChild(unreadSection); + } + + // In-progress section (session-in-progress icon + count) + if (hasActiveSessions) { + const activeSection = $('span.agent-status-badge-section.active'); + const runningIcon = $('span.agent-status-icon'); + reset(runningIcon, renderIcon(Codicon.sessionInProgress)); + activeSection.appendChild(runningIcon); + const runningCount = $('span.agent-status-text'); + runningCount.textContent = String(activeSessions.length); + activeSection.appendChild(runningCount); + badge.appendChild(activeSection); + } + + // Setup hover with combined tooltip + const hoverDelegate = getDefaultHoverDelegate('mouse'); + disposables.add(this.hoverService.setupManagedHover(hoverDelegate, badge, () => { + const parts: string[] = []; + if (hasUnreadSessions) { + parts.push(unreadSessions.length === 1 + ? localize('unreadSessionsTooltip1', "{0} unread session", unreadSessions.length) + : localize('unreadSessionsTooltip', "{0} unread sessions", unreadSessions.length)); + } + if (hasActiveSessions) { + parts.push(activeSessions.length === 1 + ? localize('activeSessionsTooltip1', "{0} session in progress", activeSessions.length) + : localize('activeSessionsTooltip', "{0} sessions in progress", activeSessions.length)); + } + return parts.join(', '); + })); + } + + /** + * Render the escape button for exiting session projection mode. + */ + private _renderEscapeButton(disposables: DisposableStore, parent: HTMLElement): void { + const escButton = $('span.agent-status-esc-button'); + escButton.textContent = 'Esc'; + escButton.setAttribute('role', 'button'); + escButton.setAttribute('aria-label', localize('exitAgentSessionProjection', "Exit Agent Session Projection")); + escButton.tabIndex = 0; + parent.appendChild(escButton); + + // Setup hover + const hoverDelegate = getDefaultHoverDelegate('mouse'); + disposables.add(this.hoverService.setupManagedHover(hoverDelegate, escButton, localize('exitAgentSessionProjectionTooltip', "Exit Agent Session Projection (Escape)"))); + + // Click handler + disposables.add(addDisposableListener(escButton, EventType.MOUSE_DOWN, (e) => { + e.preventDefault(); + e.stopPropagation(); + this.commandService.executeCommand(ExitAgentSessionProjectionAction.ID); + })); + + disposables.add(addDisposableListener(escButton, EventType.CLICK, (e) => { + e.preventDefault(); + e.stopPropagation(); + this.commandService.executeCommand(ExitAgentSessionProjectionAction.ID); + })); + + // Keyboard handler + disposables.add(addDisposableListener(escButton, EventType.KEY_DOWN, (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + this.commandService.executeCommand(ExitAgentSessionProjectionAction.ID); + } + })); + } + + // #endregion + + // #region Click Handlers + /** * Handle pill click - opens the displayed session if showing progress, otherwise executes default action */ @@ -337,17 +501,21 @@ export class AgentStatusWidget extends BaseActionViewItem { } } + // #endregion + + // #region Session Helpers + /** - * Get the most recently interacted active session and its progress text. - * Returns undefined session if no active sessions. + * Get the session most urgently needing user attention (approval/confirmation/input). + * Returns undefined if no sessions need attention. */ - private _getMostRecentActiveSession(activeSessions: IAgentSession[]): { session: IAgentSession | undefined; progress: string | undefined } { - if (activeSessions.length === 0) { + private _getSessionNeedingAttention(attentionNeededSessions: IAgentSession[]): { session: IAgentSession | undefined; progress: string | undefined } { + if (attentionNeededSessions.length === 0) { return { session: undefined, progress: undefined }; } // Sort by most recently started request - const sorted = [...activeSessions].sort((a, b) => { + const sorted = [...attentionNeededSessions].sort((a, b) => { const timeA = a.timing.lastRequestStarted ?? a.timing.created; const timeB = b.timing.lastRequestStarted ?? b.timing.created; return timeB - timeA; @@ -355,7 +523,7 @@ export class AgentStatusWidget extends BaseActionViewItem { const mostRecent = sorted[0]; if (!mostRecent.description) { - return { session: mostRecent, progress: undefined }; + return { session: mostRecent, progress: mostRecent.label }; } // Convert markdown to plain text if needed @@ -366,6 +534,10 @@ export class AgentStatusWidget extends BaseActionViewItem { return { session: mostRecent, progress }; } + // #endregion + + // #region Label Helpers + /** * Compute the label to display, matching the command center behavior. * Includes prefix and suffix decorations (remote host, extension dev host, etc.) @@ -419,4 +591,6 @@ export class AgentStatusWidget extends BaseActionViewItem { return { prefix, suffix }; } + + // #endregion } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css index 541e3bf02a5..0b7063507a4 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css @@ -76,7 +76,7 @@ Agent Status Widget - Titlebar control } .agent-status-pill.chat-input-mode.has-active .agent-status-label { - color: var(--vscode-progressBar-background); + color: var(--vscode-foreground); opacity: 1; } @@ -85,6 +85,22 @@ Agent Status Widget - Titlebar control font-size: 8px; } +/* Needs attention state - session requires user approval/confirmation/input */ +.agent-status-pill.chat-input-mode.needs-attention { + background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent); + border: 1px solid color-mix(in srgb, var(--vscode-progressBar-background) 50%, transparent); +} + +.agent-status-pill.chat-input-mode.needs-attention:hover { + background-color: color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent); + border-color: color-mix(in srgb, var(--vscode-progressBar-background) 70%, transparent); +} + +.agent-status-pill.chat-input-mode.needs-attention .agent-status-label { + color: var(--vscode-foreground); + opacity: 1; +} + /* Session mode (viewing a session) */ .agent-status-pill.session-mode { background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent); @@ -111,7 +127,7 @@ Agent Status Widget - Titlebar control /* Progress label - fade in animation when showing session progress */ .agent-status-label.has-progress { animation: agentStatusFadeIn 0.3s ease-out; - color: var(--vscode-progressBar-background); + color: var(--vscode-foreground); opacity: 1; } @@ -159,6 +175,11 @@ Agent Status Widget - Titlebar control align-items: center; color: var(--vscode-foreground); opacity: 0.7; + flex-shrink: 0; +} + +.agent-status-send.hidden { + display: none; } .agent-status-pill.has-active .agent-status-send { @@ -166,6 +187,28 @@ Agent Status Widget - Titlebar control opacity: 1; } +/* Left icon (sparkle default, report+count when attention needed, search on hover) */ +.agent-status-left-icon { + display: flex; + align-items: center; + justify-content: center; + gap: 3px; + color: var(--vscode-foreground); + opacity: 0.7; + flex-shrink: 0; +} + +/* Left icon with attention - show report icon + count */ +.agent-status-left-icon.has-attention { + color: var(--vscode-foreground); + opacity: 1; +} + +.agent-status-left-icon.has-attention .agent-status-attention-count { + font-size: 11px; + font-weight: 500; +} + /* Session title */ .agent-status-title { flex: 1; @@ -208,26 +251,78 @@ Agent Status Widget - Titlebar control outline-offset: 1px; } -/* Search button (right of pill) */ +/* Search button (inside pill on left) */ .agent-status-search { display: flex; align-items: center; justify-content: center; - width: 22px; - height: 22px; - border-radius: 4px; - cursor: pointer; color: var(--vscode-foreground); opacity: 0.7; -webkit-app-region: no-drag; + flex-shrink: 0; } .agent-status-search:hover { opacity: 1; - background-color: var(--vscode-commandCenter-activeBackground, rgba(0, 0, 0, 0.1)); } .agent-status-search:focus { outline: 1px solid var(--vscode-focusBorder); outline-offset: -1px; } + +/* Status badge (separate rectangle on right of pill) */ +.agent-status-badge { + display: flex; + align-items: center; + gap: 0; + height: 22px; + border-radius: 6px; + overflow: hidden; + background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent); + border: 1px solid color-mix(in srgb, var(--vscode-progressBar-background) 50%, transparent); + flex-shrink: 0; + -webkit-app-region: no-drag; + transition: opacity 0.2s ease-in-out, background-color 0.2s ease-in-out, border-color 0.2s ease-in-out; + opacity: 1; + /* Reserve minimum width to prevent layout shift */ + min-width: 50px; + justify-content: center; +} + +/* Empty badge - invisible but reserves space to prevent layout shift */ +.agent-status-badge.empty { + opacity: 0; + pointer-events: none; + background-color: transparent; + border-color: transparent; +} + +/* Badge section (for split UI) */ +.agent-status-badge-section { + display: flex; + align-items: center; + gap: 4px; + padding: 0 8px; + height: 100%; +} + +/* Separator between sections */ +.agent-status-badge-section + .agent-status-badge-section { + border-left: 1px solid color-mix(in srgb, var(--vscode-progressBar-background) 40%, transparent); +} + +/* Unread section styling */ +.agent-status-badge-section.unread { + color: var(--vscode-foreground); +} + +.agent-status-badge-section.unread .agent-status-icon { + font-size: 8px; + color: var(--vscode-notificationsInfoIcon-foreground); +} + +/* Active/in-progress section styling */ +.agent-status-badge-section.active { + color: var(--vscode-foreground); +} diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index c9510266cbc..3fb5d98607c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -756,6 +756,9 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ public async getChatSessionItems(providersToResolve: readonly string[] | undefined, token: CancellationToken): Promise> { const results: Array<{ readonly chatSessionType: string; readonly items: IChatSessionItem[] }> = []; + const resolvedProviderTypes = new Set(); + + // First, iterate over extension point contributions for (const contrib of this.getAllChatSessionContributions()) { if (providersToResolve && !providersToResolve.includes(contrib.type)) { continue; // skip: not considered for resolving @@ -774,6 +777,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ const providerSessions = await raceCancellationError(provider.provideChatSessionItems(token), token); this._logService.trace(`[ChatSessionsService] Resolved ${providerSessions.length} sessions for provider ${provider.chatSessionType}`); results.push({ chatSessionType: provider.chatSessionType, items: providerSessions }); + resolvedProviderTypes.add(provider.chatSessionType); } catch (error) { // Log error but continue with other providers this._logService.error(`[ChatSessionsService] Failed to resolve sessions for provider ${provider.chatSessionType}`, error); @@ -781,6 +785,26 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ } } + // Also include registered items providers that don't have corresponding contributions + // (e.g., the local session provider which is built-in and not an extension contribution) + for (const [chatSessionType, provider] of this._itemsProviders) { + if (resolvedProviderTypes.has(chatSessionType)) { + continue; // already resolved via contribution + } + if (providersToResolve && !providersToResolve.includes(chatSessionType)) { + continue; // skip: not considered for resolving + } + + try { + const providerSessions = await raceCancellationError(provider.provideChatSessionItems(token), token); + this._logService.trace(`[ChatSessionsService] Resolved ${providerSessions.length} sessions for built-in provider ${chatSessionType}`); + results.push({ chatSessionType, items: providerSessions }); + } catch (error) { + this._logService.error(`[ChatSessionsService] Failed to resolve sessions for built-in provider ${chatSessionType}`, error); + continue; + } + } + return results; } From 03f79ac30bd46fc92f3d2be4cd92906e7a375761 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 15 Jan 2026 18:54:31 -0800 Subject: [PATCH 042/387] Agent skills extension file contribution API (#287671) --- .../chatPromptFilesContribution.ts | 10 +- .../service/promptsServiceImpl.ts | 95 ++++++- .../service/promptsService.test.ts | 258 ++++++++++++++++++ 3 files changed, 358 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts index 6952e85834f..6179489270e 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts @@ -21,7 +21,7 @@ interface IRawChatFileContribution { readonly description?: string; } -type ChatContributionPoint = 'chatPromptFiles' | 'chatInstructions' | 'chatAgents'; +type ChatContributionPoint = 'chatPromptFiles' | 'chatInstructions' | 'chatAgents' | 'chatSkills'; function registerChatFilesExtensionPoint(point: ChatContributionPoint) { return extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ @@ -62,12 +62,14 @@ function registerChatFilesExtensionPoint(point: ChatContributionPoint) { const epPrompt = registerChatFilesExtensionPoint('chatPromptFiles'); const epInstructions = registerChatFilesExtensionPoint('chatInstructions'); const epAgents = registerChatFilesExtensionPoint('chatAgents'); +const epSkills = registerChatFilesExtensionPoint('chatSkills'); function pointToType(contributionPoint: ChatContributionPoint): PromptsType { switch (contributionPoint) { case 'chatPromptFiles': return PromptsType.prompt; case 'chatInstructions': return PromptsType.instructions; case 'chatAgents': return PromptsType.agent; + case 'chatSkills': return PromptsType.skill; } } @@ -86,6 +88,7 @@ export class ChatPromptFilesExtensionPointHandler implements IWorkbenchContribut this.handle(epPrompt, 'chatPromptFiles'); this.handle(epInstructions, 'chatInstructions'); this.handle(epAgents, 'chatAgents'); + this.handle(epSkills, 'chatSkills'); } private handle(extensionPoint: extensionsRegistry.IExtensionPoint, contributionPoint: ChatContributionPoint) { @@ -136,15 +139,16 @@ CommandsRegistry.registerCommand('_listExtensionPromptFiles', async (accessor): const promptsService = accessor.get(IPromptsService); // Get extension prompt files for all prompt types in parallel - const [agents, instructions, prompts] = await Promise.all([ + const [agents, instructions, prompts, skills] = await Promise.all([ promptsService.listPromptFiles(PromptsType.agent, CancellationToken.None), promptsService.listPromptFiles(PromptsType.instructions, CancellationToken.None), promptsService.listPromptFiles(PromptsType.prompt, CancellationToken.None), + promptsService.listPromptFiles(PromptsType.skill, CancellationToken.None), ]); // Combine all files and collect extension-contributed ones const result: IExtensionPromptFileResult[] = []; - for (const file of [...agents, ...instructions, ...prompts]) { + for (const file of [...agents, ...instructions, ...prompts, ...skills]) { if (file.storage === PromptsStorage.extension) { result.push({ uri: file.uri.toJSON(), type: file.type }); } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 442eae11691..2be3f234d1f 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -37,6 +37,37 @@ import { Delayer } from '../../../../../../base/common/async.js'; import { Schemas } from '../../../../../../base/common/network.js'; import { IChatPromptContentStore } from '../chatPromptContentStore.js'; +/** + * Error thrown when a skill file is missing the required name attribute. + */ +export class SkillMissingNameError extends Error { + constructor(public readonly uri: URI) { + super('Skill file must have a name attribute'); + } +} + +/** + * Error thrown when a skill file is missing the required description attribute. + */ +export class SkillMissingDescriptionError extends Error { + constructor(public readonly uri: URI) { + super('Skill file must have a description attribute'); + } +} + +/** + * Error thrown when a skill's name does not match its parent folder name. + */ +export class SkillNameMismatchError extends Error { + constructor( + public readonly uri: URI, + public readonly skillName: string, + public readonly folderName: string + ) { + super(`Skill name must match folder name: expected "${folderName}" but got "${skillName}"`); + } +} + /** * Provides prompt services. */ @@ -537,6 +568,19 @@ export class PromptsService extends Disposable implements IPromptsService { return Disposable.None; } const entryPromise = (async () => { + // For skills, validate that the file follows the required structure + if (type === PromptsType.skill) { + try { + const validated = await this.validateAndSanitizeSkillFile(uri, CancellationToken.None); + name = validated.name; + description = validated.description; + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + this.logger.error(`[registerContributedFile] Extension '${extension.identifier.value}' failed to validate skill file: ${uri}`, msg); + throw e; + } + } + try { await this.filesConfigService.updateReadonly(uri, true); } catch (e) { @@ -647,6 +691,40 @@ export class PromptsService extends Disposable implements IPromptsService { return text.replace(/<[^>]+>/g, ''); } + /** + * Validates and sanitizes a skill file. Throws an error if validation fails. + * @returns The sanitized name and description + */ + private async validateAndSanitizeSkillFile(uri: URI, token: CancellationToken): Promise<{ name: string; description: string | undefined }> { + const parsedFile = await this.parseNew(uri, token); + const name = parsedFile.header?.name; + + if (!name) { + this.logger.error(`[validateAndSanitizeSkillFile] Agent skill file missing name attribute: ${uri}`); + throw new SkillMissingNameError(uri); + } + + const description = parsedFile.header?.description; + if (!description) { + this.logger.error(`[validateAndSanitizeSkillFile] Agent skill file missing description attribute: ${uri}`); + throw new SkillMissingDescriptionError(uri); + } + + // Sanitize the name first (remove XML tags and truncate) + const sanitizedName = this.truncateAgentSkillName(name, uri); + + // Validate that the sanitized name matches the parent folder name (per agentskills.io specification) + const skillFolderUri = dirname(uri); + const folderName = basename(skillFolderUri); + if (sanitizedName !== folderName) { + this.logger.error(`[validateAndSanitizeSkillFile] Agent skill name "${sanitizedName}" does not match folder name "${folderName}": ${uri}`); + throw new SkillNameMismatchError(uri, sanitizedName, folderName); + } + + const sanitizedDescription = this.truncateAgentSkillDescription(parsedFile.header?.description, uri); + return { name: sanitizedName, description: sanitizedDescription }; + } + private truncateAgentSkillName(name: string, uri: URI): string { const MAX_NAME_LENGTH = 64; const sanitized = this.sanitizeAgentSkillText(name); @@ -685,6 +763,7 @@ export class PromptsService extends Disposable implements IPromptsService { const seenNames = new Set(); const skillTypes = new Map(); let skippedMissingName = 0; + let skippedMissingDescription = 0; let skippedDuplicateName = 0; let skippedParseFailed = 0; let skippedNameMismatch = 0; @@ -725,8 +804,17 @@ export class PromptsService extends Disposable implements IPromptsService { // Track skill type skillTypes.set(source, (skillTypes.get(source) || 0) + 1); } catch (e) { - skippedParseFailed++; - this.logger.error(`[findAgentSkills] Failed to parse Agent skill file: ${uri}`, e instanceof Error ? e.message : String(e)); + if (e instanceof SkillMissingNameError) { + skippedMissingName++; + } else if (e instanceof SkillMissingDescriptionError) { + skippedMissingDescription++; + } else if (e instanceof SkillNameMismatchError) { + skippedNameMismatch++; + } else { + skippedParseFailed++; + } + const msg = e instanceof Error ? e.message : String(e); + this.logger.error(`[findAgentSkills] Failed to validate Agent skill file: ${uri}`, msg); } }; @@ -777,6 +865,7 @@ export class PromptsService extends Disposable implements IPromptsService { extensionAPI: number; skippedDuplicateName: number; skippedMissingName: number; + skippedMissingDescription: number; skippedNameMismatch: number; skippedParseFailed: number; }; @@ -793,6 +882,7 @@ export class PromptsService extends Disposable implements IPromptsService { extensionAPI: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of extension API provided skills.' }; skippedDuplicateName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of skills skipped due to duplicate names.' }; skippedMissingName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of skills skipped due to missing name attribute.' }; + skippedMissingDescription: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of skills skipped due to missing description attribute.' }; skippedNameMismatch: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of skills skipped due to name not matching folder name.' }; skippedParseFailed: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of skills skipped due to parse failures.' }; owner: 'pwang347'; @@ -811,6 +901,7 @@ export class PromptsService extends Disposable implements IPromptsService { extensionAPI: skillTypes.get(PromptFileSource.ExtensionAPI) ?? 0, skippedDuplicateName, skippedMissingName, + skippedMissingDescription, skippedNameMismatch, skippedParseFailed }); 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 705865d9d65..ac42dfb5c4b 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 @@ -1565,6 +1565,264 @@ suite('PromptsService', () => { }); }); + suite('listPromptFiles - skills', () => { + teardown(() => { + sinon.restore(); + }); + + test('should list skill files from workspace', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); + + const rootFolderName = 'list-skills-workspace'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/skills/skill1/SKILL.md`, + contents: [ + '---', + 'name: "Skill 1"', + 'description: "First skill"', + '---', + 'Skill 1 content', + ], + }, + { + path: `${rootFolder}/.claude/skills/skill2/SKILL.md`, + contents: [ + '---', + 'name: "Skill 2"', + 'description: "Second skill"', + '---', + 'Skill 2 content', + ], + }, + ]); + + const result = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); + + assert.strictEqual(result.length, 2, 'Should find 2 skills'); + + const skill1 = result.find(s => s.uri.path.includes('skill1')); + assert.ok(skill1, 'Should find skill1'); + assert.strictEqual(skill1.type, PromptsType.skill); + assert.strictEqual(skill1.storage, PromptsStorage.local); + + const skill2 = result.find(s => s.uri.path.includes('skill2')); + assert.ok(skill2, 'Should find skill2'); + assert.strictEqual(skill2.type, PromptsType.skill); + assert.strictEqual(skill2.storage, PromptsStorage.local); + }); + + test('should list skill files from user home', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); + + const rootFolderName = 'list-skills-user-home'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: '/home/user/.copilot/skills/personal-skill/SKILL.md', + contents: [ + '---', + 'name: "Personal Skill"', + 'description: "A personal skill"', + '---', + 'Personal skill content', + ], + }, + { + path: '/home/user/.claude/skills/claude-personal/SKILL.md', + contents: [ + '---', + 'name: "Claude Personal Skill"', + 'description: "A Claude personal skill"', + '---', + 'Claude personal skill content', + ], + }, + ]); + + const result = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); + + const personalSkills = result.filter(s => s.storage === PromptsStorage.user); + assert.strictEqual(personalSkills.length, 2, 'Should find 2 personal skills'); + + const copilotSkill = personalSkills.find(s => s.uri.path.includes('.copilot')); + assert.ok(copilotSkill, 'Should find copilot personal skill'); + + const claudeSkill = personalSkills.find(s => s.uri.path.includes('.claude')); + assert.ok(claudeSkill, 'Should find claude personal skill'); + }); + + test('should not list skills when not in skill folder structure', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + + const rootFolderName = 'no-skills'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + // Create files in non-skill locations + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/prompts/SKILL.md`, + contents: [ + '---', + 'name: "Not a skill"', + '---', + 'This is in prompts folder, not skills', + ], + }, + { + path: `${rootFolder}/SKILL.md`, + contents: [ + '---', + 'name: "Root skill"', + '---', + 'This is in root, not skills folder', + ], + }, + ]); + + const result = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); + + assert.strictEqual(result.length, 0, 'Should not find any skills in non-skill locations'); + }); + + test('should handle mixed workspace and user home skills', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); + + const rootFolderName = 'mixed-skills'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + // Workspace skills + { + path: `${rootFolder}/.github/skills/workspace-skill/SKILL.md`, + contents: [ + '---', + 'name: "Workspace Skill"', + 'description: "A workspace skill"', + '---', + 'Workspace skill content', + ], + }, + // User home skills + { + path: '/home/user/.copilot/skills/personal-skill/SKILL.md', + contents: [ + '---', + 'name: "Personal Skill"', + 'description: "A personal skill"', + '---', + 'Personal skill content', + ], + }, + ]); + + const result = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); + + const workspaceSkills = result.filter(s => s.storage === PromptsStorage.local); + const userSkills = result.filter(s => s.storage === PromptsStorage.user); + + assert.strictEqual(workspaceSkills.length, 1, 'Should find 1 workspace skill'); + assert.strictEqual(userSkills.length, 1, 'Should find 1 user skill'); + }); + + test('should respect disabled default paths via config', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + // Disable .github/skills, only .claude/skills should be searched + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, { + '.github/skills': false, + '.claude/skills': true, + }); + + const rootFolderName = 'disabled-default-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/skills/github-skill/SKILL.md`, + contents: [ + '---', + 'name: "GitHub Skill"', + 'description: "Should NOT be found"', + '---', + 'This skill is in a disabled folder', + ], + }, + { + path: `${rootFolder}/.claude/skills/claude-skill/SKILL.md`, + contents: [ + '---', + 'name: "Claude Skill"', + 'description: "Should be found"', + '---', + 'This skill is in an enabled folder', + ], + }, + ]); + + const result = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); + + assert.strictEqual(result.length, 1, 'Should find only 1 skill (from enabled folder)'); + assert.ok(result[0].uri.path.includes('.claude/skills'), 'Should only find skill from .claude/skills'); + assert.ok(!result[0].uri.path.includes('.github/skills'), 'Should not find skill from disabled .github/skills'); + }); + + test('should expand tilde paths in custom locations', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + // Add a tilde path as custom location + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, { + '.github/skills': false, + '.claude/skills': false, + '~/my-custom-skills': true, + }); + + const rootFolderName = 'tilde-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + // The mock user home is /home/user, so ~/my-custom-skills should resolve to /home/user/my-custom-skills + await mockFiles(fileService, [ + { + path: '/home/user/my-custom-skills/custom-skill/SKILL.md', + contents: [ + '---', + 'name: "Custom Skill"', + 'description: "A skill from tilde path"', + '---', + 'Skill content from ~/my-custom-skills', + ], + }, + ]); + + const result = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); + + assert.strictEqual(result.length, 1, 'Should find 1 skill from tilde-expanded path'); + assert.ok(result[0].uri.path.includes('/home/user/my-custom-skills'), 'Path should be expanded from tilde'); + }); + }); + suite('listPromptFiles - extensions', () => { test('Contributed prompt file', async () => { From 563f788ca95716034e7f68e7e113640d727cb8a8 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 16 Jan 2026 07:19:00 +0100 Subject: [PATCH 043/387] deps - check in a package.lock (#288233) --- test/mcp/package-lock.json | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index 00b5019d0b4..7438ab0d27a 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -839,16 +839,6 @@ "node": ">= 0.4" } }, - "node_modules/hono": { - "version": "4.11.4", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", - "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=16.9.0" - } - }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", From 58cd404d33b4076cd79d0509e86551fe72c724b8 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:19:22 +0800 Subject: [PATCH 044/387] do not pin mermaid tool call (#288226) --- .../contrib/chat/browser/widget/chatListRenderer.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 2f964a2b26b..4a665c1c095 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -1288,6 +1288,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer Date: Thu, 15 Jan 2026 22:19:38 -0800 Subject: [PATCH 045/387] Fix some subagent ui jank (#288227) Properly join runSubagent tool invocation with its tool calls --- .../widget/chatContentParts/chatSubagentContentPart.ts | 8 ++++---- .../chat/common/tools/builtinTools/runSubagentTool.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts index ed335fca9d7..e2f4dfcadbb 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts @@ -104,7 +104,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen node.tabIndex = 0; // Hide initially until there are tool calls - node.style.display = 'none'; + this.wrapper.style.display = 'none'; if (this._collapseButton && !this.element.isComplete) { this._collapseButton.icon = ThemeIcon.modify(Codicon.loading, 'spin'); @@ -293,8 +293,8 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen dom.append(this.wrapper, this.resultContainer); // Show the container if it was hidden - if (this.domNode.style.display === 'none') { - this.domNode.style.display = ''; + if (this.wrapper.style.display === 'none') { + this.wrapper.style.display = ''; } this._onDidChangeHeight.fire(); @@ -308,7 +308,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen // Show the container when first tool item is added if (!this.hasToolItems) { this.hasToolItems = true; - this.domNode.style.display = ''; + this.wrapper.style.display = ''; } // Wrap with icon like thinking parts do, but skip icon for tools needing confirmation diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts index 6f9295fd725..5ad017201b2 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -229,7 +229,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { message: args.prompt, variables: { variables: variableSet.asArray() }, location: ChatAgentLocation.Chat, - subAgentInvocationId: invocation.chatStreamToolCallId, + subAgentInvocationId: invocation.callId, userSelectedModelId: modeModelId, userSelectedTools: modeTools, modeInstructions, From e719265db2176d739401436eb2ac1ec94139e389 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:50:40 +0800 Subject: [PATCH 046/387] change thinking icons and ui fixes (#288248) * change thinking icons and style fixes * fix padding --- .../contrib/chat/browser/tools/languageModelToolsService.ts | 2 +- .../widget/chatContentParts/chatThinkingContentPart.ts | 6 +++--- .../widget/chatContentParts/media/chatThinkingContent.css | 6 ++++++ src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 1 + .../chat/test/common/tools/mockLanguageModelToolsService.ts | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index e56ca5d25cd..57608cc675b 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -169,7 +169,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo 'read', SpecedToolAliases.read, { - icon: ThemeIcon.fromId(Codicon.eye.id), + icon: ThemeIcon.fromId(Codicon.book.id), description: localize('copilot.toolSet.read.description', 'Read files in your workspace'), } )); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index b3e5994ac59..ccec61f72f8 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -55,14 +55,14 @@ export function getToolInvocationIcon(toolId: string): ThemeIcon { lowerToolId.includes('get_file') || lowerToolId.includes('problems') ) { - return Codicon.eye; + return Codicon.book; } if ( lowerToolId.includes('edit') || lowerToolId.includes('create') ) { - return Codicon.pencil; + return Codicon.wand; } if ( @@ -532,7 +532,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen let icon: ThemeIcon; if (isMarkdownEdit) { - icon = Codicon.pencil; + icon = Codicon.wand; } else if (isTerminalTool) { const terminalData = (toolInvocationOrMarkdown as IChatToolInvocation | IChatToolInvocationSerialized).toolSpecificData as { kind: 'terminal'; terminalCommandState?: { exitCode?: number } }; const exitCode = terminalData?.terminalCommandState?.exitCode; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css index 041ba90429a..bec837f2640 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css @@ -65,6 +65,7 @@ .chat-used-context-list.chat-terminal-thinking-content { border: none; padding: 0; + margin-bottom: 2px; .progress-container { margin: 0; @@ -158,6 +159,11 @@ color: var(--vscode-descriptionForeground); } + /* the default book icon has a pixel of space at the bottom */ + > .chat-thinking-icon .codicon.codicon-book { + top: 10px; + } + } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 6e06a7a8260..69e49066729 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -2197,6 +2197,7 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .chat-used-context-label .monaco-button { padding: 2px 6px 2px 2px; + font-size: var(--vscode-chat-font-size-body-m); } .interactive-session .chat-file-changes-label .monaco-button:hover { diff --git a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts index 2bc3f49c5d2..1a185e1bc97 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts @@ -19,7 +19,7 @@ export class MockLanguageModelToolsService implements ILanguageModelToolsService _serviceBrand: undefined; vscodeToolSet: ToolSet = new ToolSet('vscode', 'vscode', ThemeIcon.fromId(Codicon.code.id), ToolDataSource.Internal); executeToolSet: ToolSet = new ToolSet('execute', 'execute', ThemeIcon.fromId(Codicon.terminal.id), ToolDataSource.Internal); - readToolSet: ToolSet = new ToolSet('read', 'read', ThemeIcon.fromId(Codicon.eye.id), ToolDataSource.Internal); + readToolSet: ToolSet = new ToolSet('read', 'read', ThemeIcon.fromId(Codicon.book.id), ToolDataSource.Internal); constructor() { } From 338850982fd037b0e5cfa3f2a0ed424b701cd342 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 16 Jan 2026 09:06:24 +0100 Subject: [PATCH 047/387] Chat - background session should always use the session menu (#288254) --- .../contrib/chat/browser/widget/input/chatInputPart.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index c150ff9e16b..c170b7da1a5 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -2459,7 +2459,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge let baseLabel = entries.length === 1 ? localize('chatEditingSession.oneFile.1', '1 file changed') : localize('chatEditingSession.manyFiles.1', '{0} files changed', entries.length); let shouldShowEditingSession = added > 0 || removed > 0; - let topLevelIsSessionMenu = false; + let topLevelIsSessionMenu = sessionResource && getChatSessionType(sessionResource) !== localChatSessionType; if (added === 0 && removed === 0) { const sessionValue = sessionFileChanges.read(reader) || []; From 45d95bbbe4c12695b7f40646a297ca4f5fe158ba Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 16 Jan 2026 09:13:37 +0100 Subject: [PATCH 048/387] Defer remote extension host activation for Immediate activations When `activateByEvent` is called with `ActivationKind.Immediate`, only activate on local extension hosts (LocalProcess, LocalWebWorker) and defer remote host activation until the remote host is ready. This prevents blocking startup when extensions need immediate activation but the remote host isn't available yet. Changes: - Add `activationKind` to `IWillActivateEvent` interface - Track deferred activation events in `_pendingRemoteActivationEvents` - Filter to local hosts only for Immediate activation - Replay deferred events on remote hosts after initialization - Fire `onWillActivateByEvent` again with Normal kind when replaying Fixes #260061 --- .../common/abstractExtensionService.ts | 52 +++++++++++++- .../services/extensions/common/extensions.ts | 1 + .../test/browser/extensionService.test.ts | 69 ++++++++++++++++++- test/mcp/package-lock.json | 10 --- 4 files changed, 117 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index e61278a6153..2a05bc8a8d4 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -87,6 +87,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx private readonly _installedExtensionsReady = new Barrier(); private readonly _extensionStatus = new ExtensionIdentifierMap(); private readonly _allRequestedActivateEvents = new Set(); + private readonly _pendingRemoteActivationEvents = new Set(); private readonly _runningLocations: ExtensionRunningLocationTracker; private readonly _remoteCrashTracker = new ExtensionHostCrashTracker(); @@ -475,9 +476,43 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._releaseBarrier(); perf.mark('code/didLoadExtensions'); + + // Activate deferred remote events now that remote hosts are starting + // This is done after the barrier is released to avoid blocking initialization + this._activateDeferredRemoteEvents(); + await this._handleExtensionTests(); } + private async _activateDeferredRemoteEvents(): Promise { + if (this._pendingRemoteActivationEvents.size === 0) { + return; + } + + const remoteExtensionHosts = this._getExtensionHostManagers(ExtensionHostKind.Remote); + if (remoteExtensionHosts.length === 0) { + this._pendingRemoteActivationEvents.clear(); + return; + } + + // Wait for remote extension hosts to be ready + await Promise.all(remoteExtensionHosts.map(extHost => extHost.ready())); + + // Replay deferred activation events on remote hosts + for (const activationEvent of this._pendingRemoteActivationEvents) { + const result = Promise.all( + remoteExtensionHosts.map(extHostManager => extHostManager.activateByEvent(activationEvent, ActivationKind.Normal)) + ).then(() => { }); + this._onWillActivateByEvent.fire({ + event: activationEvent, + activation: result, + activationKind: ActivationKind.Normal + }); + } + + this._pendingRemoteActivationEvents.clear(); + } + private async _resolveAndProcessExtensions(lock: ExtensionDescriptionRegistryLock,): Promise { let resolverExtensions: IExtensionDescription[] = []; let localExtensions: IExtensionDescription[] = []; @@ -985,12 +1020,25 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } private _activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise { + let managers: IExtensionHostManager[]; + if (activationKind === ActivationKind.Immediate) { + // For immediate activation, only activate on local extension hosts + // and defer remote activation until the remote host is ready + managers = this._extensionHostManagers.filter( + extHostManager => extHostManager.kind === ExtensionHostKind.LocalProcess || extHostManager.kind === ExtensionHostKind.LocalWebWorker + ); + this._pendingRemoteActivationEvents.add(activationEvent); + } else { + managers = [...this._extensionHostManagers]; + } + const result = Promise.all( - this._extensionHostManagers.map(extHostManager => extHostManager.activateByEvent(activationEvent, activationKind)) + managers.map(extHostManager => extHostManager.activateByEvent(activationEvent, activationKind)) ).then(() => { }); this._onWillActivateByEvent.fire({ event: activationEvent, - activation: result + activation: result, + activationKind }); return result; } diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index 642bfde8c3a..c7fee71a2e2 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -368,6 +368,7 @@ export class ExtensionPointContribution { export interface IWillActivateEvent { readonly event: string; readonly activation: Promise; + readonly activationKind: ActivationKind; } export interface IResponsiveStateChangeEvent { diff --git a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts index 07b82a2c144..d9ee5186d63 100644 --- a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts +++ b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts @@ -40,7 +40,7 @@ import { IExtensionHostManager } from '../../common/extensionHostManagers.js'; import { ExtensionManifestPropertiesService, IExtensionManifestPropertiesService } from '../../common/extensionManifestPropertiesService.js'; import { ExtensionRunningLocation } from '../../common/extensionRunningLocation.js'; import { ExtensionRunningLocationTracker } from '../../common/extensionRunningLocationTracker.js'; -import { IExtensionHost, IExtensionService } from '../../common/extensions.js'; +import { ActivationKind, IExtensionHost, IExtensionService, IWillActivateEvent } from '../../common/extensions.js'; import { ExtensionsProposedApi } from '../../common/extensionsProposedApi.js'; import { ILifecycleService } from '../../../lifecycle/common/lifecycle.js'; import { IRemoteAgentService } from '../../../remote/common/remoteAgentService.js'; @@ -189,16 +189,20 @@ suite('ExtensionService', () => { private _extHostId = 0; public readonly order: string[] = []; + public readonly activationEvents: { event: string; activationKind: ActivationKind; kind: ExtensionHostKind }[] = []; protected _pickExtensionHostKind(extensionId: ExtensionIdentifier, extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean, preference: ExtensionRunningPreference): ExtensionHostKind | null { throw new Error('Method not implemented.'); } protected override _doCreateExtensionHostManager(extensionHost: IExtensionHost, initialActivationEvents: string[]): IExtensionHostManager { const order = this.order; + const activationEvents = this.activationEvents; const extensionHostId = ++this._extHostId; + const extHostKind = extensionHost.runningLocation.kind; order.push(`create ${extensionHostId}`); return new class extends mock() { override onDidExit = Event.None; override onDidChangeResponsiveState = Event.None; + override kind = extHostKind; override disconnect() { return Promise.resolve(); } @@ -211,10 +215,17 @@ suite('ExtensionService', () => { override representsRunningLocation(runningLocation: ExtensionRunningLocation): boolean { return extensionHost.runningLocation.equals(runningLocation); } + override activateByEvent(event: string, activationKind: ActivationKind): Promise { + activationEvents.push({ event, activationKind, kind: extHostKind }); + return Promise.resolve(); + } + override ready(): Promise { + return Promise.resolve(); + } }; } - protected _resolveExtensions(): AsyncIterable { - throw new Error('Method not implemented.'); + protected async *_resolveExtensions(): AsyncIterable { + // Return empty iterable - no extensions to resolve in tests } protected _scanSingleExtension(extension: IExtension): Promise { throw new Error('Method not implemented.'); @@ -300,4 +311,56 @@ suite('ExtensionService', () => { await extService.stopExtensionHosts('foo'); assert.deepStrictEqual(extService.order, (['create 1', 'create 2', 'create 3'])); }); + + test('onWillActivateByEvent includes activationKind for Normal activation', async () => { + await extService.startExtensionHosts(); + + const events: IWillActivateEvent[] = []; + disposables.add(extService.onWillActivateByEvent(e => events.push(e))); + + await extService.activateByEvent('onTest', ActivationKind.Normal); + + assert.strictEqual(events.length, 1); + assert.strictEqual(events[0].event, 'onTest'); + assert.strictEqual(events[0].activationKind, ActivationKind.Normal); + }); + + test('onWillActivateByEvent includes activationKind for Immediate activation', async () => { + await extService.startExtensionHosts(); + + const events: IWillActivateEvent[] = []; + disposables.add(extService.onWillActivateByEvent(e => events.push(e))); + + await extService.activateByEvent('onTest', ActivationKind.Immediate); + + assert.strictEqual(events.length, 1); + assert.strictEqual(events[0].event, 'onTest'); + assert.strictEqual(events[0].activationKind, ActivationKind.Immediate); + }); + + test('Immediate activation only activates local extension hosts', async () => { + await extService.startExtensionHosts(); + extService.activationEvents.length = 0; // Clear any initial activations + + await extService.activateByEvent('onTest', ActivationKind.Immediate); + + // Should only activate on local hosts (LocalProcess and LocalWebWorker), not Remote + const activatedKinds = extService.activationEvents.map(e => e.kind); + assert.ok(activatedKinds.includes(ExtensionHostKind.LocalProcess), 'Should activate on LocalProcess'); + assert.ok(activatedKinds.includes(ExtensionHostKind.LocalWebWorker), 'Should activate on LocalWebWorker'); + assert.ok(!activatedKinds.includes(ExtensionHostKind.Remote), 'Should NOT activate on Remote'); + }); + + test('Normal activation activates all extension hosts', async () => { + await extService.startExtensionHosts(); + extService.activationEvents.length = 0; // Clear any initial activations + + await extService.activateByEvent('onTest', ActivationKind.Normal); + + // Should activate on all hosts + const activatedKinds = extService.activationEvents.map(e => e.kind); + assert.ok(activatedKinds.includes(ExtensionHostKind.LocalProcess), 'Should activate on LocalProcess'); + assert.ok(activatedKinds.includes(ExtensionHostKind.LocalWebWorker), 'Should activate on LocalWebWorker'); + assert.ok(activatedKinds.includes(ExtensionHostKind.Remote), 'Should activate on Remote'); + }); }); diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index 00b5019d0b4..7438ab0d27a 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -839,16 +839,6 @@ "node": ">= 0.4" } }, - "node_modules/hono": { - "version": "4.11.4", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", - "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=16.9.0" - } - }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", From 324dec72f1dde9508d45b2dfbd1d964dd9fb013e Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 16 Jan 2026 09:20:44 +0100 Subject: [PATCH 049/387] Remove short-circuit in `ExtensionHostManager` for Immediate activation events --- .../services/extensions/common/extensionHostManager.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index 2a855c5ee83..88e8fd1d257 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -71,7 +71,6 @@ export class ExtensionHostManager extends Disposable implements IExtensionHostMa private readonly _customers: IDisposable[]; private readonly _extensionHost: IExtensionHost; private _proxy: Promise | null; - private _hasStarted = false; public get pid(): number | null { return this._extensionHost.pid; @@ -116,7 +115,6 @@ export class ExtensionHostManager extends Disposable implements IExtensionHostMa this._proxy = this._extensionHost.start().then( (protocol) => { - this._hasStarted = true; // Track healthy extension host startup const successTelemetryEvent: ExtensionHostStartupEvent = { @@ -321,10 +319,6 @@ export class ExtensionHostManager extends Disposable implements IExtensionHostMa } public activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise { - if (activationKind === ActivationKind.Immediate && !this._hasStarted) { - return Promise.resolve(); - } - if (!this._cachedActivationEvents.has(activationEvent)) { this._cachedActivationEvents.set(activationEvent, this._activateByEvent(activationEvent, activationKind)); } From a77622a536ebe00fa9e1ed2d81bd4b5a02b35782 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 16 Jan 2026 09:43:28 +0100 Subject: [PATCH 050/387] Fix unit test failures --- .../extensions/common/abstractExtensionService.ts | 3 ++- .../extensions/test/browser/extensionService.test.ts | 10 ++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 2a05bc8a8d4..a0e599cd4b4 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -736,7 +736,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx //#region Stopping / Starting / Restarting - public stopExtensionHosts(reason: string, auto?: boolean): Promise { + public async stopExtensionHosts(reason: string, auto?: boolean): Promise { + await this._initializeIfNeeded(); return this._doStopExtensionHostsWithVeto(reason, auto); } diff --git a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts index d9ee5186d63..fa3b8370531 100644 --- a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts +++ b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts @@ -185,6 +185,8 @@ suite('ExtensionService', () => { remoteAuthorityResolverService, new TestDialogService() ); + + this._initializeIfNeeded(); } private _extHostId = 0; @@ -280,19 +282,16 @@ suite('ExtensionService', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('issue #152204: Remote extension host not disposed after closing vscode client', async () => { - await extService.startExtensionHosts(); await extService.stopExtensionHosts('foo'); assert.deepStrictEqual(extService.order, (['create 1', 'create 2', 'create 3', 'dispose 3', 'dispose 2', 'dispose 1'])); }); test('Extension host disposed when awaited', async () => { - await extService.startExtensionHosts(); await extService.stopExtensionHosts('foo'); assert.deepStrictEqual(extService.order, (['create 1', 'create 2', 'create 3', 'dispose 3', 'dispose 2', 'dispose 1'])); }); test('Extension host not disposed when vetoed (sync)', async () => { - await extService.startExtensionHosts(); disposables.add(extService.onWillStop(e => e.veto(true, 'test 1'))); disposables.add(extService.onWillStop(e => e.veto(false, 'test 2'))); @@ -302,7 +301,6 @@ suite('ExtensionService', () => { }); test('Extension host not disposed when vetoed (async)', async () => { - await extService.startExtensionHosts(); disposables.add(extService.onWillStop(e => e.veto(false, 'test 1'))); disposables.add(extService.onWillStop(e => e.veto(Promise.resolve(true), 'test 2'))); @@ -313,7 +311,6 @@ suite('ExtensionService', () => { }); test('onWillActivateByEvent includes activationKind for Normal activation', async () => { - await extService.startExtensionHosts(); const events: IWillActivateEvent[] = []; disposables.add(extService.onWillActivateByEvent(e => events.push(e))); @@ -326,7 +323,6 @@ suite('ExtensionService', () => { }); test('onWillActivateByEvent includes activationKind for Immediate activation', async () => { - await extService.startExtensionHosts(); const events: IWillActivateEvent[] = []; disposables.add(extService.onWillActivateByEvent(e => events.push(e))); @@ -339,7 +335,6 @@ suite('ExtensionService', () => { }); test('Immediate activation only activates local extension hosts', async () => { - await extService.startExtensionHosts(); extService.activationEvents.length = 0; // Clear any initial activations await extService.activateByEvent('onTest', ActivationKind.Immediate); @@ -352,7 +347,6 @@ suite('ExtensionService', () => { }); test('Normal activation activates all extension hosts', async () => { - await extService.startExtensionHosts(); extService.activationEvents.length = 0; // Clear any initial activations await extService.activateByEvent('onTest', ActivationKind.Normal); From 93c8f451055502bfcb2d2fb085edcae5966795c5 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 16 Jan 2026 09:44:10 +0100 Subject: [PATCH 051/387] Revert package-lock changes --- test/mcp/package-lock.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index 7438ab0d27a..00b5019d0b4 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -839,6 +839,16 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", + "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", From 66a451a86cc9fdfad0f4111256773c9ac3183cc2 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 16 Jan 2026 10:11:06 +0100 Subject: [PATCH 052/387] Revert "Fix 100% CPU on Windows when watched file is deleted" (#288266) Revert "Fix 100% CPU on Windows when watched file is deleted (#288003)" This reverts commit fa78c64031a1067cedf074f5c389bfbd56c0ab32. --- src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts | 4 ---- src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts index 4fb2322e651..6429554e098 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts @@ -125,10 +125,6 @@ export class NodeJSFileWatcherLibrary extends Disposable { } private notifyWatchFailed(): void { - if (this.didFail) { - return; - } - this.didFail = true; this.onDidWatchFail?.(); diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index 9ee123d9635..b375d171c42 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -109,10 +109,6 @@ export class ParcelWatcherInstance extends Disposable { } notifyWatchFailed(): void { - if (this.didFail) { - return; - } - this.didFail = true; this._onDidFail.fire(); From f22245625ae38802da931e10d34273d7b972ce12 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 09:20:24 +0000 Subject: [PATCH 053/387] Agent sessions: allow context menu actions on section headers (#288148) * Initial plan * Add context menu actions for session section headers - Add new `AgentSessionSectionContext` MenuId for section context menus - Update `ArchiveAgentSessionSectionAction` to appear in section context menu - Update `UnarchiveAgentSessionSectionAction` to appear in section context menu - Add new `MarkAgentSessionSectionReadAction` for marking all sessions as read - Update `agentSessionsControl.ts` to show context menu for section headers Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> * . * . * Update src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> Co-authored-by: Benjamin Pasero Co-authored-by: Benjamin Pasero Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/vs/platform/actions/common/actions.ts | 1 + .../agentSessions.contribution.ts | 3 +- .../agentSessions/agentSessionsActions.ts | 44 +++++++++++++++++-- .../agentSessions/agentSessionsControl.ts | 41 +++++++++++++---- 4 files changed, 76 insertions(+), 13 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 530b0e30433..a66127dd044 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -287,6 +287,7 @@ export class MenuId { static readonly BrowserActionsToolbar = new MenuId('BrowserActionsToolbar'); static readonly AgentSessionsViewerFilterSubMenu = new MenuId('AgentSessionsViewerFilterSubMenu'); static readonly AgentSessionsContext = new MenuId('AgentSessionsContext'); + static readonly AgentSessionSectionContext = new MenuId('AgentSessionSectionContext'); static readonly AgentSessionsCreateSubMenu = new MenuId('AgentSessionsCreateSubMenu'); static readonly AgentSessionsToolbar = new MenuId('AgentSessionsToolbar'); static readonly AgentSessionItemToolbar = new MenuId('AgentSessionItemToolbar'); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts index 34aef21310e..adb1ea5638c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts @@ -17,7 +17,7 @@ import { IAgentSessionsService, AgentSessionsService } from './agentSessionsServ import { LocalAgentsSessionsProvider } from './localAgentSessionsProvider.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js'; import { ISubmenuItem, MenuId, MenuRegistry, registerAction2, SubmenuItemAction } from '../../../../../platform/actions/common/actions.js'; -import { ArchiveAgentSessionAction, ArchiveAgentSessionSectionAction, UnarchiveAgentSessionSectionAction, UnarchiveAgentSessionAction, OpenAgentSessionInEditorGroupAction, OpenAgentSessionInNewEditorGroupAction, OpenAgentSessionInNewWindowAction, ShowAgentSessionsSidebar, HideAgentSessionsSidebar, ToggleAgentSessionsSidebar, RefreshAgentSessionsViewerAction, FindAgentSessionInViewerAction, MarkAgentSessionUnreadAction, MarkAgentSessionReadAction, FocusAgentSessionsAction, SetAgentSessionsOrientationStackedAction, SetAgentSessionsOrientationSideBySideAction, ToggleChatViewSessionsAction, PickAgentSessionAction, ArchiveAllAgentSessionsAction, RenameAgentSessionAction, DeleteAgentSessionAction, DeleteAllLocalSessionsAction } from './agentSessionsActions.js'; +import { ArchiveAgentSessionAction, ArchiveAgentSessionSectionAction, UnarchiveAgentSessionSectionAction, UnarchiveAgentSessionAction, OpenAgentSessionInEditorGroupAction, OpenAgentSessionInNewEditorGroupAction, OpenAgentSessionInNewWindowAction, ShowAgentSessionsSidebar, HideAgentSessionsSidebar, ToggleAgentSessionsSidebar, RefreshAgentSessionsViewerAction, FindAgentSessionInViewerAction, MarkAgentSessionUnreadAction, MarkAgentSessionReadAction, MarkAgentSessionSectionReadAction, FocusAgentSessionsAction, SetAgentSessionsOrientationStackedAction, SetAgentSessionsOrientationSideBySideAction, ToggleChatViewSessionsAction, PickAgentSessionAction, ArchiveAllAgentSessionsAction, RenameAgentSessionAction, DeleteAgentSessionAction, DeleteAllLocalSessionsAction } from './agentSessionsActions.js'; import { AgentSessionsQuickAccessProvider, AGENT_SESSIONS_QUICK_ACCESS_PREFIX } from './agentSessionsQuickAccess.js'; import { IAgentSessionProjectionService, AgentSessionProjectionService } from './agentSessionProjectionService.js'; import { EnterAgentSessionProjectionAction, ExitAgentSessionProjectionAction, OpenInChatPanelAction, ToggleAgentStatusAction, ToggleAgentSessionProjectionAction } from './agentSessionProjectionActions.js'; @@ -36,6 +36,7 @@ registerAction2(PickAgentSessionAction); registerAction2(ArchiveAllAgentSessionsAction); registerAction2(ArchiveAgentSessionSectionAction); registerAction2(UnarchiveAgentSessionSectionAction); +registerAction2(MarkAgentSessionSectionReadAction); registerAction2(ArchiveAgentSessionAction); registerAction2(UnarchiveAgentSessionAction); registerAction2(RenameAgentSessionAction); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts index 623f132ddf8..bc34a756360 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts @@ -211,12 +211,17 @@ export class ArchiveAgentSessionSectionAction extends Action2 { id: 'agentSessionSection.archive', title: localize2('archiveSection', "Archive All"), icon: Codicon.archive, - menu: { + menu: [{ id: MenuId.AgentSessionSectionToolbar, group: 'navigation', order: 1, when: ChatContextKeys.agentSessionSection.notEqualsTo(AgentSessionSection.Archived), - } + }, { + id: MenuId.AgentSessionSectionContext, + group: '1_edit', + order: 2, + when: ChatContextKeys.agentSessionSection.notEqualsTo(AgentSessionSection.Archived), + }] }); } @@ -252,12 +257,17 @@ export class UnarchiveAgentSessionSectionAction extends Action2 { id: 'agentSessionSection.unarchive', title: localize2('unarchiveSection', "Unarchive All"), icon: Codicon.unarchive, - menu: { + menu: [{ id: MenuId.AgentSessionSectionToolbar, group: 'navigation', order: 1, when: ChatContextKeys.agentSessionSection.isEqualTo(AgentSessionSection.Archived), - } + }, { + id: MenuId.AgentSessionSectionContext, + group: '1_edit', + order: 2, + when: ChatContextKeys.agentSessionSection.isEqualTo(AgentSessionSection.Archived), + }] }); } @@ -285,6 +295,32 @@ export class UnarchiveAgentSessionSectionAction extends Action2 { } } +export class MarkAgentSessionSectionReadAction extends Action2 { + + constructor() { + super({ + id: 'agentSessionSection.markRead', + title: localize2('markSectionRead', "Mark All as Read"), + menu: [{ + id: MenuId.AgentSessionSectionContext, + group: '1_edit', + order: 1, + when: ChatContextKeys.agentSessionSection.notEqualsTo(AgentSessionSection.Archived), + }] + }); + } + + async run(accessor: ServicesAccessor, context?: IAgentSessionSection): Promise { + if (!context || !isAgentSessionSection(context)) { + return; + } + + for (const session of context.sessions) { + session.setRead(true); + } + } +} + //#endregion //#region Session Actions diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 1022548399a..9a1b6e8d992 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -9,7 +9,7 @@ import { IContextMenuService } from '../../../../../platform/contextview/browser import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IOpenEvent, WorkbenchCompressibleAsyncDataTree } from '../../../../../platform/list/browser/listService.js'; import { $, append, EventHelper } from '../../../../../base/browser/dom.js'; -import { AgentSessionSection, IAgentSession, IAgentSessionsModel, IMarshalledAgentSessionContext, isAgentSession, isAgentSessionSection } from './agentSessionsModel.js'; +import { AgentSessionSection, IAgentSession, IAgentSessionSection, IAgentSessionsModel, IMarshalledAgentSessionContext, isAgentSession, isAgentSessionSection } from './agentSessionsModel.js'; import { AgentSessionListItem, AgentSessionRenderer, AgentSessionsAccessibilityProvider, AgentSessionsCompressionDelegate, AgentSessionsDataSource, AgentSessionsDragAndDrop, AgentSessionsIdentityProvider, AgentSessionsKeyboardNavigationLabelProvider, AgentSessionsListDelegate, AgentSessionSectionRenderer, AgentSessionsSorter, IAgentSessionsFilter, IAgentSessionsSorterOptions } from './agentSessionsViewer.js'; import { FuzzyScore } from '../../../../../base/common/filters.js'; import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; @@ -32,6 +32,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { openSession } from './agentSessionsOpener.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js'; +import { IMouseEvent } from '../../../../../base/browser/mouseEvent.js'; export interface IAgentSessionsControlOptions extends IAgentSessionsSorterOptions { readonly overrideStyles: IStyleOverride; @@ -202,21 +203,45 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo } private async showContextMenu({ element, anchor, browserEvent }: ITreeContextMenuEvent): Promise { - if (!element || isAgentSessionSection(element)) { - return; // No context menu for section headers + if (!element) { + return; } EventHelper.stop(browserEvent, true); - await this.chatSessionsService.activateChatSessionItemProvider(element.providerType); + if (isAgentSessionSection(element)) { + this.showAgentSessionSectionContextMenu(element, anchor); + } else { + this.showAgentSessionContextMenu(element, anchor); + } + } + + private async showAgentSessionSectionContextMenu(section: IAgentSessionSection, anchor: HTMLElement | IMouseEvent): Promise { + const contextOverlay: Array<[string, boolean | string]> = []; + contextOverlay.push([ChatContextKeys.agentSessionSection.key, section.section]); + + const menu = this.menuService.createMenu(MenuId.AgentSessionSectionContext, this.contextKeyService.createOverlay(contextOverlay)); + + this.contextMenuService.showContextMenu({ + getActions: () => Separator.join(...menu.getActions({ arg: section, shouldForwardArgs: true }).map(([, actions]) => actions)), + getAnchor: () => anchor, + getActionsContext: () => section, + }); + + menu.dispose(); + } + + private async showAgentSessionContextMenu(session: IAgentSession, anchor: HTMLElement | IMouseEvent): Promise { + await this.chatSessionsService.activateChatSessionItemProvider(session.providerType); const contextOverlay: Array<[string, boolean | string]> = []; - contextOverlay.push([ChatContextKeys.isArchivedAgentSession.key, element.isArchived()]); - contextOverlay.push([ChatContextKeys.isReadAgentSession.key, element.isRead()]); - contextOverlay.push([ChatContextKeys.agentSessionType.key, element.providerType]); + contextOverlay.push([ChatContextKeys.isArchivedAgentSession.key, session.isArchived()]); + contextOverlay.push([ChatContextKeys.isReadAgentSession.key, session.isRead()]); + contextOverlay.push([ChatContextKeys.agentSessionType.key, session.providerType]); + const menu = this.menuService.createMenu(MenuId.AgentSessionsContext, this.contextKeyService.createOverlay(contextOverlay)); - const marshalledSession: IMarshalledAgentSessionContext = { session: element, $mid: MarshalledId.AgentSessionContext }; + const marshalledSession: IMarshalledAgentSessionContext = { session, $mid: MarshalledId.AgentSessionContext }; this.contextMenuService.showContextMenu({ getActions: () => Separator.join(...menu.getActions({ arg: marshalledSession, shouldForwardArgs: true }).map(([, actions]) => actions)), getAnchor: () => anchor, From f322d8ea8a3af2fd39ffe48c29b71d2a27339666 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Fri, 16 Jan 2026 10:17:16 +0100 Subject: [PATCH 054/387] Add telemetry event --- .../browser/chatSetup/chatSetupProviders.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts index 37dbdf9e408..f13d8facbad 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts @@ -362,6 +362,33 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { toolsModelReady }); + type ChatSetupTimeoutClassification = { + owner: 'chrmarti'; + comment: 'Provides insight into chat setup timeouts.'; + agentActivated: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the agent was activated.' }; + agentReady: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the agent was ready.' }; + languageModelReady: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the language model was ready.' }; + toolsModelReady: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the tools model was ready.' }; + isRemote: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether this is a remote scenario.' }; + isAnonymous: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether anonymous access is enabled.' }; + }; + type ChatSetupTimeoutEvent = { + agentActivated: boolean; + agentReady: boolean; + languageModelReady: boolean; + toolsModelReady: boolean; + isRemote: boolean; + isAnonymous: boolean; + }; + this.telemetryService.publicLog2('chatSetup.timeout', { + agentActivated, + agentReady, + languageModelReady, + toolsModelReady, + isRemote: !!this.environmentService.remoteAuthority, + isAnonymous: this.chatEntitlementService.anonymous + }); + progress({ kind: 'warning', content: new MarkdownString(warningMessage) From 79da484009c62a75572b7c2af89acdb15f267764 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:10:48 +0000 Subject: [PATCH 055/387] Mark agent sessions as read when archiving (#288228) * Initial plan * Mark session as read when archiving Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> * . * . --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> Co-authored-by: Benjamin Pasero --- .../agentSessions/agentSessionsFilter.ts | 2 +- .../agentSessions/agentSessionsModel.ts | 12 +- .../agentSessionViewModel.test.ts | 172 ++++++++++++++++++ 3 files changed, 183 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts index b9b71791d4f..d0ffac3fa58 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts @@ -262,7 +262,7 @@ export class AgentSessionsFilter extends Disposable implements Required= (session.timing.lastRequestEnded ?? session.timing.lastRequestStarted ?? session.timing.created); @@ -492,7 +500,7 @@ interface ISerializedAgentSession { readonly created: number; readonly lastRequestStarted?: number; readonly lastRequestEnded?: number; - // Old format for backward compatibility when reading + // Old format for backward compatibility when reading (TODO@bpasero remove eventually) readonly startTime?: number; readonly endTime?: number; }; @@ -571,7 +579,7 @@ class AgentSessionsCache { archived: session.archived, timing: { - // Support loading both new and old cache formats + // Support loading both new and old cache formats (TODO@bpasero remove old format support after some time) created: session.timing.created ?? session.timing.startTime ?? 0, lastRequestStarted: session.timing.lastRequestStarted ?? session.timing.startTime, lastRequestEnded: session.timing.lastRequestEnded ?? session.timing.endTime, diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts index bacf032abd9..db0468452a4 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts @@ -1645,6 +1645,178 @@ suite('Agent Sessions', () => { assert.strictEqual(session.isRead(), true); }); }); + + test('should treat archived sessions as read', async () => { + return runWithFakedTimers({}, async () => { + // Session with timing after the READ_STATE_INITIAL_DATE (December 8, 2025) + // which would normally be unread + const newSessionTiming: IChatSessionItem['timing'] = { + created: Date.UTC(2025, 11 /* December */, 10), + lastRequestStarted: Date.UTC(2025, 11 /* December */, 10), + lastRequestEnded: Date.UTC(2025, 11 /* December */, 11), + }; + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: URI.parse('test://new-session'), + label: 'New Session', + timing: newSessionTiming, + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + // Session after the initial date should be unread by default + assert.strictEqual(session.isRead(), false); + assert.strictEqual(session.isArchived(), false); + + // Archive the session + session.setArchived(true); + + // Archived sessions should always be considered read + assert.strictEqual(session.isArchived(), true); + assert.strictEqual(session.isRead(), true); + }); + }); + + test('should mark session as read when archiving', async () => { + return runWithFakedTimers({}, async () => { + // Session with timing after the READ_STATE_INITIAL_DATE (December 8, 2025) + const newSessionTiming: IChatSessionItem['timing'] = { + created: Date.UTC(2025, 11 /* December */, 10), + lastRequestStarted: Date.UTC(2025, 11 /* December */, 10), + lastRequestEnded: Date.UTC(2025, 11 /* December */, 11), + }; + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: URI.parse('test://new-session'), + label: 'New Session', + timing: newSessionTiming, + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + assert.strictEqual(session.isRead(), false); + + // Archive the session + session.setArchived(true); + + // Should be read after archiving (archived sessions are always read) + assert.strictEqual(session.isRead(), true); + + // Unarchive the session + session.setArchived(false); + + // After unarchiving, the read state depends on the stored read date vs session timing. + // When archiving marked the session as read, the read date was set to the test's + // faked Date.now() which may be earlier than the session's lastRequestEnded, + // so the session may appear unread again based on the time comparison. + assert.strictEqual(session.isArchived(), false); + }); + }); + + test('should fire onDidChangeSessions when archiving an unread session', async () => { + return runWithFakedTimers({}, async () => { + // Session with timing after the READ_STATE_INITIAL_DATE + const newSessionTiming: IChatSessionItem['timing'] = { + created: Date.UTC(2025, 11 /* December */, 10), + lastRequestStarted: Date.UTC(2025, 11 /* December */, 10), + lastRequestEnded: Date.UTC(2025, 11 /* December */, 11), + }; + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: URI.parse('test://new-session'), + label: 'New Session', + timing: newSessionTiming, + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + assert.strictEqual(session.isRead(), false); + + let changeEventCount = 0; + disposables.add(viewModel.onDidChangeSessions(() => { + changeEventCount++; + })); + + // Archive the session (which also marks as read) + session.setArchived(true); + + // Fires twice: once for setting read state, once for setting archived state + assert.strictEqual(changeEventCount, 2); + }); + }); + + test('should not fire onDidChangeSessions when archiving an already read session', async () => { + return runWithFakedTimers({}, async () => { + // Session with timing before the READ_STATE_INITIAL_DATE (already read) + const oldSessionTiming: IChatSessionItem['timing'] = { + created: Date.UTC(2025, 10 /* November */, 1), + lastRequestStarted: Date.UTC(2025, 10 /* November */, 1), + lastRequestEnded: Date.UTC(2025, 10 /* November */, 2), + }; + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: URI.parse('test://old-session'), + label: 'Old Session', + timing: oldSessionTiming, + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + // Session before the initial date should be read + assert.strictEqual(session.isRead(), true); + + let changeEventCount = 0; + disposables.add(viewModel.onDidChangeSessions(() => { + changeEventCount++; + })); + + // Archive the session + session.setArchived(true); + + // Should fire once (for archived state change only, not for read since already read) + assert.strictEqual(changeEventCount, 1); + }); + }); }); suite('AgentSessionsViewModel - State Tracking', () => { From ef452dc92e7f2f9882efa3b23278598ba326f60e Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 16 Jan 2026 11:04:12 +0000 Subject: [PATCH 056/387] Update color theme for 2026 Dark: adjust background and foreground colors for various UI elements --- extensions/theme-2026/themes/2026-dark.json | 184 ++++++++++---------- 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 5a008c8437d..1dbea210de3 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -9,148 +9,148 @@ "descriptionForeground": "#888888", "icon.foreground": "#888888", "focusBorder": "#007ABBB3", - "textBlockQuote.background": "#242424", + "textBlockQuote.background": "#22282C", "textBlockQuote.border": "#252627FF", - "textCodeBlock.background": "#242424", + "textCodeBlock.background": "#22282C", "textLink.foreground": "#0092E0", "textLink.activeForeground": "#009AEB", "textPreformat.foreground": "#888888", "textSeparator.foreground": "#252525FF", - "button.background": "#007ABB", + "button.background": "#007ABC", "button.foreground": "#FFFFFF", "button.hoverBackground": "#0080C4", "button.border": "#252627FF", - "button.secondaryBackground": "#242424", + "button.secondaryBackground": "#22282C", "button.secondaryForeground": "#bebebe", - "button.secondaryHoverBackground": "#007ABB", - "checkbox.background": "#242424", + "button.secondaryHoverBackground": "#007ABC", + "checkbox.background": "#22282C", "checkbox.border": "#252627FF", "checkbox.foreground": "#bebebe", - "dropdown.background": "#191919", + "dropdown.background": "#181E22", "dropdown.border": "#323435", "dropdown.foreground": "#bebebe", - "dropdown.listBackground": "#202020", - "input.background": "#191919", + "dropdown.listBackground": "#1E2529", + "input.background": "#181E22", "input.border": "#323435FF", "input.foreground": "#bebebe", "input.placeholderForeground": "#777777", - "inputOption.activeBackground": "#007ABB33", + "inputOption.activeBackground": "#007ABC33", "inputOption.activeForeground": "#bebebe", "inputOption.activeBorder": "#252627FF", - "inputValidation.errorBackground": "#191919", + "inputValidation.errorBackground": "#181E22", "inputValidation.errorBorder": "#252627FF", "inputValidation.errorForeground": "#bebebe", - "inputValidation.infoBackground": "#191919", + "inputValidation.infoBackground": "#181E22", "inputValidation.infoBorder": "#252627FF", "inputValidation.infoForeground": "#bebebe", - "inputValidation.warningBackground": "#191919", + "inputValidation.warningBackground": "#181E22", "inputValidation.warningBorder": "#252627FF", "inputValidation.warningForeground": "#bebebe", "scrollbar.shadow": "#191B1D4D", - "scrollbarSlider.background": "#84848433", - "scrollbarSlider.hoverBackground": "#84848466", - "scrollbarSlider.activeBackground": "#84848499", - "badge.background": "#007ABB", + "scrollbarSlider.background": "#7D848833", + "scrollbarSlider.hoverBackground": "#7D848866", + "scrollbarSlider.activeBackground": "#7D848899", + "badge.background": "#007ABC", "badge.foreground": "#FFFFFF", - "progressBar.background": "#888888", - "list.activeSelectionBackground": "#007ABB26", + "progressBar.background": "#81878B", + "list.activeSelectionBackground": "#007ABC26", "list.activeSelectionForeground": "#bebebe", - "list.inactiveSelectionBackground": "#242424", + "list.inactiveSelectionBackground": "#22282C", "list.inactiveSelectionForeground": "#bebebe", - "list.hoverBackground": "#262626", + "list.hoverBackground": "#242A2E", "list.hoverForeground": "#bebebe", - "list.dropBackground": "#007ABB1A", - "list.focusBackground": "#007ABB26", + "list.dropBackground": "#007ABC1A", + "list.focusBackground": "#007ABC26", "list.focusForeground": "#bebebe", "list.focusOutline": "#007ABBB3", "list.highlightForeground": "#bebebe", "list.invalidItemForeground": "#444444", "list.errorForeground": "#f48771", "list.warningForeground": "#e5ba7d", - "activityBar.background": "#191919", + "activityBar.background": "#181E22", "activityBar.foreground": "#bebebe", "activityBar.inactiveForeground": "#888888", "activityBar.border": "#252627FF", "activityBar.activeBorder": "#252627FF", "activityBar.activeFocusBorder": "#007ABBB3", - "activityBarBadge.background": "#007ABB", + "activityBarBadge.background": "#007ABC", "activityBarBadge.foreground": "#FFFFFF", - "sideBar.background": "#191919", + "sideBar.background": "#181E22", "sideBar.foreground": "#bebebe", "sideBar.border": "#252627FF", "sideBarTitle.foreground": "#bebebe", - "sideBarSectionHeader.background": "#191919", + "sideBarSectionHeader.background": "#181E22", "sideBarSectionHeader.foreground": "#bebebe", "sideBarSectionHeader.border": "#252627FF", - "titleBar.activeBackground": "#191919", + "titleBar.activeBackground": "#181E22", "titleBar.activeForeground": "#bebebe", - "titleBar.inactiveBackground": "#191919", + "titleBar.inactiveBackground": "#181E22", "titleBar.inactiveForeground": "#888888", "titleBar.border": "#252627FF", - "menubar.selectionBackground": "#242424", + "menubar.selectionBackground": "#22282C", "menubar.selectionForeground": "#bebebe", - "menu.background": "#202020", + "menu.background": "#1E2529", "menu.foreground": "#bebebe", - "menu.selectionBackground": "#007ABB26", + "menu.selectionBackground": "#007ABC26", "menu.selectionForeground": "#bebebe", - "menu.separatorBackground": "#848484", + "menu.separatorBackground": "#7D8488", "menu.border": "#252627FF", "commandCenter.foreground": "#bebebe", "commandCenter.activeForeground": "#bebebe", - "commandCenter.background": "#191919", - "commandCenter.activeBackground": "#262626", + "commandCenter.background": "#181E22", + "commandCenter.activeBackground": "#242A2E", "commandCenter.border": "#323435", - "editor.background": "#121212", + "editor.background": "#11171B", "editor.foreground": "#BABDBE", "editorLineNumber.foreground": "#858889", "editorLineNumber.activeForeground": "#BABDBE", "editorCursor.foreground": "#BABDBE", - "editor.selectionBackground": "#007ABB33", - "editor.inactiveSelectionBackground": "#007ABB80", - "editor.selectionHighlightBackground": "#007ABB1A", - "editor.wordHighlightBackground": "#007ABBB3", - "editor.wordHighlightStrongBackground": "#007ABBE6", - "editor.findMatchBackground": "#007ABB4D", - "editor.findMatchHighlightBackground": "#007ABB26", - "editor.findRangeHighlightBackground": "#242424", - "editor.hoverHighlightBackground": "#242424", - "editor.lineHighlightBackground": "#242424", - "editor.rangeHighlightBackground": "#242424", + "editor.selectionBackground": "#007ABC33", + "editor.inactiveSelectionBackground": "#007ABC80", + "editor.selectionHighlightBackground": "#007ABC1A", + "editor.wordHighlightBackground": "#007ABCB3", + "editor.wordHighlightStrongBackground": "#007ABCE6", + "editor.findMatchBackground": "#007ABC4D", + "editor.findMatchHighlightBackground": "#007ABC26", + "editor.findRangeHighlightBackground": "#22282C", + "editor.hoverHighlightBackground": "#22282C", + "editor.lineHighlightBackground": "#22282C", + "editor.rangeHighlightBackground": "#22282C", "editorLink.activeForeground": "#007ABB", "editorWhitespace.foreground": "#8888884D", - "editorIndentGuide.background": "#8484844D", - "editorIndentGuide.activeBackground": "#848484", + "editorIndentGuide.background": "#7D84884D", + "editorIndentGuide.activeBackground": "#7D8488", "editorRuler.foreground": "#848484", "editorCodeLens.foreground": "#888888", - "editorBracketMatch.background": "#007ABB80", + "editorBracketMatch.background": "#007ABC80", "editorBracketMatch.border": "#252627FF", - "editorWidget.background": "#202020", + "editorWidget.background": "#1E2529", "editorWidget.border": "#252627FF", "editorWidget.foreground": "#bebebe", - "editorSuggestWidget.background": "#202020", + "editorSuggestWidget.background": "#1E2529", "editorSuggestWidget.border": "#252627FF", "editorSuggestWidget.foreground": "#bebebe", "editorSuggestWidget.highlightForeground": "#bebebe", - "editorSuggestWidget.selectedBackground": "#007ABB26", - "editorHoverWidget.background": "#202020", + "editorSuggestWidget.selectedBackground": "#007ABC26", + "editorHoverWidget.background": "#1E2529", "editorHoverWidget.border": "#252627FF", "peekView.border": "#252627FF", - "peekViewEditor.background": "#191919", - "peekViewEditor.matchHighlightBackground": "#007ABB33", - "peekViewResult.background": "#242424", + "peekViewEditor.background": "#181E22", + "peekViewEditor.matchHighlightBackground": "#007ABC33", + "peekViewResult.background": "#22282C", "peekViewResult.fileForeground": "#bebebe", "peekViewResult.lineForeground": "#888888", - "peekViewResult.matchHighlightBackground": "#007ABB33", - "peekViewResult.selectionBackground": "#007ABB26", + "peekViewResult.matchHighlightBackground": "#007ABC33", + "peekViewResult.selectionBackground": "#007ABC26", "peekViewResult.selectionForeground": "#bebebe", - "peekViewTitle.background": "#242424", + "peekViewTitle.background": "#22282C", "peekViewTitleDescription.foreground": "#888888", "peekViewTitleLabel.foreground": "#bebebe", - "editorGutter.background": "#121212", - "editorGutter.addedBackground": "#73c991", - "editorGutter.deletedBackground": "#f48771", - "diffEditor.insertedTextBackground": "#73c99154", - "diffEditor.removedTextBackground": "#f4877154", + "editorGutter.background": "#11171B", + "editorGutter.addedBackground": "#6DC594", + "editorGutter.deletedBackground": "#E88676", + "diffEditor.insertedTextBackground": "#6DC59454", + "diffEditor.removedTextBackground": "#E8867654", "editorOverviewRuler.border": "#252627FF", "editorOverviewRuler.findMatchForeground": "#007ABB99", "editorOverviewRuler.modifiedForeground": "#5ba3e0", @@ -158,67 +158,67 @@ "editorOverviewRuler.deletedForeground": "#f48771", "editorOverviewRuler.errorForeground": "#f48771", "editorOverviewRuler.warningForeground": "#e5ba7d", - "panel.background": "#191919", + "panel.background": "#181E22", "panel.border": "#252627FF", "panelTitle.activeBorder": "#007ABB", "panelTitle.activeForeground": "#bebebe", "panelTitle.inactiveForeground": "#888888", - "statusBar.background": "#191919", + "statusBar.background": "#181E22", "statusBar.foreground": "#bebebe", "statusBar.border": "#252627FF", "statusBar.focusBorder": "#007ABBB3", - "statusBar.debuggingBackground": "#007ABB", + "statusBar.debuggingBackground": "#007ABC", "statusBar.debuggingForeground": "#FFFFFF", - "statusBar.noFolderBackground": "#191919", + "statusBar.noFolderBackground": "#181E22", "statusBar.noFolderForeground": "#bebebe", - "statusBarItem.activeBackground": "#007ABB", - "statusBarItem.hoverBackground": "#262626", + "statusBarItem.activeBackground": "#007ABC", + "statusBarItem.hoverBackground": "#242A2E", "statusBarItem.focusBorder": "#007ABBB3", - "statusBarItem.prominentBackground": "#007ABB", + "statusBarItem.prominentBackground": "#007ABC", "statusBarItem.prominentForeground": "#FFFFFF", - "statusBarItem.prominentHoverBackground": "#007ABB", - "tab.activeBackground": "#121212", + "statusBarItem.prominentHoverBackground": "#007ABC", + "tab.activeBackground": "#11171B", "tab.activeForeground": "#bebebe", - "tab.inactiveBackground": "#191919", + "tab.inactiveBackground": "#181E22", "tab.inactiveForeground": "#888888", "tab.border": "#252627FF", "tab.lastPinnedBorder": "#252627FF", "tab.activeBorder": "#121314", - "tab.hoverBackground": "#262626", + "tab.hoverBackground": "#242A2E", "tab.hoverForeground": "#bebebe", - "tab.unfocusedActiveBackground": "#121212", + "tab.unfocusedActiveBackground": "#11171B", "tab.unfocusedActiveForeground": "#888888", - "tab.unfocusedInactiveBackground": "#191919", + "tab.unfocusedInactiveBackground": "#181E22", "tab.unfocusedInactiveForeground": "#444444", - "editorGroupHeader.tabsBackground": "#191919", + "editorGroupHeader.tabsBackground": "#181E22", "editorGroupHeader.tabsBorder": "#252627FF", "breadcrumb.foreground": "#888888", - "breadcrumb.background": "#121212", + "breadcrumb.background": "#11171B", "breadcrumb.focusForeground": "#bebebe", "breadcrumb.activeSelectionForeground": "#bebebe", - "breadcrumbPicker.background": "#202020", + "breadcrumbPicker.background": "#1E2529", "notificationCenter.border": "#252627FF", "notificationCenterHeader.foreground": "#bebebe", - "notificationCenterHeader.background": "#242424", + "notificationCenterHeader.background": "#22282C", "notificationToast.border": "#252627FF", "notifications.foreground": "#bebebe", - "notifications.background": "#202020", + "notifications.background": "#1E2529", "notifications.border": "#252627FF", "notificationLink.foreground": "#007ABB", - "extensionButton.prominentBackground": "#007ABB", + "extensionButton.prominentBackground": "#007ABC", "extensionButton.prominentForeground": "#FFFFFF", "extensionButton.prominentHoverBackground": "#0080C4", "pickerGroup.border": "#252627FF", "pickerGroup.foreground": "#bebebe", - "quickInput.background": "#202020", + "quickInput.background": "#1E2529", "quickInput.foreground": "#bebebe", - "quickInputList.focusBackground": "#007ABB26", + "quickInputList.focusBackground": "#007ABC26", "quickInputList.focusForeground": "#bebebe", "quickInputList.focusIconForeground": "#bebebe", - "quickInputList.hoverBackground": "#525252", - "terminal.selectionBackground": "#007ABB33", + "quickInputList.hoverBackground": "#4E5458", + "terminal.selectionBackground": "#007ABC33", "terminalCursor.foreground": "#bebebe", - "terminalCursor.background": "#191919", + "terminalCursor.background": "#181E22", "gitDecoration.addedResourceForeground": "#73c991", "gitDecoration.modifiedResourceForeground": "#e5ba7d", "gitDecoration.deletedResourceForeground": "#f48771", @@ -227,10 +227,10 @@ "gitDecoration.conflictingResourceForeground": "#f48771", "gitDecoration.stageModifiedResourceForeground": "#e5ba7d", "gitDecoration.stageDeletedResourceForeground": "#f48771", - "quickInputTitle.background": "#202020", + "quickInputTitle.background": "#1E2529", "quickInput.border": "#323435", - "chat.requestBubbleBackground": "#007ABB26", - "chat.requestBubbleHoverBackground": "#007ABB46" + "chat.requestBubbleBackground": "#007ABC26", + "chat.requestBubbleHoverBackground": "#007ABC46" }, "tokenColors": [ { From 96393c4192df296af51012dfcad2431e5c0ab0d2 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 16 Jan 2026 12:40:08 +0100 Subject: [PATCH 057/387] workaround for https://github.com/microsoft/vscode/issues/288260 (#288311) --- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 968eacb5dea..970268b42f0 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -196,7 +196,7 @@ configurationRegistry.registerConfiguration({ [ChatConfiguration.AgentStatusEnabled]: { type: 'boolean', markdownDescription: nls.localize('chat.agentsControl.enabled', "Controls whether the Agent Status is shown in the title bar command center, replacing the search box with quick access to chat sessions."), - default: true, + default: false, tags: ['experimental'] }, [ChatConfiguration.AgentSessionProjectionEnabled]: { From 46657fc5af9e2383d476957b26133a73375f18e5 Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Fri, 16 Jan 2026 12:52:03 +0100 Subject: [PATCH 058/387] nes: trigger on going to next/previous problem (#288316) --- .../contrib/gotoError/browser/gotoError.ts | 16 +++++++++------- .../controller/inlineCompletionsController.ts | 5 +++++ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/vs/editor/contrib/gotoError/browser/gotoError.ts b/src/vs/editor/contrib/gotoError/browser/gotoError.ts index eea7aa4931a..6c71c87435a 100644 --- a/src/vs/editor/contrib/gotoError/browser/gotoError.ts +++ b/src/vs/editor/contrib/gotoError/browser/gotoError.ts @@ -190,7 +190,7 @@ class MarkerNavigationAction extends EditorAction { } export class NextMarkerAction extends MarkerNavigationAction { - static ID: string = 'editor.action.marker.next'; + static readonly ID = 'editor.action.marker.next'; static LABEL = nls.localize2('markerAction.next.label', "Go to Next Problem (Error, Warning, Info)"); constructor() { super(true, false, { @@ -213,8 +213,8 @@ export class NextMarkerAction extends MarkerNavigationAction { } } -class PrevMarkerAction extends MarkerNavigationAction { - static ID: string = 'editor.action.marker.prev'; +export class PrevMarkerAction extends MarkerNavigationAction { + static readonly ID = 'editor.action.marker.prev'; static LABEL = nls.localize2('markerAction.previous.label', "Go to Previous Problem (Error, Warning, Info)"); constructor() { super(false, false, { @@ -237,10 +237,11 @@ class PrevMarkerAction extends MarkerNavigationAction { } } -class NextMarkerInFilesAction extends MarkerNavigationAction { +export class NextMarkerInFilesAction extends MarkerNavigationAction { + static readonly ID = 'editor.action.marker.nextInFiles'; constructor() { super(true, true, { - id: 'editor.action.marker.nextInFiles', + id: NextMarkerInFilesAction.ID, label: nls.localize2('markerAction.nextInFiles.label', "Go to Next Problem in Files (Error, Warning, Info)"), precondition: undefined, kbOpts: { @@ -258,10 +259,11 @@ class NextMarkerInFilesAction extends MarkerNavigationAction { } } -class PrevMarkerInFilesAction extends MarkerNavigationAction { +export class PrevMarkerInFilesAction extends MarkerNavigationAction { + static readonly ID = 'editor.action.marker.prevInFiles'; constructor() { super(false, true, { - id: 'editor.action.marker.prevInFiles', + id: PrevMarkerInFilesAction.ID, label: nls.localize2('markerAction.previousInFiles.label', "Go to Previous Problem in Files (Error, Warning, Info)"), precondition: undefined, kbOpts: { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index 4c681c9719d..7a0c70e0b0d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -32,6 +32,7 @@ import { CursorChangeReason } from '../../../../common/cursorEvents.js'; import { ILanguageFeatureDebounceService } from '../../../../common/services/languageFeatureDebounce.js'; import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js'; import { FIND_IDS } from '../../../find/browser/findModel.js'; +import { NextMarkerAction, NextMarkerInFilesAction, PrevMarkerAction, PrevMarkerInFilesAction } from '../../../gotoError/browser/gotoError.js'; import { InsertLineAfterAction, InsertLineBeforeAction } from '../../../linesOperations/browser/linesOperations.js'; import { InlineSuggestionHintsContentWidget } from '../hintsWidget/inlineCompletionsHintsWidget.js'; import { TextModelChangeRecorder } from '../model/changeRecorder.js'; @@ -224,6 +225,10 @@ export class InlineCompletionsController extends Disposable { InsertLineAfterAction.ID, InsertLineBeforeAction.ID, FIND_IDS.NextMatchFindAction, + NextMarkerAction.ID, + PrevMarkerAction.ID, + NextMarkerInFilesAction.ID, + PrevMarkerInFilesAction.ID, ...TriggerInlineEditCommandsRegistry.getRegisteredCommands(), ]); this._register(this._commandService.onDidExecuteCommand((e) => { From 08ed94a9f192d44ed18058dd3b57d4672685ec74 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 16 Jan 2026 12:53:09 +0100 Subject: [PATCH 059/387] Getting font/fontFamily/lineHeight from parent scopes 2 (#286183) Fixing tokenization not working --- src/vs/editor/common/model/tokens/annotations.ts | 4 ++-- .../tokenizationFontDecorationsProvider.ts | 6 ------ .../editor/test/common/model/annotations.test.ts | 16 ++++++++-------- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/vs/editor/common/model/tokens/annotations.ts b/src/vs/editor/common/model/tokens/annotations.ts index cd763868801..cf3943442dc 100644 --- a/src/vs/editor/common/model/tokens/annotations.ts +++ b/src/vs/editor/common/model/tokens/annotations.ts @@ -84,7 +84,7 @@ export class AnnotatedString implements IAnnotatedString { startIndex = startIndexWhereToReplace; } else { const candidate = this._annotations[- (startIndexWhereToReplace + 2)]?.range; - if (candidate && offset >= candidate.start && offset <= candidate.endExclusive) { + if (candidate && offset >= candidate.start && offset < candidate.endExclusive) { startIndex = - (startIndexWhereToReplace + 2); } else { startIndex = - (startIndexWhereToReplace + 1); @@ -103,7 +103,7 @@ export class AnnotatedString implements IAnnotatedString { endIndexExclusive = endIndexWhereToReplace + 1; } else { const candidate = this._annotations[-(endIndexWhereToReplace + 1)]?.range; - if (candidate && offset >= candidate.start && offset <= candidate.endExclusive) { + if (candidate && offset > candidate.start && offset <= candidate.endExclusive) { endIndexExclusive = - endIndexWhereToReplace; } else { endIndexExclusive = - (endIndexWhereToReplace + 1); diff --git a/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts b/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts index 0ab0c461ed0..9b4f9996262 100644 --- a/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts +++ b/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts @@ -49,12 +49,6 @@ export class TokenizationFontDecorationProvider extends Disposable implements De for (const annotation of fontChanges.changes.annotations) { const startPosition = this.textModel.getPositionAt(annotation.range.start); - const endPosition = this.textModel.getPositionAt(annotation.range.endExclusive); - - if (startPosition.lineNumber !== endPosition.lineNumber) { - // The token should be always on a single line - continue; - } const lineNumber = startPosition.lineNumber; let fontTokenAnnotation: IAnnotationUpdate; diff --git a/src/vs/editor/test/common/model/annotations.test.ts b/src/vs/editor/test/common/model/annotations.test.ts index 6f5bc3b12dd..133cb256e1a 100644 --- a/src/vs/editor/test/common/model/annotations.test.ts +++ b/src/vs/editor/test/common/model/annotations.test.ts @@ -332,19 +332,19 @@ suite('Annotations Suite', () => { assert.strictEqual(result1.length, 2); assert.deepStrictEqual(result1.map(a => a.annotation), ['1', '2']); const result2 = vas.getAnnotationsIntersecting(new OffsetRange(0, 22)); - assert.strictEqual(result2.length, 3); - assert.deepStrictEqual(result2.map(a => a.annotation), ['1', '2', '3']); + assert.strictEqual(result2.length, 2); + assert.deepStrictEqual(result2.map(a => a.annotation), ['1', '2']); }); test('getAnnotationsIntersecting 2', () => { const vas = fromVisual('[1:Lorem] [2:i]p[3:s]'); const result1 = vas.getAnnotationsIntersecting(new OffsetRange(5, 7)); - assert.strictEqual(result1.length, 2); - assert.deepStrictEqual(result1.map(a => a.annotation), ['1', '2']); + assert.strictEqual(result1.length, 1); + assert.deepStrictEqual(result1.map(a => a.annotation), ['2']); const result2 = vas.getAnnotationsIntersecting(new OffsetRange(5, 9)); - assert.strictEqual(result2.length, 3); - assert.deepStrictEqual(result2.map(a => a.annotation), ['1', '2', '3']); + assert.strictEqual(result2.length, 2); + assert.deepStrictEqual(result2.map(a => a.annotation), ['2', '3']); }); test('getAnnotationsIntersecting 3', () => { @@ -370,8 +370,8 @@ suite('Annotations Suite', () => { test('getAnnotationsIntersecting 5', () => { const vas = fromVisual('[1:Lorem ipsum] [2:dol] [3:or]'); const result = vas.getAnnotationsIntersecting(new OffsetRange(1, 16)); - assert.strictEqual(result.length, 3); - assert.deepStrictEqual(result.map(a => a.annotation), ['1', '2', '3']); + assert.strictEqual(result.length, 2); + assert.deepStrictEqual(result.map(a => a.annotation), ['1', '2']); }); test('applyEdit 1 - deletion within annotation', () => { From d0eda2a6229ee5194e56f82bcdc560157cd2c172 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 16 Jan 2026 12:53:24 +0100 Subject: [PATCH 060/387] updating distro hash (#288309) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5eb5509e5d..a6112b97312 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.109.0", - "distro": "f44449f84806363760ce8bb8dbe85cd8207498ff", + "distro": "b90415e4e25274537a83463c7af88fca7e9528a7", "author": { "name": "Microsoft Corporation" }, From 766d7baedf0f38baeac12fe1ed54a92d4a6731e7 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:54:58 +0100 Subject: [PATCH 061/387] Chat - background sessions should not use the editing sessions file list (#288324) --- .../contrib/chat/browser/widget/input/chatInputPart.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index c170b7da1a5..b4c5502a759 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -2302,6 +2302,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } const modifiedEntries = derivedOpts({ equalsFn: arraysEqual }, r => { + // Background chat sessions render the working set based on the session files, and not the editing session + const sessionResource = chatEditingSession?.chatSessionResource ?? this._widget?.viewModel?.model.sessionResource; + if (sessionResource && getChatSessionType(sessionResource) !== localChatSessionType) { + return []; + } + return chatEditingSession?.entries.read(r).filter(entry => entry.state.read(r) === ModifiedFileEntryState.Modified) || []; }); From 838d69a62d08fddb5ea305641a75d8889fd26d14 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Jan 2026 03:59:16 -0800 Subject: [PATCH 062/387] Detect sed in place file edits and block appropriately Fixes #288318 --- .../commandLineFileWriteAnalyzer.ts | 15 +- .../browser/treeSitterCommandParser.ts | 216 ++++++++++++++++++ .../terminalChatAgentToolsConfiguration.ts | 7 +- .../commandLineFileWriteAnalyzer.test.ts | 34 +++ 4 files changed, 266 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineFileWriteAnalyzer.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineFileWriteAnalyzer.ts index 0a5d98d1704..4e55dac7ed3 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineFileWriteAnalyzer.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineFileWriteAnalyzer.ts @@ -47,13 +47,22 @@ export class CommandLineFileWriteAnalyzer extends Disposable implements ICommand private async _getFileWrites(options: ICommandLineAnalyzerOptions): Promise { let fileWrites: FileWrite[] = []; + + // Get file writes from redirections (via tree-sitter grammar) const capturedFileWrites = (await this._treeSitterCommandParser.getFileWrites(options.treeSitterLanguage, options.commandLine)) .map(this._mapNullDevice.bind(this, options)); - if (capturedFileWrites.length) { + + // Get file writes from sed in-place editing + const sedInPlaceFiles = (await this._treeSitterCommandParser.getSedInPlaceFiles(options.treeSitterLanguage, options.commandLine)) + .map(this._mapNullDevice.bind(this, options)); + + const allCapturedFileWrites = [...capturedFileWrites, ...sedInPlaceFiles]; + + if (allCapturedFileWrites.length) { const cwd = options.cwd; if (cwd) { this._log('Detected cwd', cwd.toString()); - fileWrites = capturedFileWrites.map(e => { + fileWrites = allCapturedFileWrites.map(e => { if (e === nullDevice) { return e; } @@ -79,7 +88,7 @@ export class CommandLineFileWriteAnalyzer extends Disposable implements ICommand }); } else { this._log('Cwd could not be detected'); - fileWrites = capturedFileWrites; + fileWrites = allCapturedFileWrites; } } this._log('File writes detected', fileWrites.map(e => e.toString())); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts index 464d8695ce5..ef3207c58f4 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts @@ -61,6 +61,222 @@ export class TreeSitterCommandParser extends Disposable { return captures.map(e => e.node.text.trim()); } + /** + * Extracts file targets from `sed` commands that use in-place editing (`-i`, `-I`, or `--in-place`). + * Returns an array of file paths that would be modified. + */ + async getSedInPlaceFiles(languageId: TreeSitterCommandParserLanguage, commandLine: string): Promise { + // This is only relevant for bash-like shells + if (languageId !== TreeSitterCommandParserLanguage.Bash) { + return []; + } + + // Query for all commands + const query = '(command) @command'; + const captures = await this._queryTree(languageId, commandLine, query); + + const result: string[] = []; + for (const capture of captures) { + const commandText = capture.node.text; + const sedFiles = this._parseSedInPlaceFiles(commandText); + result.push(...sedFiles); + } + return result; + } + + /** + * Parses a sed command to extract files being edited in-place. + * Handles: + * - `sed -i 's/foo/bar/' file.txt` (GNU) + * - `sed -i.bak 's/foo/bar/' file.txt` (GNU with backup suffix) + * - `sed -i '' 's/foo/bar/' file.txt` (macOS/BSD with empty backup suffix) + * - `sed --in-place 's/foo/bar/' file.txt` (GNU long form) + * - `sed --in-place=.bak 's/foo/bar/' file.txt` (GNU long form with backup) + * - `sed -I 's/foo/bar/' file.txt` (BSD case-insensitive variant) + */ + private _parseSedInPlaceFiles(commandText: string): string[] { + // Check if this is a sed command with in-place flag + const sedMatch = commandText.match(/^sed\s+/); + if (!sedMatch) { + return []; + } + + // Check for -i, -I, or --in-place flag + const inPlaceRegex = /(?:^|\s)(-[a-zA-Z]*[iI][a-zA-Z]*\S*|--in-place(?:=\S*)?|(-i|-I)\s*'[^']*'|(-i|-I)\s*"[^"]*")(?:\s|$)/; + if (!inPlaceRegex.test(commandText)) { + return []; + } + + // Parse the command to extract file arguments + // We need to skip: the 'sed' command, flags, and sed scripts/expressions + const tokens = this._tokenizeSedCommand(commandText); + return this._extractSedFileTargets(tokens); + } + + /** + * Tokenizes a sed command into individual arguments, handling quotes and escapes. + */ + private _tokenizeSedCommand(commandText: string): string[] { + const tokens: string[] = []; + let current = ''; + let inSingleQuote = false; + let inDoubleQuote = false; + let escaped = false; + + for (let i = 0; i < commandText.length; i++) { + const char = commandText[i]; + + if (escaped) { + current += char; + escaped = false; + continue; + } + + if (char === '\\' && !inSingleQuote) { + escaped = true; + current += char; + continue; + } + + if (char === '\'' && !inDoubleQuote) { + inSingleQuote = !inSingleQuote; + current += char; + continue; + } + + if (char === '"' && !inSingleQuote) { + inDoubleQuote = !inDoubleQuote; + current += char; + continue; + } + + if (/\s/.test(char) && !inSingleQuote && !inDoubleQuote) { + if (current) { + tokens.push(current); + current = ''; + } + continue; + } + + current += char; + } + + if (current) { + tokens.push(current); + } + + return tokens; + } + + /** + * Extracts file targets from tokenized sed command arguments. + * Files are generally the last non-option, non-script arguments. + */ + private _extractSedFileTargets(tokens: string[]): string[] { + if (tokens.length === 0 || tokens[0] !== 'sed') { + return []; + } + + const files: string[] = []; + let i = 1; // Skip 'sed' + let foundScript = false; + + while (i < tokens.length) { + const token = tokens[i]; + + // Long options + if (token.startsWith('--')) { + if (token === '--in-place' || token.startsWith('--in-place=')) { + // In-place flag (already verified we have one) + i++; + continue; + } + if (token === '--expression' || token === '--file') { + // Skip the option and its argument + i += 2; + foundScript = true; + continue; + } + if (token.startsWith('--expression=') || token.startsWith('--file=')) { + i++; + foundScript = true; + continue; + } + // Other long options like --sandbox, --debug, etc. + i++; + continue; + } + + // Short options + if (token.startsWith('-') && token.length > 1 && token[1] !== '-') { + // Could be combined flags like -ni or -i.bak + const flags = token.slice(1); + + // Check if this is -i with backup suffix attached (e.g., -i.bak) + const iIndex = flags.indexOf('i'); + const IIndex = flags.indexOf('I'); + const inPlaceIndex = iIndex >= 0 ? iIndex : IIndex; + + if (inPlaceIndex >= 0 && inPlaceIndex < flags.length - 1) { + // -i.bak style - backup suffix is attached + i++; + continue; + } + + // Check if -i or -I is the last flag and next token could be backup suffix + if ((flags.endsWith('i') || flags.endsWith('I')) && i + 1 < tokens.length) { + const nextToken = tokens[i + 1]; + // macOS/BSD style: -i '' or -i "" (empty string backup suffix) + if (nextToken === '\'\'' || nextToken === '""' || (nextToken.startsWith('\'') && nextToken.endsWith('\'')) || (nextToken.startsWith('"') && nextToken.endsWith('"'))) { + i += 2; + continue; + } + } + + // Check for -e or -f which take arguments + if (flags.includes('e') || flags.includes('f')) { + const eIndex = flags.indexOf('e'); + const fIndex = flags.indexOf('f'); + const optIndex = eIndex >= 0 ? eIndex : fIndex; + + // If -e or -f is not the last character, the rest of the token is the argument + if (optIndex < flags.length - 1) { + foundScript = true; + i++; + continue; + } + + // Otherwise, the next token is the argument + foundScript = true; + i += 2; + continue; + } + + i++; + continue; + } + + // Non-option argument + if (!foundScript) { + // First non-option is the script (unless -e/-f was used) + foundScript = true; + i++; + continue; + } + + // Subsequent non-option arguments are files + // Strip surrounding quotes from file path + let file = token; + if ((file.startsWith('\'') && file.endsWith('\'')) || (file.startsWith('"') && file.endsWith('"'))) { + file = file.slice(1, -1); + } + files.push(file); + i++; + } + + return files; + } + private async _queryTree(languageId: TreeSitterCommandParserLanguage, commandLine: string, querySource: string): Promise { const { tree, query } = await this._doQuery(languageId, commandLine, querySource); return query.captures(tree.rootNode); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index 57351d2b065..544ebe526ab 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -313,15 +313,16 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary { test('error output to /dev/null - allow', () => t('cat missing.txt 2> /dev/null', 'outsideWorkspace', true, 1)); }); + suite('sed in-place editing', () => { + // Basic -i flag variants (inside workspace) + test('sed -i inside workspace - allow', () => t('sed -i \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); + test('sed -I inside workspace - allow', () => t('sed -I \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); + test('sed --in-place inside workspace - allow', () => t('sed --in-place \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); + + // Backup suffix variants (inside workspace) + test('sed -i.bak inside workspace - allow', () => t('sed -i.bak \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); + test('sed --in-place=.bak inside workspace - allow', () => t('sed --in-place=.bak \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); + test('sed -i with empty backup (macOS) inside workspace - allow', () => t('sed -i \'\' \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); + + // Combined flags (inside workspace) + test('sed -ni inside workspace - allow', () => t('sed -ni \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); + test('sed -n -i inside workspace - allow', () => t('sed -n -i \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); + + // Multiple files (inside workspace) + test('sed -i multiple files inside workspace - allow', () => t('sed -i \'s/foo/bar/\' file1.txt file2.txt', 'outsideWorkspace', true, 1)); + + // Outside workspace + test('sed -i outside workspace - block', () => t('sed -i \'s/foo/bar/\' /tmp/file.txt', 'outsideWorkspace', false, 1)); + test('sed -i absolute path outside workspace - block', () => t('sed -i \'s/foo/bar/\' /etc/config', 'outsideWorkspace', false, 1)); + test('sed -i mixed inside/outside - block', () => t('sed -i \'s/foo/bar/\' file.txt /tmp/other.txt', 'outsideWorkspace', false, 1)); + + // With blockDetectedFileWrites: all + test('sed -i with all setting - block', () => t('sed -i \'s/foo/bar/\' file.txt', 'all', false, 1)); + + // With blockDetectedFileWrites: never + test('sed -i with never setting - allow', () => t('sed -i \'s/foo/bar/\' file.txt', 'never', true, 1)); + + // Without -i flag (should not detect as file write) + test('sed without -i - no file write detected', () => t('sed \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 0)); + test('sed with pipe - no file write detected', () => t('cat file.txt | sed \'s/foo/bar/\'', 'outsideWorkspace', true, 0)); + }); + suite('no cwd provided', () => { async function tNoCwd(commandLine: string, blockDetectedFileWrites: 'never' | 'outsideWorkspace' | 'all', expectedAutoApprove: boolean, expectedDisclaimers: number = 0) { configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.BlockDetectedFileWrites, blockDetectedFileWrites); From 110f6fb94dcf99bf9dd67f8a178fbd4c447e7dcd Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Fri, 16 Jan 2026 13:01:21 +0100 Subject: [PATCH 063/387] fixes https://github.com/microsoft/vscode/issues/287914 --- .../contrib/chat/browser/actions/chatNewActions.ts | 6 +++--- .../chat/browser/chatSessions/chatSessions.contribution.ts | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts index 7c67a0de0d5..1c525a8eaa0 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts @@ -149,10 +149,10 @@ export function registerNewChatActions() { await editingSession?.stop(); // Create a new session with the same type as the current session - if (isIChatViewViewContext(widget.viewContext)) { + const currentResource = widget.viewModel?.model.sessionResource; + const sessionType = currentResource ? getChatSessionType(currentResource) : localChatSessionType; + if (isIChatViewViewContext(widget.viewContext) && sessionType !== localChatSessionType) { // For the sidebar, we need to explicitly load a session with the same type - const currentResource = widget.viewModel?.model.sessionResource; - const sessionType = currentResource ? getChatSessionType(currentResource) : localChatSessionType; const newResource = getResourceForNewChatSession(sessionType); const view = await viewsService.openView(ChatViewId) as ChatViewPane; await view.loadSession(newResource); diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index 3fb5d98607c..7b973bc922d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -1158,7 +1158,11 @@ async function openChatSession(accessor: ServicesAccessor, openOptions: NewChatS switch (openOptions.position) { case ChatSessionPosition.Sidebar: { const view = await viewsService.openView(ChatViewId) as ChatViewPane; - await view.loadSession(resource); + if (openOptions.type === AgentSessionProviders.Local) { + await view.widget.clear(); + } else { + await view.loadSession(resource); + } view.focus(); break; } From 1d4779347fd5b15b3dd9360b1e890f3f896ff04a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Jan 2026 04:04:54 -0800 Subject: [PATCH 064/387] Pull sed parser into new file --- .../commandParsers/commandFileWriteParser.ts | 31 +++ .../commandParsers/sedFileWriteParser.ts | 201 +++++++++++++++++ .../commandLineFileWriteAnalyzer.ts | 6 +- .../browser/treeSitterCommandParser.ts | 212 ++---------------- 4 files changed, 249 insertions(+), 201 deletions(-) create mode 100644 src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/commandFileWriteParser.ts create mode 100644 src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/sedFileWriteParser.ts diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/commandFileWriteParser.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/commandFileWriteParser.ts new file mode 100644 index 00000000000..09d14d75e42 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/commandFileWriteParser.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Interface for command-specific file write parsers. + * Each parser is responsible for detecting when a specific command will write to files + * (beyond simple shell redirections which are handled separately via tree-sitter queries). + */ +export interface ICommandFileWriteParser { + /** + * The name of the command this parser handles (e.g., 'sed', 'tee'). + */ + readonly commandName: string; + + /** + * Checks if this parser can handle the given command text. + * Should return true only if the command would write to files. + * @param commandText The full text of a single command (not a pipeline). + */ + canHandle(commandText: string): boolean; + + /** + * Extracts the file paths that would be written to by this command. + * Should only be called if canHandle() returns true. + * @param commandText The full text of a single command (not a pipeline). + * @returns Array of file paths that would be modified. + */ + extractFileWrites(commandText: string): string[]; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/sedFileWriteParser.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/sedFileWriteParser.ts new file mode 100644 index 00000000000..8e9565b0ae7 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/sedFileWriteParser.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICommandFileWriteParser } from './commandFileWriteParser.js'; + +/** + * Parser for detecting file writes from `sed` commands using in-place editing. + * + * Handles: + * - `sed -i 's/foo/bar/' file.txt` (GNU) + * - `sed -i.bak 's/foo/bar/' file.txt` (GNU with backup suffix) + * - `sed -i '' 's/foo/bar/' file.txt` (macOS/BSD with empty backup suffix) + * - `sed --in-place 's/foo/bar/' file.txt` (GNU long form) + * - `sed --in-place=.bak 's/foo/bar/' file.txt` (GNU long form with backup) + * - `sed -I 's/foo/bar/' file.txt` (BSD case-insensitive variant) + */ +export class SedFileWriteParser implements ICommandFileWriteParser { + readonly commandName = 'sed'; + + canHandle(commandText: string): boolean { + // Check if this is a sed command + if (!commandText.match(/^sed\s+/)) { + return false; + } + + // Check for -i, -I, or --in-place flag + const inPlaceRegex = /(?:^|\s)(-[a-zA-Z]*[iI][a-zA-Z]*\S*|--in-place(?:=\S*)?|(-i|-I)\s*'[^']*'|(-i|-I)\s*"[^"]*")(?:\s|$)/; + return inPlaceRegex.test(commandText); + } + + extractFileWrites(commandText: string): string[] { + const tokens = this._tokenizeCommand(commandText); + return this._extractFileTargets(tokens); + } + + /** + * Tokenizes a command into individual arguments, handling quotes and escapes. + */ + private _tokenizeCommand(commandText: string): string[] { + const tokens: string[] = []; + let current = ''; + let inSingleQuote = false; + let inDoubleQuote = false; + let escaped = false; + + for (let i = 0; i < commandText.length; i++) { + const char = commandText[i]; + + if (escaped) { + current += char; + escaped = false; + continue; + } + + if (char === '\\' && !inSingleQuote) { + escaped = true; + current += char; + continue; + } + + if (char === '\'' && !inDoubleQuote) { + inSingleQuote = !inSingleQuote; + current += char; + continue; + } + + if (char === '"' && !inSingleQuote) { + inDoubleQuote = !inDoubleQuote; + current += char; + continue; + } + + if (/\s/.test(char) && !inSingleQuote && !inDoubleQuote) { + if (current) { + tokens.push(current); + current = ''; + } + continue; + } + + current += char; + } + + if (current) { + tokens.push(current); + } + + return tokens; + } + + /** + * Extracts file targets from tokenized sed command arguments. + * Files are generally the last non-option, non-script arguments. + */ + private _extractFileTargets(tokens: string[]): string[] { + if (tokens.length === 0 || tokens[0] !== 'sed') { + return []; + } + + const files: string[] = []; + let i = 1; // Skip 'sed' + let foundScript = false; + + while (i < tokens.length) { + const token = tokens[i]; + + // Long options + if (token.startsWith('--')) { + if (token === '--in-place' || token.startsWith('--in-place=')) { + // In-place flag (already verified we have one) + i++; + continue; + } + if (token === '--expression' || token === '--file') { + // Skip the option and its argument + i += 2; + foundScript = true; + continue; + } + if (token.startsWith('--expression=') || token.startsWith('--file=')) { + i++; + foundScript = true; + continue; + } + // Other long options like --sandbox, --debug, etc. + i++; + continue; + } + + // Short options + if (token.startsWith('-') && token.length > 1 && token[1] !== '-') { + // Could be combined flags like -ni or -i.bak + const flags = token.slice(1); + + // Check if this is -i with backup suffix attached (e.g., -i.bak) + const iIndex = flags.indexOf('i'); + const IIndex = flags.indexOf('I'); + const inPlaceIndex = iIndex >= 0 ? iIndex : IIndex; + + if (inPlaceIndex >= 0 && inPlaceIndex < flags.length - 1) { + // -i.bak style - backup suffix is attached + i++; + continue; + } + + // Check if -i or -I is the last flag and next token could be backup suffix + if ((flags.endsWith('i') || flags.endsWith('I')) && i + 1 < tokens.length) { + const nextToken = tokens[i + 1]; + // macOS/BSD style: -i '' or -i "" (empty string backup suffix) + if (nextToken === '\'\'' || nextToken === '""' || (nextToken.startsWith('\'') && nextToken.endsWith('\'')) || (nextToken.startsWith('"') && nextToken.endsWith('"'))) { + i += 2; + continue; + } + } + + // Check for -e or -f which take arguments + if (flags.includes('e') || flags.includes('f')) { + const eIndex = flags.indexOf('e'); + const fIndex = flags.indexOf('f'); + const optIndex = eIndex >= 0 ? eIndex : fIndex; + + // If -e or -f is not the last character, the rest of the token is the argument + if (optIndex < flags.length - 1) { + foundScript = true; + i++; + continue; + } + + // Otherwise, the next token is the argument + foundScript = true; + i += 2; + continue; + } + + i++; + continue; + } + + // Non-option argument + if (!foundScript) { + // First non-option is the script (unless -e/-f was used) + foundScript = true; + i++; + continue; + } + + // Subsequent non-option arguments are files + // Strip surrounding quotes from file path + let file = token; + if ((file.startsWith('\'') && file.endsWith('\'')) || (file.startsWith('"') && file.endsWith('"'))) { + file = file.slice(1, -1); + } + files.push(file); + i++; + } + + return files; + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineFileWriteAnalyzer.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineFileWriteAnalyzer.ts index 4e55dac7ed3..e86dcf79cfe 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineFileWriteAnalyzer.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineFileWriteAnalyzer.ts @@ -52,11 +52,11 @@ export class CommandLineFileWriteAnalyzer extends Disposable implements ICommand const capturedFileWrites = (await this._treeSitterCommandParser.getFileWrites(options.treeSitterLanguage, options.commandLine)) .map(this._mapNullDevice.bind(this, options)); - // Get file writes from sed in-place editing - const sedInPlaceFiles = (await this._treeSitterCommandParser.getSedInPlaceFiles(options.treeSitterLanguage, options.commandLine)) + // Get file writes from command-specific parsers (e.g., sed -i in-place editing) + const commandFileWrites = (await this._treeSitterCommandParser.getCommandFileWrites(options.treeSitterLanguage, options.commandLine)) .map(this._mapNullDevice.bind(this, options)); - const allCapturedFileWrites = [...capturedFileWrites, ...sedInPlaceFiles]; + const allCapturedFileWrites = [...capturedFileWrites, ...commandFileWrites]; if (allCapturedFileWrites.length) { const cwd = options.cwd; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts index ef3207c58f4..b6bea01c0f2 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts @@ -9,6 +9,8 @@ import { BugIndicatingError, ErrorNoTelemetry } from '../../../../../base/common import { Lazy } from '../../../../../base/common/lazy.js'; import { Disposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { ITreeSitterLibraryService } from '../../../../../editor/common/services/treeSitter/treeSitterLibraryService.js'; +import { ICommandFileWriteParser } from './commandParsers/commandFileWriteParser.js'; +import { SedFileWriteParser } from './commandParsers/sedFileWriteParser.js'; export const enum TreeSitterCommandParserLanguage { Bash = 'bash', @@ -18,6 +20,9 @@ export const enum TreeSitterCommandParserLanguage { export class TreeSitterCommandParser extends Disposable { private readonly _parser: Lazy>; private readonly _treeCache = this._register(new TreeCache()); + private readonly _commandFileWriteParsers: ICommandFileWriteParser[] = [ + new SedFileWriteParser(), + ]; constructor( @ITreeSitterLibraryService private readonly _treeSitterLibraryService: ITreeSitterLibraryService, @@ -62,11 +67,12 @@ export class TreeSitterCommandParser extends Disposable { } /** - * Extracts file targets from `sed` commands that use in-place editing (`-i`, `-I`, or `--in-place`). + * Extracts file targets from commands that perform file writes beyond shell redirections. + * Uses registered command parsers (e.g., for `sed -i`) to detect command-specific file writes. * Returns an array of file paths that would be modified. */ - async getSedInPlaceFiles(languageId: TreeSitterCommandParserLanguage, commandLine: string): Promise { - // This is only relevant for bash-like shells + async getCommandFileWrites(languageId: TreeSitterCommandParserLanguage, commandLine: string): Promise { + // Currently only bash-like shells are supported for command-specific parsing if (languageId !== TreeSitterCommandParserLanguage.Bash) { return []; } @@ -78,205 +84,15 @@ export class TreeSitterCommandParser extends Disposable { const result: string[] = []; for (const capture of captures) { const commandText = capture.node.text; - const sedFiles = this._parseSedInPlaceFiles(commandText); - result.push(...sedFiles); + for (const parser of this._commandFileWriteParsers) { + if (parser.canHandle(commandText)) { + result.push(...parser.extractFileWrites(commandText)); + } + } } return result; } - /** - * Parses a sed command to extract files being edited in-place. - * Handles: - * - `sed -i 's/foo/bar/' file.txt` (GNU) - * - `sed -i.bak 's/foo/bar/' file.txt` (GNU with backup suffix) - * - `sed -i '' 's/foo/bar/' file.txt` (macOS/BSD with empty backup suffix) - * - `sed --in-place 's/foo/bar/' file.txt` (GNU long form) - * - `sed --in-place=.bak 's/foo/bar/' file.txt` (GNU long form with backup) - * - `sed -I 's/foo/bar/' file.txt` (BSD case-insensitive variant) - */ - private _parseSedInPlaceFiles(commandText: string): string[] { - // Check if this is a sed command with in-place flag - const sedMatch = commandText.match(/^sed\s+/); - if (!sedMatch) { - return []; - } - - // Check for -i, -I, or --in-place flag - const inPlaceRegex = /(?:^|\s)(-[a-zA-Z]*[iI][a-zA-Z]*\S*|--in-place(?:=\S*)?|(-i|-I)\s*'[^']*'|(-i|-I)\s*"[^"]*")(?:\s|$)/; - if (!inPlaceRegex.test(commandText)) { - return []; - } - - // Parse the command to extract file arguments - // We need to skip: the 'sed' command, flags, and sed scripts/expressions - const tokens = this._tokenizeSedCommand(commandText); - return this._extractSedFileTargets(tokens); - } - - /** - * Tokenizes a sed command into individual arguments, handling quotes and escapes. - */ - private _tokenizeSedCommand(commandText: string): string[] { - const tokens: string[] = []; - let current = ''; - let inSingleQuote = false; - let inDoubleQuote = false; - let escaped = false; - - for (let i = 0; i < commandText.length; i++) { - const char = commandText[i]; - - if (escaped) { - current += char; - escaped = false; - continue; - } - - if (char === '\\' && !inSingleQuote) { - escaped = true; - current += char; - continue; - } - - if (char === '\'' && !inDoubleQuote) { - inSingleQuote = !inSingleQuote; - current += char; - continue; - } - - if (char === '"' && !inSingleQuote) { - inDoubleQuote = !inDoubleQuote; - current += char; - continue; - } - - if (/\s/.test(char) && !inSingleQuote && !inDoubleQuote) { - if (current) { - tokens.push(current); - current = ''; - } - continue; - } - - current += char; - } - - if (current) { - tokens.push(current); - } - - return tokens; - } - - /** - * Extracts file targets from tokenized sed command arguments. - * Files are generally the last non-option, non-script arguments. - */ - private _extractSedFileTargets(tokens: string[]): string[] { - if (tokens.length === 0 || tokens[0] !== 'sed') { - return []; - } - - const files: string[] = []; - let i = 1; // Skip 'sed' - let foundScript = false; - - while (i < tokens.length) { - const token = tokens[i]; - - // Long options - if (token.startsWith('--')) { - if (token === '--in-place' || token.startsWith('--in-place=')) { - // In-place flag (already verified we have one) - i++; - continue; - } - if (token === '--expression' || token === '--file') { - // Skip the option and its argument - i += 2; - foundScript = true; - continue; - } - if (token.startsWith('--expression=') || token.startsWith('--file=')) { - i++; - foundScript = true; - continue; - } - // Other long options like --sandbox, --debug, etc. - i++; - continue; - } - - // Short options - if (token.startsWith('-') && token.length > 1 && token[1] !== '-') { - // Could be combined flags like -ni or -i.bak - const flags = token.slice(1); - - // Check if this is -i with backup suffix attached (e.g., -i.bak) - const iIndex = flags.indexOf('i'); - const IIndex = flags.indexOf('I'); - const inPlaceIndex = iIndex >= 0 ? iIndex : IIndex; - - if (inPlaceIndex >= 0 && inPlaceIndex < flags.length - 1) { - // -i.bak style - backup suffix is attached - i++; - continue; - } - - // Check if -i or -I is the last flag and next token could be backup suffix - if ((flags.endsWith('i') || flags.endsWith('I')) && i + 1 < tokens.length) { - const nextToken = tokens[i + 1]; - // macOS/BSD style: -i '' or -i "" (empty string backup suffix) - if (nextToken === '\'\'' || nextToken === '""' || (nextToken.startsWith('\'') && nextToken.endsWith('\'')) || (nextToken.startsWith('"') && nextToken.endsWith('"'))) { - i += 2; - continue; - } - } - - // Check for -e or -f which take arguments - if (flags.includes('e') || flags.includes('f')) { - const eIndex = flags.indexOf('e'); - const fIndex = flags.indexOf('f'); - const optIndex = eIndex >= 0 ? eIndex : fIndex; - - // If -e or -f is not the last character, the rest of the token is the argument - if (optIndex < flags.length - 1) { - foundScript = true; - i++; - continue; - } - - // Otherwise, the next token is the argument - foundScript = true; - i += 2; - continue; - } - - i++; - continue; - } - - // Non-option argument - if (!foundScript) { - // First non-option is the script (unless -e/-f was used) - foundScript = true; - i++; - continue; - } - - // Subsequent non-option arguments are files - // Strip surrounding quotes from file path - let file = token; - if ((file.startsWith('\'') && file.endsWith('\'')) || (file.startsWith('"') && file.endsWith('"'))) { - file = file.slice(1, -1); - } - files.push(file); - i++; - } - - return files; - } - private async _queryTree(languageId: TreeSitterCommandParserLanguage, commandLine: string, querySource: string): Promise { const { tree, query } = await this._doQuery(languageId, commandLine, querySource); return query.captures(tree.rootNode); From 81a1694cfa7760048c28c5436ecdf25d8f2d02a3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Jan 2026 04:32:35 -0800 Subject: [PATCH 065/387] Fix dupe test warning --- .../commandLineAnalyzer/commandLineFileWriteAnalyzer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineAnalyzer/commandLineFileWriteAnalyzer.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineAnalyzer/commandLineFileWriteAnalyzer.test.ts index d376ee2be0b..89e21029863 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineAnalyzer/commandLineFileWriteAnalyzer.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineAnalyzer/commandLineFileWriteAnalyzer.test.ts @@ -137,7 +137,7 @@ suite('CommandLineFileWriteAnalyzer', () => { suite('sed in-place editing', () => { // Basic -i flag variants (inside workspace) test('sed -i inside workspace - allow', () => t('sed -i \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); - test('sed -I inside workspace - allow', () => t('sed -I \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); + test('sed -I (uppercase) inside workspace - allow', () => t('sed -I \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); test('sed --in-place inside workspace - allow', () => t('sed --in-place \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); // Backup suffix variants (inside workspace) From 8c65bf9f9c006a22d252df20977f0baddfe1d111 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Jan 2026 04:36:14 -0800 Subject: [PATCH 066/387] Fix tests --- .../browser/commandParsers/sedFileWriteParser.ts | 13 ++++++++++++- .../test/electron-browser/runInTerminalTool.test.ts | 3 --- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/sedFileWriteParser.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/sedFileWriteParser.ts index 8e9565b0ae7..f1442781c74 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/sedFileWriteParser.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/sedFileWriteParser.ts @@ -149,10 +149,21 @@ export class SedFileWriteParser implements ICommandFileWriteParser { if ((flags.endsWith('i') || flags.endsWith('I')) && i + 1 < tokens.length) { const nextToken = tokens[i + 1]; // macOS/BSD style: -i '' or -i "" (empty string backup suffix) - if (nextToken === '\'\'' || nextToken === '""' || (nextToken.startsWith('\'') && nextToken.endsWith('\'')) || (nextToken.startsWith('"') && nextToken.endsWith('"'))) { + // Only treat it as a backup suffix if it's empty or looks like a backup + // extension (starts with '.' and is short). Don't match sed scripts like 's/foo/bar/'. + if (nextToken === '\'\'' || nextToken === '""') { i += 2; continue; } + // Check for quoted backup suffixes like '.bak' or ".backup" + if ((nextToken.startsWith('\'') && nextToken.endsWith('\'')) || (nextToken.startsWith('"') && nextToken.endsWith('"'))) { + const unquoted = nextToken.slice(1, -1); + // Backup suffixes typically start with '.' and are short extensions + if (unquoted.startsWith('.') && unquoted.length <= 10 && !unquoted.includes('/')) { + i += 2; + continue; + } + } } // Check for -e or -f which take arguments diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts index 25a032e8e24..0df06af3a79 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts @@ -353,9 +353,6 @@ suite('RunInTerminalTool', () => { 'find . -fprint output.txt', 'rg --pre cat pattern .', 'rg --hostname-bin hostname pattern .', - 'sed -i "s/foo/bar/g" file.txt', - 'sed -i.bak "s/foo/bar/" file.txt', - 'sed -Ibak "s/foo/bar/" file.txt', 'sed --in-place "s/foo/bar/" file.txt', 'sed -e "s/a/b/" file.txt', 'sed -f script.sed file.txt', From acd5f6a352e00f3fad4282dfaecb5c08e87f6135 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 16 Jan 2026 13:24:26 +0000 Subject: [PATCH 067/387] Enhance 2026 Light Theme and Add Stealth Shadows Styles - Updated color values in 2026-light.json for improved contrast and accessibility. - Introduced new styles.css file to implement stealth shadows for various UI elements, enhancing visual depth and aesthetics across the workbench. - Adjusted box-shadow properties for components like the activity bar, sidebar, panel, and editor to create a more cohesive design. --- extensions/theme-2026/package.json | 14 +- extensions/theme-2026/themes/2026-dark.json | 150 ++++++------- extensions/theme-2026/themes/2026-light.json | 76 +++---- extensions/theme-2026/themes/styles.css | 223 +++++++++++++++++++ 4 files changed, 347 insertions(+), 116 deletions(-) create mode 100644 extensions/theme-2026/themes/styles.css diff --git a/extensions/theme-2026/package.json b/extensions/theme-2026/package.json index 593169de867..b425a019e2f 100644 --- a/extensions/theme-2026/package.json +++ b/extensions/theme-2026/package.json @@ -8,23 +8,31 @@ "engines": { "vscode": "^1.85.0" }, + "enabledApiProposals": [ + "css" + ], "categories": [ "Themes" ], "contributes": { "themes": [ { - "id": "2026-light", + "id": "2026-light-experimental", "label": "2026 Light", "uiTheme": "vs", "path": "./themes/2026-light.json" }, { - "id": "2026-dark", + "id": "2026-dark-experimental", "label": "2026 Dark", "uiTheme": "vs-dark", "path": "./themes/2026-dark.json" } - ] + ], + "css": [ + { + "path": "./themes/styles.css" + } + ] } } diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 1dbea210de3..7857cd2f1c5 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -8,99 +8,99 @@ "errorForeground": "#f48771", "descriptionForeground": "#888888", "icon.foreground": "#888888", - "focusBorder": "#007ABBB3", - "textBlockQuote.background": "#22282C", + "focusBorder": "#007ABCB3", + "textBlockQuote.background": "#242526", "textBlockQuote.border": "#252627FF", - "textCodeBlock.background": "#22282C", - "textLink.foreground": "#0092E0", - "textLink.activeForeground": "#009AEB", + "textCodeBlock.background": "#242526", + "textLink.foreground": "#0092E2", + "textLink.activeForeground": "#009AEC", "textPreformat.foreground": "#888888", "textSeparator.foreground": "#252525FF", "button.background": "#007ABC", "button.foreground": "#FFFFFF", - "button.hoverBackground": "#0080C4", + "button.hoverBackground": "#0080C5", "button.border": "#252627FF", - "button.secondaryBackground": "#22282C", + "button.secondaryBackground": "#242526", "button.secondaryForeground": "#bebebe", "button.secondaryHoverBackground": "#007ABC", - "checkbox.background": "#22282C", + "checkbox.background": "#242526", "checkbox.border": "#252627FF", "checkbox.foreground": "#bebebe", - "dropdown.background": "#181E22", + "dropdown.background": "#191A1B", "dropdown.border": "#323435", "dropdown.foreground": "#bebebe", - "dropdown.listBackground": "#1E2529", - "input.background": "#181E22", + "dropdown.listBackground": "#202122", + "input.background": "#191A1B", "input.border": "#323435FF", "input.foreground": "#bebebe", "input.placeholderForeground": "#777777", "inputOption.activeBackground": "#007ABC33", "inputOption.activeForeground": "#bebebe", "inputOption.activeBorder": "#252627FF", - "inputValidation.errorBackground": "#181E22", + "inputValidation.errorBackground": "#191A1B", "inputValidation.errorBorder": "#252627FF", "inputValidation.errorForeground": "#bebebe", - "inputValidation.infoBackground": "#181E22", + "inputValidation.infoBackground": "#191A1B", "inputValidation.infoBorder": "#252627FF", "inputValidation.infoForeground": "#bebebe", - "inputValidation.warningBackground": "#181E22", + "inputValidation.warningBackground": "#191A1B", "inputValidation.warningBorder": "#252627FF", "inputValidation.warningForeground": "#bebebe", "scrollbar.shadow": "#191B1D4D", - "scrollbarSlider.background": "#7D848833", - "scrollbarSlider.hoverBackground": "#7D848866", - "scrollbarSlider.activeBackground": "#7D848899", + "scrollbarSlider.background": "#83848533", + "scrollbarSlider.hoverBackground": "#83848566", + "scrollbarSlider.activeBackground": "#83848599", "badge.background": "#007ABC", "badge.foreground": "#FFFFFF", - "progressBar.background": "#81878B", + "progressBar.background": "#878889", "list.activeSelectionBackground": "#007ABC26", "list.activeSelectionForeground": "#bebebe", - "list.inactiveSelectionBackground": "#22282C", + "list.inactiveSelectionBackground": "#242526", "list.inactiveSelectionForeground": "#bebebe", - "list.hoverBackground": "#242A2E", + "list.hoverBackground": "#262728", "list.hoverForeground": "#bebebe", "list.dropBackground": "#007ABC1A", "list.focusBackground": "#007ABC26", "list.focusForeground": "#bebebe", - "list.focusOutline": "#007ABBB3", + "list.focusOutline": "#007ABCB3", "list.highlightForeground": "#bebebe", "list.invalidItemForeground": "#444444", "list.errorForeground": "#f48771", "list.warningForeground": "#e5ba7d", - "activityBar.background": "#181E22", + "activityBar.background": "#191A1B", "activityBar.foreground": "#bebebe", "activityBar.inactiveForeground": "#888888", "activityBar.border": "#252627FF", "activityBar.activeBorder": "#252627FF", - "activityBar.activeFocusBorder": "#007ABBB3", + "activityBar.activeFocusBorder": "#007ABCB3", "activityBarBadge.background": "#007ABC", "activityBarBadge.foreground": "#FFFFFF", - "sideBar.background": "#181E22", + "sideBar.background": "#191A1B", "sideBar.foreground": "#bebebe", "sideBar.border": "#252627FF", "sideBarTitle.foreground": "#bebebe", - "sideBarSectionHeader.background": "#181E22", + "sideBarSectionHeader.background": "#191A1B", "sideBarSectionHeader.foreground": "#bebebe", "sideBarSectionHeader.border": "#252627FF", - "titleBar.activeBackground": "#181E22", + "titleBar.activeBackground": "#191A1B", "titleBar.activeForeground": "#bebebe", - "titleBar.inactiveBackground": "#181E22", + "titleBar.inactiveBackground": "#191A1B", "titleBar.inactiveForeground": "#888888", "titleBar.border": "#252627FF", - "menubar.selectionBackground": "#22282C", + "menubar.selectionBackground": "#242526", "menubar.selectionForeground": "#bebebe", - "menu.background": "#1E2529", + "menu.background": "#202122", "menu.foreground": "#bebebe", "menu.selectionBackground": "#007ABC26", "menu.selectionForeground": "#bebebe", - "menu.separatorBackground": "#7D8488", + "menu.separatorBackground": "#838485", "menu.border": "#252627FF", "commandCenter.foreground": "#bebebe", "commandCenter.activeForeground": "#bebebe", - "commandCenter.background": "#181E22", - "commandCenter.activeBackground": "#242A2E", + "commandCenter.background": "#191A1B", + "commandCenter.activeBackground": "#262728", "commandCenter.border": "#323435", - "editor.background": "#11171B", + "editor.background": "#121314", "editor.foreground": "#BABDBE", "editorLineNumber.foreground": "#858889", "editorLineNumber.activeForeground": "#BABDBE", @@ -112,113 +112,113 @@ "editor.wordHighlightStrongBackground": "#007ABCE6", "editor.findMatchBackground": "#007ABC4D", "editor.findMatchHighlightBackground": "#007ABC26", - "editor.findRangeHighlightBackground": "#22282C", - "editor.hoverHighlightBackground": "#22282C", - "editor.lineHighlightBackground": "#22282C", - "editor.rangeHighlightBackground": "#22282C", - "editorLink.activeForeground": "#007ABB", + "editor.findRangeHighlightBackground": "#242526", + "editor.hoverHighlightBackground": "#242526", + "editor.lineHighlightBackground": "#242526", + "editor.rangeHighlightBackground": "#242526", + "editorLink.activeForeground": "#007ABC", "editorWhitespace.foreground": "#8888884D", - "editorIndentGuide.background": "#7D84884D", - "editorIndentGuide.activeBackground": "#7D8488", + "editorIndentGuide.background": "#8384854D", + "editorIndentGuide.activeBackground": "#838485", "editorRuler.foreground": "#848484", "editorCodeLens.foreground": "#888888", "editorBracketMatch.background": "#007ABC80", "editorBracketMatch.border": "#252627FF", - "editorWidget.background": "#1E2529", + "editorWidget.background": "#202122", "editorWidget.border": "#252627FF", "editorWidget.foreground": "#bebebe", - "editorSuggestWidget.background": "#1E2529", + "editorSuggestWidget.background": "#202122", "editorSuggestWidget.border": "#252627FF", "editorSuggestWidget.foreground": "#bebebe", "editorSuggestWidget.highlightForeground": "#bebebe", "editorSuggestWidget.selectedBackground": "#007ABC26", - "editorHoverWidget.background": "#1E2529", + "editorHoverWidget.background": "#202122", "editorHoverWidget.border": "#252627FF", "peekView.border": "#252627FF", - "peekViewEditor.background": "#181E22", + "peekViewEditor.background": "#191A1B", "peekViewEditor.matchHighlightBackground": "#007ABC33", - "peekViewResult.background": "#22282C", + "peekViewResult.background": "#242526", "peekViewResult.fileForeground": "#bebebe", "peekViewResult.lineForeground": "#888888", "peekViewResult.matchHighlightBackground": "#007ABC33", "peekViewResult.selectionBackground": "#007ABC26", "peekViewResult.selectionForeground": "#bebebe", - "peekViewTitle.background": "#22282C", + "peekViewTitle.background": "#242526", "peekViewTitleDescription.foreground": "#888888", "peekViewTitleLabel.foreground": "#bebebe", - "editorGutter.background": "#11171B", - "editorGutter.addedBackground": "#6DC594", - "editorGutter.deletedBackground": "#E88676", - "diffEditor.insertedTextBackground": "#6DC59454", - "diffEditor.removedTextBackground": "#E8867654", + "editorGutter.background": "#121314", + "editorGutter.addedBackground": "#72C892", + "editorGutter.deletedBackground": "#F28772", + "diffEditor.insertedTextBackground": "#72C89254", + "diffEditor.removedTextBackground": "#F2877254", "editorOverviewRuler.border": "#252627FF", - "editorOverviewRuler.findMatchForeground": "#007ABB99", + "editorOverviewRuler.findMatchForeground": "#007ABC99", "editorOverviewRuler.modifiedForeground": "#5ba3e0", "editorOverviewRuler.addedForeground": "#73c991", "editorOverviewRuler.deletedForeground": "#f48771", "editorOverviewRuler.errorForeground": "#f48771", "editorOverviewRuler.warningForeground": "#e5ba7d", - "panel.background": "#181E22", + "panel.background": "#191A1B", "panel.border": "#252627FF", - "panelTitle.activeBorder": "#007ABB", + "panelTitle.activeBorder": "#007ABC", "panelTitle.activeForeground": "#bebebe", "panelTitle.inactiveForeground": "#888888", - "statusBar.background": "#181E22", + "statusBar.background": "#191A1B", "statusBar.foreground": "#bebebe", "statusBar.border": "#252627FF", - "statusBar.focusBorder": "#007ABBB3", + "statusBar.focusBorder": "#007ABCB3", "statusBar.debuggingBackground": "#007ABC", "statusBar.debuggingForeground": "#FFFFFF", - "statusBar.noFolderBackground": "#181E22", + "statusBar.noFolderBackground": "#191A1B", "statusBar.noFolderForeground": "#bebebe", "statusBarItem.activeBackground": "#007ABC", - "statusBarItem.hoverBackground": "#242A2E", - "statusBarItem.focusBorder": "#007ABBB3", + "statusBarItem.hoverBackground": "#262728", + "statusBarItem.focusBorder": "#007ABCB3", "statusBarItem.prominentBackground": "#007ABC", "statusBarItem.prominentForeground": "#FFFFFF", "statusBarItem.prominentHoverBackground": "#007ABC", - "tab.activeBackground": "#11171B", + "tab.activeBackground": "#121314", "tab.activeForeground": "#bebebe", - "tab.inactiveBackground": "#181E22", + "tab.inactiveBackground": "#191A1B", "tab.inactiveForeground": "#888888", "tab.border": "#252627FF", "tab.lastPinnedBorder": "#252627FF", "tab.activeBorder": "#121314", - "tab.hoverBackground": "#242A2E", + "tab.hoverBackground": "#262728", "tab.hoverForeground": "#bebebe", - "tab.unfocusedActiveBackground": "#11171B", + "tab.unfocusedActiveBackground": "#121314", "tab.unfocusedActiveForeground": "#888888", - "tab.unfocusedInactiveBackground": "#181E22", + "tab.unfocusedInactiveBackground": "#191A1B", "tab.unfocusedInactiveForeground": "#444444", - "editorGroupHeader.tabsBackground": "#181E22", + "editorGroupHeader.tabsBackground": "#191A1B", "editorGroupHeader.tabsBorder": "#252627FF", "breadcrumb.foreground": "#888888", - "breadcrumb.background": "#11171B", + "breadcrumb.background": "#121314", "breadcrumb.focusForeground": "#bebebe", "breadcrumb.activeSelectionForeground": "#bebebe", - "breadcrumbPicker.background": "#1E2529", + "breadcrumbPicker.background": "#202122", "notificationCenter.border": "#252627FF", "notificationCenterHeader.foreground": "#bebebe", - "notificationCenterHeader.background": "#22282C", + "notificationCenterHeader.background": "#242526", "notificationToast.border": "#252627FF", "notifications.foreground": "#bebebe", - "notifications.background": "#1E2529", + "notifications.background": "#202122", "notifications.border": "#252627FF", - "notificationLink.foreground": "#007ABB", + "notificationLink.foreground": "#007ABC", "extensionButton.prominentBackground": "#007ABC", "extensionButton.prominentForeground": "#FFFFFF", - "extensionButton.prominentHoverBackground": "#0080C4", + "extensionButton.prominentHoverBackground": "#0080C5", "pickerGroup.border": "#252627FF", "pickerGroup.foreground": "#bebebe", - "quickInput.background": "#1E2529", + "quickInput.background": "#202122", "quickInput.foreground": "#bebebe", "quickInputList.focusBackground": "#007ABC26", "quickInputList.focusForeground": "#bebebe", "quickInputList.focusIconForeground": "#bebebe", - "quickInputList.hoverBackground": "#4E5458", + "quickInputList.hoverBackground": "#515253", "terminal.selectionBackground": "#007ABC33", "terminalCursor.foreground": "#bebebe", - "terminalCursor.background": "#181E22", + "terminalCursor.background": "#191A1B", "gitDecoration.addedResourceForeground": "#73c991", "gitDecoration.modifiedResourceForeground": "#e5ba7d", "gitDecoration.deletedResourceForeground": "#f48771", @@ -227,7 +227,7 @@ "gitDecoration.conflictingResourceForeground": "#f48771", "gitDecoration.stageModifiedResourceForeground": "#e5ba7d", "gitDecoration.stageDeletedResourceForeground": "#f48771", - "quickInputTitle.background": "#1E2529", + "quickInputTitle.background": "#202122", "quickInput.border": "#323435", "chat.requestBubbleBackground": "#007ABC26", "chat.requestBubbleHoverBackground": "#007ABC46" diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index aff045220a3..ba908cc9979 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -4,47 +4,47 @@ "type": "light", "colors": { "foreground": "#202020", - "disabledForeground": "#999999", + "disabledForeground": "#BBBBBB", "errorForeground": "#ad0707", "descriptionForeground": "#666666", "icon.foreground": "#666666", "focusBorder": "#4466CCFF", "textBlockQuote.background": "#F3F3F3", - "textBlockQuote.border": "#ECEDEEFF", + "textBlockQuote.border": "#EEEEEE00", "textCodeBlock.background": "#F3F3F3", "textLink.foreground": "#6F89D8", "textLink.activeForeground": "#7C94DB", "textPreformat.foreground": "#666666", - "textSeparator.foreground": "#EEEEEEFF", + "textSeparator.foreground": "#EEEEEE00", "button.background": "#4466CC", "button.foreground": "#FFFFFF", "button.hoverBackground": "#4F6FCF", - "button.border": "#ECEDEEFF", + "button.border": "#EEEEEE00", "button.secondaryBackground": "#F3F3F3", "button.secondaryForeground": "#202020", "button.secondaryHoverBackground": "#4466CC", "checkbox.background": "#F3F3F3", - "checkbox.border": "#ECEDEEFF", + "checkbox.border": "#EEEEEE00", "checkbox.foreground": "#202020", "dropdown.background": "#F9F9F9", - "dropdown.border": "#D0D1D2", + "dropdown.border": "#D6D7D8", "dropdown.foreground": "#202020", "dropdown.listBackground": "#FCFCFC", "input.background": "#F9F9F9", - "input.border": "#D0D1D2FF", + "input.border": "#D6D7D880", "input.foreground": "#202020", - "input.placeholderForeground": "#AAAAAA", + "input.placeholderForeground": "#999999", "inputOption.activeBackground": "#4466CC33", "inputOption.activeForeground": "#202020", - "inputOption.activeBorder": "#ECEDEEFF", + "inputOption.activeBorder": "#EEEEEE00", "inputValidation.errorBackground": "#F9F9F9", - "inputValidation.errorBorder": "#ECEDEEFF", + "inputValidation.errorBorder": "#EEEEEE00", "inputValidation.errorForeground": "#202020", "inputValidation.infoBackground": "#F9F9F9", - "inputValidation.infoBorder": "#ECEDEEFF", + "inputValidation.infoBorder": "#EEEEEE00", "inputValidation.infoForeground": "#202020", "inputValidation.warningBackground": "#F9F9F9", - "inputValidation.warningBorder": "#ECEDEEFF", + "inputValidation.warningBorder": "#EEEEEE00", "inputValidation.warningForeground": "#202020", "scrollbar.shadow": "#F5F6F84D", "scrollbarSlider.background": "#4466CC33", @@ -64,29 +64,29 @@ "list.focusForeground": "#202020", "list.focusOutline": "#4466CCFF", "list.highlightForeground": "#202020", - "list.invalidItemForeground": "#999999", + "list.invalidItemForeground": "#BBBBBB", "list.errorForeground": "#ad0707", "list.warningForeground": "#667309", "activityBar.background": "#F9F9F9", "activityBar.foreground": "#202020", "activityBar.inactiveForeground": "#666666", - "activityBar.border": "#ECEDEEFF", - "activityBar.activeBorder": "#ECEDEEFF", + "activityBar.border": "#EEEEEE00", + "activityBar.activeBorder": "#EEEEEE00", "activityBar.activeFocusBorder": "#4466CCFF", "activityBarBadge.background": "#4466CC", "activityBarBadge.foreground": "#FFFFFF", "sideBar.background": "#F9F9F9", "sideBar.foreground": "#202020", - "sideBar.border": "#ECEDEEFF", + "sideBar.border": "#EEEEEE00", "sideBarTitle.foreground": "#202020", "sideBarSectionHeader.background": "#F9F9F9", "sideBarSectionHeader.foreground": "#202020", - "sideBarSectionHeader.border": "#ECEDEEFF", + "sideBarSectionHeader.border": "#EEEEEE00", "titleBar.activeBackground": "#F9F9F9", - "titleBar.activeForeground": "#202020", + "titleBar.activeForeground": "#424242", "titleBar.inactiveBackground": "#F9F9F9", "titleBar.inactiveForeground": "#666666", - "titleBar.border": "#ECEDEEFF", + "titleBar.border": "#EEEEEE00", "menubar.selectionBackground": "#F3F3F3", "menubar.selectionForeground": "#202020", "menu.background": "#FCFCFC", @@ -94,12 +94,12 @@ "menu.selectionBackground": "#4466CC26", "menu.selectionForeground": "#202020", "menu.separatorBackground": "#F4F4F4", - "menu.border": "#ECEDEEFF", + "menu.border": "#EEEEEE00", "commandCenter.foreground": "#202020", "commandCenter.activeForeground": "#202020", "commandCenter.background": "#F9F9F9", "commandCenter.activeBackground": "#FFFFFF", - "commandCenter.border": "#D0D1D2", + "commandCenter.border": "#D6D7D880", "editor.background": "#FDFDFD", "editor.foreground": "#202123", "editorLineNumber.foreground": "#656668", @@ -123,18 +123,18 @@ "editorRuler.foreground": "#F4F4F4", "editorCodeLens.foreground": "#666666", "editorBracketMatch.background": "#4466CC80", - "editorBracketMatch.border": "#ECEDEEFF", + "editorBracketMatch.border": "#EEEEEE00", "editorWidget.background": "#FCFCFC", - "editorWidget.border": "#ECEDEEFF", + "editorWidget.border": "#EEEEEE00", "editorWidget.foreground": "#202020", "editorSuggestWidget.background": "#FCFCFC", - "editorSuggestWidget.border": "#ECEDEEFF", + "editorSuggestWidget.border": "#EEEEEE00", "editorSuggestWidget.foreground": "#202020", "editorSuggestWidget.highlightForeground": "#202020", "editorSuggestWidget.selectedBackground": "#4466CC26", "editorHoverWidget.background": "#FCFCFC", - "editorHoverWidget.border": "#ECEDEEFF", - "peekView.border": "#ECEDEEFF", + "editorHoverWidget.border": "#EEEEEE00", + "peekView.border": "#EEEEEE00", "peekViewEditor.background": "#F9F9F9", "peekViewEditor.matchHighlightBackground": "#4466CC33", "peekViewResult.background": "#F3F3F3", @@ -151,7 +151,7 @@ "editorGutter.deletedBackground": "#ad0707", "diffEditor.insertedTextBackground": "#587c0c54", "diffEditor.removedTextBackground": "#ad070754", - "editorOverviewRuler.border": "#ECEDEEFF", + "editorOverviewRuler.border": "#EEEEEE00", "editorOverviewRuler.findMatchForeground": "#4466CC99", "editorOverviewRuler.modifiedForeground": "#007acc", "editorOverviewRuler.addedForeground": "#587c0c", @@ -159,13 +159,13 @@ "editorOverviewRuler.errorForeground": "#ad0707", "editorOverviewRuler.warningForeground": "#667309", "panel.background": "#F9F9F9", - "panel.border": "#ECEDEEFF", + "panel.border": "#EEEEEE00", "panelTitle.activeBorder": "#4466CC", "panelTitle.activeForeground": "#202020", "panelTitle.inactiveForeground": "#666666", "statusBar.background": "#F9F9F9", "statusBar.foreground": "#202020", - "statusBar.border": "#ECEDEEFF", + "statusBar.border": "#EEEEEE00", "statusBar.focusBorder": "#4466CCFF", "statusBar.debuggingBackground": "#4466CC", "statusBar.debuggingForeground": "#FFFFFF", @@ -181,34 +181,34 @@ "tab.activeForeground": "#202020", "tab.inactiveBackground": "#F9F9F9", "tab.inactiveForeground": "#666666", - "tab.border": "#ECEDEEFF", - "tab.lastPinnedBorder": "#ECEDEEFF", + "tab.border": "#EEEEEE00", + "tab.lastPinnedBorder": "#EEEEEE00", "tab.activeBorder": "#FBFBFD", "tab.hoverBackground": "#FFFFFF", "tab.hoverForeground": "#202020", "tab.unfocusedActiveBackground": "#FDFDFD", "tab.unfocusedActiveForeground": "#666666", "tab.unfocusedInactiveBackground": "#F9F9F9", - "tab.unfocusedInactiveForeground": "#999999", + "tab.unfocusedInactiveForeground": "#BBBBBB", "editorGroupHeader.tabsBackground": "#F9F9F9", - "editorGroupHeader.tabsBorder": "#ECEDEEFF", + "editorGroupHeader.tabsBorder": "#EEEEEE00", "breadcrumb.foreground": "#666666", "breadcrumb.background": "#FDFDFD", "breadcrumb.focusForeground": "#202020", "breadcrumb.activeSelectionForeground": "#202020", "breadcrumbPicker.background": "#FCFCFC", - "notificationCenter.border": "#ECEDEEFF", + "notificationCenter.border": "#EEEEEE00", "notificationCenterHeader.foreground": "#202020", "notificationCenterHeader.background": "#F3F3F3", - "notificationToast.border": "#ECEDEEFF", + "notificationToast.border": "#EEEEEE00", "notifications.foreground": "#202020", "notifications.background": "#FCFCFC", - "notifications.border": "#ECEDEEFF", + "notifications.border": "#EEEEEE00", "notificationLink.foreground": "#4466CC", "extensionButton.prominentBackground": "#4466CC", "extensionButton.prominentForeground": "#FFFFFF", "extensionButton.prominentHoverBackground": "#4F6FCF", - "pickerGroup.border": "#ECEDEEFF", + "pickerGroup.border": "#EEEEEE00", "pickerGroup.foreground": "#202020", "quickInput.background": "#FCFCFC", "quickInput.foreground": "#202020", @@ -228,7 +228,7 @@ "gitDecoration.stageModifiedResourceForeground": "#667309", "gitDecoration.stageDeletedResourceForeground": "#ad0707", "quickInputTitle.background": "#FCFCFC", - "quickInput.border": "#D0D1D2", + "quickInput.border": "#D6D7D8", "chat.requestBubbleBackground": "#4466CC1A", "chat.requestBubbleHoverBackground": "#4466CC26" }, diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css new file mode 100644 index 00000000000..9622b828233 --- /dev/null +++ b/extensions/theme-2026/themes/styles.css @@ -0,0 +1,223 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* Stealth Shadows - shadow-based depth for UI elements, controlled by workbench.stealthShadows.enabled */ + +/* styles.css */ +.monaco-workbench { + --my-custom-color: blue; +} + +/* Activity Bar */ +.monaco-workbench .part.activitybar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); z-index: 50; position: relative; } +.monaco-workbench.activitybar-right .part.activitybar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); } + +/* Sidebar */ +.monaco-workbench .part.sidebar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); z-index: 40; position: relative; } +.monaco-workbench.sidebar-right .part.sidebar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); } +.monaco-workbench .part.auxiliarybar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); z-index: 40; position: relative; } + +/* Panel */ +.monaco-workbench .part.panel { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); z-index: 35; position: relative; } +.monaco-workbench.panel-position-left .part.panel { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); } +.monaco-workbench.panel-position-right .part.panel { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); } + +/* Editor */ +.monaco-workbench .part.editor { position: relative; } +.monaco-workbench .part.editor > .content .editor-group-container > .title { box-shadow: none; position: relative; z-index: 10; } +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { box-shadow: 0 0 5px rgba(0, 0, 0, 0.10); position: relative; z-index: 5; border-radius: 4px 4px 0 0; border-top: none !important; } +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover:not(.active) { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); } + +/* Title Bar */ +.monaco-workbench .part.titlebar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); z-index: 60; position: relative; overflow: visible !important; } +.monaco-workbench .part.titlebar .titlebar-container, +.monaco-workbench .part.titlebar .titlebar-center, +.monaco-workbench .part.titlebar .titlebar-center .window-title, +.monaco-workbench .part.titlebar .command-center, +.monaco-workbench .part.titlebar .command-center .monaco-action-bar, +.monaco-workbench .part.titlebar .command-center .actions-container { overflow: visible !important; } + +/* Status Bar */ +.monaco-workbench .part.statusbar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); z-index: 55; position: relative; } + +/* Quick Input (Command Palette) */ +.monaco-workbench .quick-input-widget { box-shadow: 0 0 20px rgba(0, 0, 0, 0.15) !important; border: none !important; border-radius: 12px; overflow: hidden; background: rgba(252, 252, 253, 0.2) !important; backdrop-filter: blur(20px) saturate(100%); -webkit-backdrop-filter: blur(40px) saturate(180%); } +.monaco-workbench.vs-dark .quick-input-widget, +.monaco-workbench.hc-black .quick-input-widget { background: rgba(10, 10, 11, 0.85) !important; backdrop-filter: blur(40px) saturate(180%); -webkit-backdrop-filter: blur(40px) saturate(180%); box-shadow: 0 0 20px rgba(0, 0, 0, 0.15) !important; } +.monaco-workbench .quick-input-widget .monaco-list-rows { background-color: transparent !important; } +.monaco-workbench .quick-input-widget, +.monaco-workbench .quick-input-widget *, +.monaco-workbench .quick-input-widget .quick-input-header, +.monaco-workbench .quick-input-widget .quick-input-list, +.monaco-workbench .quick-input-widget .quick-input-titlebar, +.monaco-workbench .quick-input-widget .quick-input-title, +.monaco-workbench .quick-input-widget .quick-input-description, +.monaco-workbench .quick-input-widget .quick-input-filter, +.monaco-workbench .quick-input-widget .quick-input-action, +.monaco-workbench .quick-input-widget .quick-input-message, +.monaco-workbench .quick-input-widget .monaco-inputbox, +.monaco-workbench .quick-input-widget .monaco-list, +.monaco-workbench .quick-input-widget .monaco-list-row { border: none !important; border-color: transparent !important; outline: none !important; } +.monaco-workbench .quick-input-widget .monaco-inputbox { box-shadow: none !important; background: transparent !important; } +.monaco-workbench .quick-input-widget .quick-input-filter .monaco-inputbox { background: rgba(249, 250, 251, 0.4) !important; border-radius: 6px; } +.monaco-workbench.vs-dark .quick-input-widget .quick-input-filter .monaco-inputbox, +.monaco-workbench.hc-black .quick-input-widget .quick-input-filter .monaco-inputbox { background: rgba(20, 20, 22, 0.6) !important; } + +/* Chat Widget */ +.monaco-workbench .interactive-session .chat-input-container { box-shadow: 0 0 6px rgba(0, 0, 0, 0.1); border-radius: 6px; } +.monaco-workbench .interactive-session .interactive-input-part .chat-editor-container .interactive-input-editor .monaco-editor, +.monaco-workbench .interactive-session .interactive-input-part .chat-editor-container .interactive-input-editor .monaco-editor .monaco-editor-background { background-color: var(--vscode-panel-background, var(--vscode-sideBar-background)) !important; } +.monaco-workbench .interactive-session .chat-editing-session .chat-editing-session-container { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10); border-radius: 4px 4px 0 0; } +.monaco-workbench .interactive-input-part:has(.chat-editing-session > .chat-editing-session-container) .chat-input-container { border-radius: 0 0 6px 6px; } +.monaco-workbench .part.panel .interactive-session, +.monaco-workbench .part.auxiliarybar .interactive-session { position: relative; } + +/* Notifications */ +.monaco-workbench .notifications-toasts { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); } +.monaco-workbench .notification-toast { box-shadow: 0 0 8px rgba(0, 0, 0, 0.12); border: none; border-radius: 8px; overflow: hidden; } + +/* Context Menus */ +.monaco-workbench .monaco-menu .monaco-action-bar.vertical { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 12px; overflow: hidden; backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); } +.monaco-workbench .context-view .monaco-menu { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 12px; } +.monaco-workbench.vs-dark .monaco-menu .monaco-action-bar.vertical, +.monaco-workbench.vs-dark .context-view .monaco-menu, +.monaco-workbench.hc-black .monaco-menu .monaco-action-bar.vertical, +.monaco-workbench.hc-black .context-view .monaco-menu { background: rgba(10, 10, 11, 0.85) !important; } +.monaco-workbench.vs .monaco-menu .monaco-action-bar.vertical, +.monaco-workbench.vs .context-view .monaco-menu { background: rgba(252, 252, 253, 0.85) !important; } + +/* Suggest Widget */ +.monaco-workbench .monaco-editor .suggest-widget { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 12px; overflow: hidden; backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); } +.monaco-workbench.vs-dark .monaco-editor .suggest-widget, +.monaco-workbench.hc-black .monaco-editor .suggest-widget { background: rgba(10, 10, 11, 0.85) !important; } +.monaco-workbench.vs .monaco-editor .suggest-widget { background: rgba(252, 252, 253, 0.85) !important; } + +/* Find Widget */ +.monaco-workbench .monaco-editor .find-widget { box-shadow: 0 0 8px rgba(0, 0, 0, 0.12); border: none; border-radius: 8px; } + +/* Dialog */ +.monaco-workbench .monaco-dialog-box { box-shadow: 0 0 20px rgba(0, 0, 0, 0.18); border: none; border-radius: 12px; overflow: hidden; } + +/* Peek View */ +.monaco-workbench .monaco-editor .peekview-widget { box-shadow: 0 0 8px rgba(0, 0, 0, 0.12); border: none; background: var(--vscode-editor-background, #EDEDED) !important; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-radius: 8px; overflow: hidden; } +.monaco-workbench .monaco-editor .peekview-widget .head, +.monaco-workbench .monaco-editor .peekview-widget .body { background: transparent !important; } +.monaco-workbench .monaco-editor .peekview-widget .ref-tree { background: var(--vscode-editor-background, #EDEDED) !important; } + +/* Settings */ +.monaco-workbench .settings-editor .settings-toc-container { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); } + +/* Welcome Tiles */ +.monaco-workbench .part.editor .welcomePageContainer .tile { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10); border: none; border-radius: 8px; } +.monaco-workbench .part.editor .welcomePageContainer .tile:hover { box-shadow: 0 0 8px rgba(0, 0, 0, 0.12); } + +/* Extensions */ +.monaco-workbench .extensions-list .extension-list-item { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); border: none; border-radius: 6px; margin: 4px 0; } +.monaco-workbench .extensions-list .extension-list-item:hover { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10); } + +/* Breadcrumbs */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .breadcrumbs-control { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08) !important; background: rgba(252, 252, 253, 0.65) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; } +.monaco-workbench.vs-dark .part.editor > .content .editor-group-container > .title .breadcrumbs-control, +.monaco-workbench.hc-black .part.editor > .content .editor-group-container > .title .breadcrumbs-control { background: rgba(10, 10, 11, 0.85) !important; box-shadow: 0 0 6px rgba(0, 0, 0, 0.08) !important; } + +/* Input Boxes */ +.monaco-workbench .monaco-inputbox { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); border: none; border-radius: 6px; } +.monaco-workbench .monaco-inputbox.synthetic-focus, +.monaco-workbench .monaco-inputbox:focus-within { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10), 0 0 0 2px var(--vscode-focusBorder); } + +/* Buttons */ +.monaco-workbench .monaco-button { box-shadow: 0 0 2px rgba(0, 0, 0, 0.06); } +.monaco-workbench .monaco-button:hover { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); } +.monaco-workbench .monaco-button:active { box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); } + +/* Dropdowns */ +.monaco-workbench .monaco-dropdown .dropdown-menu { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 8px; } + +/* Terminal */ +.monaco-workbench .pane-body.integrated-terminal { box-shadow: inset 0 0 4px rgba(255, 255, 255, 0.1); } + +/* SCM */ +.monaco-workbench .scm-view .scm-provider { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); border-radius: 6px; margin: 4px; } + +/* Debug Toolbar */ +.monaco-workbench .debug-toolbar { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 8px; } + +/* Action Widget */ +.monaco-workbench .action-widget { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14) !important; border: none !important; border-radius: 8px; } + +/* Parameter Hints */ +.monaco-workbench .monaco-editor .parameter-hints-widget { box-shadow: 0 0 8px rgba(0, 0, 0, 0.12); border: none; border-radius: 12px; overflow: hidden; backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); } +.monaco-workbench.vs-dark .monaco-editor .parameter-hints-widget, +.monaco-workbench.hc-black .monaco-editor .parameter-hints-widget { background: rgba(10, 10, 11, 0.85) !important; } +.monaco-workbench.vs .monaco-editor .parameter-hints-widget { background: rgba(252, 252, 253, 0.85) !important; } + +/* Minimap */ +.monaco-workbench .monaco-editor .minimap { background: rgba(252, 252, 253, 0.65) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; border-radius: 8px 0 0 8px; } +.monaco-workbench .monaco-editor .minimap canvas { opacity: 0.85; } +.monaco-workbench.vs-dark .monaco-editor .minimap, +.monaco-workbench.hc-black .monaco-editor .minimap { background: rgba(5, 5, 6, 0.85) !important; box-shadow: 0 0 6px rgba(0, 0, 0, 0.10) !important; } +.monaco-workbench .monaco-editor .minimap-shadow-visible { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10); } + +/* Sticky Scroll */ +.monaco-workbench .monaco-editor .sticky-widget { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10) !important; border-bottom: none !important; border: none !important; background: rgba(252, 252, 253, 0.65) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; border-radius: 0 0 8px 8px !important; } +.monaco-workbench .monaco-editor .sticky-widget *, +.monaco-workbench .monaco-editor .sticky-widget > *, +.monaco-workbench .monaco-editor .sticky-widget .sticky-widget-line-numbers, +.monaco-workbench .monaco-editor .sticky-widget .sticky-widget-lines-scrollable, +.monaco-workbench .monaco-editor .sticky-widget .sticky-widget-lines, +.monaco-workbench .monaco-editor .sticky-widget .sticky-line-number, +.monaco-workbench .monaco-editor .sticky-widget .sticky-line-content, +.monaco-workbench .monaco-editor .sticky-widget .sticky-line-content:hover { background-color: transparent !important; background: transparent !important; } +.monaco-workbench.vs-dark .monaco-editor .sticky-widget, +.monaco-workbench.hc-black .monaco-editor .sticky-widget { background: rgba(10, 10, 11, 0.85) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; box-shadow: 0 0 6px rgba(0, 0, 0, 0.10) !important; } +.monaco-workbench .monaco-editor .sticky-widget-focus-preview, +.monaco-workbench .monaco-editor .sticky-scroll-focus-line, +.monaco-workbench .monaco-editor .focused .sticky-widget, +.monaco-workbench .monaco-editor:has(.sticky-widget:focus-within) .sticky-widget { background: rgba(252, 252, 253, 0.75) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; box-shadow: 0 0 8px rgba(0, 0, 0, 0.12) !important; border-radius: 0 0 8px 8px !important; } +.monaco-workbench.vs-dark .monaco-editor .sticky-widget-focus-preview, +.monaco-workbench.vs-dark .monaco-editor .sticky-scroll-focus-line, +.monaco-workbench.vs-dark .monaco-editor .focused .sticky-widget, +.monaco-workbench.vs-dark .monaco-editor:has(.sticky-widget:focus-within) .sticky-widget, +.monaco-workbench.hc-black .monaco-editor .sticky-widget-focus-preview, +.monaco-workbench.hc-black .monaco-editor .sticky-scroll-focus-line, +.monaco-workbench.hc-black .monaco-editor .focused .sticky-widget, +.monaco-workbench.hc-black .monaco-editor:has(.sticky-widget:focus-within) .sticky-widget { background: rgba(10, 10, 11, 0.90) !important; box-shadow: 0 0 8px rgba(0, 0, 0, 0.12) !important; } + +/* Notebook */ +.monaco-workbench .notebookOverlay .monaco-list .cell-focus-indicator { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); border-radius: 6px; } +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10); } + +/* Inline Chat */ +.monaco-workbench .monaco-editor .inline-chat { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 12px; } + +/* Command Center */ +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center { box-shadow: 0 0 8px rgba(0, 0, 0, 0.09) !important; border-radius: 8px !important; background: rgba(249, 250, 251, 0.55) !important; backdrop-filter: blur(24px) saturate(150%); -webkit-backdrop-filter: blur(24px) saturate(150%); overflow: visible !important; } +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center:hover { box-shadow: 0 0 10px rgba(0, 0, 0, 0.11) !important; background: rgba(249, 250, 251, 0.70) !important; } +.monaco-workbench.vs-dark .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center, +.monaco-workbench.hc-black .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center { background: rgba(10, 10, 11, 0.75) !important; backdrop-filter: blur(24px) saturate(150%); -webkit-backdrop-filter: blur(24px) saturate(150%); } +.monaco-workbench.vs-dark .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center:hover, +.monaco-workbench.hc-black .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center:hover { background: rgba(15, 15, 17, 0.85) !important; } +.monaco-workbench .part.titlebar .command-center .agent-status-pill { + box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.08); +} +.monaco-workbench .part.titlebar .command-center .agent-status-pill:hover { + box-shadow: none; + background-color: transparent; +} +/* .monaco-workbench .part.titlebar .command-center, +.monaco-workbench .part.titlebar .command-center *, +.monaco-workbench .part.titlebar .command-center-center, +.monaco-workbench .part.titlebar .command-center .action-item, +.monaco-workbench .part.titlebar .command-center .search-icon, +.monaco-workbench .part.titlebar .command-center .search-label { border: none !important; border-color: transparent !important; outline: none !important; } */ + +/* Remove Borders */ +.monaco-workbench .part.sidebar { border-right: none !important; border-left: none !important; } +.monaco-workbench .part.auxiliarybar { border-right: none !important; border-left: none !important; } +.monaco-workbench .part.panel { border-top: none !important; } +.monaco-workbench .part.activitybar { border-right: none !important; border-left: none !important; } +.monaco-workbench .part.titlebar { border-bottom: none !important; } +.monaco-workbench .part.statusbar { border-top: none !important; } +.monaco-workbench .part.editor > .content .editor-group-container { border: none !important; } From bd8cacd2a93008cfaed07e5e36e262d8f0a25f33 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 16 Jan 2026 13:42:51 +0000 Subject: [PATCH 068/387] Update scrollbar slider colors and enhance quick input widget background --- extensions/theme-2026/themes/2026-light.json | 6 +++--- extensions/theme-2026/themes/styles.css | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index ba908cc9979..f971e6511b6 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -47,9 +47,9 @@ "inputValidation.warningBorder": "#EEEEEE00", "inputValidation.warningForeground": "#202020", "scrollbar.shadow": "#F5F6F84D", - "scrollbarSlider.background": "#4466CC33", - "scrollbarSlider.hoverBackground": "#4466CC66", - "scrollbarSlider.activeBackground": "#4466CC99", + "scrollbarSlider.background": "#20202033", + "scrollbarSlider.hoverBackground": "#20202066", + "scrollbarSlider.activeBackground": "#20202099", "badge.background": "#4466CC", "badge.foreground": "#FFFFFF", "progressBar.background": "#666666", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 9622b828233..98afbb818c5 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -43,12 +43,12 @@ .monaco-workbench .part.statusbar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); z-index: 55; position: relative; } /* Quick Input (Command Palette) */ -.monaco-workbench .quick-input-widget { box-shadow: 0 0 20px rgba(0, 0, 0, 0.15) !important; border: none !important; border-radius: 12px; overflow: hidden; background: rgba(252, 252, 253, 0.2) !important; backdrop-filter: blur(20px) saturate(100%); -webkit-backdrop-filter: blur(40px) saturate(180%); } +.monaco-workbench .quick-input-widget { box-shadow: 0 0 20px rgba(0, 0, 0, 0.15) !important; border: none !important; border-radius: 12px; overflow: hidden; background: rgba(252, 252, 253, 0.5) !important; backdrop-filter: blur(20px) saturate(100%); -webkit-backdrop-filter: blur(40px) saturate(180%); } .monaco-workbench.vs-dark .quick-input-widget, .monaco-workbench.hc-black .quick-input-widget { background: rgba(10, 10, 11, 0.85) !important; backdrop-filter: blur(40px) saturate(180%); -webkit-backdrop-filter: blur(40px) saturate(180%); box-shadow: 0 0 20px rgba(0, 0, 0, 0.15) !important; } .monaco-workbench .quick-input-widget .monaco-list-rows { background-color: transparent !important; } .monaco-workbench .quick-input-widget, -.monaco-workbench .quick-input-widget *, +/* .monaco-workbench .quick-input-widget *, */ .monaco-workbench .quick-input-widget .quick-input-header, .monaco-workbench .quick-input-widget .quick-input-list, .monaco-workbench .quick-input-widget .quick-input-titlebar, @@ -57,7 +57,7 @@ .monaco-workbench .quick-input-widget .quick-input-filter, .monaco-workbench .quick-input-widget .quick-input-action, .monaco-workbench .quick-input-widget .quick-input-message, -.monaco-workbench .quick-input-widget .monaco-inputbox, +/* .monaco-workbench .quick-input-widget .monaco-inputbox, */ .monaco-workbench .quick-input-widget .monaco-list, .monaco-workbench .quick-input-widget .monaco-list-row { border: none !important; border-color: transparent !important; outline: none !important; } .monaco-workbench .quick-input-widget .monaco-inputbox { box-shadow: none !important; background: transparent !important; } @@ -65,6 +65,9 @@ .monaco-workbench.vs-dark .quick-input-widget .quick-input-filter .monaco-inputbox, .monaco-workbench.hc-black .quick-input-widget .quick-input-filter .monaco-inputbox { background: rgba(20, 20, 22, 0.6) !important; } +.monaco-workbench .quick-input-widget .monaco-list.list_id_6:not(.drop-target):not(.dragging) .monaco-list-row:hover:not(.selected):not(.focused) { background-color: color-mix(in srgb, var(--vscode-list-hoverBackground) 40%, transparent); } + + /* Chat Widget */ .monaco-workbench .interactive-session .chat-input-container { box-shadow: 0 0 6px rgba(0, 0, 0, 0.1); border-radius: 6px; } .monaco-workbench .interactive-session .interactive-input-part .chat-editor-container .interactive-input-editor .monaco-editor, From ecb5023577b1cafdc98a2be53fa655463b5fb724 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Jan 2026 05:48:06 -0800 Subject: [PATCH 069/387] Format python commands specially Fixes #287772 --- .../chatTerminalToolConfirmationSubPart.ts | 10 +- .../chatTerminalToolProgressPart.ts | 7 +- .../chat/common/chatService/chatService.ts | 11 ++ .../browser/runInTerminalHelpers.ts | 38 ++++++ .../browser/tools/runInTerminalTool.ts | 14 ++- .../test/browser/runInTerminalHelpers.test.ts | 111 +++++++++++++++++- 6 files changed, 184 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts index bec0a9ba68e..45ef359a7ee 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts @@ -105,8 +105,12 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS const { title, message, disclaimer, terminalCustomActions } = state.confirmationMessages; // Use pre-computed confirmation data from runInTerminalTool (cd prefix extraction happens there for localization) - const initialContent = terminalData.confirmation?.commandLine ?? (terminalData.commandLine.toolEdited ?? terminalData.commandLine.original).trimStart(); + // Use presentationOverrides for display if available (e.g., extracted Python code) + const initialContent = terminalData.presentationOverrides?.commandLine ?? terminalData.confirmation?.commandLine ?? (terminalData.commandLine.toolEdited ?? terminalData.commandLine.original).trimStart(); const cdPrefix = terminalData.confirmation?.cdPrefix ?? ''; + // When presentationOverrides is set, the editor should be read-only since the displayed content + // differs from the actual command (e.g., extracted Python code vs full python -c command) + const isReadOnly = !!terminalData.presentationOverrides; const autoApproveEnabled = this.configurationService.getValue(TerminalContribSettingId.EnableAutoApprove) === true; const autoApproveWarningAccepted = this.storageService.getBoolean(TerminalToolConfirmationStorageKeys.TerminalAutoApproveWarningAccepted, StorageScope.APPLICATION, false); @@ -143,12 +147,12 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS verticalPadding: 5, editorOptions: { wordWrap: 'on', - readOnly: false, + readOnly: isReadOnly, tabFocusMode: true, ariaLabel: typeof title === 'string' ? title : title.value } }; - const languageId = this.languageService.getLanguageIdByLanguageName(terminalData.language ?? 'sh') ?? 'shellscript'; + const languageId = this.languageService.getLanguageIdByLanguageName(terminalData.presentationOverrides?.language ?? terminalData.language ?? 'sh') ?? 'shellscript'; const model = this._register(this.modelService.createModel( initialContent, this.languageService.createById(languageId), diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts index 752a69d67fe..6ee9001d371 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts @@ -283,12 +283,15 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart getResolvedCommand: () => this._getResolvedCommand() })); + // Use presentationOverrides for display if available (e.g., extracted Python code with syntax highlighting) + const displayCommand = terminalData.presentationOverrides?.commandLine ?? command; + const displayLanguage = terminalData.presentationOverrides?.language ?? terminalData.language; const titlePart = this._register(_instantiationService.createInstance( ChatQueryTitlePart, elements.commandBlock, new MarkdownString([ - `\`\`\`${terminalData.language}`, - `${command.replaceAll('```', '\\`\\`\\`')}`, + `\`\`\`${displayLanguage}`, + `${displayCommand.replaceAll('```', '\\`\\`\\`')}`, `\`\`\`` ].join('\n'), { supportThemeIcons: true }), undefined, diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 5e344ac04c8..f135d3353eb 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -387,6 +387,17 @@ export interface IChatTerminalToolInvocationData { /** The cd prefix to prepend back when user edits */ cdPrefix?: string; }; + /** + * Overrides to apply to the presentation of the tool call only, but not actually change the + * command that gets run. For example, python -c "print('hello')" can be presented as just + * the Python code with Python syntax highlighting. + */ + presentationOverrides?: { + /** The command line to display in the UI */ + commandLine: string; + /** The language for syntax highlighting */ + language: string; + }; /** Message for model recommending the use of an alternative tool */ alternativeRecommendation?: string; language: string; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts index 7cbf89ca9f3..df832b0d037 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts @@ -47,6 +47,44 @@ export function isFish(envShell: string, os: OperatingSystem): boolean { return /^fish$/.test(pathPosix.basename(envShell)); } +/** + * Extracts the Python code from a `python -c "..."` or `python -c '...'` command, + * returning the code with properly unescaped quotes. + * + * @param commandLine The full command line to parse + * @param shell The shell path (to determine quote escaping style) + * @param os The operating system + * @returns The extracted Python code, or undefined if not a python -c command + */ +export function extractPythonCommand(commandLine: string, shell: string, os: OperatingSystem): string | undefined { + // Match python/python3 -c "..." pattern (double quotes) + const doubleQuoteMatch = commandLine.match(/^python(?:3)?\s+-c\s+"(?.+)"$/s); + if (doubleQuoteMatch?.groups?.python) { + let pythonCode = doubleQuoteMatch.groups.python.trim(); + + // Unescape quotes based on shell type + if (isPowerShell(shell, os)) { + // PowerShell uses backtick-quote (`") to escape quotes inside double-quoted strings + pythonCode = pythonCode.replace(/`"/g, '"'); + } else { + // Bash/sh/zsh use backslash-quote (\") + pythonCode = pythonCode.replace(/\\"/g, '"'); + } + + return pythonCode; + } + + // Match python/python3 -c '...' pattern (single quotes) + // Single quotes in bash/sh/zsh are literal - no escaping inside + // Single quotes in PowerShell are also literal + const singleQuoteMatch = commandLine.match(/^python(?:3)?\s+-c\s+'(?.+)'$/s); + if (singleQuoteMatch?.groups?.python) { + return singleQuoteMatch.groups.python.trim(); + } + + return undefined; +} + // Maximum output length to prevent context overflow const MAX_OUTPUT_LENGTH = 60000; // ~60KB limit to keep context manageable export const TRUNCATION_MESSAGE = '\n\n[... PREVIOUS OUTPUT TRUNCATED ...]\n\n'; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index f251ee467d2..f533c603428 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -37,7 +37,7 @@ import type { ITerminalExecuteStrategy } from '../executeStrategy/executeStrateg import { NoneExecuteStrategy } from '../executeStrategy/noneExecuteStrategy.js'; import { RichExecuteStrategy } from '../executeStrategy/richExecuteStrategy.js'; import { getOutput } from '../outputHelpers.js'; -import { extractCdPrefix, isFish, isPowerShell, isWindowsPowerShell, isZsh } from '../runInTerminalHelpers.js'; +import { extractCdPrefix, extractPythonCommand, isFish, isPowerShell, isWindowsPowerShell, isZsh } from '../runInTerminalHelpers.js'; import { RunInTerminalToolTelemetry } from '../runInTerminalToolTelemetry.js'; import { ShellIntegrationQuality, ToolTerminalCreator, type IToolTerminal } from '../toolTerminalCreator.js'; import { TreeSitterCommandParser, TreeSitterCommandParserLanguage } from '../treeSitterCommandParser.js'; @@ -534,6 +534,18 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { : localize('runInTerminal', "Run `{0}` command?", shellType); } + // Check for Python -c command and extract the Python code for presentation + const extractedPython = extractPythonCommand(commandToDisplay, shell, os); + if (extractedPython) { + toolSpecificData.presentationOverrides = { + commandLine: extractedPython, + language: 'python', + }; + confirmationTitle = args.isBackground + ? localize('runInTerminal.python.background', "Run `Python` command in `{0}`? (background terminal)", shellType) + : localize('runInTerminal.python', "Run `Python` command in `{0}`?", shellType); + } + const confirmationMessages = isFinalAutoApproved ? undefined : { title: confirmationTitle, message: new MarkdownString(args.explanation), diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts index f8768465290..93546877dcd 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ok, strictEqual } from 'assert'; -import { generateAutoApproveActions, TRUNCATION_MESSAGE, dedupeRules, isPowerShell, sanitizeTerminalOutput, truncateOutputKeepingTail, extractCdPrefix } from '../../browser/runInTerminalHelpers.js'; +import { generateAutoApproveActions, TRUNCATION_MESSAGE, dedupeRules, isPowerShell, sanitizeTerminalOutput, truncateOutputKeepingTail, extractCdPrefix, extractPythonCommand } from '../../browser/runInTerminalHelpers.js'; import { OperatingSystem } from '../../../../../../base/common/platform.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { ConfigurationTarget } from '../../../../../../platform/configuration/common/configuration.js'; @@ -503,3 +503,112 @@ suite('extractCdPrefix', () => { }); }); }); + +suite('extractPythonCommand', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('basic extraction', () => { + test('should extract simple python -c command with double quotes', () => { + const result = extractPythonCommand('python -c "print(\'hello\')"', 'bash', OperatingSystem.Linux); + strictEqual(result, `print('hello')`); + }); + + test('should extract python3 -c command', () => { + const result = extractPythonCommand('python3 -c "print(\'hello\')"', 'bash', OperatingSystem.Linux); + strictEqual(result, `print('hello')`); + }); + + test('should return undefined for non-python commands', () => { + const result = extractPythonCommand('echo hello', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + + test('should return undefined for python without -c flag', () => { + const result = extractPythonCommand('python script.py', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + + test('should extract python -c with single quotes', () => { + const result = extractPythonCommand(`python -c 'print("hello")'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'print("hello")'); + }); + + test('should extract python3 -c with single quotes', () => { + const result = extractPythonCommand(`python3 -c 'x = 1; print(x)'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'x = 1; print(x)'); + }); + }); + + suite('quote unescaping - Bash', () => { + test('should unescape backslash-escaped quotes in bash', () => { + const result = extractPythonCommand('python -c "print(\\"hello\\")"', 'bash', OperatingSystem.Linux); + strictEqual(result, 'print("hello")'); + }); + + test('should handle multiple escaped quotes', () => { + const result = extractPythonCommand('python -c "x = \\\"hello\\\"; print(x)"', 'bash', OperatingSystem.Linux); + strictEqual(result, 'x = "hello"; print(x)'); + }); + }); + + suite('single quotes - literal content', () => { + test('should preserve content literally in single quotes (no unescaping)', () => { + // Single quotes in bash are literal - backslashes are not escape sequences + const result = extractPythonCommand(`python -c 'print(\\"hello\\")'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'print(\\"hello\\")'); + }); + + test('should handle single quotes in PowerShell', () => { + const result = extractPythonCommand(`python -c 'print("hello")'`, 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'print("hello")'); + }); + + test('should extract multiline code in single quotes', () => { + const code = `python -c 'for i in range(3):\n print(i)'`; + const result = extractPythonCommand(code, 'bash', OperatingSystem.Linux); + strictEqual(result, `for i in range(3):\n print(i)`); + }); + }); + + suite('quote unescaping - PowerShell', () => { + test('should unescape backtick-escaped quotes in PowerShell', () => { + const result = extractPythonCommand('python -c "print(`"hello`")"', 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'print("hello")'); + }); + + test('should handle multiple backtick-escaped quotes', () => { + const result = extractPythonCommand('python -c "x = `"hello`"; print(x)"', 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'x = "hello"; print(x)'); + }); + + test('should not unescape backslash quotes in PowerShell', () => { + const result = extractPythonCommand('python -c "print(\\"hello\\")"', 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'print(\\"hello\\")'); + }); + }); + + suite('multiline code', () => { + test('should extract multiline python code', () => { + const code = `python -c "for i in range(3):\n print(i)"`; + const result = extractPythonCommand(code, 'bash', OperatingSystem.Linux); + strictEqual(result, `for i in range(3):\n print(i)`); + }); + }); + + suite('edge cases', () => { + test('should handle code with trailing whitespace trimmed', () => { + const result = extractPythonCommand('python -c " print(1) "', 'bash', OperatingSystem.Linux); + strictEqual(result, 'print(1)'); + }); + + test('should return undefined for empty code', () => { + const result = extractPythonCommand('python -c ""', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + + test('should return undefined when quotes are unmatched', () => { + const result = extractPythonCommand('python -c "print(1)', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + }); +}); From 0ddbb7e8f5ea9c361265338b37a8d5253c63b136 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:48:42 +0100 Subject: [PATCH 070/387] Chat - simplify the working set rendering by removing the worktree changes divider (#288338) --- .../chatReferencesContentPart.ts | 73 +------------------ .../browser/widget/input/chatInputPart.ts | 34 ++------- .../chat/browser/widget/media/chat.css | 35 --------- 3 files changed, 9 insertions(+), 133 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts index 0885f4e78fd..8e0123937db 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts @@ -57,15 +57,7 @@ export interface IChatReferenceListItem extends IChatContentReference { excluded?: boolean; } -export interface IChatListDividerItem { - kind: 'divider'; - label: string; - menuId?: MenuId; - menuArg?: unknown; - scopedInstantiationService?: IInstantiationService; -} - -export type IChatCollapsibleListItem = IChatReferenceListItem | IChatWarningMessage | IChatListDividerItem; +export type IChatCollapsibleListItem = IChatReferenceListItem | IChatWarningMessage; export class ChatCollapsibleListContentPart extends ChatCollapsibleContentPart { @@ -218,7 +210,7 @@ export class CollapsibleListPool extends Disposable { 'ChatListRenderer', container, new CollapsibleListDelegate(), - [this.instantiationService.createInstance(CollapsibleListRenderer, resourceLabels, this.menuId), this.instantiationService.createInstance(DividerRenderer)], + [this.instantiationService.createInstance(CollapsibleListRenderer, resourceLabels, this.menuId)], { ...this.listOptions, alwaysConsumeMouseWheel: false, @@ -227,9 +219,6 @@ export class CollapsibleListPool extends Disposable { if (element.kind === 'warning') { return element.content.value; } - if (element.kind === 'divider') { - return element.label; - } const reference = element.reference; if (typeof reference === 'string') { return reference; @@ -294,9 +283,6 @@ class CollapsibleListDelegate implements IListVirtualDelegate { - static TEMPLATE_ID = 'chatListDividerRenderer'; - readonly templateId: string = DividerRenderer.TEMPLATE_ID; - - constructor( - @IInstantiationService private readonly instantiationService: IInstantiationService, - ) { } - - renderTemplate(container: HTMLElement): IDividerTemplate { - const templateDisposables = new DisposableStore(); - const elementDisposables = templateDisposables.add(new DisposableStore()); - container.classList.add('chat-list-divider'); - const label = dom.append(container, dom.$('span.chat-list-divider-label')); - const line = dom.append(container, dom.$('div.chat-list-divider-line')); - const toolbarContainer = dom.append(container, dom.$('.chat-list-divider-toolbar')); - - return { container, label, line, toolbarContainer, templateDisposables, elementDisposables, toolbar: undefined }; - } - - renderElement(data: IChatListDividerItem, index: number, templateData: IDividerTemplate): void { - templateData.label.textContent = data.label; - - // Clear element-specific disposables from previous render - templateData.elementDisposables.clear(); - templateData.toolbar = undefined; - dom.clearNode(templateData.toolbarContainer); - - if (data.menuId) { - const instantiationService = data.scopedInstantiationService || this.instantiationService; - templateData.toolbar = templateData.elementDisposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, templateData.toolbarContainer, data.menuId, { menuOptions: { arg: data.menuArg } })); - } - } - - disposeTemplate(templateData: IDividerTemplate): void { - templateData.templateDisposables.dispose(); - } -} - function getResourceLabelForGithubUri(uri: URI): IResourceLabelProps { const repoPath = uri.path.split('/').slice(1, 3).join('/'); const filePath = uri.path.split('/').slice(5); @@ -558,7 +491,7 @@ function getLineRangeFromGithubUri(uri: URI): IRange | undefined { } function getResourceForElement(element: IChatCollapsibleListItem): URI | null { - if (element.kind === 'warning' || element.kind === 'divider') { + if (element.kind === 'warning') { return null; } const { reference } = element; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index b4c5502a759..107bb9d5d50 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -2374,19 +2374,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge })) ); - const shouldRender = derived(reader => { - const sessionFilesLength = sessionFiles.read(reader).length; - const editSessionEntriesLength = editSessionEntries.read(reader).length; - - const sessionResource = chatEditingSession?.chatSessionResource ?? this._widget?.viewModel?.model.sessionResource; - if (sessionResource && getChatSessionType(sessionResource) === localChatSessionType) { - return sessionFilesLength > 0 || editSessionEntriesLength > 0; - } - - // For background sessions, only render the - // working set when there are session files - return sessionFilesLength > 0; - }); + const shouldRender = derived(reader => + editSessionEntries.read(reader).length > 0 || sessionFiles.read(reader).length > 0); this._renderingChatEdits.value = autorun(reader => { if (this.options.renderWorkingSet && shouldRender.read(reader)) { @@ -2594,21 +2583,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const editEntries = editSessionEntries.read(reader); const sessionFileEntries = sessionEntries.read(reader) ?? []; - // Combine entries with an optional divider - const allEntries: IChatCollapsibleListItem[] = [...editEntries]; - if (sessionFileEntries.length > 0) { - if (editEntries.length > 0) { - // Add divider between edit session entries and session file entries - allEntries.push({ - kind: 'divider', - label: localize('chatEditingSession.allChanges', 'Worktree Changes'), - menuId: MenuId.ChatEditingSessionChangesToolbar, - menuArg: sessionResource, - scopedInstantiationService, - }); - } - allEntries.push(...sessionFileEntries); - } + // Combine edit session entries with session file changes. At the moment, we + // we can combine these two arrays since local chat sessions use edit session + // entries, while background chat sessions use session file changes. + const allEntries = editEntries.concat(sessionFileEntries); const maxItemsShown = 6; const itemsShown = Math.min(allEntries.length, maxItemsShown); diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 69e49066729..b28b64bd950 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -2108,41 +2108,6 @@ have to be updated for changes to the rules above, or to support more deeply nes display: none; } -.interactive-session .chat-list-divider { - display: flex; - align-items: center; - padding: 2px 3px; - font-size: 11px; - color: var(--vscode-descriptionForeground); - gap: 8px; - pointer-events: none; - user-select: none; -} - -.interactive-session .monaco-list .monaco-list-row:has(.chat-list-divider) { - background-color: transparent !important; - cursor: default; -} - -.interactive-session .chat-list-divider .chat-list-divider-label { - text-transform: uppercase; - letter-spacing: 0.04em; - flex-shrink: 0; -} - -.interactive-session .chat-list-divider .chat-list-divider-line { - flex: 1; - height: 1px; - background-color: var(--vscode-editorWidget-border, var(--vscode-contrastBorder)); - opacity: 0.5; -} - -.interactive-session .chat-list-divider .chat-list-divider-toolbar { - display: flex; - align-items: center; - pointer-events: auto; -} - .interactive-session .chat-summary-list .monaco-list .monaco-list-row { border-radius: 4px; } From e71ca3c0f318710c555dcbdc535eda2d391cf622 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 16 Jan 2026 13:50:43 +0000 Subject: [PATCH 071/387] Remove synthetic focus styles from input boxes for a cleaner appearance --- extensions/theme-2026/themes/styles.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 98afbb818c5..2271036ae8b 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -127,8 +127,6 @@ /* Input Boxes */ .monaco-workbench .monaco-inputbox { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); border: none; border-radius: 6px; } -.monaco-workbench .monaco-inputbox.synthetic-focus, -.monaco-workbench .monaco-inputbox:focus-within { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10), 0 0 0 2px var(--vscode-focusBorder); } /* Buttons */ .monaco-workbench .monaco-button { box-shadow: 0 0 2px rgba(0, 0, 0, 0.06); } From 442b115770c7c7187fe2bcd443fe03026961016e Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Fri, 16 Jan 2026 14:59:59 +0100 Subject: [PATCH 072/387] only have icons for built in modes --- .../widget/input/modePickerActionItem.ts | 35 ++++++++++++++----- .../contrib/chat/common/chatModes.ts | 6 ++-- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index 5ab6e8dc9a3..8824d54fc9f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -36,6 +36,9 @@ export interface IModePickerDelegate { readonly sessionResource: () => URI | undefined; } +// TODO: there should be an icon contributed for built-in modes +const builtinDefaultIcon = Codicon.tasklist; + export class ModePickerActionItem extends ChatInputPickerActionViewItem { constructor( action: MenuItemAction, @@ -49,7 +52,7 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { @IChatModeService chatModeService: IChatModeService, @IMenuService private readonly menuService: IMenuService, @ICommandService commandService: ICommandService, - @IProductService productService: IProductService + @IProductService private readonly _productService: IProductService ) { // Category definitions const builtInCategory = { label: localize('built-in', "Built-In"), order: 0 }; @@ -95,7 +98,7 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { return { ...makeAction(mode, currentMode), tooltip: mode.description.get() ?? chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, mode.kind)?.description ?? action.tooltip, - icon: mode.icon.get(), + icon: mode.icon.get() ?? (isModeConsideredBuiltIn(mode, this._productService) ? builtinDefaultIcon : undefined), category: agentModeDisabledViaPolicy ? policyDisabledCategory : customCategory }; }; @@ -108,8 +111,7 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { const otherBuiltinModes = modes.builtin.filter(mode => mode.id !== ChatMode.Agent.id); const customModes = groupBy( modes.custom, - mode => mode.source?.storage === PromptsStorage.extension && mode.source.extensionId.value === productService.defaultChatAgent?.chatExtensionId && mode.source.type === ExtensionAgentSourceType.contribution ? - 'builtin' : 'custom'); + mode => isModeConsideredBuiltIn(mode, this._productService) ? 'builtin' : 'custom'); const customBuiltinModeActions = customModes.builtin?.map(mode => { const action = makeActionFromCustomMode(mode, currentMode); @@ -156,13 +158,21 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { protected override renderLabel(element: HTMLElement): IDisposable | null { this.setAriaLabelAttributes(element); - const isDefault = this.delegate.currentMode.get().id === ChatMode.Agent.id; - const state = this.delegate.currentMode.get().label.get(); - const icon = this.delegate.currentMode.get().icon.get(); + const currentMode = this.delegate.currentMode.get(); + const isDefault = currentMode.id === ChatMode.Agent.id; + const state = currentMode.label.get(); + let icon = currentMode.icon.get(); + + // Every built-in mode should have an icon. // TODO: this should be provided by the mode itself + if (!icon && isModeConsideredBuiltIn(currentMode, this._productService)) { + icon = builtinDefaultIcon; + } const labelElements = []; - labelElements.push(...renderLabelWithIcons(`$(${icon.id})`)); - if (!isDefault) { + if (icon) { + labelElements.push(...renderLabelWithIcons(`$(${icon.id})`)); + } + if (!isDefault || !icon) { labelElements.push(dom.$('span.chat-input-picker-label', undefined, state)); } labelElements.push(...renderLabelWithIcons(`$(chevron-down)`)); @@ -171,3 +181,10 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { return null; } } + +function isModeConsideredBuiltIn(mode: IChatMode, productService: IProductService): boolean { + if (mode.isBuiltin) { + return true; + } + return mode.source?.storage === PromptsStorage.extension && mode.source.extensionId.value === productService.defaultChatAgent?.chatExtensionId && mode.source.type === ExtensionAgentSourceType.contribution; +} diff --git a/src/vs/workbench/contrib/chat/common/chatModes.ts b/src/vs/workbench/contrib/chat/common/chatModes.ts index 97cbfaa63c5..b0998c47482 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -250,7 +250,7 @@ export interface IChatMode { readonly id: string; readonly name: IObservable; readonly label: IObservable; - readonly icon: IObservable; + readonly icon: IObservable; readonly description: IObservable; readonly isBuiltin: boolean; readonly kind: ChatModeKind; @@ -320,8 +320,8 @@ export class CustomChatMode implements IChatMode { return this._descriptionObservable; } - get icon(): IObservable { - return constObservable(Codicon.tasklist); + get icon(): IObservable { + return constObservable(undefined); } public get isBuiltin(): boolean { From 5591cd2fa63b0d72c862f74c3ffe8c7129efac10 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 16 Jan 2026 15:09:30 +0100 Subject: [PATCH 073/387] Agent sessions: rendering bug when stacked sessions list is expanded and context is added (fix #288151) (#288359) --- .../widgetHosts/viewPane/chatViewPane.ts | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 0b75f36b5a4..699077ccb70 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -650,6 +650,13 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { sessionsControl.reveal(sessionResource); } })); + + // When showing sessions stacked, adjust the height of the sessions list to make room for chat input + this._register(chatWidget.onDidChangeContentHeight(() => { + if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked && this.lastDimensions) { + this.layoutBody(this.lastDimensions.height, this.lastDimensions.width); + } + })); } private setupContextMenu(parent: HTMLElement): void { @@ -795,7 +802,22 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { //#region Layout + private layoutingBody = false; + protected override layoutBody(height: number, width: number): void { + if (this.layoutingBody) { + return; // prevent re-entrancy + } + + this.layoutingBody = true; + try { + this.doLayoutBody(height, width); + } finally { + this.layoutingBody = false; + } + } + + private doLayoutBody(height: number, width: number): void { super.layoutBody(height, width); this.lastDimensions = { height, width }; @@ -906,7 +928,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { let availableSessionsHeight = height - this.sessionsTitleContainer.offsetHeight - this.sessionsLinkContainer.offsetHeight; if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { - availableSessionsHeight -= ChatViewPane.MIN_CHAT_WIDGET_HEIGHT; // always reserve some space for chat input + availableSessionsHeight -= Math.max(ChatViewPane.MIN_CHAT_WIDGET_HEIGHT, this._widget?.input?.contentHeight ?? 0); } // Show as sidebar From 1afb0c25360e8a914826e4f30a012530d86ca099 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 16 Jan 2026 14:13:03 +0000 Subject: [PATCH 074/387] Update input box styles to enhance color consistency across UI elements --- extensions/theme-2026/themes/styles.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 2271036ae8b..c950dd33f27 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -128,6 +128,12 @@ /* Input Boxes */ .monaco-workbench .monaco-inputbox { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); border: none; border-radius: 6px; } +.monaco-inputbox .monaco-action-bar .action-item .codicon, +.monaco-workbench .search-container .input-box, +.monaco-custom-toggle { + color: var(--vscode-icon-foreground) !important; +} + /* Buttons */ .monaco-workbench .monaco-button { box-shadow: 0 0 2px rgba(0, 0, 0, 0.06); } .monaco-workbench .monaco-button:hover { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); } From 4dd1ce06e42b98bc0a4f910ea14eb3b296c6ba5e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Jan 2026 06:18:28 -0800 Subject: [PATCH 075/387] Make presenter concept generic --- .../browser/runInTerminalHelpers.ts | 38 ---- .../commandLinePresenter.ts | 41 ++++ .../pythonCommandLinePresenter.ts | 64 +++++++ .../browser/tools/runInTerminalTool.ts | 31 +-- .../pythonCommandLinePresenter.test.ts | 176 ++++++++++++++++++ .../test/browser/runInTerminalHelpers.test.ts | 109 +---------- 6 files changed, 302 insertions(+), 157 deletions(-) create mode 100644 src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/commandLinePresenter.ts create mode 100644 src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/pythonCommandLinePresenter.ts create mode 100644 src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/pythonCommandLinePresenter.test.ts diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts index df832b0d037..7cbf89ca9f3 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts @@ -47,44 +47,6 @@ export function isFish(envShell: string, os: OperatingSystem): boolean { return /^fish$/.test(pathPosix.basename(envShell)); } -/** - * Extracts the Python code from a `python -c "..."` or `python -c '...'` command, - * returning the code with properly unescaped quotes. - * - * @param commandLine The full command line to parse - * @param shell The shell path (to determine quote escaping style) - * @param os The operating system - * @returns The extracted Python code, or undefined if not a python -c command - */ -export function extractPythonCommand(commandLine: string, shell: string, os: OperatingSystem): string | undefined { - // Match python/python3 -c "..." pattern (double quotes) - const doubleQuoteMatch = commandLine.match(/^python(?:3)?\s+-c\s+"(?.+)"$/s); - if (doubleQuoteMatch?.groups?.python) { - let pythonCode = doubleQuoteMatch.groups.python.trim(); - - // Unescape quotes based on shell type - if (isPowerShell(shell, os)) { - // PowerShell uses backtick-quote (`") to escape quotes inside double-quoted strings - pythonCode = pythonCode.replace(/`"/g, '"'); - } else { - // Bash/sh/zsh use backslash-quote (\") - pythonCode = pythonCode.replace(/\\"/g, '"'); - } - - return pythonCode; - } - - // Match python/python3 -c '...' pattern (single quotes) - // Single quotes in bash/sh/zsh are literal - no escaping inside - // Single quotes in PowerShell are also literal - const singleQuoteMatch = commandLine.match(/^python(?:3)?\s+-c\s+'(?.+)'$/s); - if (singleQuoteMatch?.groups?.python) { - return singleQuoteMatch.groups.python.trim(); - } - - return undefined; -} - // Maximum output length to prevent context overflow const MAX_OUTPUT_LENGTH = 60000; // ~60KB limit to keep context manageable export const TRUNCATION_MESSAGE = '\n\n[... PREVIOUS OUTPUT TRUNCATED ...]\n\n'; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/commandLinePresenter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/commandLinePresenter.ts new file mode 100644 index 00000000000..3386df44910 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/commandLinePresenter.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { OperatingSystem } from '../../../../../../../base/common/platform.js'; + +export interface ICommandLinePresenter { + /** + * Attempts to create a presentation for the given command line. + * Command line presenters allow displaying an extracted/transformed version + * of a command (e.g., Python code from `python -c "..."`) with appropriate + * syntax highlighting, while the actual command remains unchanged. + * + * @returns The presentation result if this presenter handles the command, undefined otherwise. + */ + present(options: ICommandLinePresenterOptions): ICommandLinePresenterResult | undefined; +} + +export interface ICommandLinePresenterOptions { + commandLine: string; + shell: string; + os: OperatingSystem; +} + +export interface ICommandLinePresenterResult { + /** + * The extracted/transformed command to display (e.g., the Python code). + */ + commandLine: string; + + /** + * The language ID for syntax highlighting (e.g., 'python'). + */ + language: string; + + /** + * A human-readable name for the language (e.g., 'Python') used in UI labels. + */ + languageDisplayName: string; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/pythonCommandLinePresenter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/pythonCommandLinePresenter.ts new file mode 100644 index 00000000000..14aa3d6d14f --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/pythonCommandLinePresenter.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { OperatingSystem } from '../../../../../../../base/common/platform.js'; +import { isPowerShell } from '../../runInTerminalHelpers.js'; +import type { ICommandLinePresenter, ICommandLinePresenterOptions, ICommandLinePresenterResult } from './commandLinePresenter.js'; + +/** + * Command line presenter for Python inline commands (`python -c "..."`). + * Extracts the Python code and sets up Python syntax highlighting. + */ +export class PythonCommandLinePresenter implements ICommandLinePresenter { + present(options: ICommandLinePresenterOptions): ICommandLinePresenterResult | undefined { + const extractedPython = extractPythonCommand(options.commandLine, options.shell, options.os); + if (extractedPython) { + return { + commandLine: extractedPython, + language: 'python', + languageDisplayName: 'Python', + }; + } + return undefined; + } +} + +/** + * Extracts the Python code from a `python -c "..."` or `python -c '...'` command, + * returning the code with properly unescaped quotes. + * + * @param commandLine The full command line to parse + * @param shell The shell path (to determine quote escaping style) + * @param os The operating system + * @returns The extracted Python code, or undefined if not a python -c command + */ +export function extractPythonCommand(commandLine: string, shell: string, os: OperatingSystem): string | undefined { + // Match python/python3 -c "..." pattern (double quotes) + const doubleQuoteMatch = commandLine.match(/^python(?:3)?\s+-c\s+"(?.+)"$/s); + if (doubleQuoteMatch?.groups?.python) { + let pythonCode = doubleQuoteMatch.groups.python.trim(); + + // Unescape quotes based on shell type + if (isPowerShell(shell, os)) { + // PowerShell uses backtick-quote (`") to escape quotes inside double-quoted strings + pythonCode = pythonCode.replace(/`"/g, '"'); + } else { + // Bash/sh/zsh use backslash-quote (\") + pythonCode = pythonCode.replace(/\\"/g, '"'); + } + + return pythonCode; + } + + // Match python/python3 -c '...' pattern (single quotes) + // Single quotes in bash/sh/zsh are literal - no escaping inside + // Single quotes in PowerShell are also literal + const singleQuoteMatch = commandLine.match(/^python(?:3)?\s+-c\s+'(?.+)'$/s); + if (singleQuoteMatch?.groups?.python) { + return singleQuoteMatch.groups.python.trim(); + } + + return undefined; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index f533c603428..b483f7eeaa8 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -37,7 +37,9 @@ import type { ITerminalExecuteStrategy } from '../executeStrategy/executeStrateg import { NoneExecuteStrategy } from '../executeStrategy/noneExecuteStrategy.js'; import { RichExecuteStrategy } from '../executeStrategy/richExecuteStrategy.js'; import { getOutput } from '../outputHelpers.js'; -import { extractCdPrefix, extractPythonCommand, isFish, isPowerShell, isWindowsPowerShell, isZsh } from '../runInTerminalHelpers.js'; +import { extractCdPrefix, isFish, isPowerShell, isWindowsPowerShell, isZsh } from '../runInTerminalHelpers.js'; +import type { ICommandLinePresenter } from './commandLinePresenter/commandLinePresenter.js'; +import { PythonCommandLinePresenter } from './commandLinePresenter/pythonCommandLinePresenter.js'; import { RunInTerminalToolTelemetry } from '../runInTerminalToolTelemetry.js'; import { ShellIntegrationQuality, ToolTerminalCreator, type IToolTerminal } from '../toolTerminalCreator.js'; import { TreeSitterCommandParser, TreeSitterCommandParserLanguage } from '../treeSitterCommandParser.js'; @@ -281,6 +283,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { private readonly _commandLineRewriters: ICommandLineRewriter[]; private readonly _commandLineAnalyzers: ICommandLineAnalyzer[]; + private readonly _commandLinePresenters: ICommandLinePresenter[]; protected readonly _sessionTerminalAssociations = new ResourceMap(); @@ -330,6 +333,9 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { this._register(this._instantiationService.createInstance(CommandLineFileWriteAnalyzer, this._treeSitterCommandParser, (message, args) => this._logService.info(`RunInTerminalTool#CommandLineFileWriteAnalyzer: ${message}`, args))), this._register(this._instantiationService.createInstance(CommandLineAutoApproveAnalyzer, this._treeSitterCommandParser, this._telemetry, (message, args) => this._logService.info(`RunInTerminalTool#CommandLineAutoApproveAnalyzer: ${message}`, args))), ]; + this._commandLinePresenters = [ + new PythonCommandLinePresenter(), + ]; // Clear out warning accepted state if the setting is disabled this._register(Event.runAndSubscribe(this._configurationService.onDidChangeConfiguration, e => { @@ -534,16 +540,19 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { : localize('runInTerminal', "Run `{0}` command?", shellType); } - // Check for Python -c command and extract the Python code for presentation - const extractedPython = extractPythonCommand(commandToDisplay, shell, os); - if (extractedPython) { - toolSpecificData.presentationOverrides = { - commandLine: extractedPython, - language: 'python', - }; - confirmationTitle = args.isBackground - ? localize('runInTerminal.python.background', "Run `Python` command in `{0}`? (background terminal)", shellType) - : localize('runInTerminal.python', "Run `Python` command in `{0}`?", shellType); + // Check for presentation overrides (e.g., Python -c command extraction) + for (const presenter of this._commandLinePresenters) { + const presenterResult = presenter.present({ commandLine: commandToDisplay, shell, os }); + if (presenterResult) { + toolSpecificData.presentationOverrides = { + commandLine: presenterResult.commandLine, + language: presenterResult.language, + }; + confirmationTitle = args.isBackground + ? localize('runInTerminal.presentationOverride.background', "Run `{0}` command in `{1}`? (background terminal)", presenterResult.languageDisplayName, shellType) + : localize('runInTerminal.presentationOverride', "Run `{0}` command in `{1}`?", presenterResult.languageDisplayName, shellType); + break; + } } const confirmationMessages = isFinalAutoApproved ? undefined : { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/pythonCommandLinePresenter.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/pythonCommandLinePresenter.test.ts new file mode 100644 index 00000000000..db8b5e4313e --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/pythonCommandLinePresenter.test.ts @@ -0,0 +1,176 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ok, strictEqual } from 'assert'; +import { extractPythonCommand, PythonCommandLinePresenter } from '../../browser/tools/commandLinePresenter/pythonCommandLinePresenter.js'; +import { OperatingSystem } from '../../../../../../base/common/platform.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; + +suite('extractPythonCommand', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('basic extraction', () => { + test('should extract simple python -c command with double quotes', () => { + const result = extractPythonCommand('python -c "print(\'hello\')"', 'bash', OperatingSystem.Linux); + strictEqual(result, `print('hello')`); + }); + + test('should extract python3 -c command', () => { + const result = extractPythonCommand('python3 -c "print(\'hello\')"', 'bash', OperatingSystem.Linux); + strictEqual(result, `print('hello')`); + }); + + test('should return undefined for non-python commands', () => { + const result = extractPythonCommand('echo hello', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + + test('should return undefined for python without -c flag', () => { + const result = extractPythonCommand('python script.py', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + + test('should extract python -c with single quotes', () => { + const result = extractPythonCommand(`python -c 'print("hello")'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'print("hello")'); + }); + + test('should extract python3 -c with single quotes', () => { + const result = extractPythonCommand(`python3 -c 'x = 1; print(x)'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'x = 1; print(x)'); + }); + }); + + suite('quote unescaping - Bash', () => { + test('should unescape backslash-escaped quotes in bash', () => { + const result = extractPythonCommand('python -c "print(\\"hello\\")"', 'bash', OperatingSystem.Linux); + strictEqual(result, 'print("hello")'); + }); + + test('should handle multiple escaped quotes', () => { + const result = extractPythonCommand('python -c "x = \\\"hello\\\"; print(x)"', 'bash', OperatingSystem.Linux); + strictEqual(result, 'x = "hello"; print(x)'); + }); + }); + + suite('single quotes - literal content', () => { + test('should preserve content literally in single quotes (no unescaping)', () => { + // Single quotes in bash are literal - backslashes are not escape sequences + const result = extractPythonCommand(`python -c 'print(\\"hello\\")'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'print(\\"hello\\")'); + }); + + test('should handle single quotes in PowerShell', () => { + const result = extractPythonCommand(`python -c 'print("hello")'`, 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'print("hello")'); + }); + + test('should extract multiline code in single quotes', () => { + const code = `python -c 'for i in range(3):\n print(i)'`; + const result = extractPythonCommand(code, 'bash', OperatingSystem.Linux); + strictEqual(result, `for i in range(3):\n print(i)`); + }); + }); + + suite('quote unescaping - PowerShell', () => { + test('should unescape backtick-escaped quotes in PowerShell', () => { + const result = extractPythonCommand('python -c "print(`"hello`")"', 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'print("hello")'); + }); + + test('should handle multiple backtick-escaped quotes', () => { + const result = extractPythonCommand('python -c "x = `"hello`"; print(x)"', 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'x = "hello"; print(x)'); + }); + + test('should not unescape backslash quotes in PowerShell', () => { + const result = extractPythonCommand('python -c "print(\\"hello\\")"', 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'print(\\"hello\\")'); + }); + }); + + suite('multiline code', () => { + test('should extract multiline python code', () => { + const code = `python -c "for i in range(3):\n print(i)"`; + const result = extractPythonCommand(code, 'bash', OperatingSystem.Linux); + strictEqual(result, `for i in range(3):\n print(i)`); + }); + }); + + suite('edge cases', () => { + test('should handle code with trailing whitespace trimmed', () => { + const result = extractPythonCommand('python -c " print(1) "', 'bash', OperatingSystem.Linux); + strictEqual(result, 'print(1)'); + }); + + test('should return undefined for empty code', () => { + const result = extractPythonCommand('python -c ""', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + + test('should return undefined when quotes are unmatched', () => { + const result = extractPythonCommand('python -c "print(1)', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + }); +}); + +suite('PythonCommandLinePresenter', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + const presenter = new PythonCommandLinePresenter(); + + test('should return Python presentation for python -c command', () => { + const result = presenter.present({ + commandLine: `python -c "print('hello')"`, + shell: 'bash', + os: OperatingSystem.Linux + }); + ok(result); + strictEqual(result.commandLine, `print('hello')`); + strictEqual(result.language, 'python'); + strictEqual(result.languageDisplayName, 'Python'); + }); + + test('should return Python presentation for python3 -c command', () => { + const result = presenter.present({ + commandLine: `python3 -c 'x = 1; print(x)'`, + shell: 'bash', + os: OperatingSystem.Linux + }); + ok(result); + strictEqual(result.commandLine, 'x = 1; print(x)'); + strictEqual(result.language, 'python'); + strictEqual(result.languageDisplayName, 'Python'); + }); + + test('should return undefined for non-python commands', () => { + const result = presenter.present({ + commandLine: 'echo hello', + shell: 'bash', + os: OperatingSystem.Linux + }); + strictEqual(result, undefined); + }); + + test('should return undefined for regular python script execution', () => { + const result = presenter.present({ + commandLine: 'python script.py', + shell: 'bash', + os: OperatingSystem.Linux + }); + strictEqual(result, undefined); + }); + + test('should handle PowerShell backtick escaping', () => { + const result = presenter.present({ + commandLine: 'python -c "print(`"hello`")"', + shell: 'pwsh', + os: OperatingSystem.Windows + }); + ok(result); + strictEqual(result.commandLine, 'print("hello")'); + }); +}); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts index 93546877dcd..0e6ede9ea6f 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ok, strictEqual } from 'assert'; -import { generateAutoApproveActions, TRUNCATION_MESSAGE, dedupeRules, isPowerShell, sanitizeTerminalOutput, truncateOutputKeepingTail, extractCdPrefix, extractPythonCommand } from '../../browser/runInTerminalHelpers.js'; +import { generateAutoApproveActions, TRUNCATION_MESSAGE, dedupeRules, isPowerShell, sanitizeTerminalOutput, truncateOutputKeepingTail, extractCdPrefix } from '../../browser/runInTerminalHelpers.js'; import { OperatingSystem } from '../../../../../../base/common/platform.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { ConfigurationTarget } from '../../../../../../platform/configuration/common/configuration.js'; @@ -504,111 +504,4 @@ suite('extractCdPrefix', () => { }); }); -suite('extractPythonCommand', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - suite('basic extraction', () => { - test('should extract simple python -c command with double quotes', () => { - const result = extractPythonCommand('python -c "print(\'hello\')"', 'bash', OperatingSystem.Linux); - strictEqual(result, `print('hello')`); - }); - - test('should extract python3 -c command', () => { - const result = extractPythonCommand('python3 -c "print(\'hello\')"', 'bash', OperatingSystem.Linux); - strictEqual(result, `print('hello')`); - }); - - test('should return undefined for non-python commands', () => { - const result = extractPythonCommand('echo hello', 'bash', OperatingSystem.Linux); - strictEqual(result, undefined); - }); - - test('should return undefined for python without -c flag', () => { - const result = extractPythonCommand('python script.py', 'bash', OperatingSystem.Linux); - strictEqual(result, undefined); - }); - - test('should extract python -c with single quotes', () => { - const result = extractPythonCommand(`python -c 'print("hello")'`, 'bash', OperatingSystem.Linux); - strictEqual(result, 'print("hello")'); - }); - - test('should extract python3 -c with single quotes', () => { - const result = extractPythonCommand(`python3 -c 'x = 1; print(x)'`, 'bash', OperatingSystem.Linux); - strictEqual(result, 'x = 1; print(x)'); - }); - }); - - suite('quote unescaping - Bash', () => { - test('should unescape backslash-escaped quotes in bash', () => { - const result = extractPythonCommand('python -c "print(\\"hello\\")"', 'bash', OperatingSystem.Linux); - strictEqual(result, 'print("hello")'); - }); - - test('should handle multiple escaped quotes', () => { - const result = extractPythonCommand('python -c "x = \\\"hello\\\"; print(x)"', 'bash', OperatingSystem.Linux); - strictEqual(result, 'x = "hello"; print(x)'); - }); - }); - - suite('single quotes - literal content', () => { - test('should preserve content literally in single quotes (no unescaping)', () => { - // Single quotes in bash are literal - backslashes are not escape sequences - const result = extractPythonCommand(`python -c 'print(\\"hello\\")'`, 'bash', OperatingSystem.Linux); - strictEqual(result, 'print(\\"hello\\")'); - }); - - test('should handle single quotes in PowerShell', () => { - const result = extractPythonCommand(`python -c 'print("hello")'`, 'pwsh', OperatingSystem.Windows); - strictEqual(result, 'print("hello")'); - }); - - test('should extract multiline code in single quotes', () => { - const code = `python -c 'for i in range(3):\n print(i)'`; - const result = extractPythonCommand(code, 'bash', OperatingSystem.Linux); - strictEqual(result, `for i in range(3):\n print(i)`); - }); - }); - - suite('quote unescaping - PowerShell', () => { - test('should unescape backtick-escaped quotes in PowerShell', () => { - const result = extractPythonCommand('python -c "print(`"hello`")"', 'pwsh', OperatingSystem.Windows); - strictEqual(result, 'print("hello")'); - }); - - test('should handle multiple backtick-escaped quotes', () => { - const result = extractPythonCommand('python -c "x = `"hello`"; print(x)"', 'pwsh', OperatingSystem.Windows); - strictEqual(result, 'x = "hello"; print(x)'); - }); - - test('should not unescape backslash quotes in PowerShell', () => { - const result = extractPythonCommand('python -c "print(\\"hello\\")"', 'pwsh', OperatingSystem.Windows); - strictEqual(result, 'print(\\"hello\\")'); - }); - }); - - suite('multiline code', () => { - test('should extract multiline python code', () => { - const code = `python -c "for i in range(3):\n print(i)"`; - const result = extractPythonCommand(code, 'bash', OperatingSystem.Linux); - strictEqual(result, `for i in range(3):\n print(i)`); - }); - }); - - suite('edge cases', () => { - test('should handle code with trailing whitespace trimmed', () => { - const result = extractPythonCommand('python -c " print(1) "', 'bash', OperatingSystem.Linux); - strictEqual(result, 'print(1)'); - }); - - test('should return undefined for empty code', () => { - const result = extractPythonCommand('python -c ""', 'bash', OperatingSystem.Linux); - strictEqual(result, undefined); - }); - - test('should return undefined when quotes are unmatched', () => { - const result = extractPythonCommand('python -c "print(1)', 'bash', OperatingSystem.Linux); - strictEqual(result, undefined); - }); - }); -}); From c1d712e4314acb67ac08f52f78aac9628f89ea18 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:24:58 +0100 Subject: [PATCH 076/387] Chat - more working set cleanup and refactoring (#288362) --- .../browser/widget/input/chatInputPart.ts | 77 +++++++++---------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 107bb9d5d50..1766047413e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -84,7 +84,7 @@ import { IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from import { IChatModelInputState, IChatRequestModeInfo, IInputModel } from '../../../common/model/chatModel.js'; import { ChatMode, IChatMode, IChatModeService } from '../../../common/chatModes.js'; import { IChatFollowup, IChatService, IChatSessionContext } from '../../../common/chatService/chatService.js'; -import { IChatSessionFileChange, IChatSessionProviderOptionItem, IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; +import { IChatSessionProviderOptionItem, IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; import { getChatSessionType } from '../../../common/model/chatUri.js'; import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isStringImplicitContextValue, isStringVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; import { IChatResponseViewModel } from '../../../common/model/chatViewModel.js'; @@ -2382,10 +2382,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.renderChatEditingSessionWithEntries( reader.store, chatEditingSession, - modifiedEntries, - sessionFileChanges, editSessionEntries, - sessionFiles, + sessionFiles ); } else { dom.clearNode(this.chatEditingSessionWidgetContainer); @@ -2399,10 +2397,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private renderChatEditingSessionWithEntries( store: DisposableStore, chatEditingSession: IChatEditingSession | null, - modifiedEntries: IObservable, - sessionFileChanges: IObservable, - editSessionEntries: IObservable, - sessionEntries: IObservable, + editSessionEntriesObs: IObservable, + sessionEntriesObs: IObservable ) { // Summary of number of files changed // eslint-disable-next-line no-restricted-syntax @@ -2427,7 +2423,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge scopedContextKeyService.createKey(ChatContextKeys.agentSessionType.key, getChatSessionType(sessionResource)); } - this._chatEditsActionsDisposables.add(bindContextKey(ChatContextKeys.hasAgentSessionChanges, scopedContextKeyService, r => !!sessionEntries.read(r)?.length)); + this._chatEditsActionsDisposables.add(bindContextKey(ChatContextKeys.hasAgentSessionChanges, scopedContextKeyService, r => !!sessionEntriesObs.read(r)?.length)); const scopedInstantiationService = this._chatEditsActionsDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService]))); @@ -2442,38 +2438,36 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge })); const topLevelStats = derived(reader => { - let added = 0; - let removed = 0; - const entries = modifiedEntries.read(reader); - for (const entry of entries) { - if (entry.linesAdded && entry.linesRemoved) { - added += entry.linesAdded.read(reader); - removed += entry.linesRemoved.read(reader); + const entries = editSessionEntriesObs.read(reader); + const sessionEntries = sessionEntriesObs.read(reader); + + let added = 0, removed = 0; + + if (entries.length > 0) { + for (const entry of entries) { + if (entry.kind === 'reference' && entry.options?.diffMeta) { + added += entry.options.diffMeta.added; + removed += entry.options.diffMeta.removed; + } + } + } else { + for (const entry of sessionEntries) { + if (entry.kind === 'reference' && entry.options?.diffMeta) { + added += entry.options.diffMeta.added; + removed += entry.options.diffMeta.removed; + } } } - let baseLabel = entries.length === 1 ? localize('chatEditingSession.oneFile.1', '1 file changed') : localize('chatEditingSession.manyFiles.1', '{0} files changed', entries.length); - let shouldShowEditingSession = added > 0 || removed > 0; - let topLevelIsSessionMenu = sessionResource && getChatSessionType(sessionResource) !== localChatSessionType; + const files = entries.length > 0 ? entries.length : sessionEntries.length; + const topLevelIsSessionMenu = entries.length === 0 && sessionEntries.length > 0; + const shouldShowEditingSession = entries.length > 0 || sessionEntries.length > 0; - if (added === 0 && removed === 0) { - const sessionValue = sessionFileChanges.read(reader) || []; - for (const entry of sessionValue) { - added += entry.insertions; - removed += entry.deletions; - } - - shouldShowEditingSession = sessionValue.length > 0; - baseLabel = sessionValue.length === 1 ? localize('chatEditingSession.oneFile.2', '1 file ready to merge') : localize('chatEditingSession.manyFiles.2', '{0} files ready to merge', sessionValue.length); - topLevelIsSessionMenu = true; - } - - button.label = baseLabel; - - return { added, removed, shouldShowEditingSession, baseLabel, topLevelIsSessionMenu }; + return { files, added, removed, shouldShowEditingSession, topLevelIsSessionMenu }; }); const topLevelIsSessionMenu = topLevelStats.map(t => t.topLevelIsSessionMenu); + store.add(autorun(reader => { const isSessionMenu = topLevelIsSessionMenu.read(reader); reader.store.add(scopedInstantiationService.createInstance(MenuWorkbenchButtonBar, actionsContainer, isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar, { @@ -2495,12 +2489,17 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge })); store.add(autorun(reader => { - const { added, removed, shouldShowEditingSession, baseLabel } = topLevelStats.read(reader); + const { files, added, removed, shouldShowEditingSession } = topLevelStats.read(reader); + + const buttonLabel = files === 1 + ? localize('chatEditingSession.oneFile', '1 file changed') + : localize('chatEditingSession.manyFiles', '{0} files changed', files); + + button.label = buttonLabel; + button.element.setAttribute('aria-label', localize('chatEditingSession.ariaLabelWithCounts', '{0}, {1} lines added, {2} lines removed', buttonLabel, added, removed)); - button.label = baseLabel; this._workingSetLinesAddedSpan.value.textContent = `+${added}`; this._workingSetLinesRemovedSpan.value.textContent = `-${removed}`; - button.element.setAttribute('aria-label', localize('chatEditingSession.ariaLabelWithCounts', '{0}, {1} lines added, {2} lines removed', baseLabel, added, removed)); dom.setVisibility(shouldShowEditingSession, this.chatEditingSessionWidgetContainer); if (!shouldShowEditingSession) { @@ -2580,8 +2579,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } store.add(autorun(reader => { - const editEntries = editSessionEntries.read(reader); - const sessionFileEntries = sessionEntries.read(reader) ?? []; + const editEntries = editSessionEntriesObs.read(reader); + const sessionFileEntries = sessionEntriesObs.read(reader); // Combine edit session entries with session file changes. At the moment, we // we can combine these two arrays since local chat sessions use edit session From a243dd1e6491296b57377467030ae7f9b79c9a3e Mon Sep 17 00:00:00 2001 From: Isha Singh Date: Fri, 16 Jan 2026 19:56:16 +0530 Subject: [PATCH 077/387] Merge pull request #281302 from Ishiezz/fix/251898-implicit-activation-warning Fix: Do not suggest implicit activation message when engine does not support it --- extensions/extension-editing/src/extensionLinter.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/extensions/extension-editing/src/extensionLinter.ts b/extensions/extension-editing/src/extensionLinter.ts index 187100b563f..5c73304b4d8 100644 --- a/extensions/extension-editing/src/extensionLinter.ts +++ b/extensions/extension-editing/src/extensionLinter.ts @@ -33,7 +33,7 @@ const dataUrlsNotValid = l10n.t("Data URLs are not a valid image source."); const relativeUrlRequiresHttpsRepository = l10n.t("Relative image URLs require a repository with HTTPS protocol to be specified in the package.json."); const relativeBadgeUrlRequiresHttpsRepository = l10n.t("Relative badge URLs require a repository with HTTPS protocol to be specified in this package.json."); const apiProposalNotListed = l10n.t("This proposal cannot be used because for this extension the product defines a fixed set of API proposals. You can test your extension but before publishing you MUST reach out to the VS Code team."); -const bumpEngineForImplicitActivationEvents = l10n.t("This activation event can be removed for extensions targeting engine version ^1.75.0 as VS Code will generate these automatically from your package.json contribution declarations."); + const starActivation = l10n.t("Using '*' activation is usually a bad idea as it impacts performance."); const parsingErrorHeader = l10n.t("Error parsing the when-clause:"); @@ -162,13 +162,12 @@ export class ExtensionLinter { if (activationEventsNode?.type === 'array' && activationEventsNode.children) { for (const activationEventNode of activationEventsNode.children) { const activationEvent = getNodeValue(activationEventNode); - const isImplicitActivationSupported = info.engineVersion && info.engineVersion?.majorBase >= 1 && info.engineVersion?.minorBase >= 75; + const isImplicitActivationSupported = info.engineVersion && (info.engineVersion.majorBase > 1 || (info.engineVersion.majorBase === 1 && info.engineVersion.minorBase >= 75)); // Redundant Implicit Activation - if (info.implicitActivationEvents?.has(activationEvent) && redundantImplicitActivationEventPrefixes.some((prefix) => activationEvent.startsWith(prefix))) { + if (isImplicitActivationSupported && info.implicitActivationEvents?.has(activationEvent) && redundantImplicitActivationEventPrefixes.some((prefix) => activationEvent.startsWith(prefix))) { const start = document.positionAt(activationEventNode.offset); const end = document.positionAt(activationEventNode.offset + activationEventNode.length); - const message = isImplicitActivationSupported ? redundantImplicitActivationEvent : bumpEngineForImplicitActivationEvents; - diagnostics.push(new Diagnostic(new Range(start, end), message, isImplicitActivationSupported ? DiagnosticSeverity.Warning : DiagnosticSeverity.Information)); + diagnostics.push(new Diagnostic(new Range(start, end), redundantImplicitActivationEvent, DiagnosticSeverity.Warning)); } // Reserved Implicit Activation From eafcb64303e03a5b73999e7963e8969bfbdc0752 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Jan 2026 06:30:17 -0800 Subject: [PATCH 078/387] Polish confirmation message, add dir to presenter version --- .../browser/tools/runInTerminalTool.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index b483f7eeaa8..0b1c8723b8a 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -529,28 +529,36 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { }; confirmationTitle = args.isBackground - ? localize('runInTerminal.background.inDirectory', "Run `{0}` command in `{1}`? (background terminal)", shellType, directoryLabel) - : localize('runInTerminal.inDirectory', "Run `{0}` command in `{1}`?", shellType, directoryLabel); + ? localize('runInTerminal.background.inDirectory', "Run `{0}` command in background within `{1}`?", shellType, directoryLabel) + : localize('runInTerminal.inDirectory', "Run `{0}` command within `{1}`?", shellType, directoryLabel); } else { toolSpecificData.confirmation = { commandLine: commandToDisplay, }; confirmationTitle = args.isBackground - ? localize('runInTerminal.background', "Run `{0}` command? (background terminal)", shellType) + ? localize('runInTerminal.background', "Run `{0}` command in background?", shellType) : localize('runInTerminal', "Run `{0}` command?", shellType); } // Check for presentation overrides (e.g., Python -c command extraction) + // Use the command after cd prefix extraction if available, since that's what's displayed in the editor + const commandForPresenter = extractedCd?.command ?? commandToDisplay; for (const presenter of this._commandLinePresenters) { - const presenterResult = presenter.present({ commandLine: commandToDisplay, shell, os }); + const presenterResult = presenter.present({ commandLine: commandForPresenter, shell, os }); if (presenterResult) { toolSpecificData.presentationOverrides = { commandLine: presenterResult.commandLine, language: presenterResult.language, }; - confirmationTitle = args.isBackground - ? localize('runInTerminal.presentationOverride.background', "Run `{0}` command in `{1}`? (background terminal)", presenterResult.languageDisplayName, shellType) - : localize('runInTerminal.presentationOverride', "Run `{0}` command in `{1}`?", presenterResult.languageDisplayName, shellType); + if (extractedCd && toolSpecificData.confirmation?.cwdLabel) { + confirmationTitle = args.isBackground + ? localize('runInTerminal.presentationOverride.background.inDirectory', "Run `{0}` command in `{1}` in background within `{2}`?", presenterResult.languageDisplayName, shellType, toolSpecificData.confirmation.cwdLabel) + : localize('runInTerminal.presentationOverride.inDirectory', "Run `{0}` command in `{1}` within `{2}`?", presenterResult.languageDisplayName, shellType, toolSpecificData.confirmation.cwdLabel); + } else { + confirmationTitle = args.isBackground + ? localize('runInTerminal.presentationOverride.background', "Run `{0}` command in `{1}` in background?", presenterResult.languageDisplayName, shellType) + : localize('runInTerminal.presentationOverride', "Run `{0}` command in `{1}`?", presenterResult.languageDisplayName, shellType); + } break; } } From d11695864ee88f87af423b6983549e37d48e6bb5 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 16 Jan 2026 16:01:47 +0100 Subject: [PATCH 079/387] chat - slightly improve `relayout` (#288370) --- .../widgetHosts/viewPane/chatViewPane.ts | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 699077ccb70..163b657e5f4 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -124,6 +124,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this.viewState.sessionId = undefined; // clear persisted session on fresh start } this.sessionsViewerLimited = this.viewState.sessionsViewerLimited ?? true; + this.sessionsViewerVisible = false; // will be updated from layout code this.sessionsViewerSidebarWidth = Math.max(ChatViewPane.SESSIONS_SIDEBAR_MIN_WIDTH, this.viewState.sessionsSidebarWidth ?? ChatViewPane.SESSIONS_SIDEBAR_DEFAULT_WIDTH); // Contextkeys @@ -133,22 +134,18 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this.sessionsViewerPositionContext = ChatContextKeys.agentSessionsViewerPosition.bindTo(contextKeyService); this.sessionsViewerVisibilityContext = ChatContextKeys.agentSessionsViewerVisible.bindTo(contextKeyService); - this.updateContextKeys(false); + this.updateContextKeys(); this.registerListeners(); } - private updateContextKeys(fromEvent: boolean): void { + private updateContextKeys(): void { const { position, location } = this.getViewPositionAndLocation(); this.sessionsViewerLimitedContext.set(this.sessionsViewerLimited); this.chatViewLocationContext.set(location ?? ViewContainerLocation.AuxiliaryBar); this.sessionsViewerOrientationContext.set(this.sessionsViewerOrientation); this.sessionsViewerPositionContext.set(position === Position.RIGHT ? AgentSessionsViewerPosition.Right : AgentSessionsViewerPosition.Left); - - if (fromEvent && this.lastDimensions) { - this.layoutBody(this.lastDimensions.height, this.lastDimensions.width); - } } private getViewPositionAndLocation(): { position: Position; location: ViewContainerLocation } { @@ -192,8 +189,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this.viewPaneContainer?.classList.toggle('chat-view-position-left', position === Position.LEFT); this.viewPaneContainer?.classList.toggle('chat-view-position-right', position === Position.RIGHT); - if (fromEvent && this.lastDimensions) { - this.layoutBody(this.lastDimensions.height, this.lastDimensions.width); + if (fromEvent) { + this.relayout(); } } @@ -208,7 +205,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this.layoutService.onDidChangePanelPosition, Event.filter(this.viewDescriptorService.onDidChangeContainerLocation, e => e.viewContainer === this.viewDescriptorService.getViewContainerByViewId(this.id)) )(() => { - this.updateContextKeys(false); + this.updateContextKeys(); this.updateViewPaneClasses(true /* layout here */); })); @@ -312,6 +309,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private sessionsLink: Link | undefined; private sessionsCount = 0; private sessionsViewerLimited: boolean; + private sessionsViewerVisible: boolean; private sessionsViewerOrientation = AgentSessionsViewerOrientation.Stacked; private sessionsViewerOrientationConfiguration: 'stacked' | 'sideBySide' = 'sideBySide'; private sessionsViewerOrientationContext: IContextKey; @@ -455,8 +453,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this.configurationService.updateValue(ChatConfiguration.ChatViewSessionsOrientation, validatedOrientation); } - if (options.layout && this.lastDimensions) { - this.layoutBody(this.lastDimensions.height, this.lastDimensions.width); + if (options.layout) { + this.relayout(); } } @@ -474,8 +472,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { const updatePromise = triggerUpdate ? this.sessionsControl?.update() : undefined; - if (triggerLayout && this.lastDimensions) { - this.layoutBody(this.lastDimensions.height, this.lastDimensions.width); + if (triggerLayout) { + this.relayout(); } return updatePromise ?? Promise.resolve(); @@ -488,9 +486,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { const { changed: visibilityChanged, visible } = this.updateSessionsControlVisibility(); if (visibilityChanged || (countChanged && visible)) { - if (this.lastDimensions) { - this.layoutBody(this.lastDimensions.height, this.lastDimensions.width); - } + this.relayout(); } } @@ -532,6 +528,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { const sessionsContainerVisible = this.sessionsContainer.style.display !== 'none'; setVisibility(newSessionsContainerVisible, this.sessionsContainer); + this.sessionsViewerVisible = newSessionsContainerVisible; this.sessionsViewerVisibilityContext.set(newSessionsContainerVisible); return { @@ -614,9 +611,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { )); this._register(this.titleControl.onDidChangeHeight(() => { - if (this.lastDimensions) { - this.layoutBody(this.lastDimensions.height, this.lastDimensions.width); - } + this.relayout(); })); } @@ -652,9 +647,9 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { })); // When showing sessions stacked, adjust the height of the sessions list to make room for chat input - this._register(chatWidget.onDidChangeContentHeight(() => { - if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked && this.lastDimensions) { - this.layoutBody(this.lastDimensions.height, this.lastDimensions.width); + this._register(chatWidget.input.onDidChangeHeight(() => { + if (this.sessionsViewerVisible && this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { + this.relayout(); } })); } @@ -804,6 +799,12 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private layoutingBody = false; + private relayout(): void { + if (this.lastDimensions) { + this.layoutBody(this.lastDimensions.height, this.lastDimensions.width); + } + } + protected override layoutBody(height: number, width: number): void { if (this.layoutingBody) { return; // prevent re-entrancy @@ -1018,9 +1019,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this.sessionsViewerSidebarWidth = ChatViewPane.SESSIONS_SIDEBAR_DEFAULT_WIDTH; this.viewState.sessionsSidebarWidth = this.sessionsViewerSidebarWidth; - if (this.lastDimensions) { - this.layoutBody(this.lastDimensions.height, this.lastDimensions.width); - } + this.relayout(); })); } From becd634c3fbf21cf986bff2e5c05326e120c29cf Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 16 Jan 2026 16:06:47 +0100 Subject: [PATCH 080/387] agent sessions - workaround redundant action showing (#288082) (#288371) --- .../chat/browser/agentSessions/agentSessions.contribution.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts index adb1ea5638c..680f16c8f61 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts @@ -20,7 +20,7 @@ import { ISubmenuItem, MenuId, MenuRegistry, registerAction2, SubmenuItemAction import { ArchiveAgentSessionAction, ArchiveAgentSessionSectionAction, UnarchiveAgentSessionSectionAction, UnarchiveAgentSessionAction, OpenAgentSessionInEditorGroupAction, OpenAgentSessionInNewEditorGroupAction, OpenAgentSessionInNewWindowAction, ShowAgentSessionsSidebar, HideAgentSessionsSidebar, ToggleAgentSessionsSidebar, RefreshAgentSessionsViewerAction, FindAgentSessionInViewerAction, MarkAgentSessionUnreadAction, MarkAgentSessionReadAction, MarkAgentSessionSectionReadAction, FocusAgentSessionsAction, SetAgentSessionsOrientationStackedAction, SetAgentSessionsOrientationSideBySideAction, ToggleChatViewSessionsAction, PickAgentSessionAction, ArchiveAllAgentSessionsAction, RenameAgentSessionAction, DeleteAgentSessionAction, DeleteAllLocalSessionsAction } from './agentSessionsActions.js'; import { AgentSessionsQuickAccessProvider, AGENT_SESSIONS_QUICK_ACCESS_PREFIX } from './agentSessionsQuickAccess.js'; import { IAgentSessionProjectionService, AgentSessionProjectionService } from './agentSessionProjectionService.js'; -import { EnterAgentSessionProjectionAction, ExitAgentSessionProjectionAction, OpenInChatPanelAction, ToggleAgentStatusAction, ToggleAgentSessionProjectionAction } from './agentSessionProjectionActions.js'; +import { EnterAgentSessionProjectionAction, ExitAgentSessionProjectionAction, ToggleAgentStatusAction, ToggleAgentSessionProjectionAction } from './agentSessionProjectionActions.js'; import { IAgentStatusService, AgentStatusService } from './agentStatusService.js'; import { AgentStatusWidget } from './agentStatusWidget.js'; import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js'; @@ -59,7 +59,7 @@ registerAction2(SetAgentSessionsOrientationSideBySideAction); // Agent Session Projection registerAction2(EnterAgentSessionProjectionAction); registerAction2(ExitAgentSessionProjectionAction); -registerAction2(OpenInChatPanelAction); +// registerAction2(OpenInChatPanelAction); // TODO@joshspicer https://github.com/microsoft/vscode/issues/288082 registerAction2(ToggleAgentStatusAction); registerAction2(ToggleAgentSessionProjectionAction); From f87751f968c2f9a278fc87d440bb1ca03ebc7498 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 16 Jan 2026 15:06:56 +0000 Subject: [PATCH 081/387] Refine color themes and styles for improved UI consistency and aesthetics --- extensions/theme-2026/themes/2026-dark.json | 4 ++-- extensions/theme-2026/themes/2026-light.json | 20 ++++++++++---------- extensions/theme-2026/themes/styles.css | 18 ++++++++++++++---- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 7857cd2f1c5..8e3a178fb19 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -149,8 +149,8 @@ "editorGutter.background": "#121314", "editorGutter.addedBackground": "#72C892", "editorGutter.deletedBackground": "#F28772", - "diffEditor.insertedTextBackground": "#72C89254", - "diffEditor.removedTextBackground": "#F2877254", + "diffEditor.insertedTextBackground": "#72C89233", + "diffEditor.removedTextBackground": "#F2877233", "editorOverviewRuler.border": "#252627FF", "editorOverviewRuler.findMatchForeground": "#007ABC99", "editorOverviewRuler.modifiedForeground": "#5ba3e0", diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index f971e6511b6..0ad9277604d 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -12,8 +12,8 @@ "textBlockQuote.background": "#F3F3F3", "textBlockQuote.border": "#EEEEEE00", "textCodeBlock.background": "#F3F3F3", - "textLink.foreground": "#6F89D8", - "textLink.activeForeground": "#7C94DB", + "textLink.foreground": "#3457C0", + "textLink.activeForeground": "#395DC9", "textPreformat.foreground": "#666666", "textSeparator.foreground": "#EEEEEE00", "button.background": "#4466CC", @@ -47,9 +47,9 @@ "inputValidation.warningBorder": "#EEEEEE00", "inputValidation.warningForeground": "#202020", "scrollbar.shadow": "#F5F6F84D", - "scrollbarSlider.background": "#20202033", - "scrollbarSlider.hoverBackground": "#20202066", - "scrollbarSlider.activeBackground": "#20202099", + "scrollbarSlider.background": "#4466CC33", + "scrollbarSlider.hoverBackground": "#4466CC4D", + "scrollbarSlider.activeBackground": "#4466CC4D", "badge.background": "#4466CC", "badge.foreground": "#FFFFFF", "progressBar.background": "#666666", @@ -105,10 +105,10 @@ "editorLineNumber.foreground": "#656668", "editorLineNumber.activeForeground": "#202123", "editorCursor.foreground": "#202123", - "editor.selectionBackground": "#4466CC33", + "editor.selectionBackground": "#4466CC26", "editor.inactiveSelectionBackground": "#4466CC80", "editor.selectionHighlightBackground": "#4466CC1A", - "editor.wordHighlightBackground": "#4466CCB3", + "editor.wordHighlightBackground": "#4466CC33", "editor.wordHighlightStrongBackground": "#4466CCE6", "editor.findMatchBackground": "#4466CC4D", "editor.findMatchHighlightBackground": "#4466CC26", @@ -122,7 +122,7 @@ "editorIndentGuide.activeBackground": "#F4F4F4", "editorRuler.foreground": "#F4F4F4", "editorCodeLens.foreground": "#666666", - "editorBracketMatch.background": "#4466CC80", + "editorBracketMatch.background": "#4466CC55", "editorBracketMatch.border": "#EEEEEE00", "editorWidget.background": "#FCFCFC", "editorWidget.border": "#EEEEEE00", @@ -149,8 +149,8 @@ "editorGutter.background": "#FDFDFD", "editorGutter.addedBackground": "#587c0c", "editorGutter.deletedBackground": "#ad0707", - "diffEditor.insertedTextBackground": "#587c0c54", - "diffEditor.removedTextBackground": "#ad070754", + "diffEditor.insertedTextBackground": "#587c0c26", + "diffEditor.removedTextBackground": "#ad070726", "editorOverviewRuler.border": "#EEEEEE00", "editorOverviewRuler.findMatchForeground": "#4466CC99", "editorOverviewRuler.modifiedForeground": "#007acc", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index c950dd33f27..e475f6dbeb3 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -69,17 +69,22 @@ /* Chat Widget */ -.monaco-workbench .interactive-session .chat-input-container { box-shadow: 0 0 6px rgba(0, 0, 0, 0.1); border-radius: 6px; } +.monaco-workbench .interactive-session .chat-input-container { box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.08); border-radius: 6px; } .monaco-workbench .interactive-session .interactive-input-part .chat-editor-container .interactive-input-editor .monaco-editor, .monaco-workbench .interactive-session .interactive-input-part .chat-editor-container .interactive-input-editor .monaco-editor .monaco-editor-background { background-color: var(--vscode-panel-background, var(--vscode-sideBar-background)) !important; } -.monaco-workbench .interactive-session .chat-editing-session .chat-editing-session-container { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10); border-radius: 4px 4px 0 0; } +.monaco-workbench .interactive-session .chat-editing-session .chat-editing-session-container { border-radius: 4px 4px 0 0; } .monaco-workbench .interactive-input-part:has(.chat-editing-session > .chat-editing-session-container) .chat-input-container { border-radius: 0 0 6px 6px; } .monaco-workbench .part.panel .interactive-session, .monaco-workbench .part.auxiliarybar .interactive-session { position: relative; } +.monaco-workbench .interactive-session .chat-editing-session .chat-editing-session-container { + background-color: transparent !important; +} + /* Notifications */ .monaco-workbench .notifications-toasts { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); } -.monaco-workbench .notification-toast { box-shadow: 0 0 8px rgba(0, 0, 0, 0.12); border: none; border-radius: 8px; overflow: hidden; } +.monaco-workbench .notification-toast { box-shadow: none !important; border: none !important; } +.monaco-workbench .notifications-center { border: none !important; } /* Context Menus */ .monaco-workbench .monaco-menu .monaco-action-bar.vertical { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 12px; overflow: hidden; backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); } @@ -126,7 +131,8 @@ .monaco-workbench.hc-black .part.editor > .content .editor-group-container > .title .breadcrumbs-control { background: rgba(10, 10, 11, 0.85) !important; box-shadow: 0 0 6px rgba(0, 0, 0, 0.08) !important; } /* Input Boxes */ -.monaco-workbench .monaco-inputbox { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); border: none; border-radius: 6px; } +.monaco-workbench .monaco-inputbox, +.monaco-workbench .suggest-input-container { box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.08); border: none; } .monaco-inputbox .monaco-action-bar .action-item .codicon, .monaco-workbench .search-container .input-box, @@ -134,6 +140,10 @@ color: var(--vscode-icon-foreground) !important; } +/* .scm-view .scm-editor { + box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.08); +} */ + /* Buttons */ .monaco-workbench .monaco-button { box-shadow: 0 0 2px rgba(0, 0, 0, 0.06); } .monaco-workbench .monaco-button:hover { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); } From 9934a5d9ed9ee7e48a582d253bcc7db022c0b124 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:14:10 +0100 Subject: [PATCH 082/387] Git - enable copy-on-write to worktree include files when the file system supports it (#288376) --- extensions/git/src/repository.ts | 42 ++++++++++++-------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index b528a89cff0..24d2dae8040 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1928,36 +1928,26 @@ export class Repository implements Disposable { try { // Copy files - let copiedFiles = 0; - const results = await window.withProgress({ - location: ProgressLocation.Notification, - title: l10n.t('Copying additional files to the worktree'), - cancellable: false - }, async (progress) => { - const limiter = new Limiter(10); - const files = Array.from(ignoredFiles); + const startTime = Date.now(); + const limiter = new Limiter(15); + const files = Array.from(ignoredFiles); - return Promise.allSettled(files.map(sourceFile => - limiter.queue(async () => { - const targetFile = path.join(worktreePath, relativePath(this.root, sourceFile)); - await fsPromises.mkdir(path.dirname(targetFile), { recursive: true }); - await fsPromises.cp(sourceFile, targetFile, { - force: true, - recursive: false, - verbatimSymlinks: true - }); - - copiedFiles++; - progress.report({ - increment: 100 / ignoredFiles.size, - message: l10n.t('({0}/{1})', copiedFiles, ignoredFiles.size) - }); - }) - )); - }); + const results = await Promise.allSettled(files.map(sourceFile => + limiter.queue(async () => { + const targetFile = path.join(worktreePath, relativePath(this.root, sourceFile)); + await fsPromises.mkdir(path.dirname(targetFile), { recursive: true }); + await fsPromises.cp(sourceFile, targetFile, { + force: true, + mode: fs.constants.COPYFILE_FICLONE, + recursive: false, + verbatimSymlinks: true + }); + }) + )); // Log any failed operations const failedOperations = results.filter(r => r.status === 'rejected'); + this.logger.info(`[Repository][_copyWorktreeIncludeFiles] Copied ${files.length - failedOperations.length} files to worktree. Failed to copy ${failedOperations.length} files. [${Date.now() - startTime}ms]`); if (failedOperations.length > 0) { window.showWarningMessage(l10n.t('Failed to copy {0} files to the worktree.', failedOperations.length)); From 10771d939c5acfa72f1216d83231ebfb02cdd3c2 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Fri, 16 Jan 2026 16:16:48 +0100 Subject: [PATCH 083/387] Respect file system case sensitivity when reading .gitignore files (#287555) --- .../contrib/files/browser/views/explorerViewer.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index cc16e91151e..a4a00dfa7ea 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -9,7 +9,7 @@ import * as glob from '../../../../../base/common/glob.js'; import { IListVirtualDelegate, ListDragOverEffectPosition, ListDragOverEffectType } from '../../../../../base/browser/ui/list/list.js'; import { IProgressService, ProgressLocation, } from '../../../../../platform/progress/common/progress.js'; import { INotificationService, Severity } from '../../../../../platform/notification/common/notification.js'; -import { IFileService, FileKind, FileOperationError, FileOperationResult, FileChangeType } from '../../../../../platform/files/common/files.js'; +import { IFileService, FileKind, FileOperationError, FileOperationResult, FileChangeType, FileSystemProviderCapabilities } from '../../../../../platform/files/common/files.js'; import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; import { isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js'; import { IDisposable, Disposable, dispose, toDisposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; @@ -1367,9 +1367,10 @@ export class FilesFilter implements ITreeFilter { const ignoreFile = ignoreTree.get(dirUri); ignoreFile?.updateContents(content.value.toString()); } else { - // Otherwise we create a new ignorefile and add it to the tree + // Otherwise we create a new ignore file and add it to the tree const ignoreParent = ignoreTree.findSubstr(dirUri); - const ignoreFile = new IgnoreFile(content.value.toString(), dirUri.path, ignoreParent); + const ignoreCase = !this.fileService.hasCapability(ignoreFileResource, FileSystemProviderCapabilities.PathCaseSensitive); + const ignoreFile = new IgnoreFile(content.value.toString(), dirUri.path, ignoreParent, ignoreCase); ignoreTree.set(dirUri, ignoreFile); // If we haven't seen this resource before then we need to add it to the list of resources we're tracking if (!this.ignoreFileResourcesPerRoot.get(root)?.has(ignoreFileResource)) { From 209376d8b126d48a6c634e7cab40883f64188df2 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 16 Jan 2026 16:24:02 +0100 Subject: [PATCH 084/387] agent sessions - more tweaks to prevent excessive layout (#288381) --- .../chat/browser/widgetHosts/viewPane/chatViewPane.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 163b657e5f4..2a2f5fb00ce 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -647,9 +647,14 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { })); // When showing sessions stacked, adjust the height of the sessions list to make room for chat input + let lastChatInputHeight: number | undefined; this._register(chatWidget.input.onDidChangeHeight(() => { if (this.sessionsViewerVisible && this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { - this.relayout(); + const chatInputHeight = this._widget?.input?.contentHeight; + if (chatInputHeight && chatInputHeight !== lastChatInputHeight) { // ensure we only layout on actual height changes + lastChatInputHeight = chatInputHeight; + this.relayout(); + } } })); } From ba9765f67e204dab0e432901d8d3c37f8fb34650 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 16 Jan 2026 16:52:24 +0100 Subject: [PATCH 085/387] fix #288229 (#288386) --- .../chat/browser/chatManagement/media/chatModelsWidget.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/media/chatModelsWidget.css b/src/vs/workbench/contrib/chat/browser/chatManagement/media/chatModelsWidget.css index c70f5b6ba08..5de78f83dc1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/media/chatModelsWidget.css +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/media/chatModelsWidget.css @@ -122,17 +122,17 @@ flex: 0 1 auto; } -.models-widget .models-table-container .monaco-table-tr.models-status-row .monaco-table-td .model-name-container .model-name .monaco-highlighted-label.error-status { +.models-widget .models-table-container .monaco-list-row:not(.selected) .monaco-table-tr.models-status-row .monaco-table-td .model-name-container .model-name .monaco-highlighted-label.error-status { color: var(--vscode-errorForeground); } -.models-widget .models-table-container .monaco-table-tr.models-status-row .monaco-table-td .model-name-container .model-name .monaco-highlighted-label.warning-status { +.models-widget .models-table-container .monaco-list-row:not(.selected) .monaco-table-tr.models-status-row .monaco-table-td .model-name-container .model-name .monaco-highlighted-label.warning-status { color: var(--vscode-editorWarning-foreground); } /** Actions column styling **/ -.models-widget .models-table-container .monaco-table-tr.models-model-row.model-hidden .models-table-column.models-actions-column { +.models-widget .models-table-container .monaco-list-row .monaco-table-tr.models-model-row.model-hidden .models-table-column.models-actions-column { opacity: 1; } From a61aec9852a211d315f3a75b2353d8a7e9e6f8dc Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 16 Jan 2026 16:52:48 +0100 Subject: [PATCH 086/387] eng - opt the team into using sessions (#288384) --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index d9aeed84432..059bcb77f5f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,7 @@ "scripts/test-integration.bat": true, "scripts/test-integration.sh": true, }, + "chat.viewSessions.enabled": true, // --- Editor --- "editor.insertSpaces": false, From 26b2a48e063361735fc3d7c63c0c5ebd70fd45dd Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 16 Jan 2026 17:06:14 +0100 Subject: [PATCH 087/387] agent sessions - make full list the default and change showing recent to a setting (#288390) --- .../agentSessions.contribution.ts | 6 +- .../agentSessions/agentSessionsActions.ts | 79 ++++++++++++++++--- .../contrib/chat/browser/chat.contribution.ts | 5 ++ .../widgetHosts/viewPane/chatViewPane.ts | 51 +++++------- .../viewPane/media/chatViewPane.css | 21 ----- .../contrib/chat/common/constants.ts | 1 + 6 files changed, 99 insertions(+), 64 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts index 680f16c8f61..e9a78c12610 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts @@ -17,7 +17,7 @@ import { IAgentSessionsService, AgentSessionsService } from './agentSessionsServ import { LocalAgentsSessionsProvider } from './localAgentSessionsProvider.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js'; import { ISubmenuItem, MenuId, MenuRegistry, registerAction2, SubmenuItemAction } from '../../../../../platform/actions/common/actions.js'; -import { ArchiveAgentSessionAction, ArchiveAgentSessionSectionAction, UnarchiveAgentSessionSectionAction, UnarchiveAgentSessionAction, OpenAgentSessionInEditorGroupAction, OpenAgentSessionInNewEditorGroupAction, OpenAgentSessionInNewWindowAction, ShowAgentSessionsSidebar, HideAgentSessionsSidebar, ToggleAgentSessionsSidebar, RefreshAgentSessionsViewerAction, FindAgentSessionInViewerAction, MarkAgentSessionUnreadAction, MarkAgentSessionReadAction, MarkAgentSessionSectionReadAction, FocusAgentSessionsAction, SetAgentSessionsOrientationStackedAction, SetAgentSessionsOrientationSideBySideAction, ToggleChatViewSessionsAction, PickAgentSessionAction, ArchiveAllAgentSessionsAction, RenameAgentSessionAction, DeleteAgentSessionAction, DeleteAllLocalSessionsAction } from './agentSessionsActions.js'; +import { ArchiveAgentSessionAction, ArchiveAgentSessionSectionAction, UnarchiveAgentSessionSectionAction, UnarchiveAgentSessionAction, OpenAgentSessionInEditorGroupAction, OpenAgentSessionInNewEditorGroupAction, OpenAgentSessionInNewWindowAction, ShowAgentSessionsSidebar, HideAgentSessionsSidebar, ToggleAgentSessionsSidebar, RefreshAgentSessionsViewerAction, FindAgentSessionInViewerAction, MarkAgentSessionUnreadAction, MarkAgentSessionReadAction, MarkAgentSessionSectionReadAction, FocusAgentSessionsAction, SetAgentSessionsOrientationStackedAction, SetAgentSessionsOrientationSideBySideAction, ShowAllAgentSessionsAction, ShowRecentAgentSessionsAction, HideAgentSessionsAction, PickAgentSessionAction, ArchiveAllAgentSessionsAction, RenameAgentSessionAction, DeleteAgentSessionAction, DeleteAllLocalSessionsAction } from './agentSessionsActions.js'; import { AgentSessionsQuickAccessProvider, AGENT_SESSIONS_QUICK_ACCESS_PREFIX } from './agentSessionsQuickAccess.js'; import { IAgentSessionProjectionService, AgentSessionProjectionService } from './agentSessionProjectionService.js'; import { EnterAgentSessionProjectionAction, ExitAgentSessionProjectionAction, ToggleAgentStatusAction, ToggleAgentSessionProjectionAction } from './agentSessionProjectionActions.js'; @@ -52,7 +52,9 @@ registerAction2(FindAgentSessionInViewerAction); registerAction2(ShowAgentSessionsSidebar); registerAction2(HideAgentSessionsSidebar); registerAction2(ToggleAgentSessionsSidebar); -registerAction2(ToggleChatViewSessionsAction); +registerAction2(ShowAllAgentSessionsAction); +registerAction2(ShowRecentAgentSessionsAction); +registerAction2(HideAgentSessionsAction); registerAction2(SetAgentSessionsOrientationStackedAction); registerAction2(SetAgentSessionsOrientationSideBySideAction); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts index bc34a756360..5537672599a 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts @@ -36,18 +36,29 @@ import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; //#region Chat View -export class ToggleChatViewSessionsAction extends Action2 { +const showSessionsSubmenu = new MenuId('chatShowSessionsSubmenu'); +MenuRegistry.appendMenuItem(MenuId.ChatWelcomeContext, { + submenu: showSessionsSubmenu, + title: localize2('chat.showSessions', "Show Sessions"), + group: '0_sessions', + order: 1, + when: ChatContextKeys.inChatEditor.negate() +}); + +export class ShowAllAgentSessionsAction extends Action2 { constructor() { super({ - id: 'workbench.action.chat.toggleChatViewSessions', - title: localize2('chat.toggleChatViewSessions.label', "Show Sessions"), - toggled: ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsEnabled}`, true), + id: 'workbench.action.chat.showAllAgentSessions', + title: localize2('chat.showSessions.all', "All"), + toggled: ContextKeyExpr.and( + ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsEnabled}`, true), + ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsShowRecentOnly}`, false) + ), menu: { - id: MenuId.ChatWelcomeContext, - group: '0_sessions', - order: 1, - when: ChatContextKeys.inChatEditor.negate() + id: showSessionsSubmenu, + group: 'navigation', + order: 1 } }); } @@ -55,8 +66,56 @@ export class ToggleChatViewSessionsAction extends Action2 { async run(accessor: ServicesAccessor): Promise { const configurationService = accessor.get(IConfigurationService); - const chatViewSessionsEnabled = configurationService.getValue(ChatConfiguration.ChatViewSessionsEnabled); - await configurationService.updateValue(ChatConfiguration.ChatViewSessionsEnabled, !chatViewSessionsEnabled); + await configurationService.updateValue(ChatConfiguration.ChatViewSessionsEnabled, true); + await configurationService.updateValue(ChatConfiguration.ChatViewSessionsShowRecentOnly, false); + } +} + +export class ShowRecentAgentSessionsAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.chat.showRecentAgentSessions', + title: localize2('chat.showSessions.recent', "Recent"), + toggled: ContextKeyExpr.and( + ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsEnabled}`, true), + ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsShowRecentOnly}`, true) + ), + menu: { + id: showSessionsSubmenu, + group: 'navigation', + order: 2 + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + + await configurationService.updateValue(ChatConfiguration.ChatViewSessionsEnabled, true); + await configurationService.updateValue(ChatConfiguration.ChatViewSessionsShowRecentOnly, true); + } +} + +export class HideAgentSessionsAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.chat.hideAgentSessions', + title: localize2('chat.showSessions.none', "None"), + toggled: ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsEnabled}`, false), + menu: { + id: showSessionsSubmenu, + group: 'navigation', + order: 3 + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + + await configurationService.updateValue(ChatConfiguration.ChatViewSessionsEnabled, false); } } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 970268b42f0..c4c1011d904 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -387,6 +387,11 @@ configurationRegistry.registerConfiguration({ default: true, description: nls.localize('chat.viewSessions.enabled', "Show chat agent sessions when chat is empty or to the side when chat view is wide enough."), }, + [ChatConfiguration.ChatViewSessionsShowRecentOnly]: { + type: 'boolean', + default: false, + description: nls.localize('chat.viewSessions.showRecentOnly', "When enabled, only show recent sessions in the stacked sessions view. When disabled, show all sessions."), + }, [ChatConfiguration.ChatViewSessionsOrientation]: { type: 'string', enum: ['stacked', 'sideBySide'], diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 2a2f5fb00ce..82e01d19a97 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -50,7 +50,6 @@ import { ChatWidget } from '../../widget/chatWidget.js'; import { ChatViewWelcomeController, IViewWelcomeDelegate } from '../../viewsWelcome/chatViewWelcomeController.js'; import { IWorkbenchLayoutService, LayoutSettings, Position } from '../../../../../services/layout/browser/layoutService.js'; import { AgentSessionsViewerOrientation, AgentSessionsViewerPosition } from '../../agentSessions/agentSessions.js'; -import { Link } from '../../../../../../platform/opener/browser/link.js'; import { IProgressService } from '../../../../../../platform/progress/common/progress.js'; import { ChatViewId } from '../../chat.js'; import { disposableTimeout } from '../../../../../../base/common/async.js'; @@ -63,7 +62,6 @@ import { IChatEntitlementService } from '../../../../../services/chat/common/cha interface IChatViewPaneState extends Partial { sessionId?: string; - sessionsViewerLimited?: boolean; sessionsSidebarWidth?: number; } @@ -123,7 +121,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { ) { this.viewState.sessionId = undefined; // clear persisted session on fresh start } - this.sessionsViewerLimited = this.viewState.sessionsViewerLimited ?? true; + this.sessionsViewerLimited = this.configurationService.getValue(ChatConfiguration.ChatViewSessionsShowRecentOnly) ?? false; this.sessionsViewerVisible = false; // will be updated from layout code this.sessionsViewerSidebarWidth = Math.max(ChatViewPane.SESSIONS_SIDEBAR_MIN_WIDTH, this.viewState.sessionsSidebarWidth ?? ChatViewPane.SESSIONS_SIDEBAR_DEFAULT_WIDTH); @@ -214,6 +212,22 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { return e.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION); })(() => this.updateViewPaneClasses(true))); + // Sessions viewer limited setting changes + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => { + return e.affectsConfiguration(ChatConfiguration.ChatViewSessionsShowRecentOnly); + })(() => { + const oldSessionsViewerLimited = this.sessionsViewerLimited; + if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.SideBySide) { + this.sessionsViewerLimited = false; // side by side always shows all + } else { + this.sessionsViewerLimited = this.configurationService.getValue(ChatConfiguration.ChatViewSessionsShowRecentOnly) ?? false; + } + + if (oldSessionsViewerLimited !== this.sessionsViewerLimited) { + this.notifySessionsControlLimitedChanged(true /* layout */, true /* update */); + } + })); + // Entitlement changes this._register(this.chatEntitlementService.onDidChangeSentiment(() => { this.updateViewPaneClasses(true); @@ -305,8 +319,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private sessionsTitle: HTMLElement | undefined; private sessionsControlContainer: HTMLElement | undefined; private sessionsControl: AgentSessionsControl | undefined; - private sessionsLinkContainer: HTMLElement | undefined; - private sessionsLink: Link | undefined; private sessionsCount = 0; private sessionsViewerLimited: boolean; private sessionsViewerVisible: boolean; @@ -401,22 +413,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { sessionsToolbar.context = sessionsControl; - // Link to Sessions View - this.sessionsLinkContainer = append(sessionsContainer, $('.agent-sessions-link-container')); - this.sessionsLink = this._register(this.instantiationService.createInstance(Link, this.sessionsLinkContainer, { - label: this.sessionsViewerLimited ? localize('showAllSessions', "Show More") : localize('showRecentSessions', "Show Less"), - href: '', - }, { - opener: () => { - this.sessionsViewerLimited = !this.sessionsViewerLimited; - this.viewState.sessionsViewerLimited = this.sessionsViewerLimited; - - this.notifySessionsControlLimitedChanged(true /* layout */, true /* update */); - - sessionsControl.focus(); - } - })); - // Deal with orientation configuration this._register(Event.runAndSubscribe(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ChatConfiguration.ChatViewSessionsOrientation)), e => { const newSessionsViewerOrientationConfiguration = this.configurationService.getValue<'stacked' | 'sideBySide' | unknown>(ChatConfiguration.ChatViewSessionsOrientation); @@ -463,13 +459,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this.updateSessionsControlTitle(); - if (this.sessionsLink) { - this.sessionsLink.link = { - label: this.sessionsViewerLimited ? localize('showAllSessions', "Show More") : localize('showRecentSessions', "Show Less"), - href: '' - }; - } - const updatePromise = triggerUpdate ? this.sessionsControl?.update() : undefined; if (triggerLayout) { @@ -850,7 +839,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { let heightReduction = 0; let widthReduction = 0; - if (!this.sessionsContainer || !this.sessionsControlContainer || !this.sessionsControl || !this.viewPaneContainer || !this.sessionsTitleContainer || !this.sessionsLinkContainer || !this.sessionsTitle || !this.sessionsLink) { + if (!this.sessionsContainer || !this.sessionsControlContainer || !this.sessionsControl || !this.viewPaneContainer || !this.sessionsTitleContainer || !this.sessionsTitle) { return { heightReduction, widthReduction }; } @@ -894,7 +883,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.SideBySide) { this.sessionsViewerLimited = false; // side by side always shows all } else { - this.sessionsViewerLimited = this.viewState.sessionsViewerLimited ?? true; + this.sessionsViewerLimited = this.configurationService.getValue(ChatConfiguration.ChatViewSessionsShowRecentOnly) ?? false; } let updatePromise: Promise; @@ -932,7 +921,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { return { heightReduction: 0, widthReduction: 0 }; } - let availableSessionsHeight = height - this.sessionsTitleContainer.offsetHeight - this.sessionsLinkContainer.offsetHeight; + let availableSessionsHeight = height - this.sessionsTitleContainer.offsetHeight; if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { availableSessionsHeight -= Math.max(ChatViewPane.MIN_CHAT_WIDGET_HEIGHT, this._widget?.input?.contentHeight ?? 0); } diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css index 31fa06e03ee..b4eadde9ef2 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css @@ -77,22 +77,6 @@ background-color: var(--vscode-inputOption-activeBackground); } } - - .agent-sessions-link-container { - padding: 8px 0; - font-size: 12px; - text-align: center; - } - - .agent-sessions-link-container a { - color: var(--vscode-descriptionForeground); - } - - .agent-sessions-link-container a:hover, - .agent-sessions-link-container a:active { - text-decoration: none; - color: var(--vscode-textLink-foreground); - } } /* Sessions control: stacked */ @@ -121,11 +105,6 @@ border-left: 1px solid var(--vscode-panel-border); } } - - .agent-sessions-link-container { - /* hide link to show more when side by side */ - display: none; - } } /* diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 0487a189c3d..b441782ef7b 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -28,6 +28,7 @@ export enum ChatConfiguration { TodosShowWidget = 'chat.tools.todos.showWidget', NotifyWindowOnResponseReceived = 'chat.notifyWindowOnResponseReceived', ChatViewSessionsEnabled = 'chat.viewSessions.enabled', + ChatViewSessionsShowRecentOnly = 'chat.viewSessions.showRecentOnly', ChatViewSessionsOrientation = 'chat.viewSessions.orientation', ChatViewTitleEnabled = 'chat.viewTitle.enabled', SubagentToolCustomAgents = 'chat.customAgentInSubagent.enabled', From cce0642bfb890ca622a922cdb202bf834a3f71ba Mon Sep 17 00:00:00 2001 From: John Heilman <1735575+Infro@users.noreply.github.com> Date: Fri, 16 Jan 2026 08:21:56 -0800 Subject: [PATCH 088/387] If the users selects a language, let's have it actually choose the language they selected. (Yaml vs yaml) (#288153) * If the users selects a language, let's have it actually choose the language they selected. (Yaml vs yaml) * polish --------- Co-authored-by: Benjamin Pasero --- src/vs/workbench/browser/parts/editor/editorStatus.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 7bcbfe65845..88bc828b318 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -1198,6 +1198,7 @@ export class ChangeLanguageAction extends Action2 { } return { + id: languageId, label: languageName, meta: extensions, iconClasses: getIconClassesForLanguageId(languageId), @@ -1280,8 +1281,7 @@ export class ChangeLanguageAction extends Action2 { } } } else { - const languageId = languageService.getLanguageIdByLanguageName(pick.label); - languageSelection = languageService.createById(languageId); + languageSelection = languageService.createById(pick.id); if (resource) { // fire and forget to not slow things down From e4223fc0bfb4b4cd5582cf106c1ef780dfb4e517 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 16 Jan 2026 16:52:59 +0000 Subject: [PATCH 089/387] Refine color values and styles in 2026 Dark and Light themes for improved UI consistency --- extensions/theme-2026/themes/2026-dark.json | 372 +++++++++---------- extensions/theme-2026/themes/2026-light.json | 30 +- extensions/theme-2026/themes/styles.css | 29 +- 3 files changed, 215 insertions(+), 216 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 8e3a178fb19..5031dc271c7 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -3,222 +3,222 @@ "name": "2026 Dark", "type": "dark", "colors": { - "foreground": "#bebebe", + "foreground": "#bfbfbf", "disabledForeground": "#444444", "errorForeground": "#f48771", "descriptionForeground": "#888888", "icon.foreground": "#888888", - "focusBorder": "#007ABCB3", - "textBlockQuote.background": "#242526", - "textBlockQuote.border": "#252627FF", - "textCodeBlock.background": "#242526", - "textLink.foreground": "#0092E2", - "textLink.activeForeground": "#009AEC", + "focusBorder": "#498FADB3", + "textBlockQuote.background": "#232627", + "textBlockQuote.border": "#2A2B2CFF", + "textCodeBlock.background": "#232627", + "textLink.foreground": "#589BB8", + "textLink.activeForeground": "#61A0BC", "textPreformat.foreground": "#888888", - "textSeparator.foreground": "#252525FF", - "button.background": "#007ABC", + "textSeparator.foreground": "#2a2a2aFF", + "button.background": "#498FAE", "button.foreground": "#FFFFFF", - "button.hoverBackground": "#0080C5", - "button.border": "#252627FF", - "button.secondaryBackground": "#242526", - "button.secondaryForeground": "#bebebe", - "button.secondaryHoverBackground": "#007ABC", - "checkbox.background": "#242526", - "checkbox.border": "#252627FF", - "checkbox.foreground": "#bebebe", - "dropdown.background": "#191A1B", - "dropdown.border": "#323435", - "dropdown.foreground": "#bebebe", - "dropdown.listBackground": "#202122", - "input.background": "#191A1B", - "input.border": "#323435FF", - "input.foreground": "#bebebe", + "button.hoverBackground": "#4D94B4", + "button.border": "#2A2B2CFF", + "button.secondaryBackground": "#232627", + "button.secondaryForeground": "#bfbfbf", + "button.secondaryHoverBackground": "#303234", + "checkbox.background": "#232627", + "checkbox.border": "#2A2B2CFF", + "checkbox.foreground": "#bfbfbf", + "dropdown.background": "#191B1D", + "dropdown.border": "#333536", + "dropdown.foreground": "#bfbfbf", + "dropdown.listBackground": "#1F2223", + "input.background": "#191B1D", + "input.border": "#333536FF", + "input.foreground": "#bfbfbf", "input.placeholderForeground": "#777777", - "inputOption.activeBackground": "#007ABC33", - "inputOption.activeForeground": "#bebebe", - "inputOption.activeBorder": "#252627FF", - "inputValidation.errorBackground": "#191A1B", - "inputValidation.errorBorder": "#252627FF", - "inputValidation.errorForeground": "#bebebe", - "inputValidation.infoBackground": "#191A1B", - "inputValidation.infoBorder": "#252627FF", - "inputValidation.infoForeground": "#bebebe", - "inputValidation.warningBackground": "#191A1B", - "inputValidation.warningBorder": "#252627FF", - "inputValidation.warningForeground": "#bebebe", + "inputOption.activeBackground": "#498FAE33", + "inputOption.activeForeground": "#bfbfbf", + "inputOption.activeBorder": "#2A2B2CFF", + "inputValidation.errorBackground": "#191B1D", + "inputValidation.errorBorder": "#2A2B2CFF", + "inputValidation.errorForeground": "#bfbfbf", + "inputValidation.infoBackground": "#191B1D", + "inputValidation.infoBorder": "#2A2B2CFF", + "inputValidation.infoForeground": "#bfbfbf", + "inputValidation.warningBackground": "#191B1D", + "inputValidation.warningBorder": "#2A2B2CFF", + "inputValidation.warningForeground": "#bfbfbf", "scrollbar.shadow": "#191B1D4D", - "scrollbarSlider.background": "#83848533", - "scrollbarSlider.hoverBackground": "#83848566", - "scrollbarSlider.activeBackground": "#83848599", - "badge.background": "#007ABC", + "scrollbarSlider.background": "#81848533", + "scrollbarSlider.hoverBackground": "#81848566", + "scrollbarSlider.activeBackground": "#81848599", + "badge.background": "#498FAE", "badge.foreground": "#FFFFFF", - "progressBar.background": "#878889", - "list.activeSelectionBackground": "#007ABC26", - "list.activeSelectionForeground": "#bebebe", - "list.inactiveSelectionBackground": "#242526", - "list.inactiveSelectionForeground": "#bebebe", - "list.hoverBackground": "#262728", - "list.hoverForeground": "#bebebe", - "list.dropBackground": "#007ABC1A", - "list.focusBackground": "#007ABC26", - "list.focusForeground": "#bebebe", - "list.focusOutline": "#007ABCB3", - "list.highlightForeground": "#bebebe", + "progressBar.background": "#858889", + "list.activeSelectionBackground": "#498FAE26", + "list.activeSelectionForeground": "#bfbfbf", + "list.inactiveSelectionBackground": "#232627", + "list.inactiveSelectionForeground": "#bfbfbf", + "list.hoverBackground": "#252829", + "list.hoverForeground": "#bfbfbf", + "list.dropBackground": "#498FAE1A", + "list.focusBackground": "#498FAE26", + "list.focusForeground": "#bfbfbf", + "list.focusOutline": "#498FADB3", + "list.highlightForeground": "#bfbfbf", "list.invalidItemForeground": "#444444", "list.errorForeground": "#f48771", "list.warningForeground": "#e5ba7d", - "activityBar.background": "#191A1B", - "activityBar.foreground": "#bebebe", + "activityBar.background": "#191B1D", + "activityBar.foreground": "#bfbfbf", "activityBar.inactiveForeground": "#888888", - "activityBar.border": "#252627FF", - "activityBar.activeBorder": "#252627FF", - "activityBar.activeFocusBorder": "#007ABCB3", - "activityBarBadge.background": "#007ABC", + "activityBar.border": "#2A2B2CFF", + "activityBar.activeBorder": "#2A2B2CFF", + "activityBar.activeFocusBorder": "#498FADB3", + "activityBarBadge.background": "#498FAE", "activityBarBadge.foreground": "#FFFFFF", - "sideBar.background": "#191A1B", - "sideBar.foreground": "#bebebe", - "sideBar.border": "#252627FF", - "sideBarTitle.foreground": "#bebebe", - "sideBarSectionHeader.background": "#191A1B", - "sideBarSectionHeader.foreground": "#bebebe", - "sideBarSectionHeader.border": "#252627FF", - "titleBar.activeBackground": "#191A1B", - "titleBar.activeForeground": "#bebebe", - "titleBar.inactiveBackground": "#191A1B", + "sideBar.background": "#191B1D", + "sideBar.foreground": "#bfbfbf", + "sideBar.border": "#2A2B2CFF", + "sideBarTitle.foreground": "#bfbfbf", + "sideBarSectionHeader.background": "#191B1D", + "sideBarSectionHeader.foreground": "#bfbfbf", + "sideBarSectionHeader.border": "#2A2B2CFF", + "titleBar.activeBackground": "#191B1D", + "titleBar.activeForeground": "#bfbfbf", + "titleBar.inactiveBackground": "#191B1D", "titleBar.inactiveForeground": "#888888", - "titleBar.border": "#252627FF", - "menubar.selectionBackground": "#242526", - "menubar.selectionForeground": "#bebebe", - "menu.background": "#202122", - "menu.foreground": "#bebebe", - "menu.selectionBackground": "#007ABC26", - "menu.selectionForeground": "#bebebe", - "menu.separatorBackground": "#838485", - "menu.border": "#252627FF", - "commandCenter.foreground": "#bebebe", - "commandCenter.activeForeground": "#bebebe", - "commandCenter.background": "#191A1B", - "commandCenter.activeBackground": "#262728", - "commandCenter.border": "#323435", - "editor.background": "#121314", - "editor.foreground": "#BABDBE", + "titleBar.border": "#2A2B2CFF", + "menubar.selectionBackground": "#232627", + "menubar.selectionForeground": "#bfbfbf", + "menu.background": "#1F2223", + "menu.foreground": "#bfbfbf", + "menu.selectionBackground": "#498FAE26", + "menu.selectionForeground": "#bfbfbf", + "menu.separatorBackground": "#818485", + "menu.border": "#2A2B2CFF", + "commandCenter.foreground": "#bfbfbf", + "commandCenter.activeForeground": "#bfbfbf", + "commandCenter.background": "#191B1D", + "commandCenter.activeBackground": "#252829", + "commandCenter.border": "#333536", + "editor.background": "#121416", + "editor.foreground": "#BBBEBF", "editorLineNumber.foreground": "#858889", - "editorLineNumber.activeForeground": "#BABDBE", - "editorCursor.foreground": "#BABDBE", - "editor.selectionBackground": "#007ABC33", - "editor.inactiveSelectionBackground": "#007ABC80", - "editor.selectionHighlightBackground": "#007ABC1A", - "editor.wordHighlightBackground": "#007ABCB3", - "editor.wordHighlightStrongBackground": "#007ABCE6", - "editor.findMatchBackground": "#007ABC4D", - "editor.findMatchHighlightBackground": "#007ABC26", - "editor.findRangeHighlightBackground": "#242526", - "editor.hoverHighlightBackground": "#242526", - "editor.lineHighlightBackground": "#242526", - "editor.rangeHighlightBackground": "#242526", - "editorLink.activeForeground": "#007ABC", + "editorLineNumber.activeForeground": "#BBBEBF", + "editorCursor.foreground": "#BBBEBF", + "editor.selectionBackground": "#498FAE33", + "editor.inactiveSelectionBackground": "#498FAE80", + "editor.selectionHighlightBackground": "#498FAE1A", + "editor.wordHighlightBackground": "#498FAE33", + "editor.wordHighlightStrongBackground": "#498FAE33", + "editor.findMatchBackground": "#498FAE4D", + "editor.findMatchHighlightBackground": "#498FAE26", + "editor.findRangeHighlightBackground": "#232627", + "editor.hoverHighlightBackground": "#232627", + "editor.lineHighlightBackground": "#232627", + "editor.rangeHighlightBackground": "#232627", + "editorLink.activeForeground": "#4a8fad", "editorWhitespace.foreground": "#8888884D", - "editorIndentGuide.background": "#8384854D", - "editorIndentGuide.activeBackground": "#838485", + "editorIndentGuide.background": "#8184854D", + "editorIndentGuide.activeBackground": "#818485", "editorRuler.foreground": "#848484", "editorCodeLens.foreground": "#888888", - "editorBracketMatch.background": "#007ABC80", - "editorBracketMatch.border": "#252627FF", - "editorWidget.background": "#202122", - "editorWidget.border": "#252627FF", - "editorWidget.foreground": "#bebebe", - "editorSuggestWidget.background": "#202122", - "editorSuggestWidget.border": "#252627FF", - "editorSuggestWidget.foreground": "#bebebe", - "editorSuggestWidget.highlightForeground": "#bebebe", - "editorSuggestWidget.selectedBackground": "#007ABC26", - "editorHoverWidget.background": "#202122", - "editorHoverWidget.border": "#252627FF", - "peekView.border": "#252627FF", - "peekViewEditor.background": "#191A1B", - "peekViewEditor.matchHighlightBackground": "#007ABC33", - "peekViewResult.background": "#242526", - "peekViewResult.fileForeground": "#bebebe", + "editorBracketMatch.background": "#498FAE55", + "editorBracketMatch.border": "#2A2B2CFF", + "editorWidget.background": "#1F2223", + "editorWidget.border": "#2A2B2CFF", + "editorWidget.foreground": "#bfbfbf", + "editorSuggestWidget.background": "#1F2223", + "editorSuggestWidget.border": "#2A2B2CFF", + "editorSuggestWidget.foreground": "#bfbfbf", + "editorSuggestWidget.highlightForeground": "#bfbfbf", + "editorSuggestWidget.selectedBackground": "#498FAE26", + "editorHoverWidget.background": "#1F2223", + "editorHoverWidget.border": "#2A2B2CFF", + "peekView.border": "#2A2B2CFF", + "peekViewEditor.background": "#191B1D", + "peekViewEditor.matchHighlightBackground": "#498FAE33", + "peekViewResult.background": "#232627", + "peekViewResult.fileForeground": "#bfbfbf", "peekViewResult.lineForeground": "#888888", - "peekViewResult.matchHighlightBackground": "#007ABC33", - "peekViewResult.selectionBackground": "#007ABC26", - "peekViewResult.selectionForeground": "#bebebe", - "peekViewTitle.background": "#242526", + "peekViewResult.matchHighlightBackground": "#498FAE33", + "peekViewResult.selectionBackground": "#498FAE26", + "peekViewResult.selectionForeground": "#bfbfbf", + "peekViewTitle.background": "#232627", "peekViewTitleDescription.foreground": "#888888", - "peekViewTitleLabel.foreground": "#bebebe", - "editorGutter.background": "#121314", - "editorGutter.addedBackground": "#72C892", - "editorGutter.deletedBackground": "#F28772", - "diffEditor.insertedTextBackground": "#72C89233", - "diffEditor.removedTextBackground": "#F2877233", - "editorOverviewRuler.border": "#252627FF", - "editorOverviewRuler.findMatchForeground": "#007ABC99", - "editorOverviewRuler.modifiedForeground": "#5ba3e0", + "peekViewTitleLabel.foreground": "#bfbfbf", + "editorGutter.background": "#121416", + "editorGutter.addedBackground": "#71C792", + "editorGutter.deletedBackground": "#EF8773", + "diffEditor.insertedTextBackground": "#71C79233", + "diffEditor.removedTextBackground": "#EF877333", + "editorOverviewRuler.border": "#2A2B2CFF", + "editorOverviewRuler.findMatchForeground": "#4a8fad99", + "editorOverviewRuler.modifiedForeground": "#6ab890", "editorOverviewRuler.addedForeground": "#73c991", "editorOverviewRuler.deletedForeground": "#f48771", "editorOverviewRuler.errorForeground": "#f48771", "editorOverviewRuler.warningForeground": "#e5ba7d", - "panel.background": "#191A1B", - "panel.border": "#252627FF", - "panelTitle.activeBorder": "#007ABC", - "panelTitle.activeForeground": "#bebebe", + "panel.background": "#191B1D", + "panel.border": "#2A2B2CFF", + "panelTitle.activeBorder": "#498FAD", + "panelTitle.activeForeground": "#bfbfbf", "panelTitle.inactiveForeground": "#888888", - "statusBar.background": "#191A1B", - "statusBar.foreground": "#bebebe", - "statusBar.border": "#252627FF", - "statusBar.focusBorder": "#007ABCB3", - "statusBar.debuggingBackground": "#007ABC", + "statusBar.background": "#191B1D", + "statusBar.foreground": "#bfbfbf", + "statusBar.border": "#2A2B2CFF", + "statusBar.focusBorder": "#498FADB3", + "statusBar.debuggingBackground": "#498FAE", "statusBar.debuggingForeground": "#FFFFFF", - "statusBar.noFolderBackground": "#191A1B", - "statusBar.noFolderForeground": "#bebebe", - "statusBarItem.activeBackground": "#007ABC", - "statusBarItem.hoverBackground": "#262728", - "statusBarItem.focusBorder": "#007ABCB3", - "statusBarItem.prominentBackground": "#007ABC", + "statusBar.noFolderBackground": "#191B1D", + "statusBar.noFolderForeground": "#bfbfbf", + "statusBarItem.activeBackground": "#498FAE", + "statusBarItem.hoverBackground": "#252829", + "statusBarItem.focusBorder": "#498FADB3", + "statusBarItem.prominentBackground": "#498FAE", "statusBarItem.prominentForeground": "#FFFFFF", - "statusBarItem.prominentHoverBackground": "#007ABC", - "tab.activeBackground": "#121314", - "tab.activeForeground": "#bebebe", - "tab.inactiveBackground": "#191A1B", + "statusBarItem.prominentHoverBackground": "#498FAE", + "tab.activeBackground": "#121416", + "tab.activeForeground": "#bfbfbf", + "tab.inactiveBackground": "#191B1D", "tab.inactiveForeground": "#888888", - "tab.border": "#252627FF", - "tab.lastPinnedBorder": "#252627FF", + "tab.border": "#2A2B2CFF", + "tab.lastPinnedBorder": "#2A2B2CFF", "tab.activeBorder": "#121314", - "tab.hoverBackground": "#262728", - "tab.hoverForeground": "#bebebe", - "tab.unfocusedActiveBackground": "#121314", + "tab.hoverBackground": "#252829", + "tab.hoverForeground": "#bfbfbf", + "tab.unfocusedActiveBackground": "#121416", "tab.unfocusedActiveForeground": "#888888", - "tab.unfocusedInactiveBackground": "#191A1B", + "tab.unfocusedInactiveBackground": "#191B1D", "tab.unfocusedInactiveForeground": "#444444", - "editorGroupHeader.tabsBackground": "#191A1B", - "editorGroupHeader.tabsBorder": "#252627FF", + "editorGroupHeader.tabsBackground": "#191B1D", + "editorGroupHeader.tabsBorder": "#2A2B2CFF", "breadcrumb.foreground": "#888888", - "breadcrumb.background": "#121314", - "breadcrumb.focusForeground": "#bebebe", - "breadcrumb.activeSelectionForeground": "#bebebe", - "breadcrumbPicker.background": "#202122", - "notificationCenter.border": "#252627FF", - "notificationCenterHeader.foreground": "#bebebe", - "notificationCenterHeader.background": "#242526", - "notificationToast.border": "#252627FF", - "notifications.foreground": "#bebebe", - "notifications.background": "#202122", - "notifications.border": "#252627FF", - "notificationLink.foreground": "#007ABC", - "extensionButton.prominentBackground": "#007ABC", + "breadcrumb.background": "#121416", + "breadcrumb.focusForeground": "#bfbfbf", + "breadcrumb.activeSelectionForeground": "#bfbfbf", + "breadcrumbPicker.background": "#1F2223", + "notificationCenter.border": "#2A2B2CFF", + "notificationCenterHeader.foreground": "#bfbfbf", + "notificationCenterHeader.background": "#232627", + "notificationToast.border": "#2A2B2CFF", + "notifications.foreground": "#bfbfbf", + "notifications.background": "#1F2223", + "notifications.border": "#2A2B2CFF", + "notificationLink.foreground": "#4a8fad", + "extensionButton.prominentBackground": "#498FAE", "extensionButton.prominentForeground": "#FFFFFF", - "extensionButton.prominentHoverBackground": "#0080C5", - "pickerGroup.border": "#252627FF", - "pickerGroup.foreground": "#bebebe", - "quickInput.background": "#202122", - "quickInput.foreground": "#bebebe", - "quickInputList.focusBackground": "#007ABC26", - "quickInputList.focusForeground": "#bebebe", - "quickInputList.focusIconForeground": "#bebebe", - "quickInputList.hoverBackground": "#515253", - "terminal.selectionBackground": "#007ABC33", - "terminalCursor.foreground": "#bebebe", - "terminalCursor.background": "#191A1B", + "extensionButton.prominentHoverBackground": "#4D94B4", + "pickerGroup.border": "#2A2B2CFF", + "pickerGroup.foreground": "#bfbfbf", + "quickInput.background": "#1F2223", + "quickInput.foreground": "#bfbfbf", + "quickInputList.focusBackground": "#498FAE26", + "quickInputList.focusForeground": "#bfbfbf", + "quickInputList.focusIconForeground": "#bfbfbf", + "quickInputList.hoverBackground": "#505354", + "terminal.selectionBackground": "#498FAE33", + "terminalCursor.foreground": "#bfbfbf", + "terminalCursor.background": "#191B1D", "gitDecoration.addedResourceForeground": "#73c991", "gitDecoration.modifiedResourceForeground": "#e5ba7d", "gitDecoration.deletedResourceForeground": "#f48771", @@ -227,10 +227,10 @@ "gitDecoration.conflictingResourceForeground": "#f48771", "gitDecoration.stageModifiedResourceForeground": "#e5ba7d", "gitDecoration.stageDeletedResourceForeground": "#f48771", - "quickInputTitle.background": "#202122", - "quickInput.border": "#323435", - "chat.requestBubbleBackground": "#007ABC26", - "chat.requestBubbleHoverBackground": "#007ABC46" + "quickInputTitle.background": "#1F2223", + "quickInput.border": "#333536", + "chat.requestBubbleBackground": "#498FAE26", + "chat.requestBubbleHoverBackground": "#498FAE46" }, "tokenColors": [ { diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index 0ad9277604d..2ef9666b656 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -9,9 +9,9 @@ "descriptionForeground": "#666666", "icon.foreground": "#666666", "focusBorder": "#4466CCFF", - "textBlockQuote.background": "#F3F3F3", + "textBlockQuote.background": "#E9E9E9", "textBlockQuote.border": "#EEEEEE00", - "textCodeBlock.background": "#F3F3F3", + "textCodeBlock.background": "#E9E9E9", "textLink.foreground": "#3457C0", "textLink.activeForeground": "#395DC9", "textPreformat.foreground": "#666666", @@ -20,10 +20,10 @@ "button.foreground": "#FFFFFF", "button.hoverBackground": "#4F6FCF", "button.border": "#EEEEEE00", - "button.secondaryBackground": "#F3F3F3", + "button.secondaryBackground": "#E9E9E9", "button.secondaryForeground": "#202020", - "button.secondaryHoverBackground": "#4466CC", - "checkbox.background": "#F3F3F3", + "button.secondaryHoverBackground": "#F5F5F5", + "checkbox.background": "#E9E9E9", "checkbox.border": "#EEEEEE00", "checkbox.foreground": "#202020", "dropdown.background": "#F9F9F9", @@ -55,7 +55,7 @@ "progressBar.background": "#666666", "list.activeSelectionBackground": "#4466CC26", "list.activeSelectionForeground": "#202020", - "list.inactiveSelectionBackground": "#F3F3F3", + "list.inactiveSelectionBackground": "#E9E9E9", "list.inactiveSelectionForeground": "#202020", "list.hoverBackground": "#FFFFFF", "list.hoverForeground": "#202020", @@ -87,7 +87,7 @@ "titleBar.inactiveBackground": "#F9F9F9", "titleBar.inactiveForeground": "#666666", "titleBar.border": "#EEEEEE00", - "menubar.selectionBackground": "#F3F3F3", + "menubar.selectionBackground": "#E9E9E9", "menubar.selectionForeground": "#202020", "menu.background": "#FCFCFC", "menu.foreground": "#202020", @@ -109,13 +109,13 @@ "editor.inactiveSelectionBackground": "#4466CC80", "editor.selectionHighlightBackground": "#4466CC1A", "editor.wordHighlightBackground": "#4466CC33", - "editor.wordHighlightStrongBackground": "#4466CCE6", + "editor.wordHighlightStrongBackground": "#4466CC33", "editor.findMatchBackground": "#4466CC4D", "editor.findMatchHighlightBackground": "#4466CC26", - "editor.findRangeHighlightBackground": "#F3F3F3", - "editor.hoverHighlightBackground": "#F3F3F3", - "editor.lineHighlightBackground": "#F3F3F3", - "editor.rangeHighlightBackground": "#F3F3F3", + "editor.findRangeHighlightBackground": "#E9E9E9", + "editor.hoverHighlightBackground": "#E9E9E9", + "editor.lineHighlightBackground": "#E9E9E9", + "editor.rangeHighlightBackground": "#E9E9E9", "editorLink.activeForeground": "#4466CC", "editorWhitespace.foreground": "#6666664D", "editorIndentGuide.background": "#F4F4F44D", @@ -137,13 +137,13 @@ "peekView.border": "#EEEEEE00", "peekViewEditor.background": "#F9F9F9", "peekViewEditor.matchHighlightBackground": "#4466CC33", - "peekViewResult.background": "#F3F3F3", + "peekViewResult.background": "#E9E9E9", "peekViewResult.fileForeground": "#202020", "peekViewResult.lineForeground": "#666666", "peekViewResult.matchHighlightBackground": "#4466CC33", "peekViewResult.selectionBackground": "#4466CC26", "peekViewResult.selectionForeground": "#202020", - "peekViewTitle.background": "#F3F3F3", + "peekViewTitle.background": "#E9E9E9", "peekViewTitleDescription.foreground": "#666666", "peekViewTitleLabel.foreground": "#202020", "editorGutter.background": "#FDFDFD", @@ -199,7 +199,7 @@ "breadcrumbPicker.background": "#FCFCFC", "notificationCenter.border": "#EEEEEE00", "notificationCenterHeader.foreground": "#202020", - "notificationCenterHeader.background": "#F3F3F3", + "notificationCenterHeader.background": "#E9E9E9", "notificationToast.border": "#EEEEEE00", "notifications.foreground": "#202020", "notifications.background": "#FCFCFC", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index e475f6dbeb3..60c4dd98ee3 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -43,11 +43,11 @@ .monaco-workbench .part.statusbar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); z-index: 55; position: relative; } /* Quick Input (Command Palette) */ -.monaco-workbench .quick-input-widget { box-shadow: 0 0 20px rgba(0, 0, 0, 0.15) !important; border: none !important; border-radius: 12px; overflow: hidden; background: rgba(252, 252, 253, 0.5) !important; backdrop-filter: blur(20px) saturate(100%); -webkit-backdrop-filter: blur(40px) saturate(180%); } +.monaco-workbench .quick-input-widget { box-shadow: 0 0 20px rgba(0, 0, 0, 0.15) !important; border-radius: 12px; overflow: hidden; background: rgba(252, 252, 253, 0.5) !important; backdrop-filter: blur(20px) saturate(100%); -webkit-backdrop-filter: blur(40px) saturate(180%); } .monaco-workbench.vs-dark .quick-input-widget, -.monaco-workbench.hc-black .quick-input-widget { background: rgba(10, 10, 11, 0.85) !important; backdrop-filter: blur(40px) saturate(180%); -webkit-backdrop-filter: blur(40px) saturate(180%); box-shadow: 0 0 20px rgba(0, 0, 0, 0.15) !important; } +.monaco-workbench.hc-black .quick-input-widget { background: rgba(10, 10, 11, 0.5) !important; backdrop-filter: blur(40px) saturate(180%); -webkit-backdrop-filter: blur(40px) saturate(180%); box-shadow: 0 0 20px rgba(0, 0, 0, 0.15) !important; } .monaco-workbench .quick-input-widget .monaco-list-rows { background-color: transparent !important; } -.monaco-workbench .quick-input-widget, +/* .monaco-workbench .quick-input-widget, */ /* .monaco-workbench .quick-input-widget *, */ .monaco-workbench .quick-input-widget .quick-input-header, .monaco-workbench .quick-input-widget .quick-input-list, @@ -96,6 +96,10 @@ .monaco-workbench.vs .monaco-menu .monaco-action-bar.vertical, .monaco-workbench.vs .context-view .monaco-menu { background: rgba(252, 252, 253, 0.85) !important; } +.monaco-workbench .action-widget { background: rgba(252, 252, 253, 0.5) !important; backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%);} +.monaco-workbench.vs-dark .action-widget { background: rgba(10, 10, 11, 0.5) !important; backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%);} +.monaco-workbench .action-widget .action-widget-action-bar {background: transparent;} + /* Suggest Widget */ .monaco-workbench .monaco-editor .suggest-widget { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 12px; overflow: hidden; backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); } .monaco-workbench.vs-dark .monaco-editor .suggest-widget, @@ -156,7 +160,7 @@ .monaco-workbench .pane-body.integrated-terminal { box-shadow: inset 0 0 4px rgba(255, 255, 255, 0.1); } /* SCM */ -.monaco-workbench .scm-view .scm-provider { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); border-radius: 6px; margin: 4px; } +.monaco-workbench .scm-view .scm-provider { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); border-radius: 6px; } /* Debug Toolbar */ .monaco-workbench .debug-toolbar { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 8px; } @@ -223,18 +227,13 @@ box-shadow: none; background-color: transparent; } -/* .monaco-workbench .part.titlebar .command-center, -.monaco-workbench .part.titlebar .command-center *, -.monaco-workbench .part.titlebar .command-center-center, -.monaco-workbench .part.titlebar .command-center .action-item, -.monaco-workbench .part.titlebar .command-center .search-icon, -.monaco-workbench .part.titlebar .command-center .search-label { border: none !important; border-color: transparent !important; outline: none !important; } */ /* Remove Borders */ -.monaco-workbench .part.sidebar { border-right: none !important; border-left: none !important; } -.monaco-workbench .part.auxiliarybar { border-right: none !important; border-left: none !important; } +.monaco-workbench.vs .part.sidebar { border-right: none !important; border-left: none !important; } +.monaco-workbench.vs .part.auxiliarybar { border-right: none !important; border-left: none !important; } .monaco-workbench .part.panel { border-top: none !important; } -.monaco-workbench .part.activitybar { border-right: none !important; border-left: none !important; } -.monaco-workbench .part.titlebar { border-bottom: none !important; } -.monaco-workbench .part.statusbar { border-top: none !important; } +.monaco-workbench.vs .part.activitybar { border-right: none !important; border-left: none !important; } +.monaco-workbench.vs .part.titlebar { border-bottom: none !important; } +.monaco-workbench.vs .part.statusbar { border-top: none !important; } .monaco-workbench .part.editor > .content .editor-group-container { border: none !important; } +.monaco-workbench .pane-composite-part:not(.empty) > .header { border-bottom: none !important; } From 8a2c71b62683e9a2d779cb328ed476e5012e9e23 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 16 Jan 2026 16:59:00 +0000 Subject: [PATCH 090/387] Remove unused custom color definition from styles.css for cleaner code --- extensions/theme-2026/themes/styles.css | 5 ----- 1 file changed, 5 deletions(-) diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 60c4dd98ee3..589033dccf3 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -5,11 +5,6 @@ /* Stealth Shadows - shadow-based depth for UI elements, controlled by workbench.stealthShadows.enabled */ -/* styles.css */ -.monaco-workbench { - --my-custom-color: blue; -} - /* Activity Bar */ .monaco-workbench .part.activitybar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); z-index: 50; position: relative; } .monaco-workbench.activitybar-right .part.activitybar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); } From 6921e3e84a2d72caa2da89707a067bed6593cb89 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 16 Jan 2026 16:59:50 +0000 Subject: [PATCH 091/387] Remove theme customization and README files for cleaner repository structure --- extensions/theme-2026/CUSTOMIZATION.md | 91 -------------------------- extensions/theme-2026/README.md | 55 ---------------- 2 files changed, 146 deletions(-) delete mode 100644 extensions/theme-2026/CUSTOMIZATION.md delete mode 100644 extensions/theme-2026/README.md diff --git a/extensions/theme-2026/CUSTOMIZATION.md b/extensions/theme-2026/CUSTOMIZATION.md deleted file mode 100644 index 03c07d0e2c0..00000000000 --- a/extensions/theme-2026/CUSTOMIZATION.md +++ /dev/null @@ -1,91 +0,0 @@ -# Theme Customization Guide - -The 2026 theme supports granular customization through **variables** and **overrides** in the config files. - -## Variables - -Define reusable color values in the `variables` section that can be referenced throughout your entire config: - -```json -{ - "variables": { - "myBlue": "#0066DD", - "myRed": "#DD0000", - "sidebarBg": "#161616" - } -} -``` - -Variables can be used in: -- **Config sections**: `textConfig`, `backgroundConfig`, `editorConfig`, `panelConfig`, `baseColors` -- **Overrides**: Any VS Code theme color property - -## Overrides - -Override any VS Code theme color property in the `overrides` section. You can use: -- Hex colors directly: `"#161616"` -- Variable references: `"${myBlue}"` - -```json -{ - "overrides": { - "sideBar.background": "${sidebarBg}", - "activityBar.background": "#161616", - "statusBar.background": "${myBlue}" - } -} -``` - -## Example Configuration - -**theme.config.dark.json:** - -```json -{ - "paletteScale": 21, - "accentUsage": "interactive-and-status", - ... - "editorConfig": { - "background": "${darkBlue}", - "foreground": "${textPrimary}" - }, - "backgroundConfig": { - "primary": "${primaryBg}", - "secondary": "${secondaryBg}" - }, - "variables": { - "darkBlue": "#001133", - "brightAccent": "#00AAFF", - "primaryBg": "#161616", - "secondaryBg": "#222222", - "textPrimary": "#cccccc" - }, - "overrides": { - "focusBorder": "${brightAccent}", - "button.background": "#007ACC" - } -} -``` - -## Finding Theme Properties - -To find available theme color properties: -1. Open Command Palette (Cmd+Shift+P) -2. Run "Developer: Generate Color Theme From Current Settings" -3. View generated theme to see all available properties - -Or refer to the [VS Code Theme Color Reference](https://code.visualstudio.com/api/references/theme-color). - -## Workflow - -1. Edit `theme.config.dark.json` or `theme.config.light.json` -2. Add variables and overrides sections -3. Run `npm run build` to regenerate themes -4. Reload VS Code to see changes - -## Tips - -- **Variables** help avoid repeating the same color values -- Use **overrides** to fine-tune specific elements without modifying the generator code -- Changes in config files persist across theme updates -- Both variants (light/dark) support independent variables and overrides diff --git a/extensions/theme-2026/README.md b/extensions/theme-2026/README.md deleted file mode 100644 index 59d0ab9b7d5..00000000000 --- a/extensions/theme-2026/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# 2026 Themes - -Modern, minimal light and dark themes for VS Code with a consistent neutral palette and accessible color contrast. - -> **Note**: These themes are generated using an external theme generator. The source code for the generator is maintained in a separate repository: [vscode-2026-theme-generator](../../../vscode-2026-theme-generator) - -## Design Philosophy - -- **Minimal and modern**: Clean, distraction-free interface -- **Consistent palette**: Limited base colors (5 neutral shades + accent) for visual coherence -- **Accessible**: WCAG AA compliant contrast ratios (minimum 4.5:1 for text) -- **Generated externally**: Themes are generated from a TypeScript-based generator with configurable color palettes - -## Color Palette - -### Light Theme - -| Purpose | Color | Usage | -|---------|-------|-------| -| Text Primary | `#1A1A1A` | Main text content | -| Text Secondary | `#6B6B6B` | Secondary text, line numbers | -| Background Primary | `#FFFFFF` | Main editor background | -| Background Secondary | `#F5F5F5` | Sidebars, inactive tabs | -| Border Default | `#848484` | Component borders | -| Accent | `#0066CC` | Interactive elements, focus states | - -### Dark Theme - -| Purpose | Color | Usage | -|---------|-------|-------| -| Text Primary | `#bbbbbb` | Main text content | -| Text Secondary | `#888888` | Secondary text, line numbers | -| Background Primary | `#191919` | Main editor background | -| Background Secondary | `#242424` | Sidebars, inactive tabs | -| Border Default | `#848484` | Component borders | -| Accent | `#007ACC` | Interactive elements, focus states | - -## Modifying These Themes - -These theme files are **generated** and should not be edited directly. To customize or modify the themes: - -1. Navigate to the theme generator repository (one level up from vscode root) -2. Modify the configuration files (`theme.config.light.json` and `theme.config.dark.json`) -3. Run the generator to create new theme files -4. Copy the generated files back to this directory - -See the [theme generator README](../../../vscode-2026-theme-generator/README.md) for detailed documentation on configuration options, color customization, and the generation process. - -## Accessibility - -All text/background combinations meet WCAG AA standards (4.5:1 contrast ratio minimum). - -## License - -MIT From f75062a3c2491dc15e5f37e9f43ce2ede2556cf6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Jan 2026 09:16:25 -0800 Subject: [PATCH 092/387] Fix test expectations --- .../test/electron-browser/runInTerminalTool.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts index 25a032e8e24..0d479c0ec7b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts @@ -453,7 +453,7 @@ suite('RunInTerminalTool', () => { explanation: 'Start watching for file changes', isBackground: true }); - assertConfirmationRequired(result, 'Run `bash` command? (background terminal)'); + assertConfirmationRequired(result, 'Run `bash` command in background?'); }); test('should auto-approve background commands in allow list', async () => { From c16546a2402591917280d569f72ef26cecaa3fb9 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 16 Jan 2026 19:22:21 +0100 Subject: [PATCH 093/387] Call provideWorkspaceContext initially (#288425) Fixes #280657 --- src/vs/workbench/api/common/extHostChatContext.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/api/common/extHostChatContext.ts b/src/vs/workbench/api/common/extHostChatContext.ts index 761fca70dbb..74710e0309e 100644 --- a/src/vs/workbench/api/common/extHostChatContext.ts +++ b/src/vs/workbench/api/common/extHostChatContext.ts @@ -167,7 +167,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext if (!provider.onDidChangeWorkspaceChatContext || !provider.provideWorkspaceChatContext) { return; } - disposables.add(provider.onDidChangeWorkspaceChatContext(async () => { + const provideWorkspaceContext = async () => { const workspaceContexts = await provider.provideWorkspaceChatContext!(CancellationToken.None); const resolvedContexts: IChatContextItem[] = []; for (const item of workspaceContexts ?? []) { @@ -183,9 +183,12 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext const resolved = await this._doResolve(provider, contextItem, item, CancellationToken.None); resolvedContexts.push(resolved); } + return this._proxy.$updateWorkspaceContextItems(handle, resolvedContexts); + }; - this._proxy.$updateWorkspaceContextItems(handle, resolvedContexts); - })); + disposables.add(provider.onDidChangeWorkspaceChatContext(async () => provideWorkspaceContext())); + // kick off initial workspace context fetch + provideWorkspaceContext(); } private _getProvider(handle: number): vscode.ChatContextProvider { From ea6ce12828c272697926762402173c0395ad9511 Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Fri, 16 Jan 2026 19:25:28 +0100 Subject: [PATCH 094/387] inline completions: fix passing changeHint context (#288428) --- src/vs/workbench/api/common/extHostLanguageFeatures.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 4311937f5c2..826c9ef4644 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1406,6 +1406,7 @@ class InlineCompletionAdapter { requestUuid: context.requestUuid, requestIssuedDateTime: context.requestIssuedDateTime, earliestShownDateTime: context.earliestShownDateTime, + changeHint: context.changeHint, }, token); if (!result) { From 6c661bc76d8202bc54959c84739ebf5ff3b71af2 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 19:25:51 +0100 Subject: [PATCH 095/387] Merge pull request #288330 from microsoft/copilot/fix-default-expanded-file-nodes Expand file nodes by default in breakpoints tree view --- .../workbench/contrib/debug/browser/breakpointsView.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 6c86e01fcd5..fdb06fe25e3 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -279,7 +279,7 @@ export class BreakpointsView extends ViewPane { } })); - // Track collapsed state and update size (items are collapsed by default) + // Track collapsed state and update size (items are expanded by default) this._register(this.tree.onDidChangeCollapseState(e => { const element = e.node.element; if (element instanceof BreakpointsFolderItem) { @@ -542,15 +542,9 @@ export class BreakpointsView extends ViewPane { result.push({ element: folderItem, incompressible: false, - collapsed: this.collapsedState.has(folderItem.getId()) || !this.collapsedState.has(`_init_${folderItem.getId()}`), + collapsed: this.collapsedState.has(folderItem.getId()), children }); - - // Mark as initialized (will be collapsed by default on first render) - if (!this.collapsedState.has(`_init_${folderItem.getId()}`)) { - this.collapsedState.add(`_init_${folderItem.getId()}`); - this.collapsedState.add(folderItem.getId()); - } } } else { // Flat mode - just add all source breakpoints From 739c2c5a35a84f0364b30d9cfec97d7614a99f3c Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 16 Jan 2026 13:49:57 -0500 Subject: [PATCH 096/387] fix leak (#288434) fix #288432 --- .../toolInvocationParts/chatTerminalToolProgressPart.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts index 6ee9001d371..4117c381f8d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts @@ -1006,7 +1006,7 @@ class ChatTerminalToolOutputSection extends Disposable { return true; } await liveTerminalInstance.xtermReadyPromise; - if (liveTerminalInstance.isDisposed || !liveTerminalInstance.xterm) { + if (this._store.isDisposed || liveTerminalInstance.isDisposed || !liveTerminalInstance.xterm) { this._disposeLiveMirror(); return false; } @@ -1040,6 +1040,9 @@ class ChatTerminalToolOutputSection extends Disposable { this._layoutOutput(snapshot.lineCount ?? 0, this._lastRenderedMaxColumnWidth); return; } + if (this._store.isDisposed) { + return; + } dom.clearNode(this._terminalContainer); this._snapshotMirror = this._register(this._instantiationService.createInstance(DetachedTerminalSnapshotMirror, snapshot, this._getStoredTheme)); await this._snapshotMirror.attach(this._terminalContainer); From 5ec07a920747d0d710363bedbd32bc36b69a1839 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:00:03 -0800 Subject: [PATCH 097/387] Add node presenter Fixes #287773 --- .../nodeCommandLinePresenter.ts | 64 ++++++ .../browser/tools/runInTerminalTool.ts | 2 + .../browser/nodeCommandLinePresenter.test.ts | 203 ++++++++++++++++++ 3 files changed, 269 insertions(+) create mode 100644 src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/nodeCommandLinePresenter.ts create mode 100644 src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/nodeCommandLinePresenter.test.ts diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/nodeCommandLinePresenter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/nodeCommandLinePresenter.ts new file mode 100644 index 00000000000..ec6a5125daa --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/nodeCommandLinePresenter.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { OperatingSystem } from '../../../../../../../base/common/platform.js'; +import { isPowerShell } from '../../runInTerminalHelpers.js'; +import type { ICommandLinePresenter, ICommandLinePresenterOptions, ICommandLinePresenterResult } from './commandLinePresenter.js'; + +/** + * Command line presenter for Node.js inline commands (`node -e "..."`). + * Extracts the JavaScript code and sets up JavaScript syntax highlighting. + */ +export class NodeCommandLinePresenter implements ICommandLinePresenter { + present(options: ICommandLinePresenterOptions): ICommandLinePresenterResult | undefined { + const extractedNode = extractNodeCommand(options.commandLine, options.shell, options.os); + if (extractedNode) { + return { + commandLine: extractedNode, + language: 'javascript', + languageDisplayName: 'Node.js', + }; + } + return undefined; + } +} + +/** + * Extracts the JavaScript code from a `node -e "..."` or `node -e '...'` command, + * returning the code with properly unescaped quotes. + * + * @param commandLine The full command line to parse + * @param shell The shell path (to determine quote escaping style) + * @param os The operating system + * @returns The extracted JavaScript code, or undefined if not a node -e/--eval command + */ +export function extractNodeCommand(commandLine: string, shell: string, os: OperatingSystem): string | undefined { + // Match node/nodejs -e/--eval "..." pattern (double quotes) + const doubleQuoteMatch = commandLine.match(/^node(?:js)?\s+(?:-e|--eval)\s+"(?.+)"$/s); + if (doubleQuoteMatch?.groups?.code) { + let jsCode = doubleQuoteMatch.groups.code.trim(); + + // Unescape quotes based on shell type + if (isPowerShell(shell, os)) { + // PowerShell uses backtick-quote (`") to escape quotes inside double-quoted strings + jsCode = jsCode.replace(/`"/g, '"'); + } else { + // Bash/sh/zsh use backslash-quote (\") + jsCode = jsCode.replace(/\\"/g, '"'); + } + + return jsCode; + } + + // Match node/nodejs -e/--eval '...' pattern (single quotes) + // Single quotes in bash/sh/zsh are literal - no escaping inside + // Single quotes in PowerShell are also literal + const singleQuoteMatch = commandLine.match(/^node(?:js)?\s+(?:-e|--eval)\s+'(?.+)'$/s); + if (singleQuoteMatch?.groups?.code) { + return singleQuoteMatch.groups.code.trim(); + } + + return undefined; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index 0b1c8723b8a..e231d2ffeb6 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -39,6 +39,7 @@ import { RichExecuteStrategy } from '../executeStrategy/richExecuteStrategy.js'; import { getOutput } from '../outputHelpers.js'; import { extractCdPrefix, isFish, isPowerShell, isWindowsPowerShell, isZsh } from '../runInTerminalHelpers.js'; import type { ICommandLinePresenter } from './commandLinePresenter/commandLinePresenter.js'; +import { NodeCommandLinePresenter } from './commandLinePresenter/nodeCommandLinePresenter.js'; import { PythonCommandLinePresenter } from './commandLinePresenter/pythonCommandLinePresenter.js'; import { RunInTerminalToolTelemetry } from '../runInTerminalToolTelemetry.js'; import { ShellIntegrationQuality, ToolTerminalCreator, type IToolTerminal } from '../toolTerminalCreator.js'; @@ -334,6 +335,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { this._register(this._instantiationService.createInstance(CommandLineAutoApproveAnalyzer, this._treeSitterCommandParser, this._telemetry, (message, args) => this._logService.info(`RunInTerminalTool#CommandLineAutoApproveAnalyzer: ${message}`, args))), ]; this._commandLinePresenters = [ + new NodeCommandLinePresenter(), new PythonCommandLinePresenter(), ]; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/nodeCommandLinePresenter.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/nodeCommandLinePresenter.test.ts new file mode 100644 index 00000000000..5beabdd32ce --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/nodeCommandLinePresenter.test.ts @@ -0,0 +1,203 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ok, strictEqual } from 'assert'; +import { extractNodeCommand, NodeCommandLinePresenter } from '../../browser/tools/commandLinePresenter/nodeCommandLinePresenter.js'; +import { OperatingSystem } from '../../../../../../base/common/platform.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; + +suite('extractNodeCommand', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('basic extraction', () => { + test('should extract simple node -e command with double quotes', () => { + const result = extractNodeCommand(`node -e "console.log('hello')"`, 'bash', OperatingSystem.Linux); + strictEqual(result, `console.log('hello')`); + }); + + test('should extract nodejs -e command', () => { + const result = extractNodeCommand(`nodejs -e "console.log('hello')"`, 'bash', OperatingSystem.Linux); + strictEqual(result, `console.log('hello')`); + }); + + test('should extract node --eval command', () => { + const result = extractNodeCommand(`node --eval "console.log('hello')"`, 'bash', OperatingSystem.Linux); + strictEqual(result, `console.log('hello')`); + }); + + test('should extract nodejs --eval command', () => { + const result = extractNodeCommand(`nodejs --eval "console.log('hello')"`, 'bash', OperatingSystem.Linux); + strictEqual(result, `console.log('hello')`); + }); + + test('should return undefined for non-node commands', () => { + const result = extractNodeCommand('echo hello', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + + test('should return undefined for node without -e flag', () => { + const result = extractNodeCommand('node script.js', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + + test('should extract node -e with single quotes', () => { + const result = extractNodeCommand(`node -e 'console.log("hello")'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'console.log("hello")'); + }); + + test('should extract nodejs -e with single quotes', () => { + const result = extractNodeCommand(`nodejs -e 'const x = 1; console.log(x)'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'const x = 1; console.log(x)'); + }); + + test('should extract node --eval with single quotes', () => { + const result = extractNodeCommand(`node --eval 'console.log("hello")'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'console.log("hello")'); + }); + }); + + suite('quote unescaping - Bash', () => { + test('should unescape backslash-escaped quotes in bash', () => { + const result = extractNodeCommand('node -e "console.log(\\"hello\\")"', 'bash', OperatingSystem.Linux); + strictEqual(result, 'console.log("hello")'); + }); + + test('should handle multiple escaped quotes', () => { + const result = extractNodeCommand('node -e "const x = \\"hello\\"; console.log(x)"', 'bash', OperatingSystem.Linux); + strictEqual(result, 'const x = "hello"; console.log(x)'); + }); + }); + + suite('single quotes - literal content', () => { + test('should preserve content literally in single quotes (no unescaping)', () => { + // Single quotes in bash are literal - backslashes are not escape sequences + const result = extractNodeCommand(`node -e 'console.log(\\"hello\\")'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'console.log(\\"hello\\")'); + }); + + test('should handle single quotes in PowerShell', () => { + const result = extractNodeCommand(`node -e 'console.log("hello")'`, 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'console.log("hello")'); + }); + + test('should extract multiline code in single quotes', () => { + const code = `node -e 'for (let i = 0; i < 3; i++) {\n console.log(i);\n}'`; + const result = extractNodeCommand(code, 'bash', OperatingSystem.Linux); + strictEqual(result, `for (let i = 0; i < 3; i++) {\n console.log(i);\n}`); + }); + }); + + suite('quote unescaping - PowerShell', () => { + test('should unescape backtick-escaped quotes in PowerShell', () => { + const result = extractNodeCommand('node -e "console.log(`"hello`")"', 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'console.log("hello")'); + }); + + test('should handle multiple backtick-escaped quotes', () => { + const result = extractNodeCommand('node -e "const x = `"hello`"; console.log(x)"', 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'const x = "hello"; console.log(x)'); + }); + + test('should not unescape backslash quotes in PowerShell', () => { + const result = extractNodeCommand('node -e "console.log(\\"hello\\")"', 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'console.log(\\"hello\\")'); + }); + }); + + suite('multiline code', () => { + test('should extract multiline JavaScript code', () => { + const code = `node -e "for (let i = 0; i < 3; i++) {\n console.log(i);\n}"`; + const result = extractNodeCommand(code, 'bash', OperatingSystem.Linux); + strictEqual(result, `for (let i = 0; i < 3; i++) {\n console.log(i);\n}`); + }); + }); + + suite('edge cases', () => { + test('should handle code with trailing whitespace trimmed', () => { + const result = extractNodeCommand('node -e " console.log(1) "', 'bash', OperatingSystem.Linux); + strictEqual(result, 'console.log(1)'); + }); + + test('should return undefined for empty code', () => { + const result = extractNodeCommand('node -e ""', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + + test('should return undefined when quotes are unmatched', () => { + const result = extractNodeCommand('node -e "console.log(1)', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + }); +}); + +suite('NodeCommandLinePresenter', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + const presenter = new NodeCommandLinePresenter(); + + test('should return JavaScript presentation for node -e command', () => { + const result = presenter.present({ + commandLine: `node -e "console.log('hello')"`, + shell: 'bash', + os: OperatingSystem.Linux + }); + ok(result); + strictEqual(result.commandLine, `console.log('hello')`); + strictEqual(result.language, 'javascript'); + strictEqual(result.languageDisplayName, 'Node.js'); + }); + + test('should return JavaScript presentation for nodejs -e command', () => { + const result = presenter.present({ + commandLine: `nodejs -e 'const x = 1; console.log(x)'`, + shell: 'bash', + os: OperatingSystem.Linux + }); + ok(result); + strictEqual(result.commandLine, 'const x = 1; console.log(x)'); + strictEqual(result.language, 'javascript'); + strictEqual(result.languageDisplayName, 'Node.js'); + }); + + test('should return JavaScript presentation for node --eval command', () => { + const result = presenter.present({ + commandLine: `node --eval "console.log('hello')"`, + shell: 'bash', + os: OperatingSystem.Linux + }); + ok(result); + strictEqual(result.commandLine, `console.log('hello')`); + strictEqual(result.language, 'javascript'); + strictEqual(result.languageDisplayName, 'Node.js'); + }); + + test('should return undefined for non-node commands', () => { + const result = presenter.present({ + commandLine: 'echo hello', + shell: 'bash', + os: OperatingSystem.Linux + }); + strictEqual(result, undefined); + }); + + test('should return undefined for regular node script execution', () => { + const result = presenter.present({ + commandLine: 'node script.js', + shell: 'bash', + os: OperatingSystem.Linux + }); + strictEqual(result, undefined); + }); + + test('should handle PowerShell backtick escaping', () => { + const result = presenter.present({ + commandLine: 'node -e "console.log(`"hello`")"', + shell: 'pwsh', + os: OperatingSystem.Windows + }); + ok(result); + strictEqual(result.commandLine, 'console.log("hello")'); + }); +}); From 62ecf835b0172d5a2048f100d806a24c7a206faf Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:10:18 -0800 Subject: [PATCH 098/387] Add ruby presenter Fixes #288360 --- .../rubyCommandLinePresenter.ts | 64 ++++++++ .../browser/tools/runInTerminalTool.ts | 2 + .../browser/rubyCommandLinePresenter.test.ts | 153 ++++++++++++++++++ 3 files changed, 219 insertions(+) create mode 100644 src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts create mode 100644 src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/rubyCommandLinePresenter.test.ts diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts new file mode 100644 index 00000000000..14f087f85eb --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { OperatingSystem } from '../../../../../../../base/common/platform.js'; +import { isPowerShell } from '../../runInTerminalHelpers.js'; +import type { ICommandLinePresenter, ICommandLinePresenterOptions, ICommandLinePresenterResult } from './commandLinePresenter.js'; + +/** + * Command line presenter for Ruby inline commands (`ruby -e "..."`). + * Extracts the Ruby code and sets up Ruby syntax highlighting. + */ +export class RubyCommandLinePresenter implements ICommandLinePresenter { + present(options: ICommandLinePresenterOptions): ICommandLinePresenterResult | undefined { + const extractedRuby = extractRubyCommand(options.commandLine, options.shell, options.os); + if (extractedRuby) { + return { + commandLine: extractedRuby, + language: 'ruby', + languageDisplayName: 'Ruby', + }; + } + return undefined; + } +} + +/** + * Extracts the Ruby code from a `ruby -e "..."` or `ruby -e '...'` command, + * returning the code with properly unescaped quotes. + * + * @param commandLine The full command line to parse + * @param shell The shell path (to determine quote escaping style) + * @param os The operating system + * @returns The extracted Ruby code, or undefined if not a ruby -e command + */ +export function extractRubyCommand(commandLine: string, shell: string, os: OperatingSystem): string | undefined { + // Match ruby -e "..." pattern (double quotes) + const doubleQuoteMatch = commandLine.match(/^ruby\s+-e\s+"(?.+)"$/s); + if (doubleQuoteMatch?.groups?.code) { + let rubyCode = doubleQuoteMatch.groups.code.trim(); + + // Unescape quotes based on shell type + if (isPowerShell(shell, os)) { + // PowerShell uses backtick-quote (`") to escape quotes inside double-quoted strings + rubyCode = rubyCode.replace(/`"/g, '"'); + } else { + // Bash/sh/zsh use backslash-quote (\") + rubyCode = rubyCode.replace(/\\"/g, '"'); + } + + return rubyCode; + } + + // Match ruby -e '...' pattern (single quotes) + // Single quotes in bash/sh/zsh are literal - no escaping inside + // Single quotes in PowerShell are also literal + const singleQuoteMatch = commandLine.match(/^ruby\s+-e\s+'(?.+)'$/s); + if (singleQuoteMatch?.groups?.code) { + return singleQuoteMatch.groups.code.trim(); + } + + return undefined; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index e231d2ffeb6..a806534880f 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -41,6 +41,7 @@ import { extractCdPrefix, isFish, isPowerShell, isWindowsPowerShell, isZsh } fro import type { ICommandLinePresenter } from './commandLinePresenter/commandLinePresenter.js'; import { NodeCommandLinePresenter } from './commandLinePresenter/nodeCommandLinePresenter.js'; import { PythonCommandLinePresenter } from './commandLinePresenter/pythonCommandLinePresenter.js'; +import { RubyCommandLinePresenter } from './commandLinePresenter/rubyCommandLinePresenter.js'; import { RunInTerminalToolTelemetry } from '../runInTerminalToolTelemetry.js'; import { ShellIntegrationQuality, ToolTerminalCreator, type IToolTerminal } from '../toolTerminalCreator.js'; import { TreeSitterCommandParser, TreeSitterCommandParserLanguage } from '../treeSitterCommandParser.js'; @@ -337,6 +338,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { this._commandLinePresenters = [ new NodeCommandLinePresenter(), new PythonCommandLinePresenter(), + new RubyCommandLinePresenter(), ]; // Clear out warning accepted state if the setting is disabled diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/rubyCommandLinePresenter.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/rubyCommandLinePresenter.test.ts new file mode 100644 index 00000000000..c6d1b7a7d66 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/rubyCommandLinePresenter.test.ts @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ok, strictEqual } from 'assert'; +import { extractRubyCommand, RubyCommandLinePresenter } from '../../browser/tools/commandLinePresenter/rubyCommandLinePresenter.js'; +import { OperatingSystem } from '../../../../../../base/common/platform.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; + +suite('extractRubyCommand', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('basic extraction', () => { + test('should extract simple ruby -e command with double quotes', () => { + const result = extractRubyCommand(`ruby -e "puts 'hello'"`, 'bash', OperatingSystem.Linux); + strictEqual(result, `puts 'hello'`); + }); + + test('should return undefined for non-ruby commands', () => { + const result = extractRubyCommand('echo hello', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + + test('should return undefined for ruby without -e flag', () => { + const result = extractRubyCommand('ruby script.rb', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + + test('should extract ruby -e with single quotes', () => { + const result = extractRubyCommand(`ruby -e 'puts "hello"'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'puts "hello"'); + }); + }); + + suite('quote unescaping - Bash', () => { + test('should unescape backslash-escaped quotes in bash', () => { + const result = extractRubyCommand('ruby -e "puts \\"hello\\""', 'bash', OperatingSystem.Linux); + strictEqual(result, 'puts "hello"'); + }); + + test('should handle multiple escaped quotes', () => { + const result = extractRubyCommand('ruby -e "x = \\"hello\\"; puts x"', 'bash', OperatingSystem.Linux); + strictEqual(result, 'x = "hello"; puts x'); + }); + }); + + suite('single quotes - literal content', () => { + test('should preserve content literally in single quotes (no unescaping)', () => { + const result = extractRubyCommand(`ruby -e 'puts \\"hello\\"'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'puts \\"hello\\"'); + }); + + test('should handle single quotes in PowerShell', () => { + const result = extractRubyCommand(`ruby -e 'puts "hello"'`, 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'puts "hello"'); + }); + + test('should extract multiline code in single quotes', () => { + const code = `ruby -e '3.times do |i|\n puts i\nend'`; + const result = extractRubyCommand(code, 'bash', OperatingSystem.Linux); + strictEqual(result, `3.times do |i|\n puts i\nend`); + }); + }); + + suite('quote unescaping - PowerShell', () => { + test('should unescape backtick-escaped quotes in PowerShell', () => { + const result = extractRubyCommand('ruby -e "puts `"hello`""', 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'puts "hello"'); + }); + + test('should handle multiple backtick-escaped quotes', () => { + const result = extractRubyCommand('ruby -e "x = `"hello`"; puts x"', 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'x = "hello"; puts x'); + }); + + test('should not unescape backslash quotes in PowerShell', () => { + const result = extractRubyCommand('ruby -e "puts \\"hello\\""', 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'puts \\"hello\\"'); + }); + }); + + suite('multiline code', () => { + test('should extract multiline Ruby code', () => { + const code = `ruby -e "3.times do |i|\n puts i\nend"`; + const result = extractRubyCommand(code, 'bash', OperatingSystem.Linux); + strictEqual(result, `3.times do |i|\n puts i\nend`); + }); + }); + + suite('edge cases', () => { + test('should handle code with trailing whitespace trimmed', () => { + const result = extractRubyCommand('ruby -e " puts 1 "', 'bash', OperatingSystem.Linux); + strictEqual(result, 'puts 1'); + }); + + test('should return undefined for empty code', () => { + const result = extractRubyCommand('ruby -e ""', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + + test('should return undefined when quotes are unmatched', () => { + const result = extractRubyCommand('ruby -e "puts 1', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + }); +}); + +suite('RubyCommandLinePresenter', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + const presenter = new RubyCommandLinePresenter(); + + test('should return Ruby presentation for ruby -e command', () => { + const result = presenter.present({ + commandLine: `ruby -e "puts 'hello'"`, + shell: 'bash', + os: OperatingSystem.Linux + }); + ok(result); + strictEqual(result.commandLine, `puts 'hello'`); + strictEqual(result.language, 'ruby'); + strictEqual(result.languageDisplayName, 'Ruby'); + }); + + test('should return undefined for non-ruby commands', () => { + const result = presenter.present({ + commandLine: 'echo hello', + shell: 'bash', + os: OperatingSystem.Linux + }); + strictEqual(result, undefined); + }); + + test('should return undefined for regular ruby script execution', () => { + const result = presenter.present({ + commandLine: 'ruby script.rb', + shell: 'bash', + os: OperatingSystem.Linux + }); + strictEqual(result, undefined); + }); + + test('should handle PowerShell backtick escaping', () => { + const result = presenter.present({ + commandLine: 'ruby -e "puts `"hello`""', + shell: 'pwsh', + os: OperatingSystem.Windows + }); + ok(result); + strictEqual(result.commandLine, 'puts "hello"'); + }); +}); From 1b4cd523aac3670395dba12ae917e4e1fe67ddf7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 16 Jan 2026 20:15:07 +0100 Subject: [PATCH 099/387] Leak (fix #288398) (#288437) --- .../browser/parts/editor/editorStatus.ts | 34 ++++++------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 88bc828b318..05a7a113243 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -10,7 +10,7 @@ import { format, compare, splitLines } from '../../../../base/common/strings.js' import { extname, basename, isEqual } from '../../../../base/common/resources.js'; import { areFunctions, assertReturnsDefined } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; -import { Action } from '../../../../base/common/actions.js'; +import { IAction, toAction } from '../../../../base/common/actions.js'; import { Language } from '../../../../base/common/platform.js'; import { UntitledTextEditorInput } from '../../../services/untitled/common/untitledTextEditorInput.js'; import { IFileEditorInput, EditorResourceAccessor, IEditorPane, SideBySideEditor } from '../../../common/editor.js'; @@ -1107,25 +1107,6 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { } } -export class ShowLanguageExtensionsAction extends Action { - - static readonly ID = 'workbench.action.showLanguageExtensions'; - - constructor( - private fileExtension: string, - @ICommandService private readonly commandService: ICommandService, - @IExtensionGalleryService galleryService: IExtensionGalleryService - ) { - super(ShowLanguageExtensionsAction.ID, localize('showLanguageExtensions', "Search Marketplace Extensions for '{0}'...", fileExtension)); - - this.enabled = galleryService.isEnabled(); - } - - override async run(): Promise { - await this.commandService.executeCommand('workbench.extensions.action.showExtensionsForLanguage', this.fileExtension); - } -} - export class ChangeLanguageAction extends Action2 { static readonly ID = 'workbench.action.editor.changeLanguageMode'; @@ -1159,9 +1140,10 @@ export class ChangeLanguageAction extends Action2 { const languageDetectionService = accessor.get(ILanguageDetectionService); const textFileService = accessor.get(ITextFileService); const preferencesService = accessor.get(IPreferencesService); - const instantiationService = accessor.get(IInstantiationService); const configurationService = accessor.get(IConfigurationService); const telemetryService = accessor.get(ITelemetryService); + const commandService = accessor.get(ICommandService); + const galleryService = accessor.get(IExtensionGalleryService); const activeTextEditorControl = getCodeEditor(editorService.activeTextEditorControl); if (!activeTextEditorControl) { @@ -1211,12 +1193,16 @@ export class ChangeLanguageAction extends Action2 { // Offer action to configure via settings let configureLanguageAssociations: IQuickPickItem | undefined; let configureLanguageSettings: IQuickPickItem | undefined; - let galleryAction: Action | undefined; + let galleryAction: IAction | undefined; if (hasLanguageSupport && resource) { const ext = extname(resource) || basename(resource); - galleryAction = instantiationService.createInstance(ShowLanguageExtensionsAction, ext); - if (galleryAction.enabled) { + if (galleryService.isEnabled()) { + galleryAction = toAction({ + id: 'workbench.action.showLanguageExtensions', + label: localize('showLanguageExtensions', "Search Marketplace Extensions for '{0}'...", ext), + run: () => commandService.executeCommand('workbench.extensions.action.showExtensionsForLanguage', ext) + }); picks.unshift(galleryAction); } From f2c0237048174f8aaea1344fbb593e6739d40410 Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:20:54 -0800 Subject: [PATCH 100/387] Follow system theme in Integrated Browser (#288436) --- .../browserView/electron-main/browserView.ts | 65 +------------------ 1 file changed, 1 insertion(+), 64 deletions(-) diff --git a/src/vs/platform/browserView/electron-main/browserView.ts b/src/vs/platform/browserView/electron-main/browserView.ts index a6e0856069e..b511c59081e 100644 --- a/src/vs/platform/browserView/electron-main/browserView.ts +++ b/src/vs/platform/browserView/electron-main/browserView.ts @@ -9,12 +9,10 @@ import { Emitter, Event } from '../../../base/common/event.js'; import { VSBuffer } from '../../../base/common/buffer.js'; import { IBrowserViewBounds, IBrowserViewDevToolsStateEvent, IBrowserViewFocusEvent, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewNavigationEvent, IBrowserViewLoadingEvent, IBrowserViewLoadError, IBrowserViewTitleChangeEvent, IBrowserViewFaviconChangeEvent, IBrowserViewNewPageRequest, BrowserViewStorageScope, IBrowserViewCaptureScreenshotOptions } from '../common/browserView.js'; import { EVENT_KEY_CODE_MAP, KeyCode, KeyMod, SCAN_CODE_STR_TO_EVENT_KEY_CODE } from '../../../base/common/keyCodes.js'; -import { IThemeMainService } from '../../theme/electron-main/themeMainService.js'; import { IWindowsMainService } from '../../windows/electron-main/windows.js'; import { IBaseWindow, ICodeWindow } from '../../window/electron-main/window.js'; import { IAuxiliaryWindowsMainService } from '../../auxiliaryWindow/electron-main/auxiliaryWindows.js'; import { IAuxiliaryWindow } from '../../auxiliaryWindow/electron-main/auxiliaryWindow.js'; -import { ILogService } from '../../log/common/log.js'; import { isMacintosh } from '../../../base/common/platform.js'; /** Key combinations that are used in system-level shortcuts. */ @@ -74,10 +72,8 @@ export class BrowserView extends Disposable { constructor( viewSession: Electron.Session, private readonly storageScope: BrowserViewStorageScope, - @IThemeMainService private readonly themeMainService: IThemeMainService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - @IAuxiliaryWindowsMainService private readonly auxiliaryWindowsMainService: IAuxiliaryWindowsMainService, - @ILogService private readonly logService: ILogService + @IAuxiliaryWindowsMainService private readonly auxiliaryWindowsMainService: IAuxiliaryWindowsMainService ) { super(); @@ -111,9 +107,6 @@ export class BrowserView extends Disposable { }); this.setupEventListeners(); - - // Create and register plugins for this web contents - this._register(new ThemePlugin(this._view, this.themeMainService, this.logService)); } private setupEventListeners(): void { @@ -519,59 +512,3 @@ export class BrowserView extends Disposable { return this.auxiliaryWindowsMainService.getWindowByWebContents(contents); } } - -export class ThemePlugin extends Disposable { - private readonly _webContents: Electron.WebContents; - private _injectedCSSKey?: string; - - constructor( - private readonly _view: Electron.WebContentsView, - private readonly themeMainService: IThemeMainService, - private readonly logService: ILogService - ) { - super(); - this._webContents = _view.webContents; - - // Set view background to match editor background - this.applyBackgroundColor(); - - // Apply theme when page loads - this._webContents.on('did-finish-load', () => this.applyTheme()); - - // Update theme when VS Code theme changes - this._register(this.themeMainService.onDidChangeColorScheme(() => { - this.applyBackgroundColor(); - this.applyTheme(); - })); - } - - private applyBackgroundColor(): void { - const backgroundColor = this.themeMainService.getBackgroundColor(); - this._view.setBackgroundColor(backgroundColor); - } - - private async applyTheme(): Promise { - if (this._webContents.isDestroyed()) { - return; - } - - const colorScheme = this.themeMainService.getColorScheme().dark ? 'dark' : 'light'; - - try { - // Remove previous theme CSS if it exists - if (this._injectedCSSKey) { - await this._webContents.removeInsertedCSS(this._injectedCSSKey); - } - - // Insert new theme CSS - this._injectedCSSKey = await this._webContents.insertCSS(` - /* VS Code theme override */ - :root { - color-scheme: ${colorScheme}; - } - `); - } catch (error) { - this.logService.error('ThemePlugin: Failed to inject CSS', error); - } - } -} From 8319fd9be629aa456a84e79f79c1b63d2ee64c5e Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 16 Jan 2026 14:23:56 -0500 Subject: [PATCH 101/387] process dependency tasks in parallel (#288440) fix #288439 --- .../chatAgentTools/browser/taskHelpers.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts index 10d7575374d..7f8f1e8d39e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts @@ -207,9 +207,11 @@ export async function collectTerminalResults( taskLabelToTaskMap[dependencyTask._label] = dependencyTask; } - for (const instance of terminals) { - progress.report({ message: new MarkdownString(`Checking output for \`${instance.shellLaunchConfig.name ?? 'unknown'}\``) }); + // Process all terminals in parallel + const terminalNames = terminals.map(t => t.shellLaunchConfig.name ?? t.title ?? 'unknown'); + progress.report({ message: new MarkdownString(`Checking output for ${terminalNames.map(n => `\`${n}\``).join(', ')}`) }); + const terminalPromises = terminals.map(async (instance) => { let terminalTask = task; // For composite tasks, find the actual dependency task running in this terminal @@ -257,7 +259,7 @@ export async function collectTerminalResults( const outputMonitor = disposableStore.add(instantiationService.createInstance(OutputMonitor, execution, taskProblemPollFn, invocationContext, token, task._label)); await Event.toPromise(outputMonitor.onDidFinishCommand); const pollingResult = outputMonitor.pollingResult; - results.push({ + return { name: instance.shellLaunchConfig.name ?? instance.title ?? 'unknown', output: pollingResult?.output ?? '', pollDurationMs: pollingResult?.pollDurationMs ?? 0, @@ -271,8 +273,11 @@ export async function collectTerminalResults( inputToolManualShownCount: outputMonitor.outputMonitorTelemetryCounters.inputToolManualShownCount ?? 0, inputToolFreeFormInputShownCount: outputMonitor.outputMonitorTelemetryCounters.inputToolFreeFormInputShownCount ?? 0, inputToolFreeFormInputCount: outputMonitor.outputMonitorTelemetryCounters.inputToolFreeFormInputCount ?? 0, - }); - } + }; + }); + + const parallelResults = await Promise.all(terminalPromises); + results.push(...parallelResults); return results; } From ae524952a1de76a3cc4d612775536bfd4cde7e97 Mon Sep 17 00:00:00 2001 From: Robo Date: Sat, 17 Jan 2026 04:25:42 +0900 Subject: [PATCH 102/387] fix: disable skia graphite backend (#288141) * fix: disable skia graphite backend Refs https://gist.github.com/deepak1556/434964e5e379339be1d02db2a9afb743 * chore: rm enable-graphite-invalid-recording-recovery switch * Revert "feat: add setting to control throttling for chat sessions (#280591)" This reverts commit 5efc1d01549dc9b89d5e0e4fc81fc85a17f233fd. --- src/main.ts | 10 +++++----- .../contrib/chat/browser/chat.contribution.ts | 6 ------ src/vs/workbench/contrib/chat/common/constants.ts | 1 - .../contrib/chat/electron-browser/chat.contribution.ts | 10 ++-------- .../workbench/electron-browser/desktop.contribution.ts | 4 ---- 5 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/main.ts b/src/main.ts index fc2d71affbd..ecbbb165479 100644 --- a/src/main.ts +++ b/src/main.ts @@ -227,10 +227,7 @@ function configureCommandlineSwitchesSync(cliArgs: NativeParsedArgs) { // bypass any specified proxy for the given semi-colon-separated list of hosts 'proxy-bypass-list', - 'remote-debugging-port', - - // Enable recovery from invalid Graphite recordings - 'enable-graphite-invalid-recording-recovery' + 'remote-debugging-port' ]; if (process.platform === 'linux') { @@ -359,6 +356,10 @@ function configureCommandlineSwitchesSync(cliArgs: NativeParsedArgs) { // use up to 2 app.commandLine.appendSwitch('max-active-webgl-contexts', '32'); + // Disable Skia Graphite backend. + // Refs https://github.com/microsoft/vscode/issues/284162 + app.commandLine.appendSwitch('disable-skia-graphite'); + return argvConfig; } @@ -377,7 +378,6 @@ interface IArgvConfig { readonly 'use-inmemory-secretstorage'?: boolean; readonly 'enable-rdp-display-tracking'?: boolean; readonly 'remote-debugging-port'?: string; - readonly 'enable-graphite-invalid-recording-recovery'?: boolean; } function readArgvConfigSync(): IArgvConfig { diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index c4c1011d904..9f80ffb1cb0 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -347,12 +347,6 @@ configurationRegistry.registerConfiguration({ }, } }, - [ChatConfiguration.SuspendThrottling]: { // TODO@deepak1556 remove this once https://github.com/microsoft/vscode/issues/263554 is resolved. - type: 'boolean', - description: nls.localize('chat.suspendThrottling', "Controls whether background throttling is suspended when a chat request is in progress, allowing the chat session to continue even when the window is not in focus."), - default: true, - tags: ['preview'] - }, 'chat.sendElementsToChat.enabled': { default: true, description: nls.localize('chat.sendElementsToChat.enabled', "Controls whether elements can be sent to chat from the Simple Browser."), diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index b441782ef7b..ba407ce37fe 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -35,7 +35,6 @@ export enum ChatConfiguration { ShowCodeBlockProgressAnimation = 'chat.agent.codeBlockProgress', RestoreLastPanelSession = 'chat.restoreLastPanelSession', ExitAfterDelegation = 'chat.exitAfterDelegation', - SuspendThrottling = 'chat.suspendThrottling', } /** diff --git a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts index e4c7c9cfbab..dc0d3a78777 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts @@ -12,7 +12,6 @@ import { ipcRenderer } from '../../../../base/parts/sandbox/electron-browser/glo import { localize } from '../../../../nls.js'; import { registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { ILogService } from '../../../../platform/log/common/log.js'; @@ -27,7 +26,7 @@ import { ILifecycleService, ShutdownReason } from '../../../services/lifecycle/c import { ACTION_ID_NEW_CHAT, CHAT_OPEN_ACTION_ID, IChatViewOpenOptions } from '../browser/actions/chatActions.js'; import { IChatWidgetService } from '../browser/chat.js'; import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; -import { ChatConfiguration, ChatModeKind } from '../common/constants.js'; +import { ChatModeKind } from '../common/constants.js'; import { IChatService } from '../common/chatService/chatService.js'; import { registerChatDeveloperActions } from './actions/chatDeveloperActions.js'; import { registerChatExportZipAction } from './actions/chatExportZip.js'; @@ -99,15 +98,10 @@ class ChatSuspendThrottlingHandler extends Disposable { constructor( @INativeHostService nativeHostService: INativeHostService, - @IChatService chatService: IChatService, - @IConfigurationService configurationService: IConfigurationService + @IChatService chatService: IChatService ) { super(); - if (!configurationService.getValue(ChatConfiguration.SuspendThrottling)) { - return; - } - this._register(autorun(reader => { const running = chatService.requestInProgressObs.read(reader); diff --git a/src/vs/workbench/electron-browser/desktop.contribution.ts b/src/vs/workbench/electron-browser/desktop.contribution.ts index a18ff87a5d8..5fad6f93177 100644 --- a/src/vs/workbench/electron-browser/desktop.contribution.ts +++ b/src/vs/workbench/electron-browser/desktop.contribution.ts @@ -454,10 +454,6 @@ import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '../../platform/window/electron-b 'remote-debugging-port': { type: 'string', description: localize('argv.remoteDebuggingPort', "Specifies the port to use for remote debugging.") - }, - 'enable-graphite-invalid-recording-recovery': { - type: 'boolean', - description: localize('argv.enableGraphiteInvalidRecordingRecovery', "Enables recovery from invalid Graphite recordings.") } } }; From cce8f69edba80175461aa099a2060f3a175b0527 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 16 Jan 2026 11:35:54 -0800 Subject: [PATCH 103/387] chat: fix autoconfirm briefly causing requests to still need input (#288438) Closes https://github.com/microsoft/vscode-copilot-evaluation/issues/2012 --- .../tools/languageModelToolsService.ts | 19 ++++---- .../chatProgressTypes/chatToolInvocation.ts | 44 ++++++++++--------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index 57608cc675b..a6338486bf7 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -52,7 +52,6 @@ interface IToolEntry { } interface ITrackedCall { - invocation?: ChatToolInvocation; store: IDisposable; } @@ -385,19 +384,23 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo preparedInvocation = await this.prepareToolInvocation(tool, dto, token); prepareTimeWatch.stop(); + const autoConfirmed = await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters); + + + // Important: a tool invocation that will be autoconfirmed should never + // be in the chat response in the `NeedsConfirmation` state, even briefly, + // as that triggers notifications and causes issues in eval. if (hadPendingInvocation && toolInvocation) { // Transition from streaming to executing/waiting state - toolInvocation.transitionFromStreaming(preparedInvocation, dto.parameters); + toolInvocation.transitionFromStreaming(preparedInvocation, dto.parameters, autoConfirmed); } else { // Create a new tool invocation (no streaming phase) toolInvocation = new ChatToolInvocation(preparedInvocation, tool.data, dto.callId, dto.subAgentInvocationId, dto.parameters); - this._chatService.appendProgress(request, toolInvocation); - } + if (autoConfirmed) { + IChatToolInvocation.confirmWith(toolInvocation, autoConfirmed); + } - trackedCall.invocation = toolInvocation; - const autoConfirmed = await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters); - if (autoConfirmed) { - IChatToolInvocation.confirmWith(toolInvocation, autoConfirmed); + this._chatService.appendProgress(request, toolInvocation); } dto.toolSpecificData = toolInvocation?.toolSpecificData; diff --git a/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts index 8b7545f1beb..3b3dc98eccd 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts @@ -144,7 +144,7 @@ export class ChatToolInvocation implements IChatToolInvocation { * Transition from streaming state to prepared/executing state. * Called when the full tool call is ready. */ - public transitionFromStreaming(preparedInvocation: IPreparedToolInvocation | undefined, parameters: unknown): void { + public transitionFromStreaming(preparedInvocation: IPreparedToolInvocation | undefined, parameters: unknown, autoConfirmed: ConfirmedReason | undefined): void { const currentState = this._state.get(); if (currentState.type !== IChatToolInvocation.StateKind.Streaming) { return; // Only transition from streaming state @@ -168,8 +168,29 @@ export class ChatToolInvocation implements IChatToolInvocation { this.toolSpecificData = preparedInvocation.toolSpecificData; } + const confirm = (reason: ConfirmedReason) => { + if (reason.type === ToolConfirmKind.Denied || reason.type === ToolConfirmKind.Skipped) { + this._state.set({ + type: IChatToolInvocation.StateKind.Cancelled, + reason: reason.type, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }, undefined); + } else { + this._state.set({ + type: IChatToolInvocation.StateKind.Executing, + confirmed: reason, + progress: this._progress, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }, undefined); + } + }; + // Transition to the appropriate state - if (!this.confirmationMessages?.title) { + if (autoConfirmed) { + confirm(autoConfirmed); + } if (!this.confirmationMessages?.title) { this._state.set({ type: IChatToolInvocation.StateKind.Executing, confirmed: { type: ToolConfirmKind.ConfirmationNotNeeded, reason: this.confirmationMessages?.confirmationNotNeededReason }, @@ -182,24 +203,7 @@ export class ChatToolInvocation implements IChatToolInvocation { type: IChatToolInvocation.StateKind.WaitingForConfirmation, parameters: this.parameters, confirmationMessages: this.confirmationMessages, - confirm: reason => { - if (reason.type === ToolConfirmKind.Denied || reason.type === ToolConfirmKind.Skipped) { - this._state.set({ - type: IChatToolInvocation.StateKind.Cancelled, - reason: reason.type, - parameters: this.parameters, - confirmationMessages: this.confirmationMessages, - }, undefined); - } else { - this._state.set({ - type: IChatToolInvocation.StateKind.Executing, - confirmed: reason, - progress: this._progress, - parameters: this.parameters, - confirmationMessages: this.confirmationMessages, - }, undefined); - } - } + confirm, }, undefined); } } From 8c0fe5e8b3223dc885dfa391eb2b4a23f93774e8 Mon Sep 17 00:00:00 2001 From: dileepyavan <52841896+dileepyavan@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:01:00 -0800 Subject: [PATCH 104/387] Enabling sandboxing for terminal commands execution through copilot chat. (#280236) * Enable sandboxing for terminal commands * removing unused types * code review comments update * refactored the code and added utility for sandboxing * refactored the code and added utility for sandboxing * refactored the code and added utility for sandboxing * fixing build error * review suggestions * review suggestions * changes for retry * Update src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * updating anthropic sandbox runtime to 0.0.23 * fixing tests for runInTerminalTool * refactoring changes --------- Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> --- package-lock.json | 80 ++++++++++- package.json | 1 + .../terminal/terminalContribExports.ts | 2 +- .../terminal.chatAgentTools.contribution.ts | 8 ++ .../browser/tools/runInTerminalTool.ts | 66 ++++++--- .../terminalChatAgentToolsConfiguration.ts | 97 +++++++++++++ .../chatAgentTools/common/terminalSandbox.ts | 24 ++++ .../common/terminalSandboxService.ts | 129 ++++++++++++++++++ .../runInTerminalTool.test.ts | 9 ++ 9 files changed, 392 insertions(+), 24 deletions(-) create mode 100644 src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandbox.ts create mode 100644 src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts diff --git a/package-lock.json b/package-lock.json index b3bc0bec88c..d6d45b9c4a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "@anthropic-ai/sandbox-runtime": "0.0.23", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.4", @@ -178,6 +179,35 @@ "node": ">=6.0.0" } }, + "node_modules/@anthropic-ai/sandbox-runtime": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.23.tgz", + "integrity": "sha512-Np0VRH6D71cGoJZvd8hCz1LMfwg9ERJovrOJSCz5aSQSQJPWPNIFPV1wfc8oAhJpStOuYkot+EmXOkRRxuGMCQ==", + "license": "Apache-2.0", + "dependencies": { + "@pondwader/socks5-server": "^1.0.10", + "@types/lodash-es": "^4.17.12", + "commander": "^12.1.0", + "lodash-es": "^4.17.21", + "shell-quote": "^1.8.3", + "zod": "^3.24.1" + }, + "bin": { + "srt": "dist/cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@anthropic-ai/sandbox-runtime/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@azure-rest/ai-translation-text": { "version": "1.0.0-beta.1", "resolved": "https://registry.npmjs.org/@azure-rest/ai-translation-text/-/ai-translation-text-1.0.0-beta.1.tgz", @@ -2032,6 +2062,12 @@ "node": ">=18" } }, + "node_modules/@pondwader/socks5-server": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@pondwader/socks5-server/-/socks5-server-1.0.10.tgz", + "integrity": "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg==", + "license": "MIT" + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -2328,6 +2364,21 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -11775,6 +11826,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash-es": { + "version": "4.17.22", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz", + "integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==", + "license": "MIT" + }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -15339,10 +15396,16 @@ } }, "node_modules/shell-quote": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", - "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", - "dev": true + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/sigmund": { "version": "1.0.1", @@ -18279,6 +18342,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zx": { "version": "8.8.5", "resolved": "https://registry.npmjs.org/zx/-/zx-8.8.5.tgz", diff --git a/package.json b/package.json index a6112b97312..b3198745e54 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "update-build-ts-version": "npm install -D typescript@next && npm install -D @typescript/native-preview && (cd build && npm run typecheck)" }, "dependencies": { + "@anthropic-ai/sandbox-runtime": "0.0.23", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.4", diff --git a/src/vs/workbench/contrib/terminal/terminalContribExports.ts b/src/vs/workbench/contrib/terminal/terminalContribExports.ts index 59f7bae295e..1fd3bb600a0 100644 --- a/src/vs/workbench/contrib/terminal/terminalContribExports.ts +++ b/src/vs/workbench/contrib/terminal/terminalContribExports.ts @@ -43,7 +43,7 @@ export const enum TerminalContribSettingId { AutoApprove = TerminalChatAgentToolsSettingId.AutoApprove, EnableAutoApprove = TerminalChatAgentToolsSettingId.EnableAutoApprove, ShellIntegrationTimeout = TerminalChatAgentToolsSettingId.ShellIntegrationTimeout, - OutputLocation = TerminalChatAgentToolsSettingId.OutputLocation + OutputLocation = TerminalChatAgentToolsSettingId.OutputLocation, } // HACK: Export some context key strings from `terminalContrib/` that are depended upon elsewhere. diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts index e9f18f15afa..db7751a57b0 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts @@ -29,6 +29,14 @@ import { RunInTerminalTool, createRunInTerminalToolData } from './tools/runInTer import { CreateAndRunTaskTool, CreateAndRunTaskToolData } from './tools/task/createAndRunTaskTool.js'; import { GetTaskOutputTool, GetTaskOutputToolData } from './tools/task/getTaskOutputTool.js'; import { RunTaskTool, RunTaskToolData } from './tools/task/runTaskTool.js'; +import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js'; +import { ITerminalSandboxService, TerminalSandboxService } from '../common/terminalSandboxService.js'; + +// #region Services + +registerSingleton(ITerminalSandboxService, TerminalSandboxService, InstantiationType.Delayed); + +// #endregion Services class ShellIntegrationTimeoutMigrationContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'terminal.shellIntegrationTimeoutMigration'; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index 0b1c8723b8a..f98d0a87f08 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -21,7 +21,7 @@ import { IConfigurationService } from '../../../../../../platform/configuration/ import { IInstantiationService, type ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; -import { TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; +import { ICommandDetectionCapability, TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; import { ITerminalLogService, ITerminalProfile } from '../../../../../../platform/terminal/common/terminal.js'; import { IRemoteAgentService } from '../../../../../services/remote/common/remoteAgentService.js'; import { TerminalToolConfirmationStorageKeys } from '../../../../chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.js'; @@ -48,6 +48,7 @@ import { CommandLineAutoApproveAnalyzer } from './commandLineAnalyzer/commandLin import { CommandLineFileWriteAnalyzer } from './commandLineAnalyzer/commandLineFileWriteAnalyzer.js'; import { OutputMonitor } from './monitoring/outputMonitor.js'; import { IPollingResult, OutputMonitorState } from './monitoring/types.js'; +import { ITerminalSandboxService } from '../../common/terminalSandboxService.js'; import { chatSessionResourceToId, LocalChatSessionUri } from '../../../../chat/common/model/chatUri.js'; import { URI } from '../../../../../../base/common/uri.js'; import type { ICommandLineRewriter } from './commandLineRewriter/commandLineRewriter.js'; @@ -313,6 +314,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { @ITerminalService private readonly _terminalService: ITerminalService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, + @ITerminalSandboxService private readonly _sandboxService: ITerminalSandboxService, ) { super(); @@ -344,6 +346,15 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { this._storageService.remove(TerminalToolConfirmationStorageKeys.TerminalAutoApproveWarningAccepted, StorageScope.APPLICATION); } } + // If terminal sandbox settings changed, update sandbox config. + if ( + e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxEnabled) || + e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork) || + e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxLinuxFileSystem) || + e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxMacFileSystem) + ) { + this._sandboxService.setNeedsForceUpdateConfigFile(); + } })); // Restore terminal associations from storage @@ -426,6 +437,15 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { }; } + // If in sandbox mode, skip confirmation logic. In sandbox mode, commands are run in a restricted environment and explicit + // user confirmation is not required. + if (this._sandboxService.isEnabled()) { + toolSpecificData.autoApproveInfo = new MarkdownString(localize('autoApprove.sandbox', 'In sandbox mode')); + return { + toolSpecificData + }; + } + // Determine auto approval, this happens even when auto approve is off to that reasoning // can be reviewed in the terminal channel. It also allows gauging the effective set of // commands that would be auto approved if it were enabled. @@ -593,11 +613,11 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { const args = invocation.parameters as IRunInTerminalInputParams; this._logService.debug(`RunInTerminalTool: Invoking with options ${JSON.stringify(args)}`); - let toolResultMessage: string | undefined; + let toolResultMessage: string | IMarkdownString | undefined; const chatSessionResource = invocation.context?.sessionResource ?? LocalChatSessionUri.forSession(invocation.context?.sessionId ?? 'no-chat-session'); const chatSessionId = chatSessionResourceToId(chatSessionResource); - const command = toolSpecificData.commandLine.userEdited ?? toolSpecificData.commandLine.toolEdited ?? toolSpecificData.commandLine.original; + let command = toolSpecificData.commandLine.userEdited ?? toolSpecificData.commandLine.toolEdited ?? toolSpecificData.commandLine.original; const didUserEditCommand = ( toolSpecificData.commandLine.userEdited !== undefined && toolSpecificData.commandLine.userEdited !== toolSpecificData.commandLine.original @@ -608,6 +628,12 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { toolSpecificData.commandLine.toolEdited !== toolSpecificData.commandLine.original ); + if (this._sandboxService.isEnabled()) { + await this._sandboxService.getSandboxConfigPath(); + this._logService.info(`RunInTerminalTool: Sandboxing is enabled, wrapping command with srt.`); + command = this._sandboxService.wrapCommand(command); + } + if (token.isCancellationRequested) { throw new CancellationError(); } @@ -751,21 +777,9 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } try { - let strategy: ITerminalExecuteStrategy; - switch (toolTerminal.shellIntegrationQuality) { - case ShellIntegrationQuality.None: { - strategy = this._instantiationService.createInstance(NoneExecuteStrategy, toolTerminal.instance, () => toolTerminal.receivedUserInput ?? false); - toolResultMessage = '$(info) Enable [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) to improve command detection'; - break; - } - case ShellIntegrationQuality.Basic: { - strategy = this._instantiationService.createInstance(BasicExecuteStrategy, toolTerminal.instance, () => toolTerminal.receivedUserInput ?? false, commandDetection!); - break; - } - case ShellIntegrationQuality.Rich: { - strategy = this._instantiationService.createInstance(RichExecuteStrategy, toolTerminal.instance, commandDetection!); - break; - } + const strategy: ITerminalExecuteStrategy = this._getExecuteStrategy(toolTerminal.shellIntegrationQuality, toolTerminal, commandDetection!); + if (toolTerminal.shellIntegrationQuality === ShellIntegrationQuality.None) { + toolResultMessage = '$(info) Enable [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) to improve command detection'; } this._logService.debug(`RunInTerminalTool: Using \`${strategy.type}\` execute strategy for command \`${command}\``); store.add(strategy.onDidCreateStartMarker(startMarker => { @@ -825,7 +839,6 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } terminalResult = resultArr.join('\n\n'); } - } catch (e) { // Handle timeout case - get output collected so far and return it if (didTimeout && e instanceof CancellationError) { @@ -1069,6 +1082,21 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } } + private _getExecuteStrategy(shellIntegrationQuality: ShellIntegrationQuality, toolTerminal: IToolTerminal, commandDetection: ICommandDetectionCapability): ITerminalExecuteStrategy { + let strategy: ITerminalExecuteStrategy; + switch (shellIntegrationQuality) { + case ShellIntegrationQuality.None: + strategy = this._instantiationService.createInstance(NoneExecuteStrategy, toolTerminal.instance, () => toolTerminal.receivedUserInput ?? false); + break; + case ShellIntegrationQuality.Basic: + strategy = this._instantiationService.createInstance(BasicExecuteStrategy, toolTerminal.instance, () => toolTerminal.receivedUserInput ?? false, commandDetection!); + break; + case ShellIntegrationQuality.Rich: + strategy = this._instantiationService.createInstance(RichExecuteStrategy, toolTerminal.instance, commandDetection!); + break; + } + return strategy; + } // #endregion } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index 544ebe526ab..be29b27e9e6 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -20,6 +20,10 @@ export const enum TerminalChatAgentToolsSettingId { ShellIntegrationTimeout = 'chat.tools.terminal.shellIntegrationTimeout', AutoReplyToPrompts = 'chat.tools.terminal.autoReplyToPrompts', OutputLocation = 'chat.tools.terminal.outputLocation', + TerminalSandboxEnabled = 'chat.tools.terminal.sandbox.enabled', + TerminalSandboxNetwork = 'chat.tools.terminal.sandbox.network', + TerminalSandboxLinuxFileSystem = 'chat.tools.terminal.sandbox.linuxFileSystem', + TerminalSandboxMacFileSystem = 'chat.tools.terminal.sandbox.macFileSystem', PreventShellHistory = 'chat.tools.terminal.preventShellHistory', EnforceTimeoutFromModel = 'chat.tools.terminal.enforceTimeoutFromModel', @@ -504,6 +508,99 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary('terminalSandboxService'); + +export interface ITerminalSandboxService { + readonly _serviceBrand: undefined; + isEnabled(): boolean; + wrapCommand(command: string): string; + getSandboxConfigPath(forceRefresh?: boolean): Promise; + getTempDir(): URI | undefined; + setNeedsForceUpdateConfigFile(): void; +} + +export class TerminalSandboxService implements ITerminalSandboxService { + readonly _serviceBrand: undefined; + private _srtPath: string; + private _sandboxConfigPath: string | undefined; + private _needsForceUpdateConfigFile = true; + private _tempDir: URI | undefined; + private _sandboxSettingsId: string | undefined; + private _os: OperatingSystem = OS; + + constructor( + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IFileService private readonly _fileService: IFileService, + @IEnvironmentService private readonly _environmentService: IEnvironmentService, + @ILogService private readonly _logService: ILogService, + @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, + ) { + const appRoot = dirname(FileAccess.asFileUri('').fsPath); + this._srtPath = join(appRoot, 'node_modules', '.bin', 'srt'); + this._sandboxSettingsId = generateUuid(); + this._initTempDir(); + this._remoteAgentService.getEnvironment().then(remoteEnv => this._os = remoteEnv?.os ?? OS); + } + + public isEnabled(): boolean { + if (this._os === OperatingSystem.Windows) { + return false; + } + return this._configurationService.getValue(TerminalChatAgentToolsSettingId.TerminalSandboxEnabled); + } + + public wrapCommand(command: string): string { + if (!this._sandboxConfigPath || !this._tempDir) { + throw new Error('Sandbox config path or temp dir not initialized'); + } + return `"${this._srtPath}" TMPDIR=${this._tempDir.fsPath} --settings "${this._sandboxConfigPath}" "${command}"`; + } + + public getTempDir(): URI | undefined { + return this._tempDir; + } + + public setNeedsForceUpdateConfigFile(): void { + this._needsForceUpdateConfigFile = true; + } + + public async getSandboxConfigPath(forceRefresh: boolean = false): Promise { + if (!this._sandboxConfigPath || forceRefresh || this._needsForceUpdateConfigFile) { + this._sandboxConfigPath = await this._createSandboxConfig(); + this._needsForceUpdateConfigFile = false; + } + return this._sandboxConfigPath; + } + + private async _createSandboxConfig(): Promise { + + if (this.isEnabled() && !this._tempDir) { + this._initTempDir(); + } + if (this._tempDir) { + const networkSetting = this._configurationService.getValue(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork) ?? {}; + const linuxFileSystemSetting = this._os === OperatingSystem.Linux + ? this._configurationService.getValue(TerminalChatAgentToolsSettingId.TerminalSandboxLinuxFileSystem) ?? {} + : {}; + const macFileSystemSetting = this._os === OperatingSystem.Macintosh + ? this._configurationService.getValue(TerminalChatAgentToolsSettingId.TerminalSandboxMacFileSystem) ?? {} + : {}; + const configFileUri = joinPath(this._tempDir, `vscode-sandbox-settings-${this._sandboxSettingsId}.json`); + const sandboxSettings = { + network: { + allowedDomains: networkSetting.allowedDomains ?? [], + deniedDomains: networkSetting.deniedDomains ?? [] + }, + filesystem: { + denyRead: this._os === OperatingSystem.Macintosh ? macFileSystemSetting.denyRead : linuxFileSystemSetting.denyRead, + allowWrite: this._os === OperatingSystem.Macintosh ? macFileSystemSetting.allowWrite : linuxFileSystemSetting.allowWrite, + denyWrite: this._os === OperatingSystem.Macintosh ? macFileSystemSetting.denyWrite : linuxFileSystemSetting.denyWrite, + } + }; + this._sandboxConfigPath = configFileUri.fsPath; + await this._fileService.createFile(configFileUri, VSBuffer.fromString(JSON.stringify(sandboxSettings, null, '\t')), { overwrite: true }); + return this._sandboxConfigPath; + } + return undefined; + } + + private _initTempDir(): void { + if (this.isEnabled() && isNative) { + this._needsForceUpdateConfigFile = true; + const environmentService = this._environmentService as IEnvironmentService & { tmpDir?: URI }; + this._tempDir = environmentService.tmpDir; + if (!this._tempDir) { + this._logService.warn('TerminalSandboxService: Cannot create sandbox settings file because no tmpDir is available in this environment'); + return; + } + } + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts index 13050308bd1..a8412b10371 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts @@ -32,6 +32,7 @@ import { TestIPCFileSystemProvider } from '../../../../../test/electron-browser/ import { TerminalToolConfirmationStorageKeys } from '../../../../chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.js'; import { IChatService, type IChatTerminalToolInvocationData } from '../../../../chat/common/chatService/chatService.js'; import { LocalChatSessionUri } from '../../../../chat/common/model/chatUri.js'; +import { ITerminalSandboxService } from '../../common/terminalSandboxService.js'; import { ILanguageModelToolsService, IPreparedToolInvocation, IToolInvocationPreparationContext, type ToolConfirmationAction } from '../../../../chat/common/tools/languageModelToolsService.js'; import { ITerminalChatService, ITerminalService, type ITerminalInstance } from '../../../../terminal/browser/terminal.js'; import { ITerminalProfileResolverService } from '../../../../terminal/common/terminal.js'; @@ -91,6 +92,14 @@ suite('RunInTerminalTool', () => { instantiationService.stub(IHistoryService, { getLastActiveWorkspaceRoot: () => undefined }); + instantiationService.stub(ITerminalSandboxService, { + _serviceBrand: undefined, + isEnabled: () => false, + wrapCommand: command => command, + getSandboxConfigPath: async () => undefined, + getTempDir: () => undefined, + setNeedsForceUpdateConfigFile: () => { } + }); const treeSitterLibraryService = store.add(instantiationService.createInstance(TreeSitterLibraryService)); treeSitterLibraryService.isTest = true; From 05e134a0f5907bc705c8785b9fa87b38acbc7794 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 16 Jan 2026 21:09:18 +0100 Subject: [PATCH 105/387] Agent sessions: explore a prominent button to create new sessions (fix #288001) (#288456) --- .../browser/widgetHosts/viewPane/chatViewPane.ts | 14 ++++++++++++++ .../widgetHosts/viewPane/media/chatViewPane.css | 9 +++++++++ 2 files changed, 23 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 82e01d19a97..c86f0d77bd2 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -6,6 +6,7 @@ import './media/chatViewPane.css'; import { $, addDisposableListener, append, EventHelper, EventType, getWindow, setVisibility } from '../../../../../../base/browser/dom.js'; import { StandardMouseEvent } from '../../../../../../base/browser/mouseEvent.js'; +import { Button } from '../../../../../../base/browser/ui/button/button.js'; import { Orientation, Sash } from '../../../../../../base/browser/ui/sash/sash.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Event } from '../../../../../../base/common/event.js'; @@ -16,6 +17,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { localize } from '../../../../../../nls.js'; import { MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; import { MenuId } from '../../../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js'; @@ -27,6 +29,7 @@ import { ILogService } from '../../../../../../platform/log/common/log.js'; import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { defaultButtonStyles } from '../../../../../../platform/theme/browser/defaultStyles.js'; import { editorBackground } from '../../../../../../platform/theme/common/colorRegistry.js'; import { ChatViewTitleControl } from './chatViewTitleControl.js'; import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; @@ -46,6 +49,7 @@ import { LocalChatSessionUri, getChatSessionType } from '../../../common/model/c import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../../common/constants.js'; import { AgentSessionsControl } from '../../agentSessions/agentSessionsControl.js'; import { AgentSessionsListDelegate } from '../../agentSessions/agentSessionsViewer.js'; +import { ACTION_ID_NEW_CHAT } from '../../actions/chatActions.js'; import { ChatWidget } from '../../widget/chatWidget.js'; import { ChatViewWelcomeController, IViewWelcomeDelegate } from '../../viewsWelcome/chatViewWelcomeController.js'; import { IWorkbenchLayoutService, LayoutSettings, Position } from '../../../../../services/layout/browser/layoutService.js'; @@ -108,6 +112,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { @IProgressService private readonly progressService: IProgressService, @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, + @ICommandService private readonly commandService: ICommandService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -317,6 +322,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private sessionsContainer: HTMLElement | undefined; private sessionsTitleContainer: HTMLElement | undefined; private sessionsTitle: HTMLElement | undefined; + private sessionsNewButtonContainer: HTMLElement | undefined; private sessionsControlContainer: HTMLElement | undefined; private sessionsControl: AgentSessionsControl | undefined; private sessionsCount = 0; @@ -379,6 +385,12 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { sessionsToolbarContainer.classList.toggle('filtered', !sessionsFilter.isDefault()); })); + // New Session Button + const newSessionButtonContainer = this.sessionsNewButtonContainer = append(sessionsContainer, $('.agent-sessions-new-button-container')); + const newSessionButton = this._register(new Button(newSessionButtonContainer, { ...defaultButtonStyles, secondary: true })); + newSessionButton.label = localize('newSession', "New Session"); + this._register(newSessionButton.onDidClick(() => this.commandService.executeCommand(ACTION_ID_NEW_CHAT))); + // Sessions Control this.sessionsControlContainer = append(sessionsContainer, $('.agent-sessions-control-container')); const sessionsControl = this.sessionsControl = this._register(this.instantiationService.createInstance(AgentSessionsControl, this.sessionsControlContainer, { @@ -924,6 +936,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { let availableSessionsHeight = height - this.sessionsTitleContainer.offsetHeight; if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { availableSessionsHeight -= Math.max(ChatViewPane.MIN_CHAT_WIDGET_HEIGHT, this._widget?.input?.contentHeight ?? 0); + } else { + availableSessionsHeight -= this.sessionsNewButtonContainer?.offsetHeight ?? 0; } // Show as sidebar diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css index b4eadde9ef2..8f48fa4e03b 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css @@ -85,6 +85,11 @@ .agent-sessions-container { border-bottom: 1px solid var(--vscode-panel-border); } + + .agent-sessions-new-button-container { + /* hide new session button when stacked */ + display: none; + } } /* Sessions control: side by side */ @@ -105,6 +110,10 @@ border-left: 1px solid var(--vscode-panel-border); } } + + .agent-sessions-new-button-container { + padding: 8px 12px; + } } /* From 5afeaf6d1928105397adf6591c2aa52fe1c0ae01 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 16 Jan 2026 21:17:15 +0100 Subject: [PATCH 106/387] Implements simple AI Rate chart (#288451) --- .../browser/editStats/aiStatsChart.ts | 285 ++++++++++++++++++ .../browser/editStats/aiStatsFeature.ts | 9 + .../browser/editStats/aiStatsStatusBar.ts | 100 ++++-- .../editTelemetry/browser/editStats/media.css | 33 ++ 4 files changed, 409 insertions(+), 18 deletions(-) create mode 100644 src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsChart.ts diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsChart.ts b/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsChart.ts new file mode 100644 index 00000000000..b02f21ebd85 --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsChart.ts @@ -0,0 +1,285 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $ } from '../../../../../base/browser/dom.js'; +import { localize } from '../../../../../nls.js'; +import { asCssVariable } from '../../../../../platform/theme/common/colorUtils.js'; +import { chartsBlue, chartsForeground, chartsLines } from '../../../../../platform/theme/common/colorRegistry.js'; + +export interface ISessionData { + startTime: number; + typedCharacters: number; + aiCharacters: number; + acceptedInlineSuggestions: number | undefined; + chatEditCount: number | undefined; +} + +export interface IDailyAggregate { + date: string; // ISO date string (YYYY-MM-DD) + displayDate: string; // Formatted for display + aiRate: number; + totalAiChars: number; + totalTypedChars: number; + inlineSuggestions: number; + chatEdits: number; + sessionCount: number; +} + +export type ChartViewMode = 'days' | 'sessions'; + +export function aggregateSessionsByDay(sessions: readonly ISessionData[]): IDailyAggregate[] { + const dayMap = new Map(); + + for (const session of sessions) { + const date = new Date(session.startTime); + const isoDate = date.toISOString().split('T')[0]; + const displayDate = date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + + let aggregate = dayMap.get(isoDate); + if (!aggregate) { + aggregate = { + date: isoDate, + displayDate, + aiRate: 0, + totalAiChars: 0, + totalTypedChars: 0, + inlineSuggestions: 0, + chatEdits: 0, + sessionCount: 0, + }; + dayMap.set(isoDate, aggregate); + } + + aggregate.totalAiChars += session.aiCharacters; + aggregate.totalTypedChars += session.typedCharacters; + aggregate.inlineSuggestions += session.acceptedInlineSuggestions ?? 0; + aggregate.chatEdits += session.chatEditCount ?? 0; + aggregate.sessionCount += 1; + } + + // Calculate AI rate for each day + for (const aggregate of dayMap.values()) { + const total = aggregate.totalAiChars + aggregate.totalTypedChars; + aggregate.aiRate = total > 0 ? aggregate.totalAiChars / total : 0; + } + + // Sort by date + return Array.from(dayMap.values()).sort((a, b) => a.date.localeCompare(b.date)); +} + +export interface IAiStatsChartOptions { + sessions: readonly ISessionData[]; + viewMode: ChartViewMode; +} + +export function createAiStatsChart( + options: IAiStatsChartOptions +): HTMLElement { + const { sessions: sessionsData, viewMode: mode } = options; + + const width = 280; + const height = 100; + const margin = { top: 10, right: 10, bottom: 25, left: 30 }; + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + const container = $('.ai-stats-chart-container'); + container.style.position = 'relative'; + container.style.marginTop = '8px'; + + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('width', `${width}px`); + svg.setAttribute('height', `${height}px`); + svg.setAttribute('viewBox', `0 0 ${width} ${height}`); + svg.style.display = 'block'; + container.appendChild(svg); + + const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + g.setAttribute('transform', `translate(${margin.left},${margin.top})`); + svg.appendChild(g); + + if (sessionsData.length === 0) { + const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + text.setAttribute('x', `${innerWidth / 2}`); + text.setAttribute('y', `${innerHeight / 2}`); + text.setAttribute('text-anchor', 'middle'); + text.setAttribute('fill', asCssVariable(chartsForeground)); + text.setAttribute('font-size', '11px'); + text.textContent = localize('noData', "No data yet"); + g.appendChild(text); + return container; + } + + // Draw axes + const xAxisLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + xAxisLine.setAttribute('x1', '0'); + xAxisLine.setAttribute('y1', `${innerHeight}`); + xAxisLine.setAttribute('x2', `${innerWidth}`); + xAxisLine.setAttribute('y2', `${innerHeight}`); + xAxisLine.setAttribute('stroke', asCssVariable(chartsLines)); + xAxisLine.setAttribute('stroke-width', '1px'); + g.appendChild(xAxisLine); + + const yAxisLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + yAxisLine.setAttribute('x1', '0'); + yAxisLine.setAttribute('y1', '0'); + yAxisLine.setAttribute('x2', '0'); + yAxisLine.setAttribute('y2', `${innerHeight}`); + yAxisLine.setAttribute('stroke', asCssVariable(chartsLines)); + yAxisLine.setAttribute('stroke-width', '1px'); + g.appendChild(yAxisLine); + + // Y-axis labels (0%, 50%, 100%) + for (const pct of [0, 50, 100]) { + const y = innerHeight - (pct / 100) * innerHeight; + const label = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + label.setAttribute('x', '-4'); + label.setAttribute('y', `${y + 3}`); + label.setAttribute('text-anchor', 'end'); + label.setAttribute('fill', asCssVariable(chartsForeground)); + label.setAttribute('font-size', '9px'); + label.textContent = `${pct}%`; + g.appendChild(label); + + if (pct > 0) { + const gridLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + gridLine.setAttribute('x1', '0'); + gridLine.setAttribute('y1', `${y}`); + gridLine.setAttribute('x2', `${innerWidth}`); + gridLine.setAttribute('y2', `${y}`); + gridLine.setAttribute('stroke', asCssVariable(chartsLines)); + gridLine.setAttribute('stroke-width', '0.5px'); + gridLine.setAttribute('stroke-dasharray', '2,2'); + g.appendChild(gridLine); + } + } + + if (mode === 'days') { + renderDaysView(); + } else { + renderSessionsView(); + } + + function renderDaysView() { + const dailyData = aggregateSessionsByDay(sessionsData); + const barCount = dailyData.length; + const barWidth = Math.min(20, (innerWidth - (barCount - 1) * 2) / barCount); + const gap = 2; + const totalBarSpace = barCount * barWidth + (barCount - 1) * gap; + const startX = (innerWidth - totalBarSpace) / 2; + + // Calculate which labels to show based on available space + // Each label needs roughly 40px of space to not overlap + const minLabelSpacing = 40; + const totalWidth = totalBarSpace; + const maxLabels = Math.max(2, Math.floor(totalWidth / minLabelSpacing)); + const labelStep = Math.max(1, Math.ceil(barCount / maxLabels)); + + dailyData.forEach((day, i) => { + const x = startX + i * (barWidth + gap); + const barHeight = day.aiRate * innerHeight; + const y = innerHeight - barHeight; + + // Bar for AI rate + const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + rect.setAttribute('x', `${x}`); + rect.setAttribute('y', `${y}`); + rect.setAttribute('width', `${barWidth}`); + rect.setAttribute('height', `${Math.max(1, barHeight)}`); + rect.setAttribute('fill', asCssVariable(chartsBlue)); + rect.setAttribute('rx', '2'); + g.appendChild(rect); + + // X-axis label - only show at calculated intervals to avoid overlap + const isFirst = i === 0; + const isLast = i === barCount - 1; + const isAtInterval = i % labelStep === 0; + + if (isFirst || isLast || (isAtInterval && barCount > 2)) { + // Skip middle labels if they would be too close to first/last + if (!isFirst && !isLast) { + const distFromFirst = i * (barWidth + gap); + const distFromLast = (barCount - 1 - i) * (barWidth + gap); + if (distFromFirst < minLabelSpacing || distFromLast < minLabelSpacing) { + return; // Skip this label + } + } + + const label = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + label.setAttribute('x', `${x + barWidth / 2}`); + label.setAttribute('y', `${innerHeight + 12}`); + label.setAttribute('text-anchor', 'middle'); + label.setAttribute('fill', asCssVariable(chartsForeground)); + label.setAttribute('font-size', '8px'); + label.textContent = day.displayDate; + g.appendChild(label); + } + }); + } + + function renderSessionsView() { + const sessionCount = sessionsData.length; + const barWidth = Math.min(8, (innerWidth - (sessionCount - 1) * 1) / sessionCount); + const gap = 1; + const totalBarSpace = sessionCount * barWidth + (sessionCount - 1) * gap; + const startX = (innerWidth - totalBarSpace) / 2; + + sessionsData.forEach((session, i) => { + const total = session.aiCharacters + session.typedCharacters; + const aiRate = total > 0 ? session.aiCharacters / total : 0; + const x = startX + i * (barWidth + gap); + const barHeight = aiRate * innerHeight; + const y = innerHeight - barHeight; + + // Bar for AI rate + const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + rect.setAttribute('x', `${x}`); + rect.setAttribute('y', `${y}`); + rect.setAttribute('width', `${barWidth}`); + rect.setAttribute('height', `${Math.max(1, barHeight)}`); + rect.setAttribute('fill', asCssVariable(chartsBlue)); + rect.setAttribute('rx', '1'); + g.appendChild(rect); + }); + + // X-axis labels: only show first and last to avoid overlap + // Each label is roughly 40px wide (e.g., "Jan 15") + const minLabelSpacing = 40; + + if (sessionCount === 0) { + return; + } + + // Always show first label + const firstSession = sessionsData[0]; + const firstX = startX; + const firstDate = new Date(firstSession.startTime); + const firstLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + firstLabel.setAttribute('x', `${firstX + barWidth / 2}`); + firstLabel.setAttribute('y', `${innerHeight + 12}`); + firstLabel.setAttribute('text-anchor', 'start'); + firstLabel.setAttribute('fill', asCssVariable(chartsForeground)); + firstLabel.setAttribute('font-size', '8px'); + firstLabel.textContent = firstDate.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + g.appendChild(firstLabel); + + // Show last label if there's enough space and more than 1 session + if (sessionCount > 1 && totalBarSpace >= minLabelSpacing) { + const lastSession = sessionsData[sessionCount - 1]; + const lastX = startX + (sessionCount - 1) * (barWidth + gap); + const lastDate = new Date(lastSession.startTime); + const lastLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + lastLabel.setAttribute('x', `${lastX + barWidth / 2}`); + lastLabel.setAttribute('y', `${innerHeight + 12}`); + lastLabel.setAttribute('text-anchor', 'end'); + lastLabel.setAttribute('fill', asCssVariable(chartsForeground)); + lastLabel.setAttribute('font-size', '8px'); + lastLabel.textContent = lastDate.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + g.appendChild(lastLabel); + } + } + + return container; +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsFeature.ts b/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsFeature.ts index da6c2ea7955..922e0ac5acd 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsFeature.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsFeature.ts @@ -113,6 +113,15 @@ export class AiStatsFeature extends Disposable { return val.sessions.length; }); + public readonly sessions = derived(this, r => { + this._dataVersion.read(r); + const val = this._data.getValue(); + if (!val) { + return []; + } + return val.sessions; + }); + public readonly acceptedInlineSuggestionsToday = derived(this, r => { this._dataVersion.read(r); const val = this._data.getValue(); diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsStatusBar.ts b/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsStatusBar.ts index 16248f06f26..9838eb00d44 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsStatusBar.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsStatusBar.ts @@ -9,7 +9,7 @@ import { IAction } from '../../../../../base/common/actions.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { createHotClass } from '../../../../../base/common/hotReloadHelpers.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { autorun, derived } from '../../../../../base/common/observable.js'; +import { autorun, derived, observableValue } from '../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { localize } from '../../../../../nls.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; @@ -18,11 +18,14 @@ import { ITelemetryService } from '../../../../../platform/telemetry/common/tele import { IStatusbarService, StatusbarAlignment } from '../../../../services/statusbar/browser/statusbar.js'; import { AI_STATS_SETTING_ID } from '../settingIds.js'; import type { AiStatsFeature } from './aiStatsFeature.js'; +import { ChartViewMode, createAiStatsChart } from './aiStatsChart.js'; import './media.css'; export class AiStatsStatusBar extends Disposable { public static readonly hot = createHotClass(this); + private readonly _chartViewMode = observableValue(this, 'days'); + constructor( private readonly _aiStatsFeature: AiStatsFeature, @IStatusbarService private readonly _statusbarService: IStatusbarService, @@ -129,7 +132,7 @@ export class AiStatsStatusBar extends Disposable { n.div({ class: 'header', style: { - minWidth: '200px', + minWidth: '280px', } }, [ @@ -154,28 +157,89 @@ export class AiStatsStatusBar extends Disposable { n.div({ style: { flex: 1, paddingRight: '4px' } }, [ localize('text1', "AI vs Typing Average: {0}", aiRatePercent.get()), ]), - /* - TODO: Write article that explains the ratio and link to it. - - n.div({ style: { marginLeft: 'auto' } }, actionBar([ - { - action: { - id: 'aiStatsStatusBar.openSettings', - label: '', - enabled: true, - run: () => { }, - class: ThemeIcon.asClassName(Codicon.info), - tooltip: '' - }, - options: { icon: true, label: true, } - } - ]))*/ ]), n.div({ style: { flex: 1, paddingRight: '4px' } }, [ localize('text2', "Accepted inline suggestions today: {0}", this._aiStatsFeature.acceptedInlineSuggestionsToday.get()), ]), + + // Chart section + n.div({ + style: { + marginTop: '8px', + borderTop: '1px solid var(--vscode-widget-border)', + paddingTop: '8px', + } + }, [ + // Chart header with toggle + n.div({ + class: 'header', + style: { + display: 'flex', + alignItems: 'center', + marginBottom: '4px', + } + }, [ + n.div({ style: { flex: 1 } }, [ + this._chartViewMode.map(mode => + mode === 'days' + ? localize('chartHeaderDays', "AI Rate by Day") + : localize('chartHeaderSessions', "AI Rate by Session") + ) + ]), + n.div({ + class: 'chart-view-toggle', + style: { marginLeft: 'auto', display: 'flex', gap: '2px' } + }, [ + this._createToggleButton('days', localize('viewByDays', "Days"), Codicon.calendar), + this._createToggleButton('sessions', localize('viewBySessions', "Sessions"), Codicon.listFlat), + ]) + ]), + + // Chart container + derived(reader => { + const sessions = this._aiStatsFeature.sessions.read(reader); + const viewMode = this._chartViewMode.read(reader); + return n.div({ + ref: (container) => { + const chart = createAiStatsChart({ + sessions, + viewMode, + }); + container.appendChild(chart); + } + }); + }), + ]), ]); } + + private _createToggleButton(mode: ChartViewMode, tooltip: string, icon: ThemeIcon) { + return derived(reader => { + const currentMode = this._chartViewMode.read(reader); + const isActive = currentMode === mode; + + return n.div({ + class: ['chart-toggle-button', isActive ? 'active' : ''], + style: { + padding: '2px 4px', + borderRadius: '3px', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + onclick: () => { + this._chartViewMode.set(mode, undefined); + }, + title: tooltip, + }, [ + n.div({ + class: ThemeIcon.asClassName(icon), + style: { fontSize: '14px' } + }) + ]); + }); + } } function actionBar(actions: { action: IAction; options: IActionOptions }[], options?: IActionBarOptions) { diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editStats/media.css b/src/vs/workbench/contrib/editTelemetry/browser/editStats/media.css index e0eaa8eff4a..3668b5565fd 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editStats/media.css +++ b/src/vs/workbench/contrib/editTelemetry/browser/editStats/media.css @@ -33,6 +33,39 @@ margin-bottom: 5px; } + /* Chart toggle buttons */ + .chart-view-toggle { + display: flex; + gap: 2px; + } + + .chart-toggle-button { + padding: 2px 6px; + border-radius: 3px; + cursor: pointer; + opacity: 0.6; + transition: opacity 0.15s, background-color 0.15s; + } + + .chart-toggle-button:hover { + opacity: 1; + background-color: var(--vscode-toolbar-hoverBackground); + } + + .chart-toggle-button.active { + opacity: 1; + background-color: var(--vscode-toolbar-activeBackground); + } + + /* Chart container */ + .ai-stats-chart-container { + margin-top: 4px; + } + + .ai-stats-chart-container svg { + overflow: visible; + } + /* Setup for New User */ .setup .chat-feature-container { From 14cddf39522b1ce5c386e9e0f90e8141df8bc1b6 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 16 Jan 2026 12:18:44 -0800 Subject: [PATCH 107/387] Add skill provider API (#287948) --- .../workbench/api/common/extHost.api.impl.ts | 5 ++ .../api/common/extHostChatAgents2.ts | 16 ++-- src/vs/workbench/api/common/extHostTypes.ts | 5 ++ .../vscode.proposed.chatPromptFiles.d.ts | 73 +++++++++++++++++-- 4 files changed, 87 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 120c1acb4bc..38849ed2a4a 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1558,6 +1558,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatPromptFiles'); return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.prompt, provider); }, + registerSkillProvider(provider: vscode.SkillProvider): vscode.Disposable { + checkProposedApiEnabled(extension, 'chatPromptFiles'); + return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.skill, provider); + }, }; // namespace: lm @@ -1963,6 +1967,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I CustomAgentChatResource: extHostTypes.CustomAgentChatResource, InstructionsChatResource: extHostTypes.InstructionsChatResource, PromptFileChatResource: extHostTypes.PromptFileChatResource, + SkillChatResource: extHostTypes.SkillChatResource, }; }; } diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 7a724abf1c2..a9285d5534d 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -417,7 +417,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS private readonly _relatedFilesProviders = new Map(); private static _contributionsProviderIdPool = 0; - private readonly _promptFileProviders = new Map(); + private readonly _promptFileProviders = new Map(); private readonly _sessionDisposables: DisposableResourceMap = this._register(new DisposableResourceMap()); private readonly _completionDisposables: DisposableMap = this._register(new DisposableMap()); @@ -499,9 +499,9 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS /** * Internal method that handles all prompt file provider types. - * Routes custom agents, instructions, and prompt files to the unified internal implementation. + * Routes custom agents, instructions, prompt files, and skills to the unified internal implementation. */ - registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: vscode.CustomAgentProvider | vscode.InstructionsProvider | vscode.PromptFileProvider): vscode.Disposable { + registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: vscode.CustomAgentProvider | vscode.InstructionsProvider | vscode.PromptFileProvider | vscode.SkillProvider): vscode.Disposable { const handle = ExtHostChatAgents2._contributionsProviderIdPool++; this._promptFileProviders.set(handle, { extension, provider }); this._proxy.$registerPromptFileProvider(handle, type, extension.identifier); @@ -521,6 +521,9 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS case PromptsType.prompt: changeEvent = (provider as vscode.PromptFileProvider).onDidChangePromptFiles; break; + case PromptsType.skill: + changeEvent = (provider as vscode.SkillProvider).onDidChangeSkills; + break; } if (changeEvent) { @@ -554,7 +557,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS } const provider = providerData.provider; - let resources: vscode.CustomAgentChatResource[] | vscode.InstructionsChatResource[] | vscode.PromptFileChatResource[] | undefined; + let resources: vscode.CustomAgentChatResource[] | vscode.InstructionsChatResource[] | vscode.PromptFileChatResource[] | vscode.SkillChatResource[] | undefined; switch (type) { case PromptsType.agent: resources = await (provider as vscode.CustomAgentProvider).provideCustomAgents(context, token) ?? undefined; @@ -566,7 +569,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS resources = await (provider as vscode.PromptFileProvider).providePromptFiles(context, token) ?? undefined; break; case PromptsType.skill: - throw new Error('Skills prompt file provider not implemented yet'); + resources = await (provider as vscode.SkillProvider).provideSkills(context, token) ?? undefined; + break; } // Convert ChatResourceDescriptor to IPromptFileResource format @@ -583,7 +587,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS }); } - convertChatResourceDescriptorToPromptFileResource(resource: vscode.ChatResourceDescriptor, extensionId: string): IPromptFileResource { + convertChatResourceDescriptorToPromptFileResource(resource: vscode.ChatResourceDescriptor | vscode.ChatResourceUriDescriptor, extensionId: string): IPromptFileResource { if (URI.isUri(resource)) { // Plain URI return { uri: resource }; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 03dc0c22075..b24085ac813 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3902,4 +3902,9 @@ export class InstructionsChatResource implements vscode.InstructionsChatResource export class PromptFileChatResource implements vscode.PromptFileChatResource { constructor(public readonly resource: vscode.ChatResourceDescriptor) { } } + +@es5ClassCompat +export class SkillChatResource implements vscode.SkillChatResource { + constructor(public readonly resource: vscode.ChatResourceUriDescriptor) { } +} //#endregion diff --git a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts index b0da5fe1321..8e35b35ba92 100644 --- a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts +++ b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts @@ -6,18 +6,20 @@ // version: 1 declare module 'vscode' { - // #region Resource Classes + /** + * Describes a chat resource URI with optional editability. + */ + export type ChatResourceUriDescriptor = + | Uri + | { uri: Uri; isEditable?: boolean }; + /** * Describes a chat resource file. */ export type ChatResourceDescriptor = - | Uri - | { - uri: Uri; - isEditable?: boolean; - } + | ChatResourceUriDescriptor | { id: string; content: string; @@ -71,6 +73,23 @@ declare module 'vscode' { constructor(resource: ChatResourceDescriptor); } + /** + * Represents a skill file resource (SKILL.md) + */ + export class SkillChatResource { + /** + * The skill resource descriptor. + */ + readonly resource: ChatResourceUriDescriptor; + + /** + * Creates a new skill resource from the specified resource URI pointing to SKILL.md. + * The parent folder name needs to match the name of the skill in the frontmatter. + * @param resource The chat resource descriptor. + */ + constructor(resource: ChatResourceUriDescriptor); + } + // #endregion // #region Providers @@ -170,6 +189,41 @@ declare module 'vscode' { // #endregion + // #region SkillProvider + + /** + * Context for querying skills. + */ + export type SkillContext = object; + + /** + * A provider that supplies SKILL.md resources for agents. + */ + export interface SkillProvider { + /** + * A human-readable label for this provider. + */ + readonly label: string; + + /** + * An optional event to signal that skills have changed. + */ + readonly onDidChangeSkills?: Event; + + /** + * Provide the list of skills available. + * @param context Context for the query. + * @param token A cancellation token. + * @returns An array of skill resources or a promise that resolves to such. + */ + provideSkills( + context: SkillContext, + token: CancellationToken + ): ProviderResult; + } + + // #endregion + // #region Chat Provider Registration export namespace chat { @@ -199,6 +253,13 @@ declare module 'vscode' { export function registerPromptFileProvider( provider: PromptFileProvider ): Disposable; + + /** + * Register a provider for skills. + * @param provider The skill provider. + * @returns A disposable that unregisters the provider when disposed. + */ + export function registerSkillProvider(provider: SkillProvider): Disposable; } // #endregion From 0e28a4b7604b3ea6b26e1db4576bb3cf4e14f552 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Fri, 16 Jan 2026 21:36:23 +0100 Subject: [PATCH 108/387] Add `Git: Delete` action to run `git rm` command on the current document (#285411) --- extensions/git/package.json | 16 +++++++++++++++ extensions/git/package.nls.json | 2 ++ extensions/git/src/commands.ts | 35 +++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/extensions/git/package.json b/extensions/git/package.json index bb18ee9bf6b..47dbceb9092 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -321,6 +321,13 @@ "icon": "$(discard)", "enablement": "!operationInProgress" }, + { + "command": "git.delete", + "title": "%command.delete%", + "category": "Git", + "icon": "$(trash)", + "enablement": "!operationInProgress" + }, { "command": "git.commit", "title": "%command.commit%", @@ -1297,6 +1304,10 @@ "command": "git.rename", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && resourceScheme == file && scmActiveResourceRepository" }, + { + "command": "git.delete", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && resourceScheme == file" + }, { "command": "git.commit", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" @@ -3303,6 +3314,11 @@ "description": "%config.confirmSync%", "default": true }, + "git.confirmCommittedDelete": { + "type": "boolean", + "description": "%config.confirmCommittedDelete%", + "default": true + }, "git.countBadge": { "type": "string", "enum": [ diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index ef41d0d6f44..94a1f61a516 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -36,6 +36,7 @@ "command.unstageChange": "Unstage Change", "command.unstageSelectedRanges": "Unstage Selected Ranges", "command.rename": "Rename", + "command.delete": "Delete", "command.clean": "Discard Changes", "command.cleanAll": "Discard All Changes", "command.cleanAllTracked": "Discard All Tracked Changes", @@ -167,6 +168,7 @@ "config.autofetch": "When set to true, commits will automatically be fetched from the default remote of the current Git repository. Setting to `all` will fetch from all remotes.", "config.autofetchPeriod": "Duration in seconds between each automatic git fetch, when `#git.autofetch#` is enabled.", "config.confirmSync": "Confirm before synchronizing Git repositories.", + "config.confirmCommittedDelete": "Confirm before deleting committed files with Git.", "config.countBadge": "Controls the Git count badge.", "config.countBadge.all": "Count all changes.", "config.countBadge.tracked": "Count only tracked changes.", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index a0e362d56cf..d531ac92e49 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1405,6 +1405,41 @@ export class CommandCenter { await commands.executeCommand('vscode.open', Uri.file(path.join(repository.root, to)), { viewColumn: ViewColumn.Active }); } + @command('git.delete') + async delete(uri: Uri | undefined): Promise { + const activeDocument = window.activeTextEditor?.document; + uri = uri ?? activeDocument?.uri; + if (!uri) { + return; + } + + const repository = this.model.getRepository(uri); + if (!repository) { + return; + } + + const allChangedResources = [ + ...repository.workingTreeGroup.resourceStates, + ...repository.indexGroup.resourceStates, + ...repository.mergeGroup.resourceStates, + ...repository.untrackedGroup.resourceStates + ]; + + // Check if file has uncommitted changes + const uriString = uri.toString(); + if (allChangedResources.some(o => pathEquals(o.resourceUri.toString(), uriString))) { + window.showInformationMessage(l10n.t('Git: Delete can only be performed on committed files without uncommitted changes.')); + return; + } + + await repository.rm([uri]); + + // Close the active editor if it's not dirty + if (activeDocument && !activeDocument.isDirty && pathEquals(activeDocument.uri.toString(), uriString)) { + await commands.executeCommand('workbench.action.closeActiveEditor'); + } + } + @command('git.stage') async stage(...resourceStates: SourceControlResourceState[]): Promise { this.logger.debug(`[CommandCenter][stage] git.stage ${resourceStates.length} `); From 998f544bc301ed5601871e38a5051a4bfa27608f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 16 Jan 2026 21:42:52 +0100 Subject: [PATCH 109/387] Chat empty view exp is odd (fix #288400) (#288472) --- .../contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index c86f0d77bd2..10963bf00ea 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -512,6 +512,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { // Sessions control: stacked if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { newSessionsContainerVisible = + !!this.chatEntitlementService.sentiment.installed && // chat is installed (otherwise make room for terms and welcome) (!this._widget || this._widget?.isEmpty()) && // chat widget empty !this.welcomeController?.isShowingWelcome.get() && // welcome not showing (this.sessionsCount > 0 || !this.sessionsViewerLimited); // has sessions or is showing all sessions From 87b2e355dd5afedb1d75ce4d7c5e4c42c9088b0e Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 16 Jan 2026 12:51:15 -0800 Subject: [PATCH 110/387] Remove preview feature requirement for Agent Skills (#288477) --- .../service/promptsServiceImpl.ts | 6 +-- .../service/promptsService.test.ts | 42 ------------------- 2 files changed, 1 insertion(+), 47 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 2be3f234d1f..83c2c30e2d5 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -27,7 +27,6 @@ import { ITelemetryService } from '../../../../../../platform/telemetry/common/t import { IUserDataProfileService } from '../../../../../services/userDataProfile/common/userDataProfile.js'; import { IVariableReference } from '../../chatModes.js'; import { PromptsConfig } from '../config/config.js'; -import { IDefaultAccountService } from '../../../../../../platform/defaultAccount/common/defaultAccount.js'; import { getCleanPromptName, IResolvedPromptFile, PromptFileSource } from '../config/promptFileLocations.js'; import { PROMPT_LANGUAGE_ID, PromptsType, getPromptsTypeForLanguageId } from '../promptTypes.js'; import { PromptFilesLocator } from '../utils/promptFilesLocator.js'; @@ -129,7 +128,6 @@ export class PromptsService extends Disposable implements IPromptsService { @IFilesConfigurationService private readonly filesConfigService: IFilesConfigurationService, @IStorageService private readonly storageService: IStorageService, @IExtensionService private readonly extensionService: IExtensionService, - @IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IChatPromptContentStore private readonly chatPromptContentStore: IChatPromptContentStore ) { @@ -756,9 +754,7 @@ export class PromptsService extends Disposable implements IPromptsService { public async findAgentSkills(token: CancellationToken): Promise { const useAgentSkills = this.configurationService.getValue(PromptsConfig.USE_AGENT_SKILLS); - const defaultAccount = await this.defaultAccountService.getDefaultAccount(); - const previewFeaturesEnabled = defaultAccount?.chat_preview_features_enabled ?? true; - if (useAgentSkills && previewFeaturesEnabled) { + if (useAgentSkills) { const result: IAgentSkill[] = []; const seenNames = new Set(); const skillTypes = new Map(); 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 ac42dfb5c4b..e24a22e0802 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,8 +47,6 @@ 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 { IDefaultAccountService } from '../../../../../../../platform/defaultAccount/common/defaultAccount.js'; -import { IDefaultAccount } from '../../../../../../../base/common/defaultAccount.js'; suite('PromptsService', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -84,10 +82,6 @@ suite('PromptsService', () => { activateByEvent: () => Promise.resolve() }); - instaService.stub(IDefaultAccountService, { - getDefaultAccount: () => Promise.resolve({ chat_preview_features_enabled: true } as IDefaultAccount) - }); - fileService = disposables.add(instaService.createInstance(FileService)); instaService.stub(IFileService, fileService); @@ -2360,42 +2354,6 @@ suite('PromptsService', () => { assert.strictEqual(result, undefined); }); - test('should return undefined when chat_preview_features_enabled is false', async () => { - testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); - instaService.stub(IDefaultAccountService, { - getDefaultAccount: () => Promise.resolve({ chat_preview_features_enabled: false } as IDefaultAccount) - }); - - // Recreate service with new stub - service = disposables.add(instaService.createInstance(PromptsService)); - - const result = await service.findAgentSkills(CancellationToken.None); - assert.strictEqual(result, undefined); - - // Restore default stub for other tests - instaService.stub(IDefaultAccountService, { - getDefaultAccount: () => Promise.resolve({ chat_preview_features_enabled: true } as IDefaultAccount) - }); - }); - - test('should return undefined when USE_AGENT_SKILLS is enabled but chat_preview_features_enabled is false', async () => { - testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); - instaService.stub(IDefaultAccountService, { - getDefaultAccount: () => Promise.resolve({ chat_preview_features_enabled: false } as IDefaultAccount) - }); - - // Recreate service with new stub - service = disposables.add(instaService.createInstance(PromptsService)); - - const result = await service.findAgentSkills(CancellationToken.None); - assert.strictEqual(result, undefined); - - // Restore default stub for other tests - instaService.stub(IDefaultAccountService, { - getDefaultAccount: () => Promise.resolve({ chat_preview_features_enabled: true } as IDefaultAccount) - }); - }); - test('should find skills in workspace and user home', async () => { testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); From 05cfe3a2ec29252cf3fcccb79f0c4cf7fefa632c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 16 Jan 2026 22:00:32 +0100 Subject: [PATCH 111/387] agent sessions - tweak how icons show (#288479) * agent sessions - tweak how icons show * . --- .../agentSessions/agentSessionsViewer.ts | 10 ++--- .../viewPane/chatViewTitleControl.ts | 42 ++----------------- 2 files changed, 7 insertions(+), 45 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 17c8d9f3a5a..c7a9442143b 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -13,7 +13,7 @@ import { ICompressedTreeNode } from '../../../../../base/browser/ui/tree/compres import { ICompressibleKeyboardNavigationLabelProvider, ICompressibleTreeRenderer } from '../../../../../base/browser/ui/tree/objectTree.js'; import { ITreeNode, ITreeElementRenderDetails, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction } from '../../../../../base/browser/ui/tree/tree.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { AgentSessionSection, AgentSessionStatus, getAgentChangesSummary, hasValidDiff, IAgentSession, IAgentSessionSection, IAgentSessionsModel, isAgentSession, isAgentSessionSection, isAgentSessionsModel, isLocalAgentSessionItem, isSessionInProgressStatus } from './agentSessionsModel.js'; +import { AgentSessionSection, AgentSessionStatus, getAgentChangesSummary, hasValidDiff, IAgentSession, IAgentSessionSection, IAgentSessionsModel, isAgentSession, isAgentSessionSection, isAgentSessionsModel, isSessionInProgressStatus } from './agentSessionsModel.js'; import { IconLabel } from '../../../../../base/browser/ui/iconLabel/iconLabel.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { Codicon } from '../../../../../base/common/codicons.js'; @@ -329,12 +329,8 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer { if (action.id === ChatViewTitleControl.PICK_AGENT_SESSION_ACTION_ID) { this.titleLabel.value = new ChatViewTitleLabel(action); - this.titleLabel.value.updateTitle(this.title ?? ChatViewTitleControl.DEFAULT_TITLE, this.getIcon()); + this.titleLabel.value.updateTitle(this.title ?? ChatViewTitleControl.DEFAULT_TITLE); return this.titleLabel.value; } @@ -177,7 +174,7 @@ export class ChatViewTitleControl extends Disposable { } this.titleContainer.classList.toggle('visible', this.shouldRender()); - this.titleLabel.value?.updateTitle(title, this.getIcon()); + this.titleLabel.value?.updateTitle(title); const currentHeight = this.getHeight(); if (currentHeight !== this.lastKnownHeight) { @@ -187,17 +184,6 @@ export class ChatViewTitleControl extends Disposable { } } - private getIcon(): ThemeIcon | undefined { - const sessionType = this.model?.contributedChatSession?.chatSessionType; - switch (sessionType) { - case AgentSessionProviders.Background: - case AgentSessionProviders.Cloud: - return getAgentSessionProviderIcon(sessionType); - } - - return undefined; - } - private shouldRender(): boolean { if (!this.isEnabled()) { return false; // title hidden via setting @@ -222,10 +208,8 @@ export class ChatViewTitleControl extends Disposable { class ChatViewTitleLabel extends ActionViewItem { private title: string | undefined; - private icon: ThemeIcon | undefined; private titleLabel: HTMLSpanElement | undefined = undefined; - private titleIcon: HTMLSpanElement | undefined = undefined; constructor(action: IAction, options?: IActionViewItemOptions) { super(null, action, { ...options, icon: false, label: true }); @@ -237,19 +221,15 @@ class ChatViewTitleLabel extends ActionViewItem { container.classList.add('chat-view-title-action-item'); this.label?.classList.add('chat-view-title-label-container'); - this.titleIcon = this.label?.appendChild(h('span').root); this.titleLabel = this.label?.appendChild(h('span.chat-view-title-label').root); this.updateLabel(); - this.updateIcon(); } - updateTitle(title: string, icon: ThemeIcon | undefined): void { + updateTitle(title: string): void { this.title = title; - this.icon = icon; this.updateLabel(); - this.updateIcon(); } protected override updateLabel(): void { @@ -263,18 +243,4 @@ class ChatViewTitleLabel extends ActionViewItem { this.titleLabel.textContent = ''; } } - - private updateIcon(): void { - if (!this.titleIcon) { - return; - } - - if (this.icon) { - this.titleIcon.className = ThemeIcon.asClassName(this.icon); - show(this.titleIcon); - } else { - this.titleIcon.className = ''; - hide(this.titleIcon); - } - } } From 293d7f252caa4f1b29538ef51e507973fca18470 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:00:43 -0800 Subject: [PATCH 112/387] Support non-default 'debug.toolBarLocation' settings in agentsControl fixes https://github.com/microsoft/vscode/issues/288260 --- .../agentSessions/agentStatusWidget.ts | 81 ++++++++++++++++++- .../agentSessions/media/agentStatusWidget.css | 16 ++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts index 4c3887db7ba..2128bf52bfd 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts @@ -19,7 +19,7 @@ import { ExitAgentSessionProjectionAction } from './agentSessionProjectionAction import { IAgentSessionsService } from './agentSessionsService.js'; import { AgentSessionStatus, IAgentSession, isSessionInProgressStatus } from './agentSessionsModel.js'; import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; -import { IAction } from '../../../../../base/common/actions.js'; +import { IAction, SubmenuAction } from '../../../../../base/common/actions.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IBrowserWorkbenchEnvironmentService } from '../../../../services/environment/browser/environmentService.js'; @@ -30,6 +30,10 @@ import { Schemas } from '../../../../../base/common/network.js'; import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; import { openSession } from './agentSessionsOpener.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { HiddenItemStrategy, WorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; +import { createActionViewItem } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; // Action triggered when clicking the main pill - change this to modify the primary action const ACTION_ID = 'workbench.action.quickchat.toggle'; @@ -49,6 +53,8 @@ const TITLE_DIRTY = '\u25cf '; */ export class AgentStatusWidget extends BaseActionViewItem { + private static readonly _quickOpenCommandId = 'workbench.action.quickOpenWithModes'; + private _container: HTMLElement | undefined; private readonly _dynamicDisposables = this._register(new DisposableStore()); @@ -72,9 +78,14 @@ export class AgentStatusWidget extends BaseActionViewItem { @IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService, @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @IEditorService private readonly editorService: IEditorService, + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(undefined, action, options); + // Create menu for CommandCenterCenter to get items like debug toolbar + const commandCenterMenu = this._register(this.menuService.createMenu(MenuId.CommandCenterCenter, this.contextKeyService)); + // Re-render when control mode or session info changes this._register(this.agentStatusService.onDidChangeMode(() => { this._render(); @@ -100,6 +111,12 @@ export class AgentStatusWidget extends BaseActionViewItem { this._render(); } })); + + // Re-render when command center menu changes (e.g., debug toolbar visibility) + this._register(commandCenterMenu.onDidChange(() => { + this._lastRenderState = undefined; // Force re-render + this._render(); + })); } override render(container: HTMLElement): void { @@ -210,6 +227,9 @@ export class AgentStatusWidget extends BaseActionViewItem { const { activeSessions, unreadSessions, attentionNeededSessions, hasAttentionNeeded } = this._getSessionStats(); + // Render command center items (like debug toolbar) FIRST - to the left + this._renderCommandCenterToolbar(disposables); + // Create pill const pill = $('div.agent-status-pill.chat-input-mode'); if (hasAttentionNeeded) { @@ -315,6 +335,9 @@ export class AgentStatusWidget extends BaseActionViewItem { const { activeSessions, unreadSessions } = this._getSessionStats(); + // Render command center items (like debug toolbar) FIRST - to the left + this._renderCommandCenterToolbar(disposables); + const pill = $('div.agent-status-pill.session-mode'); this._container.appendChild(pill); @@ -345,6 +368,62 @@ export class AgentStatusWidget extends BaseActionViewItem { // #region Reusable Components + /** + * Render command center toolbar items (like debug toolbar) that are registered to CommandCenterCenter. + * Filters out the quick open action since we provide our own search UI. + * Adds a dot separator after the toolbar if content was rendered. + */ + private _renderCommandCenterToolbar(disposables: DisposableStore): void { + if (!this._container) { + return; + } + + // Get menu actions from CommandCenterCenter (e.g., debug toolbar) + const menu = this.menuService.createMenu(MenuId.CommandCenterCenter, this.contextKeyService); + disposables.add(menu); + + const allActions: IAction[] = []; + for (const [, actions] of menu.getActions({ shouldForwardArgs: true })) { + for (const action of actions) { + // Filter out the quick open action - we provide our own search UI + if (action.id === AgentStatusWidget._quickOpenCommandId) { + continue; + } + // For submenus (like debug toolbar), add the submenu actions + if (action instanceof SubmenuAction) { + allActions.push(...action.actions); + } else { + allActions.push(action); + } + } + } + + // Only render toolbar if there are actions + if (allActions.length === 0) { + return; + } + + const hoverDelegate = getDefaultHoverDelegate('mouse'); + const toolbarContainer = $('div.agent-status-command-center-toolbar'); + this._container.appendChild(toolbarContainer); + + const toolbar = this.instantiationService.createInstance(WorkbenchToolBar, toolbarContainer, { + hiddenItemStrategy: HiddenItemStrategy.NoHide, + telemetrySource: 'agentStatusCommandCenter', + actionViewItemProvider: (action, options) => { + return createActionViewItem(this.instantiationService, action, { ...options, hoverDelegate }); + } + }); + disposables.add(toolbar); + + toolbar.setActions(allActions); + + // Add dot separator after the toolbar (matching command center style) + const separator = renderIcon(Codicon.circleSmallFilled); + separator.classList.add('agent-status-separator'); + this._container.appendChild(separator); + } + /** * Render the search button. If parent is provided, appends to parent; otherwise appends to container. */ diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css index 0b7063507a4..fbebd6099bb 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css @@ -271,6 +271,22 @@ Agent Status Widget - Titlebar control outline-offset: -1px; } +/* Command center toolbar (debug toolbar, etc.) */ +.agent-status-command-center-toolbar { + display: flex; + align-items: center; + -webkit-app-region: no-drag; +} + +/* Separator dot between command center toolbar and agent status pill */ +.agent-status-separator { + padding: 0 8px; + height: 100%; + opacity: 0.5; + display: flex; + align-items: center; +} + /* Status badge (separate rectangle on right of pill) */ .agent-status-badge { display: flex; From b122f8c643252d8fd0be6d12670f4346cb842344 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 16 Jan 2026 16:04:56 -0500 Subject: [PATCH 113/387] add name to model picker aria label (#288473) fixes #288460 --- .../chat/browser/widget/input/modelPickerActionItem.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts index f0576287c7a..a6ed9a671a4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts @@ -184,6 +184,12 @@ export class ModelPickerActionItem extends ChatInputPickerActionViewItem { return statusIcon && tooltip ? `${label} • ${tooltip}` : label; } + protected override setAriaLabelAttributes(element: HTMLElement): void { + super.setAriaLabelAttributes(element); + const modelName = this.currentModel?.metadata.name ?? localize('chat.modelPicker.auto', "Auto"); + element.ariaLabel = localize('chat.modelPicker.ariaLabel', "Pick Model, {0}", modelName); + } + protected override renderLabel(element: HTMLElement): IDisposable | null { const { name, statusIcon } = this.currentModel?.metadata || {}; const domChildren = []; From 96a5d8276cb942a1bf4a49b7ff56c742b9eb03f8 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 16 Jan 2026 16:14:21 -0500 Subject: [PATCH 114/387] improve chat response accessible view content (#288490) fix #284313 --- .../chatResponseAccessibleView.ts | 202 +++++++++-- .../chatResponseAccessibleView.test.ts | 342 ++++++++++++++++++ 2 files changed, 511 insertions(+), 33 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts diff --git a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts index 4cb1a09d8c5..d7cd83cd4bf 100644 --- a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts @@ -5,9 +5,10 @@ import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; -import { isMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { IMarkdownString, isMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; import { stripIcons } from '../../../../../base/common/iconLabels.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from '../../../../../platform/accessibility/browser/accessibleView.js'; import { IAccessibleViewImplementation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; @@ -15,10 +16,11 @@ import { ServicesAccessor } from '../../../../../platform/instantiation/common/i import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { migrateLegacyTerminalToolSpecificData } from '../../common/chat.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; -import { IChatToolInvocation } from '../../common/chatService/chatService.js'; +import { IChatExtensionsContent, IChatPullRequestContent, IChatSubagentToolInvocationData, IChatTerminalToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, ILegacyChatTerminalToolInvocationData, IToolResultOutputDetailsSerialized, isLegacyChatTerminalToolInvocationData } from '../../common/chatService/chatService.js'; import { isResponseVM } from '../../common/model/chatViewModel.js'; -import { isToolResultInputOutputDetails, isToolResultOutputDetails, toolContentToA11yString } from '../../common/tools/languageModelToolsService.js'; +import { IToolResultInputOutputDetails, IToolResultOutputDetails, isToolResultInputOutputDetails, isToolResultOutputDetails, toolContentToA11yString } from '../../common/tools/languageModelToolsService.js'; import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js'; +import { Location } from '../../../../../editor/common/languages.js'; export class ChatResponseAccessibleView implements IAccessibleViewImplementation { readonly priority = 100; @@ -46,6 +48,137 @@ export class ChatResponseAccessibleView implements IAccessibleViewImplementation } } +type ToolSpecificData = IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent | IChatSubagentToolInvocationData; +type ResultDetails = Array | IToolResultInputOutputDetails | IToolResultOutputDetails | IToolResultOutputDetailsSerialized; + +function isOutputDetailsSerialized(obj: unknown): obj is IToolResultOutputDetailsSerialized { + return typeof obj === 'object' && obj !== null && 'output' in obj && + typeof (obj as IToolResultOutputDetailsSerialized).output === 'object' && + (obj as IToolResultOutputDetailsSerialized).output?.type === 'data' && + typeof (obj as IToolResultOutputDetailsSerialized).output?.base64Data === 'string'; +} + +export function getToolSpecificDataDescription(toolSpecificData: ToolSpecificData | undefined): string { + if (!toolSpecificData) { + return ''; + } + + if (isLegacyChatTerminalToolInvocationData(toolSpecificData) || toolSpecificData.kind === 'terminal') { + const terminalData = migrateLegacyTerminalToolSpecificData(toolSpecificData); + return terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original; + } + + switch (toolSpecificData.kind) { + case 'subagent': { + const parts: string[] = []; + if (toolSpecificData.agentName) { + parts.push(localize('subagentName', "Agent: {0}", toolSpecificData.agentName)); + } + if (toolSpecificData.description) { + parts.push(toolSpecificData.description); + } + if (toolSpecificData.prompt) { + parts.push(localize('subagentPrompt', "Task: {0}", toolSpecificData.prompt)); + } + return parts.join('. ') || ''; + } + case 'extensions': + return toolSpecificData.extensions.length > 0 + ? localize('extensionsList', "Extensions: {0}", toolSpecificData.extensions.join(', ')) + : ''; + case 'todoList': { + const todos = toolSpecificData.todoList; + if (todos.length === 0) { + return ''; + } + const todoDescriptions = todos.map(t => + localize('todoItem', "{0} ({1}): {2}", t.title, t.status, t.description) + ); + return localize('todoListCount', "{0} items: {1}", todos.length, todoDescriptions.join('; ')); + } + case 'pullRequest': + return localize('pullRequestInfo', "PR: {0} by {1}", toolSpecificData.title, toolSpecificData.author); + case 'input': + return typeof toolSpecificData.rawInput === 'string' + ? toolSpecificData.rawInput + : JSON.stringify(toolSpecificData.rawInput); + default: + return ''; + } +} + +export function getResultDetailsDescription(resultDetails: ResultDetails | undefined): { input?: string; files?: string[]; isError?: boolean } { + if (!resultDetails) { + return {}; + } + + if (Array.isArray(resultDetails)) { + const files = resultDetails.map(ref => { + if (URI.isUri(ref)) { + return ref.fsPath || ref.path; + } + return ref.uri.fsPath || ref.uri.path; + }); + return { files }; + } + + if (isToolResultInputOutputDetails(resultDetails)) { + return { + input: resultDetails.input, + isError: resultDetails.isError + }; + } + + if (isOutputDetailsSerialized(resultDetails)) { + return { + input: localize('binaryOutput', "{0} data", resultDetails.output.mimeType) + }; + } + + if (isToolResultOutputDetails(resultDetails)) { + return { + input: localize('binaryOutput', "{0} data", resultDetails.output.mimeType) + }; + } + + return {}; +} + +export function getToolInvocationA11yDescription( + invocationMessage: string | undefined, + pastTenseMessage: string | undefined, + toolSpecificData: ToolSpecificData | undefined, + resultDetails: ResultDetails | undefined, + isComplete: boolean +): string { + const parts: string[] = []; + + const message = isComplete && pastTenseMessage ? pastTenseMessage : invocationMessage; + if (message) { + parts.push(message); + } + + const toolDataDesc = getToolSpecificDataDescription(toolSpecificData); + if (toolDataDesc) { + parts.push(toolDataDesc); + } + + if (isComplete && resultDetails) { + const details = getResultDetailsDescription(resultDetails); + if (details.isError) { + parts.unshift(localize('errored', "Errored")); + } + if (details.input && !toolDataDesc) { + parts.push(localize('input', "Input: {0}", details.input)); + } + if (details.files && details.files.length > 0) { + parts.push(localize('files', "Files: {0}", details.files.join(', '))); + } + } + + return parts.join('. '); +} + class ChatResponseAccessibleProvider extends Disposable implements IAccessibleViewContentProvider { private _focusedItem!: ChatTreeItem; private readonly _focusedItemDisposables = this._register(new DisposableStore()); @@ -76,6 +209,10 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi } } + private _renderMessageAsPlaintext(message: string | IMarkdownString): string { + return typeof message === 'string' ? message : stripIcons(renderAsPlaintext(message, { useLinkFormatter: true })); + } + private _getContent(item: ChatTreeItem): string { let responseContent = isResponseVM(item) ? item.response.toString() : ''; if (!responseContent && 'errorDetails' in item && item.errorDetails) { @@ -100,30 +237,17 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi for (const toolInvocation of toolInvocations) { const state = toolInvocation.state.get(); if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation && state.confirmationMessages?.title) { - const title = typeof state.confirmationMessages.title === 'string' ? state.confirmationMessages.title : state.confirmationMessages.title.value; - const message = typeof state.confirmationMessages.message === 'string' ? state.confirmationMessages.message : stripIcons(renderAsPlaintext(state.confirmationMessages.message!)); - let input = ''; - if (toolInvocation.toolSpecificData) { - if (toolInvocation.toolSpecificData?.kind === 'terminal') { - const terminalData = migrateLegacyTerminalToolSpecificData(toolInvocation.toolSpecificData); - input = terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original; - } else if (toolInvocation.toolSpecificData?.kind === 'subagent') { - input = toolInvocation.toolSpecificData.description ?? ''; - } else { - input = toolInvocation.toolSpecificData?.kind === 'extensions' - ? JSON.stringify(toolInvocation.toolSpecificData.extensions) - : toolInvocation.toolSpecificData?.kind === 'todoList' - ? JSON.stringify(toolInvocation.toolSpecificData.todoList) - : toolInvocation.toolSpecificData?.kind === 'pullRequest' - ? JSON.stringify(toolInvocation.toolSpecificData) - : JSON.stringify(toolInvocation.toolSpecificData.rawInput); - } - } + const title = this._renderMessageAsPlaintext(state.confirmationMessages.title); + const message = state.confirmationMessages.message ? this._renderMessageAsPlaintext(state.confirmationMessages.message) : ''; + const toolDataDesc = getToolSpecificDataDescription(toolInvocation.toolSpecificData); responseContent += `${title}`; - if (input) { - responseContent += `: ${input}`; + if (toolDataDesc) { + responseContent += `: ${toolDataDesc}`; } - responseContent += `\n${message}\n`; + if (message) { + responseContent += `\n${message}`; + } + responseContent += '\n'; } else if (state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { const postApprovalDetails = isToolResultInputOutputDetails(state.resultDetails) ? state.resultDetails.input @@ -133,23 +257,35 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi responseContent += localize('toolPostApprovalA11yView', "Approve results of {0}? Result: ", toolInvocation.toolId) + (postApprovalDetails ?? '') + '\n'; } else { const resultDetails = IChatToolInvocation.resultDetails(toolInvocation); - if (resultDetails && 'input' in resultDetails) { - responseContent += '\n' + (resultDetails.isError ? 'Errored ' : 'Completed '); - responseContent += `${`${typeof toolInvocation.invocationMessage === 'string' ? toolInvocation.invocationMessage : stripIcons(renderAsPlaintext(toolInvocation.invocationMessage))} with input: ${resultDetails.input}`}\n`; + const isComplete = IChatToolInvocation.isComplete(toolInvocation); + const description = getToolInvocationA11yDescription( + this._renderMessageAsPlaintext(toolInvocation.invocationMessage), + toolInvocation.pastTenseMessage ? this._renderMessageAsPlaintext(toolInvocation.pastTenseMessage) : undefined, + toolInvocation.toolSpecificData, + resultDetails, + isComplete + ); + if (description) { + responseContent += '\n' + description + '\n'; } } } const pastConfirmations = item.response.value.filter(item => item.kind === 'toolInvocationSerialized'); for (const pastConfirmation of pastConfirmations) { - if (pastConfirmation.isComplete && pastConfirmation.resultDetails && 'input' in pastConfirmation.resultDetails) { - if (pastConfirmation.pastTenseMessage) { - responseContent += `\n${`${typeof pastConfirmation.pastTenseMessage === 'string' ? pastConfirmation.pastTenseMessage : stripIcons(renderAsPlaintext(pastConfirmation.pastTenseMessage))} with input: ${pastConfirmation.resultDetails.input}`}\n`; - } + const description = getToolInvocationA11yDescription( + this._renderMessageAsPlaintext(pastConfirmation.invocationMessage), + pastConfirmation.pastTenseMessage ? this._renderMessageAsPlaintext(pastConfirmation.pastTenseMessage) : undefined, + pastConfirmation.toolSpecificData, + pastConfirmation.resultDetails, + pastConfirmation.isComplete + ); + if (description) { + responseContent += '\n' + description + '\n'; } } } - const plainText = renderAsPlaintext(new MarkdownString(responseContent), { includeCodeBlocksFences: true }); + const plainText = renderAsPlaintext(new MarkdownString(responseContent), { includeCodeBlocksFences: true, useLinkFormatter: true }); return this._normalizeWhitespace(plainText); } diff --git a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts new file mode 100644 index 00000000000..f8710991564 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts @@ -0,0 +1,342 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { URI } from '../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { Location } from '../../../../../../editor/common/languages.js'; +import { getToolSpecificDataDescription, getResultDetailsDescription, getToolInvocationA11yDescription } from '../../../browser/accessibility/chatResponseAccessibleView.js'; +import { IChatExtensionsContent, IChatPullRequestContent, IChatSubagentToolInvocationData, IChatTerminalToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData } from '../../../common/chatService/chatService.js'; + +suite('ChatResponseAccessibleView', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('getToolSpecificDataDescription', () => { + test('returns empty string for undefined', () => { + assert.strictEqual(getToolSpecificDataDescription(undefined), ''); + }); + + test('returns command line for terminal data', () => { + const terminalData: IChatTerminalToolInvocationData = { + kind: 'terminal', + commandLine: { + original: 'npm install', + toolEdited: 'npm ci', + userEdited: 'npm install --save-dev' + }, + language: 'bash' + }; + // Should prefer userEdited over toolEdited over original + assert.strictEqual(getToolSpecificDataDescription(terminalData), 'npm install --save-dev'); + }); + + test('returns tool edited command for terminal data without user edit', () => { + const terminalData: IChatTerminalToolInvocationData = { + kind: 'terminal', + commandLine: { + original: 'npm install', + toolEdited: 'npm ci' + }, + language: 'bash' + }; + assert.strictEqual(getToolSpecificDataDescription(terminalData), 'npm ci'); + }); + + test('returns original command for terminal data without edits', () => { + const terminalData: IChatTerminalToolInvocationData = { + kind: 'terminal', + commandLine: { + original: 'npm install' + }, + language: 'bash' + }; + assert.strictEqual(getToolSpecificDataDescription(terminalData), 'npm install'); + }); + + test('returns description for subagent data', () => { + const subagentData: IChatSubagentToolInvocationData = { + kind: 'subagent', + agentName: 'TestAgent', + description: 'Running analysis', + prompt: 'Analyze the code' + }; + const result = getToolSpecificDataDescription(subagentData); + assert.ok(result.includes('TestAgent')); + assert.ok(result.includes('Running analysis')); + assert.ok(result.includes('Analyze the code')); + }); + + test('handles subagent with only description', () => { + const subagentData: IChatSubagentToolInvocationData = { + kind: 'subagent', + description: 'Running analysis' + }; + const result = getToolSpecificDataDescription(subagentData); + assert.strictEqual(result, 'Running analysis'); + }); + + test('returns extensions list for extensions data', () => { + const extensionsData: IChatExtensionsContent = { + kind: 'extensions', + extensions: ['eslint', 'prettier', 'typescript'] + }; + const result = getToolSpecificDataDescription(extensionsData); + assert.ok(result.includes('eslint')); + assert.ok(result.includes('prettier')); + assert.ok(result.includes('typescript')); + }); + + test('returns empty for empty extensions array', () => { + const extensionsData: IChatExtensionsContent = { + kind: 'extensions', + extensions: [] + }; + assert.strictEqual(getToolSpecificDataDescription(extensionsData), ''); + }); + + test('returns todo list description for todoList data', () => { + const todoData: IChatTodoListContent = { + kind: 'todoList', + sessionId: 'session-1', + todoList: [ + { id: '1', title: 'Task 1', description: 'Do something', status: 'in-progress' }, + { id: '2', title: 'Task 2', description: 'Do something else', status: 'completed' } + ] + }; + const result = getToolSpecificDataDescription(todoData); + assert.ok(result.includes('2 items')); + assert.ok(result.includes('Task 1')); + assert.ok(result.includes('in-progress')); + assert.ok(result.includes('Task 2')); + assert.ok(result.includes('completed')); + }); + + test('returns empty for empty todo list', () => { + const todoData: IChatTodoListContent = { + kind: 'todoList', + sessionId: 'session-1', + todoList: [] + }; + assert.strictEqual(getToolSpecificDataDescription(todoData), ''); + }); + + test('returns PR info for pullRequest data', () => { + const prData: IChatPullRequestContent = { + kind: 'pullRequest', + uri: URI.file('/test'), + title: 'Add new feature', + description: 'This PR adds a great feature', + author: 'testuser', + linkTag: '#123' + }; + const result = getToolSpecificDataDescription(prData); + assert.ok(result.includes('Add new feature')); + assert.ok(result.includes('testuser')); + }); + + test('returns raw input for input data (string)', () => { + const inputData: IChatToolInputInvocationData = { + kind: 'input', + rawInput: 'some input string' + }; + assert.strictEqual(getToolSpecificDataDescription(inputData), 'some input string'); + }); + + test('returns JSON stringified for input data (object)', () => { + const inputData: IChatToolInputInvocationData = { + kind: 'input', + rawInput: { key: 'value', nested: { data: 123 } } + }; + const result = getToolSpecificDataDescription(inputData); + assert.ok(result.includes('key')); + assert.ok(result.includes('value')); + }); + }); + + suite('getResultDetailsDescription', () => { + test('returns empty object for undefined', () => { + assert.deepStrictEqual(getResultDetailsDescription(undefined), {}); + }); + + test('returns files for URI array', () => { + const uris = [ + URI.file('/path/to/file1.ts'), + URI.file('/path/to/file2.ts') + ]; + const result = getResultDetailsDescription(uris); + assert.ok(result.files); + assert.strictEqual(result.files!.length, 2); + assert.ok(result.files![0].includes('file1.ts')); + assert.ok(result.files![1].includes('file2.ts')); + }); + + test('returns files for Location array', () => { + const locations: Location[] = [ + { uri: URI.file('/path/to/file1.ts'), range: new Range(1, 1, 10, 1) }, + { uri: URI.file('/path/to/file2.ts'), range: new Range(5, 1, 15, 1) } + ]; + const result = getResultDetailsDescription(locations); + assert.ok(result.files); + assert.strictEqual(result.files!.length, 2); + }); + + test('returns input and isError for IToolResultInputOutputDetails', () => { + const details = { + input: 'create_file path=/test/file.ts', + output: [], + isError: false + }; + const result = getResultDetailsDescription(details); + assert.strictEqual(result.input, 'create_file path=/test/file.ts'); + assert.strictEqual(result.isError, false); + }); + + test('returns isError true for errored IToolResultInputOutputDetails', () => { + const details = { + input: 'create_file path=/test/file.ts', + output: [], + isError: true + }; + const result = getResultDetailsDescription(details); + assert.strictEqual(result.isError, true); + }); + }); + + suite('getToolInvocationA11yDescription', () => { + test('returns invocation message when not complete', () => { + const result = getToolInvocationA11yDescription( + 'Creating file', + 'Created file', + undefined, + undefined, + false + ); + assert.strictEqual(result, 'Creating file'); + }); + + test('returns past tense message when complete', () => { + const result = getToolInvocationA11yDescription( + 'Creating file', + 'Created file', + undefined, + undefined, + true + ); + assert.strictEqual(result, 'Created file'); + }); + + test('includes tool-specific data description', () => { + const terminalData: IChatTerminalToolInvocationData = { + kind: 'terminal', + commandLine: { original: 'npm test' }, + language: 'bash' + }; + const result = getToolInvocationA11yDescription( + 'Running command', + 'Ran command', + terminalData, + undefined, + true + ); + assert.ok(result.includes('Ran command')); + assert.ok(result.includes('npm test')); + }); + + test('includes files from result details when complete', () => { + const uris = [ + URI.file('/path/to/file1.ts'), + URI.file('/path/to/file2.ts') + ]; + const result = getToolInvocationA11yDescription( + 'Creating files', + 'Created files', + undefined, + uris, + true + ); + assert.ok(result.includes('Created files')); + assert.ok(result.includes('file1.ts')); + assert.ok(result.includes('file2.ts')); + }); + + test('includes error status when result has error', () => { + const details = { + input: 'create_file path=/test/file.ts', + output: [], + isError: true + }; + const result = getToolInvocationA11yDescription( + 'Creating file', + 'Created file', + undefined, + details, + true + ); + assert.ok(result.includes('Errored')); + }); + + test('does not show input when tool-specific data is provided', () => { + const terminalData: IChatTerminalToolInvocationData = { + kind: 'terminal', + commandLine: { original: 'npm test' }, + language: 'bash' + }; + const details = { + input: 'some redundant input', + output: [], + isError: false + }; + const result = getToolInvocationA11yDescription( + 'Running command', + 'Ran command', + terminalData, + details, + true + ); + // Should have tool-specific data but not the "Input:" label + assert.ok(result.includes('npm test')); + assert.ok(!result.includes('Input:')); + }); + + test('shows input when no tool-specific data', () => { + const details = { + input: 'apply_patch file=/test/file.ts', + output: [], + isError: false + }; + const result = getToolInvocationA11yDescription( + 'Applying patch', + 'Applied patch', + undefined, + details, + true + ); + assert.ok(result.includes('Applied patch')); + assert.ok(result.includes('Input:')); + assert.ok(result.includes('apply_patch')); + }); + + test('handles all parts together', () => { + const subagentData: IChatSubagentToolInvocationData = { + kind: 'subagent', + agentName: 'CodeReviewer', + description: 'Reviewing code changes' + }; + const uris = [URI.file('/src/test.ts')]; + const result = getToolInvocationA11yDescription( + 'Starting code review', + 'Completed code review', + subagentData, + uris, + true + ); + assert.ok(result.includes('Completed code review')); + assert.ok(result.includes('CodeReviewer')); + assert.ok(result.includes('Reviewing code changes')); + assert.ok(result.includes('test.ts')); + }); + }); +}); From 0b53fd0e1d1628e86dc3bc1c143b409eca264fcc Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 21:14:29 +0000 Subject: [PATCH 115/387] Fix Playwright MCP invalid JSON schema for tuple parameters (#288464) * Initial plan * Fix invalid JSON schema for settings tool using z.tuple() Co-authored-by: TylerLeonhardt <2644648+TylerLeonhardt@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: TylerLeonhardt <2644648+TylerLeonhardt@users.noreply.github.com> --- test/mcp/src/automationTools/settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/mcp/src/automationTools/settings.ts b/test/mcp/src/automationTools/settings.ts index 91502fe1cc9..46f91fe8fbf 100644 --- a/test/mcp/src/automationTools/settings.ts +++ b/test/mcp/src/automationTools/settings.ts @@ -37,7 +37,7 @@ export function applySettingsTools(server: McpServer, appService: ApplicationSer 'vscode_automation_settings_add_user_settings', 'Add multiple user settings at once', { - settings: z.array(z.array(z.string()).length(2)).describe('Array of [key, value] setting pairs') + settings: z.array(z.tuple([z.string(), z.string()])).describe('Array of [key, value] setting pairs') }, async (args) => { const { settings } = args; From e394995282f6d7b6502e62d493e632ce5d6e77e0 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 16 Jan 2026 16:17:04 -0500 Subject: [PATCH 116/387] update content instead of recreating it in the accessible view (#288482) fix #288468 --- .../accessibility/browser/accessibleView.ts | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 376f1dda25e..428c63041da 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -22,7 +22,7 @@ import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/b import { IPosition, Position } from '../../../../editor/common/core/position.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { IModelService } from '../../../../editor/common/services/model.js'; -import { ITextModelContentProvider, ITextModelService } from '../../../../editor/common/services/resolverService.js'; + import { AccessibilityHelpNLS } from '../../../../editor/common/standaloneStrings.js'; import { CodeActionController } from '../../../../editor/contrib/codeAction/browser/codeActionController.js'; import { FloatingEditorToolbar } from '../../../../editor/contrib/floatingMenu/browser/floatingMenu.js'; @@ -66,7 +66,7 @@ interface ICodeBlock { chatSessionResource: URI | undefined; } -export class AccessibleView extends Disposable implements ITextModelContentProvider { +export class AccessibleView extends Disposable { private _editorWidget: CodeEditorWidget; private _accessiblityHelpIsShown: IContextKey; @@ -111,7 +111,6 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi @ICommandService private readonly _commandService: ICommandService, @IChatCodeBlockContextProviderService private readonly _codeBlockContextProviderService: IChatCodeBlockContextProviderService, @IStorageService private readonly _storageService: IStorageService, - @ITextModelService private readonly textModelResolverService: ITextModelService, @IQuickInputService private readonly _quickInputService: IQuickInputService, @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, ) { @@ -167,7 +166,6 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi readOnly: true, fontFamily: 'var(--monaco-monospace-font)' }; - this.textModelResolverService.registerTextModelContentProvider(Schemas.accessibleView, this); this._editorWidget = this._register(this._instantiationService.createInstance(CodeEditorWidget, this._container, editorOptions, codeEditorWidgetOptions)); this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => { @@ -216,10 +214,6 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi } } - provideTextContent(resource: URI): Promise | null { - return this._getTextModel(resource); - } - private _resetContextKeys(): void { this._accessiblityHelpIsShown.reset(); this._accessibleViewIsShown.reset(); @@ -545,6 +539,10 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi this._accessibleViewGoToSymbolSupported.set(this._goToSymbolsSupported() ? this.getSymbols()?.length! > 0 : false); } + private _getStableUri(providerId: string): URI { + return URI.from({ path: `accessible-view-${providerId}`, scheme: Schemas.accessibleView }); + } + private _updateContent(provider: AccesibleViewContentProvider, updatedContent?: string): void { let content = updatedContent ?? provider.provideContent(); if (provider.options.type === AccessibleViewType.View) { @@ -590,11 +588,20 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi this.calculateCodeBlocks(this._currentContent); this._updateContextKeys(provider, true); const widgetIsFocused = this._editorWidget.hasTextFocus() || this._editorWidget.hasWidgetFocus(); - this._getTextModel(URI.from({ path: `accessible-view-${provider.id}`, scheme: Schemas.accessibleView, fragment: this._currentContent })).then((model) => { + const stableUri = this._getStableUri(provider.id); + this._getTextModel(stableUri).then((model) => { if (!model) { return; } - this._editorWidget.setModel(model); + // Update the content of the existing model instead of creating a new one + // This preserves the cursor position when content changes + const currentContent = this._currentContent ?? ''; + if (model.getValue() !== currentContent) { + model.setValue(currentContent); + } + if (this._editorWidget.getModel() !== model) { + this._editorWidget.setModel(model); + } const domNode = this._editorWidget.getDomNode(); if (!domNode) { return; @@ -720,7 +727,8 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi if (existing && !existing.isDisposed()) { return existing; } - return this._modelService.createModel(resource.fragment, null, resource, false); + // Create an empty model - content will be set via setValue() to preserve cursor position + return this._modelService.createModel('', null, resource, false); } private _goToSymbolsSupported(): boolean { From a0278fa8412132e818a4d7023b103f883175ba60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 21:49:24 +0000 Subject: [PATCH 117/387] Initial plan From 64baba2b0e3bb58363263015722c8e4e98ea3b2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 21:58:34 +0000 Subject: [PATCH 118/387] Add explicit empty code checks in Ruby presenter Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com> --- .../commandLinePresenter/rubyCommandLinePresenter.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts index 14f087f85eb..7f6f078d070 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts @@ -40,6 +40,11 @@ export function extractRubyCommand(commandLine: string, shell: string, os: Opera if (doubleQuoteMatch?.groups?.code) { let rubyCode = doubleQuoteMatch.groups.code.trim(); + // Return undefined if the trimmed code is empty + if (!rubyCode) { + return undefined; + } + // Unescape quotes based on shell type if (isPowerShell(shell, os)) { // PowerShell uses backtick-quote (`") to escape quotes inside double-quoted strings @@ -57,7 +62,9 @@ export function extractRubyCommand(commandLine: string, shell: string, os: Opera // Single quotes in PowerShell are also literal const singleQuoteMatch = commandLine.match(/^ruby\s+-e\s+'(?.+)'$/s); if (singleQuoteMatch?.groups?.code) { - return singleQuoteMatch.groups.code.trim(); + const rubyCode = singleQuoteMatch.groups.code.trim(); + // Return undefined if the trimmed code is empty + return rubyCode || undefined; } return undefined; From 2406cf346ce65ea5bad61251cb61992da9111a0a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 22:03:05 +0000 Subject: [PATCH 119/387] Use consistent pattern for empty code checks Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com> --- .../tools/commandLinePresenter/rubyCommandLinePresenter.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts index 7f6f078d070..bcd28aa5268 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts @@ -63,8 +63,13 @@ export function extractRubyCommand(commandLine: string, shell: string, os: Opera const singleQuoteMatch = commandLine.match(/^ruby\s+-e\s+'(?.+)'$/s); if (singleQuoteMatch?.groups?.code) { const rubyCode = singleQuoteMatch.groups.code.trim(); + // Return undefined if the trimmed code is empty - return rubyCode || undefined; + if (!rubyCode) { + return undefined; + } + + return rubyCode; } return undefined; From 5ebbcae8ffa37943c8a719f3d4f4be5cfd267cb8 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Sat, 17 Jan 2026 06:06:58 +0800 Subject: [PATCH 120/387] fix confirmation widget appearing inside reasoning (#288494) * double check if our tool has confirmations after streaming * fix package.json * fix again --- .../chat/browser/widget/chatListRenderer.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 4a665c1c095..a1ef74cebd9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -1315,6 +1315,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer confirmation transition to finalize thinking + if (toolInvocation.kind === 'toolInvocation' && IChatToolInvocation.isStreaming(toolInvocation)) { + let wasStreaming = true; + part.addDisposable(autorun(reader => { + const state = toolInvocation.state.read(reader); + if (wasStreaming && state.type !== IChatToolInvocation.StateKind.Streaming) { + wasStreaming = false; + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) { + if (part.domNode) { + const wrapper = part.domNode.parentElement; + if (wrapper?.classList.contains('chat-thinking-tool-wrapper')) { + wrapper.remove(); + } + templateData.value.appendChild(part.domNode); + } + this.finalizeCurrentThinkingPart(context, templateData); + } + } + })); + } } } else { this.finalizeCurrentThinkingPart(context, templateData); From 68f8ad5318b96637355c17086bc8ed40b49cc23a Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:27:05 -0800 Subject: [PATCH 121/387] show filtered agents view when clicking on notification - also brings back chat control button, user can disable via context menu (fix https://github.com/microsoft/vscode/issues/288272) --- .../chat/browser/actions/chatActions.ts | 7 +- .../agentSessions/agentStatusWidget.ts | 112 +++++++++++++++++- .../chat/common/actions/chatContextKeys.ts | 2 + 3 files changed, 117 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 2e30ae24cec..c6b4113cbd4 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -948,9 +948,12 @@ MenuRegistry.appendMenuItem(MenuId.CommandCenter, { ChatContextKeys.Setup.disabled.negate() ), ContextKeyExpr.has('config.chat.commandCenter.enabled'), - ContextKeyExpr.has(`config.${ChatConfiguration.AgentStatusEnabled}`).negate() // Hide when agent status is shown + ContextKeyExpr.or( + ContextKeyExpr.has(`config.${ChatConfiguration.AgentStatusEnabled}`).negate(), // Show when agent status is disabled + ChatContextKeys.agentStatusHasNotifications.negate() // Or when agent status has no notifications + ) ), - order: 10001 // to the right of command center + order: 10003 // to the right of agent controls }); // Add to the global title bar if command center is disabled diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts index 2128bf52bfd..449213eef3e 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts @@ -34,6 +34,8 @@ import { IMenuService, MenuId } from '../../../../../platform/actions/common/act import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { HiddenItemStrategy, WorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; import { createActionViewItem } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { FocusAgentSessionsAction } from './agentSessionsActions.js'; // Action triggered when clicking the main pill - change this to modify the primary action const ACTION_ID = 'workbench.action.quickchat.toggle'; @@ -80,6 +82,7 @@ export class AgentStatusWidget extends BaseActionViewItem { @IEditorService private readonly editorService: IEditorService, @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IStorageService private readonly storageService: IStorageService, ) { super(undefined, action, options); @@ -468,7 +471,7 @@ export class AgentStatusWidget extends BaseActionViewItem { /** * Render the status badge showing in-progress and/or unread session counts. * Shows split UI with both indicators when both types exist. - * Always renders for smooth fade transitions - uses visibility classes. + * When no notifications, shows a chat sparkle icon. */ private _renderStatusBadge(disposables: DisposableStore, activeSessions: IAgentSession[], unreadSessions: IAgentSession[]): void { if (!this._container) { @@ -480,14 +483,19 @@ export class AgentStatusWidget extends BaseActionViewItem { const hasContent = hasActiveSessions || hasUnreadSessions; const badge = $('div.agent-status-badge'); + this._container.appendChild(badge); + + // When no notifications, hide the badge if (!hasContent) { badge.classList.add('empty'); + return; } - this._container.appendChild(badge); // Unread section (blue dot + count) if (hasUnreadSessions) { const unreadSection = $('span.agent-status-badge-section.unread'); + unreadSection.setAttribute('role', 'button'); + unreadSection.tabIndex = 0; const unreadIcon = $('span.agent-status-icon'); reset(unreadIcon, renderIcon(Codicon.circleFilled)); unreadSection.appendChild(unreadIcon); @@ -495,11 +503,27 @@ export class AgentStatusWidget extends BaseActionViewItem { unreadCount.textContent = String(unreadSessions.length); unreadSection.appendChild(unreadCount); badge.appendChild(unreadSection); + + // Click handler - filter to unread sessions + disposables.add(addDisposableListener(unreadSection, EventType.CLICK, (e) => { + e.preventDefault(); + e.stopPropagation(); + this._openSessionsWithFilter('unread'); + })); + disposables.add(addDisposableListener(unreadSection, EventType.KEY_DOWN, (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + this._openSessionsWithFilter('unread'); + } + })); } // In-progress section (session-in-progress icon + count) if (hasActiveSessions) { const activeSection = $('span.agent-status-badge-section.active'); + activeSection.setAttribute('role', 'button'); + activeSection.tabIndex = 0; const runningIcon = $('span.agent-status-icon'); reset(runningIcon, renderIcon(Codicon.sessionInProgress)); activeSection.appendChild(runningIcon); @@ -507,6 +531,20 @@ export class AgentStatusWidget extends BaseActionViewItem { runningCount.textContent = String(activeSessions.length); activeSection.appendChild(runningCount); badge.appendChild(activeSection); + + // Click handler - filter to in-progress sessions + disposables.add(addDisposableListener(activeSection, EventType.CLICK, (e) => { + e.preventDefault(); + e.stopPropagation(); + this._openSessionsWithFilter('inProgress'); + })); + disposables.add(addDisposableListener(activeSection, EventType.KEY_DOWN, (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + this._openSessionsWithFilter('inProgress'); + } + })); } // Setup hover with combined tooltip @@ -527,6 +565,76 @@ export class AgentStatusWidget extends BaseActionViewItem { })); } + /** + * Opens the agent sessions view with a specific filter applied, or clears filter if already applied. + * @param filterType 'unread' to show only unread sessions, 'inProgress' to show only in-progress sessions + */ + private _openSessionsWithFilter(filterType: 'unread' | 'inProgress'): void { + const FILTER_STORAGE_KEY = 'agentSessions.filterExcludes.agentsessionsviewerfiltersubmenu'; + + // Check current filter to see if we should toggle off + const currentFilterStr = this.storageService.get(FILTER_STORAGE_KEY, StorageScope.PROFILE); + let currentFilter: { providers: string[]; states: AgentSessionStatus[]; archived: boolean; read: boolean } | undefined; + if (currentFilterStr) { + try { + currentFilter = JSON.parse(currentFilterStr); + } catch { + // Ignore parse errors + } + } + + // Determine if the current filter matches what we're clicking + const isCurrentlyFilteredToUnread = currentFilter?.read === true && currentFilter.states.length === 0; + const isCurrentlyFilteredToInProgress = currentFilter?.states?.length === 2 && currentFilter.read === false; + + // Build filter excludes based on filter type + let excludes: { providers: string[]; states: AgentSessionStatus[]; archived: boolean; read: boolean }; + + if (filterType === 'unread') { + if (isCurrentlyFilteredToUnread) { + // Toggle off - clear all filters + excludes = { + providers: [], + states: [], + archived: true, + read: false + }; + } else { + // Exclude read sessions to show only unread + excludes = { + providers: [], + states: [], + archived: true, + read: true // exclude read sessions + }; + } + } else { + if (isCurrentlyFilteredToInProgress) { + // Toggle off - clear all filters + excludes = { + providers: [], + states: [], + archived: true, + read: false + }; + } else { + // Exclude Completed and Failed to show InProgress and NeedsInput + excludes = { + providers: [], + states: [AgentSessionStatus.Completed, AgentSessionStatus.Failed], + archived: true, + read: false + }; + } + } + + // Store the filter + this.storageService.store(FILTER_STORAGE_KEY, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER); + + // Open the sessions view + this.commandService.executeCommand(FocusAgentSessionsAction.id); + } + /** * Render the escape button for exiting session projection mode. */ diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index 2a5fc753855..736f3689420 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -109,6 +109,8 @@ export namespace ChatContextKeys { export const isKatexMathElement = new RawContextKey('chatIsKatexMathElement', false, { type: 'boolean', description: localize('chatIsKatexMathElement', "True when focusing a KaTeX math element.") }); export const inAgentSessionProjection = new RawContextKey('chatInAgentSessionProjection', false, { type: 'boolean', description: localize('chatInAgentSessionProjection', "True when the workbench is in agent session projection mode for reviewing an agent session.") }); + + export const agentStatusHasNotifications = new RawContextKey('agentStatusHasNotifications', false, { type: 'boolean', description: localize('agentStatusHasNotifications', "True when the agent status widget has unread or in-progress sessions.") }); } export namespace ChatContextKeyExprs { From 884c14e4e089b8391462456b3e5e7cb9c65f8c85 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:30:08 -0800 Subject: [PATCH 122/387] prettier vertical divider in agent-status-badge-section --- .../browser/agentSessions/media/agentStatusWidget.css | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css index fbebd6099bb..062103a04cb 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css @@ -321,11 +321,18 @@ Agent Status Widget - Titlebar control gap: 4px; padding: 0 8px; height: 100%; + position: relative; } /* Separator between sections */ -.agent-status-badge-section + .agent-status-badge-section { - border-left: 1px solid color-mix(in srgb, var(--vscode-progressBar-background) 40%, transparent); +.agent-status-badge-section + .agent-status-badge-section::before { + content: ''; + position: absolute; + left: 0; + top: 4px; + bottom: 4px; + width: 1px; + background-color: color-mix(in srgb, var(--vscode-progressBar-background) 40%, transparent); } /* Unread section styling */ From a9d855955d01d99ce9df3484149ab2eb97326f76 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:35:14 -0800 Subject: [PATCH 123/387] auto enable command center when toggling 'Agent Status' --- .../browser/agentSessions/agentSessions.contribution.ts | 7 +++++++ src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts index e9a78c12610..ab1beebd19d 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts @@ -28,6 +28,7 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ChatConfiguration } from '../../common/constants.js'; import { AuxiliaryBarMaximizedContext } from '../../../../common/contextkeys.js'; +import { LayoutSettings } from '../../../../services/layout/browser/layoutService.js'; //#region Actions and Menus @@ -240,9 +241,15 @@ class AgentStatusRendering extends Disposable implements IWorkbenchContribution }, undefined)); // Add/remove CSS class on workbench based on setting + // Also force enable command center when agent status is enabled const updateClass = () => { const enabled = configurationService.getValue(ChatConfiguration.AgentStatusEnabled) === true; mainWindow.document.body.classList.toggle('agent-status-enabled', enabled); + + // Force enable command center when agent status is enabled + if (enabled && configurationService.getValue(LayoutSettings.COMMAND_CENTER) !== true) { + configurationService.updateValue(LayoutSettings.COMMAND_CENTER, true); + } }; updateClass(); this._register(configurationService.onDidChangeConfiguration(e => { diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 9f80ffb1cb0..7ec83d99a67 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -195,7 +195,7 @@ configurationRegistry.registerConfiguration({ }, [ChatConfiguration.AgentStatusEnabled]: { type: 'boolean', - markdownDescription: nls.localize('chat.agentsControl.enabled', "Controls whether the Agent Status is shown in the title bar command center, replacing the search box with quick access to chat sessions."), + markdownDescription: nls.localize('chat.agentsControl.enabled', "Controls whether the Agent Status is shown in the title bar command center, replacing the search box with quick access to chat sessions. Enabling this setting will automatically enable {0}.", '`#window.commandCenter#`'), default: false, tags: ['experimental'] }, From fc9ba44a8a34ffbd5138e13d34d0511c1f9ac03a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:37:50 -0800 Subject: [PATCH 124/387] Format document --- .../tools/commandLinePresenter/rubyCommandLinePresenter.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts index bcd28aa5268..6f0f6b6fafd 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts @@ -63,12 +63,12 @@ export function extractRubyCommand(commandLine: string, shell: string, os: Opera const singleQuoteMatch = commandLine.match(/^ruby\s+-e\s+'(?.+)'$/s); if (singleQuoteMatch?.groups?.code) { const rubyCode = singleQuoteMatch.groups.code.trim(); - + // Return undefined if the trimmed code is empty if (!rubyCode) { return undefined; } - + return rubyCode; } From 5e2f44d3f02db26f21db89655095f2e8ae040017 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Fri, 16 Jan 2026 14:39:02 -0800 Subject: [PATCH 125/387] Refactor sessions picker visibility --- .../browser/actions/chatExecuteActions.ts | 12 +- .../browser/widget/input/chatInputPart.ts | 228 +++++++++--------- 2 files changed, 120 insertions(+), 120 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 0d297efe2ff..728a3f112f0 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -479,12 +479,14 @@ export class ChatSessionPrimaryPickerAction extends Action2 { order: 4, group: 'navigation', when: - ContextKeyExpr.or( + ContextKeyExpr.and( ChatContextKeys.chatSessionHasModels, - ChatContextKeys.lockedToCodingAgent, - ContextKeyExpr.and( - ChatContextKeys.inAgentSessionsWelcome, - ChatContextKeys.chatSessionType.notEqualsTo('local') + ContextKeyExpr.or( + ChatContextKeys.lockedToCodingAgent, + ContextKeyExpr.and( + ChatContextKeys.inAgentSessionsWelcome, + ChatContextKeys.chatSessionType.notEqualsTo('local') + ) ) ) } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 1766047413e..33d2fc18eb4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -84,7 +84,7 @@ import { IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from import { IChatModelInputState, IChatRequestModeInfo, IInputModel } from '../../../common/model/chatModel.js'; import { ChatMode, IChatMode, IChatModeService } from '../../../common/chatModes.js'; import { IChatFollowup, IChatService, IChatSessionContext } from '../../../common/chatService/chatService.js'; -import { IChatSessionProviderOptionItem, IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; +import { IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; import { getChatSessionType } from '../../../common/model/chatUri.js'; import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isStringImplicitContextValue, isStringVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; import { IChatResponseViewModel } from '../../../common/model/chatViewModel.js'; @@ -519,6 +519,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Listen for session type changes from the welcome page delegate if (this.options.sessionTypePickerDelegate?.onDidChangeActiveSessionProvider) { this._register(this.options.sessionTypePickerDelegate.onDidChangeActiveSessionProvider(async (newSessionType) => { + this.computeVisibleOptionGroups(); this.agentSessionTypeKey.set(newSessionType); this.updateWidgetLockStateFromSessionType(newSessionType); this.refreshChatSessionPickers(); @@ -754,58 +755,18 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private createChatSessionPickerWidgets(action: MenuItemAction): (ChatSessionPickerActionItem | SearchableOptionPickerActionItem)[] { this._lastSessionPickerAction = action; - // Helper to resolve chat session context - const resolveChatSessionContext = () => { - const sessionResource = this._widget?.viewModel?.model.sessionResource; - if (!sessionResource) { - return undefined; - } - return this.chatService.getChatSessionFromInternalUri(sessionResource); - }; - - // Get all option groups for the current session type - const ctx = resolveChatSessionContext(); - const effectiveSessionType = this.getEffectiveSessionType(ctx, this.options.sessionTypePickerDelegate); - const usingDelegateSessionType = effectiveSessionType !== ctx?.chatSessionType; - const optionGroups = effectiveSessionType ? this.chatSessionsService.getOptionGroupsForSessionType(effectiveSessionType) : undefined; - if (!optionGroups || optionGroups.length === 0) { + const result = this.computeVisibleOptionGroups(); + if (!result) { return []; } + const { visibleGroupIds, optionGroups, effectiveSessionType } = result; // Clear existing widgets this.disposeSessionPickerWidgets(); - // Init option group context keys - for (const optionGroup of optionGroups) { - if (!ctx) { - continue; - } - const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id); - if (currentOption) { - const optionId = typeof currentOption === 'string' ? currentOption : currentOption.id; - this.updateOptionContextKey(optionGroup.id, optionId); - } - } - const widgets: (ChatSessionPickerActionItem | SearchableOptionPickerActionItem)[] = []; for (const optionGroup of optionGroups) { - // For delegate session types, we don't require ctx or session values - if (!usingDelegateSessionType && !ctx) { - continue; - } - - const hasSessionValue = ctx ? this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id) : undefined; - const hasItems = optionGroup.items.length > 0; - // For delegate session types, only check if items exist; otherwise check session value or items - if (!usingDelegateSessionType && !hasSessionValue && !hasItems) { - // This session does not have a value to contribute for this option group - continue; - } - if (usingDelegateSessionType && !hasItems) { - continue; - } - - if (!this.evaluateOptionGroupVisibility(optionGroup)) { + if (!visibleGroupIds.has(optionGroup.id)) { continue; } @@ -821,11 +782,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.updateOptionContextKey(optionGroup.id, option.id); this.getOrCreateOptionEmitter(optionGroup.id).fire(option); - // Only notify session options change if we have an actual session (not delegate-only) - const ctx = resolveChatSessionContext(); - if (ctx && !usingDelegateSessionType) { + // Notify session if we have one (not in welcome view before session creation) + const sessionResource = this._widget?.viewModel?.model.sessionResource; + const currentCtx = sessionResource ? this.chatService.getChatSessionFromInternalUri(sessionResource) : undefined; + if (currentCtx) { this.chatSessionsService.notifySessionOptionsChange( - ctx.chatSessionResource, + currentCtx.chatSessionResource, [{ optionId: optionGroup.id, value: option }] ).catch(err => this.logService.error(`Failed to notify extension of ${optionGroup.id} change:`, err)); } @@ -834,10 +796,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.refreshChatSessionPickers(); }, getOptionGroup: () => { - // Use the effective session type (delegate's type takes precedence) - // effectiveSessionType is guaranteed to be defined here since we've already - // validated optionGroups exist at this point - const groups = effectiveSessionType ? this.chatSessionsService.getOptionGroupsForSessionType(effectiveSessionType) : undefined; + const groups = this.chatSessionsService.getOptionGroupsForSessionType(effectiveSessionType); return groups?.find(g => g.id === optionGroup.id); } }; @@ -1396,84 +1355,118 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } /** - * Refresh all registered option groups for the current chat session. - * Fires events for each option group with their current selection. + * Computes which option groups should be visible for the current session. + * + * A picker should show if and only if: + * 1. We can determine a session type (from session context OR delegate) + * 2. That session type has option groups registered + * 3. At least one option group has items AND passes its `when` clause + * + * This method also updates the `chatSessionHasOptions` context key, which controls + * whether the picker action is shown in the toolbar via its `when` clause. + * + * @returns The result containing visible group IDs and related context, or undefined + * if there are no visible option groups */ - private refreshChatSessionPickers(): void { - const sessionResource = this._widget?.viewModel?.model.sessionResource; - const hideAll = () => { + private computeVisibleOptionGroups(): { + visibleGroupIds: Set; + optionGroups: IChatSessionProviderOptionGroup[]; + ctx: IChatSessionContext | undefined; + effectiveSessionType: string; + } | undefined { + const setNoOptions = () => { this.chatSessionHasOptions.set(false); - this.chatSessionOptionsValid.set(true); // No options means nothing to validate - this.hideAllSessionPickerWidgets(); + this.chatSessionOptionsValid.set(true); }; - if (!sessionResource) { - return hideAll(); - } - const ctx = this.chatService.getChatSessionFromInternalUri(sessionResource); - if (!ctx) { - return hideAll(); + // Step 1: Determine the session type + // - Panel/Editor: Use actual session's type (ctx available) + // - Welcome view: Use delegate's type (ctx may not exist yet) + const sessionResource = this._widget?.viewModel?.model.sessionResource; + const ctx = sessionResource ? this.chatService.getChatSessionFromInternalUri(sessionResource) : undefined; + const delegateSessionType = this.options.sessionTypePickerDelegate?.getActiveSessionProvider?.(); + const effectiveSessionType = delegateSessionType ?? ctx?.chatSessionType; + + if (!effectiveSessionType) { + setNoOptions(); + return undefined; } - const effectiveSessionType = this.getEffectiveSessionType(ctx, this.options.sessionTypePickerDelegate); - const usingDelegateSessionType = effectiveSessionType !== ctx.chatSessionType; + // Step 2: Get option groups for this session type const optionGroups = this.chatSessionsService.getOptionGroupsForSessionType(effectiveSessionType); if (!optionGroups || optionGroups.length === 0) { - return hideAll(); + setNoOptions(); + return undefined; } - // For delegate-provided session types, we don't require the actual session to have options - // because the actual session might be local while the delegate selects a different type - if (!usingDelegateSessionType && !this.chatSessionsService.hasAnySessionOptions(ctx.chatSessionResource)) { - return hideAll(); - } - - // First update all context keys with current values (before evaluating visibility) - for (const optionGroup of optionGroups) { - const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id); - if (currentOption) { - const optionId = typeof currentOption === 'string' ? currentOption : currentOption.id; - this.updateOptionContextKey(optionGroup.id, optionId); - } else { - this.logService.trace(`[ChatInputPart] No session option set for group '${optionGroup.id}'`); + // Update context keys with current option values before evaluating `when` clauses. + // This ensures interdependent `when` expressions work correctly. + if (ctx) { + for (const optionGroup of optionGroups) { + const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id); + if (currentOption) { + const optionId = typeof currentOption === 'string' ? currentOption : currentOption.id; + this.updateOptionContextKey(optionGroup.id, optionId); + } } } - // Compute which option groups should be visible based on when expressions + // Step 3: Filter to visible groups (has items AND passes `when` clause) const visibleGroupIds = new Set(); for (const optionGroup of optionGroups) { - if (!this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id)) { - continue; - } - if (this.evaluateOptionGroupVisibility(optionGroup)) { + const hasItems = optionGroup.items.length > 0; + const passesWhenClause = this.evaluateOptionGroupVisibility(optionGroup); + + if (hasItems && passesWhenClause) { visibleGroupIds.add(optionGroup.id); } } - // Only show the picker if there are visible option groups if (visibleGroupIds.size === 0) { - return hideAll(); + setNoOptions(); + return undefined; } - // Validate that all selected options exist in their respective option group items + // Validate selected options exist in their respective groups let allOptionsValid = true; - for (const optionGroup of optionGroups) { - const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id); - if (currentOption) { - const currentOptionId = typeof currentOption === 'string' ? currentOption : currentOption.id; - const isValidOption = optionGroup.items.some(item => item.id === currentOptionId); - if (!isValidOption) { - this.logService.trace(`[ChatInputPart] Selected option '${currentOptionId}' is not valid for group '${optionGroup.id}'`); - allOptionsValid = false; + if (ctx) { + for (const groupId of visibleGroupIds) { + const optionGroup = optionGroups.find(g => g.id === groupId); + const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, groupId); + if (optionGroup && currentOption) { + const currentOptionId = typeof currentOption === 'string' ? currentOption : currentOption.id; + if (!optionGroup.items.some(item => item.id === currentOptionId)) { + allOptionsValid = false; + break; + } } } } - this.chatSessionOptionsValid.set(allOptionsValid); this.chatSessionHasOptions.set(true); + this.chatSessionOptionsValid.set(allOptionsValid); + return { visibleGroupIds, optionGroups, ctx, effectiveSessionType }; + } + + /** + * Refresh all registered option groups for the current chat session. + * Fires events for each option group with their current selection. + */ + private refreshChatSessionPickers(): void { + // Use the shared helper to compute visibility and update context keys + const result = this.computeVisibleOptionGroups(); + + if (!result) { + // No visible options - helper already updated context keys + this.hideAllSessionPickerWidgets(); + return; + } + + const { visibleGroupIds, optionGroups, ctx } = result; + + // Check if widgets need recreation (different set of visible groups) const currentWidgetGroupIds = new Set(this.chatSessionPickerWidgets.keys()); - const needsRecreation = currentWidgetGroupIds.size !== visibleGroupIds.size || !Array.from(visibleGroupIds).every(id => currentWidgetGroupIds.has(id)); @@ -1492,20 +1485,24 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.chatSessionPickerContainer.style.display = ''; } - for (const [optionGroupId] of this.chatSessionPickerWidgets.entries()) { - const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroupId); - if (currentOption) { - const optionGroup = optionGroups.find(g => g.id === optionGroupId); - if (optionGroup) { - const currentOptionId = typeof currentOption === 'string' ? currentOption : currentOption.id; - const item = optionGroup.items.find(m => m.id === currentOptionId); - if (item) { - // If currentOption is an object (not a string ID), it represents a complete option item and should be used directly. - // Otherwise, if it's a string ID, look up the corresponding item and use that. - if (typeof currentOption === 'string') { - this.getOrCreateOptionEmitter(optionGroupId).fire(item); - } else { - this.getOrCreateOptionEmitter(optionGroupId).fire(currentOption); + // Fire option change events for existing widgets to sync their state + // (only if we have a session context - in welcome view, options aren't persisted yet) + if (ctx) { + for (const [optionGroupId] of this.chatSessionPickerWidgets.entries()) { + const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroupId); + if (currentOption) { + const optionGroup = optionGroups.find(g => g.id === optionGroupId); + if (optionGroup) { + const currentOptionId = typeof currentOption === 'string' ? currentOption : currentOption.id; + const item = optionGroup.items.find((m: IChatSessionProviderOptionItem) => m.id === currentOptionId); + if (item) { + // If currentOption is an object (not a string ID), it represents a complete option item and should be used directly. + // Otherwise, if it's a string ID, look up the corresponding item and use that. + if (typeof currentOption === 'string') { + this.getOrCreateOptionEmitter(optionGroupId).fire(item); + } else { + this.getOrCreateOptionEmitter(optionGroupId).fire(currentOption); + } } } } @@ -1630,6 +1627,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge render(container: HTMLElement, initialValue: string, widget: IChatWidget) { this._widget = widget; + this.computeVisibleOptionGroups(); this._register(widget.onDidChangeViewModel(() => { // Update agentSessionType when view model changes From e97df5b642b63a7f885446db034a6f1c2f8f3848 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:46:06 -0800 Subject: [PATCH 126/387] tidy up commands (fix https://github.com/microsoft/vscode/issues/288082) --- .../agentSessionProjectionActions.ts | 42 +------------------ .../agentSessions.contribution.ts | 1 - .../agentSessions/agentSessionsActions.ts | 9 ---- 3 files changed, 1 insertion(+), 51 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionActions.ts index 0be275274f0..7571d0f8e50 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionActions.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize, localize2 } from '../../../../../nls.js'; -import { Action2, MenuId } from '../../../../../platform/actions/common/actions.js'; +import { Action2 } from '../../../../../platform/actions/common/actions.js'; import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { KeyCode } from '../../../../../base/common/keyCodes.js'; @@ -15,7 +15,6 @@ import { IAgentSessionProjectionService } from './agentSessionProjectionService. import { IAgentSession, isMarshalledAgentSessionContext, IMarshalledAgentSessionContext } from './agentSessionsModel.js'; import { IAgentSessionsService } from './agentSessionsService.js'; import { CHAT_CATEGORY } from '../actions/chatActions.js'; -import { openSessionInChatWidget } from './agentSessionsOpener.js'; import { ToggleTitleBarConfigAction } from '../../../../browser/parts/titlebar/titlebarActions.js'; import { IsCompactTitleBarContext } from '../../../../common/contextkeys.js'; @@ -90,45 +89,6 @@ export class ExitAgentSessionProjectionAction extends Action2 { //#endregion -//#region Open in Chat Panel - -export class OpenInChatPanelAction extends Action2 { - static readonly ID = 'agentSession.openInChatPanel'; - - constructor() { - super({ - id: OpenInChatPanelAction.ID, - title: localize2('openInChatPanel', "Open in Chat Panel"), - category: CHAT_CATEGORY, - precondition: ChatContextKeys.enabled, - menu: [{ - id: MenuId.AgentSessionsContext, - group: '1_open', - order: 1, - }] - }); - } - - override async run(accessor: ServicesAccessor, context?: IAgentSession | IMarshalledAgentSessionContext): Promise { - const agentSessionsService = accessor.get(IAgentSessionsService); - - let session: IAgentSession | undefined; - if (context) { - if (isMarshalledAgentSessionContext(context)) { - session = agentSessionsService.getSession(context.session.resource); - } else { - session = context; - } - } - - if (session) { - await openSessionInChatWidget(accessor, session); - } - } -} - -//#endregion - //#region Toggle Agent Status export class ToggleAgentStatusAction extends ToggleTitleBarConfigAction { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts index ab1beebd19d..10c3927eab9 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts @@ -62,7 +62,6 @@ registerAction2(SetAgentSessionsOrientationSideBySideAction); // Agent Session Projection registerAction2(EnterAgentSessionProjectionAction); registerAction2(ExitAgentSessionProjectionAction); -// registerAction2(OpenInChatPanelAction); // TODO@joshspicer https://github.com/microsoft/vscode/issues/288082 registerAction2(ToggleAgentStatusAction); registerAction2(ToggleAgentSessionProjectionAction); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts index 5537672599a..1510faf82ec 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts @@ -196,15 +196,6 @@ export class PickAgentSessionAction extends Action2 { group: 'navigation', order: 2 }, - { - id: MenuId.ViewTitle, - when: ContextKeyExpr.and( - ContextKeyExpr.equals('view', ChatViewId), - ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsEnabled}`, true) - ), - group: '2_history', - order: 1 - }, { id: MenuId.EditorTitle, when: ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), From 8f3cef17ff6c1c992d855e396b4933eb8a72f169 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:49:38 -0800 Subject: [PATCH 127/387] handle layout with chat control --- .../agentSessions/media/agentStatusWidget.css | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css index 062103a04cb..e1d663108da 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css @@ -299,19 +299,11 @@ Agent Status Widget - Titlebar control border: 1px solid color-mix(in srgb, var(--vscode-progressBar-background) 50%, transparent); flex-shrink: 0; -webkit-app-region: no-drag; - transition: opacity 0.2s ease-in-out, background-color 0.2s ease-in-out, border-color 0.2s ease-in-out; - opacity: 1; - /* Reserve minimum width to prevent layout shift */ - min-width: 50px; - justify-content: center; } -/* Empty badge - invisible but reserves space to prevent layout shift */ +/* Empty badge - completely hidden */ .agent-status-badge.empty { - opacity: 0; - pointer-events: none; - background-color: transparent; - border-color: transparent; + display: none; } /* Badge section (for split UI) */ From 4bd05d1f5ae96c9e45c928a45066170eea9017e6 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:53:29 -0800 Subject: [PATCH 128/387] clear agent session filter when filtered category is completed --- .../agentSessions/agentStatusWidget.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts index 449213eef3e..120424e3bed 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts @@ -482,6 +482,9 @@ export class AgentStatusWidget extends BaseActionViewItem { const hasUnreadSessions = unreadSessions.length > 0; const hasContent = hasActiveSessions || hasUnreadSessions; + // Auto-clear filter if the filtered category becomes empty + this._clearFilterIfCategoryEmpty(hasUnreadSessions, hasActiveSessions); + const badge = $('div.agent-status-badge'); this._container.appendChild(badge); @@ -565,6 +568,46 @@ export class AgentStatusWidget extends BaseActionViewItem { })); } + /** + * Clear the filter if the currently filtered category becomes empty. + * For example, if filtered to "unread" but no unread sessions exist, clear the filter. + */ + private _clearFilterIfCategoryEmpty(hasUnreadSessions: boolean, hasActiveSessions: boolean): void { + const FILTER_STORAGE_KEY = 'agentSessions.filterExcludes.agentsessionsviewerfiltersubmenu'; + + const currentFilterStr = this.storageService.get(FILTER_STORAGE_KEY, StorageScope.PROFILE); + if (!currentFilterStr) { + return; + } + + let currentFilter: { providers: string[]; states: AgentSessionStatus[]; archived: boolean; read: boolean } | undefined; + try { + currentFilter = JSON.parse(currentFilterStr); + } catch { + return; + } + + if (!currentFilter) { + return; + } + + // Detect if filtered to unread (read=true excludes read sessions, leaving only unread) + const isFilteredToUnread = currentFilter.read === true && currentFilter.states.length === 0; + // Detect if filtered to in-progress (2 excluded states = Completed + Failed) + const isFilteredToInProgress = currentFilter.states?.length === 2 && currentFilter.read === false; + + // Clear filter if filtered category is now empty + if ((isFilteredToUnread && !hasUnreadSessions) || (isFilteredToInProgress && !hasActiveSessions)) { + const clearedFilter = { + providers: [], + states: [], + archived: true, + read: false + }; + this.storageService.store(FILTER_STORAGE_KEY, JSON.stringify(clearedFilter), StorageScope.PROFILE, StorageTarget.USER); + } + } + /** * Opens the agent sessions view with a specific filter applied, or clears filter if already applied. * @param filterType 'unread' to show only unread sessions, 'inProgress' to show only in-progress sessions From dcd93ff50e4ec957ced1522d25e82a3f1bfec428 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:00:37 -0800 Subject: [PATCH 129/387] fix how to detect if an agent's artifacts count as 'projectable' --- .../agentSessionProjectionService.ts | 64 +++++++++++++------ 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionService.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionService.ts index db02aad380e..8521dd2ecd7 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionService.ts @@ -142,7 +142,11 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS } } - private async _openSessionFiles(session: IAgentSession): Promise { + /** + * Open the session's files in a multi-diff editor. + * @returns true if any files were opened, false if nothing to display + */ + private async _openSessionFiles(session: IAgentSession): Promise { // Clear editors first await this.editorGroupsService.applyWorkingSet('empty', { preserveFocus: true }); @@ -178,11 +182,14 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS const sessionKey = session.resource.toString(); const newWorkingSet = this.editorGroupsService.saveWorkingSet(`agent-session-projection-${sessionKey}`); this._sessionWorkingSets.set(sessionKey, newWorkingSet); + return true; } else { this.logService.trace(`[AgentSessionProjection] No files with diffs to display (all changes missing originalUri)`); + return false; } } else { this.logService.trace(`[AgentSessionProjection] Session has no changes to display`); + return false; } } @@ -222,24 +229,45 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS this._sessionWorkingSets.set(previousSessionKey, previousWorkingSet); } - // Always open session files to ensure they're displayed - await this._openSessionFiles(session); - - // Set active state - const wasActive = this._isActive; - this._isActive = true; - this._activeSession = session; - this._inProjectionModeContextKey.set(true); - this.layoutService.mainContainer.classList.add('agent-session-projection-active'); - - // Update the agent status to show session mode - this.agentStatusService.enterSessionMode(session.resource.toString(), session.label); - - if (!wasActive) { - this._onDidChangeProjectionMode.fire(true); + // For local sessions, changes are shown via chatEditing.viewChanges, not _openSessionFiles + // For other providers, try to open session files from session.changes + let filesOpened = false; + if (session.providerType === AgentSessionProviders.Local) { + // Local sessions use editing session for changes - we already verified hasUndecidedChanges above + // Clear editors to prepare for the changes view + await this.editorGroupsService.applyWorkingSet('empty', { preserveFocus: true }); + filesOpened = true; + } else { + // Try to open session files - only continue with projection if files were displayed + filesOpened = await this._openSessionFiles(session); + } + + if (!filesOpened) { + this.logService.trace('[AgentSessionProjection] No files to display, opening chat without projection mode'); + // Restore the working set we just saved if this was our first attempt + if (!this._isActive && this._preProjectionWorkingSet) { + await this.editorGroupsService.applyWorkingSet(this._preProjectionWorkingSet); + this.editorGroupsService.deleteWorkingSet(this._preProjectionWorkingSet); + this._preProjectionWorkingSet = undefined; + } + // Fall through to just open the chat panel + } else { + // Set active state + const wasActive = this._isActive; + this._isActive = true; + this._activeSession = session; + this._inProjectionModeContextKey.set(true); + this.layoutService.mainContainer.classList.add('agent-session-projection-active'); + + // Update the agent status to show session mode + this.agentStatusService.enterSessionMode(session.resource.toString(), session.label); + + if (!wasActive) { + this._onDidChangeProjectionMode.fire(true); + } + // Always fire session change event (for title updates when switching sessions) + this._onDidChangeActiveSession.fire(session); } - // Always fire session change event (for title updates when switching sessions) - this._onDidChangeActiveSession.fire(session); } // Open the session in the chat panel (always, even without changes) From 6334cf0591d652e9a36fbb0e0a57462441c3027a Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Sat, 17 Jan 2026 00:05:22 +0100 Subject: [PATCH 130/387] chat: feat: allow rendering links as links (#288142) --- .../contrib/chat/browser/chat.contribution.ts | 10 ++++++++ .../chatInlineAnchorWidget.ts | 17 ++++++++++++++ .../media/chatInlineAnchorWidget.css | 23 +++++++++++++++++++ .../contrib/chat/common/constants.ts | 1 + 4 files changed, 51 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 9f80ffb1cb0..b6d81f1f24a 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -262,6 +262,16 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.renderRelatedFiles', "Controls whether related files should be rendered in the chat input."), default: false }, + [ChatConfiguration.InlineReferencesStyle]: { + type: 'string', + enum: ['box', 'link'], + enumDescriptions: [ + nls.localize('chat.inlineReferences.style.box', "Display file and symbol references as boxed widgets with icons."), + nls.localize('chat.inlineReferences.style.link', "Display file and symbol references as simple blue links without icons.") + ], + description: nls.localize('chat.inlineReferences.style', "Controls how file and symbol references are displayed in chat messages."), + default: 'box' + }, 'chat.notifyWindowOnConfirmation': { type: 'boolean', description: nls.localize('chat.notifyWindowOnConfirmation', "Controls whether a chat session should present the user with an OS notification when a confirmation is needed while the window is not in focus. This includes a window badge as well as notification toast."), diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts index a163fb84ca5..91d6ad46898 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts @@ -44,6 +44,8 @@ import { IChatContentInlineReference } from '../../../common/chatService/chatSer import { IChatWidgetService } from '../../chat.js'; import { chatAttachmentResourceContextKey, hookUpSymbolAttachmentDragAndContextMenu } from '../../attachments/chatAttachmentWidgets.js'; import { IChatMarkdownAnchorService } from './chatMarkdownAnchorService.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { ChatConfiguration } from '../../../common/constants.js'; type ContentRefData = | { readonly kind: 'symbol'; readonly symbol: IWorkspaceSymbol } @@ -113,6 +115,7 @@ export class InlineAnchorWidget extends Disposable { private readonly element: HTMLAnchorElement | HTMLElement, public readonly inlineReference: IChatContentInlineReference, private readonly metadata: InlineAnchorWidgetMetadata | undefined, + @IConfigurationService private readonly configurationService: IConfigurationService, @IContextKeyService originalContextKeyService: IContextKeyService, @IContextMenuService contextMenuService: IContextMenuService, @IFileService fileService: IFileService, @@ -248,6 +251,14 @@ export class InlineAnchorWidget extends Disposable { const relativeLabel = labelService.getUriLabel(location.uri, { relative: true }); this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('element'), element, relativeLabel)); + // Apply link-style if configured + this.updateAppearance(); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(ChatConfiguration.InlineReferencesStyle)) { + this.updateAppearance(); + } + })); + // Drag and drop if (this.data.kind !== 'symbol') { element.draggable = true; @@ -268,6 +279,12 @@ export class InlineAnchorWidget extends Disposable { return this.element; } + private updateAppearance(): void { + const style = this.configurationService.getValue(ChatConfiguration.InlineReferencesStyle); + const useLinkStyle = style === 'link'; + this.element.classList.toggle('link-style', useLinkStyle); + } + private getCellIndex(location: URI) { const notebook = this.notebookDocumentService.getNotebook(location); const index = notebook?.getCellIndex(location) ?? -1; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatInlineAnchorWidget.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatInlineAnchorWidget.css index c6de9263a3b..d277f221964 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatInlineAnchorWidget.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatInlineAnchorWidget.css @@ -59,3 +59,26 @@ flex-shrink: 0; } + +/* Link-style appearance - no box, no icon */ +.chat-inline-anchor-widget.link-style, +.interactive-item-container .value .rendered-markdown .chat-inline-anchor-widget.link-style { + border: none; + background-color: transparent; + padding: 0; + margin: 0; + color: var(--vscode-textLink-foreground); +} + +.chat-inline-anchor-widget.link-style:hover { + background-color: transparent; + text-decoration: underline; +} + +.chat-inline-anchor-widget.link-style .icon { + display: none; +} + +.chat-inline-anchor-widget.link-style .icon-label { + padding: 0; +} diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index ba407ce37fe..075e980921f 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -16,6 +16,7 @@ export enum ChatConfiguration { ExtensionToolsEnabled = 'chat.extensionTools.enabled', RepoInfoEnabled = 'chat.repoInfo.enabled', EditRequests = 'chat.editRequests', + InlineReferencesStyle = 'chat.inlineReferences.style', GlobalAutoApprove = 'chat.tools.global.autoApprove', AutoApproveEdits = 'chat.tools.edits.autoApprove', AutoApprovedUrls = 'chat.tools.urls.autoApprove', From 2dcb4670d55e9ffccc2d296299b078ab58f8cca0 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:07:10 -0800 Subject: [PATCH 131/387] optimize how we store/handle commandCenterMenu --- .../browser/agentSessions/agentStatusWidget.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts index 120424e3bed..6bfea0a5b27 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts @@ -66,6 +66,9 @@ export class AgentStatusWidget extends BaseActionViewItem { /** Cached render state to avoid unnecessary DOM rebuilds */ private _lastRenderState: string | undefined; + /** Reusable menu for CommandCenterCenter items (e.g., debug toolbar) */ + private readonly _commandCenterMenu; + constructor( action: IAction, options: IBaseActionViewItemOptions | undefined, @@ -87,7 +90,7 @@ export class AgentStatusWidget extends BaseActionViewItem { super(undefined, action, options); // Create menu for CommandCenterCenter to get items like debug toolbar - const commandCenterMenu = this._register(this.menuService.createMenu(MenuId.CommandCenterCenter, this.contextKeyService)); + this._commandCenterMenu = this._register(this.menuService.createMenu(MenuId.CommandCenterCenter, this.contextKeyService)); // Re-render when control mode or session info changes this._register(this.agentStatusService.onDidChangeMode(() => { @@ -116,7 +119,7 @@ export class AgentStatusWidget extends BaseActionViewItem { })); // Re-render when command center menu changes (e.g., debug toolbar visibility) - this._register(commandCenterMenu.onDidChange(() => { + this._register(this._commandCenterMenu.onDidChange(() => { this._lastRenderState = undefined; // Force re-render this._render(); })); @@ -372,7 +375,7 @@ export class AgentStatusWidget extends BaseActionViewItem { // #region Reusable Components /** - * Render command center toolbar items (like debug toolbar) that are registered to CommandCenterCenter. + * Render command center toolbar items (like debug toolbar) that are registered to CommandCenter * Filters out the quick open action since we provide our own search UI. * Adds a dot separator after the toolbar if content was rendered. */ @@ -382,11 +385,8 @@ export class AgentStatusWidget extends BaseActionViewItem { } // Get menu actions from CommandCenterCenter (e.g., debug toolbar) - const menu = this.menuService.createMenu(MenuId.CommandCenterCenter, this.contextKeyService); - disposables.add(menu); - const allActions: IAction[] = []; - for (const [, actions] of menu.getActions({ shouldForwardArgs: true })) { + for (const [, actions] of this._commandCenterMenu.getActions({ shouldForwardArgs: true })) { for (const action of actions) { // Filter out the quick open action - we provide our own search UI if (action.id === AgentStatusWidget._quickOpenCommandId) { From c13eb90a29f9bcb5b13c1e2b204c83db71a11798 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Sat, 17 Jan 2026 00:09:12 +0100 Subject: [PATCH 132/387] Load instructions on demand and don't add images (#288503) * Load instructions on demand * Don't collect image references --- .github/instructions/chat.instructions.md | 1 - .github/instructions/interactive.instructions.md | 3 +-- .github/instructions/learnings.instructions.md | 1 - .github/instructions/notebook.instructions.md | 1 - .../contrib/chat/common/promptSyntax/promptFileParser.ts | 3 +++ .../test/common/promptSyntax/service/newPromptsParser.test.ts | 4 ++-- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/instructions/chat.instructions.md b/.github/instructions/chat.instructions.md index c1d1061bb56..657866be205 100644 --- a/.github/instructions/chat.instructions.md +++ b/.github/instructions/chat.instructions.md @@ -1,5 +1,4 @@ --- -applyTo: '**/chat/**' description: Chat feature area coding guidelines --- diff --git a/.github/instructions/interactive.instructions.md b/.github/instructions/interactive.instructions.md index 21ed92f6460..d6867257e66 100644 --- a/.github/instructions/interactive.instructions.md +++ b/.github/instructions/interactive.instructions.md @@ -1,6 +1,5 @@ --- -applyTo: '**/interactive/**' -description: Architecture documentation for VS Code interactive window component +description: Architecture documentation for VS Code interactive window component. Use when working in folder --- # Interactive Window diff --git a/.github/instructions/learnings.instructions.md b/.github/instructions/learnings.instructions.md index 9358a943e3d..22fa31ae474 100644 --- a/.github/instructions/learnings.instructions.md +++ b/.github/instructions/learnings.instructions.md @@ -1,5 +1,4 @@ --- -applyTo: ** description: This document describes how to deal with learnings that you make. (meta instruction) --- diff --git a/.github/instructions/notebook.instructions.md b/.github/instructions/notebook.instructions.md index 3d78e744d31..890b0c20db2 100644 --- a/.github/instructions/notebook.instructions.md +++ b/.github/instructions/notebook.instructions.md @@ -1,5 +1,4 @@ --- -applyTo: '**/notebook/**' description: Architecture documentation for VS Code notebook and interactive window components --- diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts index 59d21a4755a..33094047675 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts @@ -346,6 +346,9 @@ export class PromptBody { // Match markdown links: [text](link) const linkMatch = line.matchAll(/\[(.*?)\]\((.+?)\)/g); for (const match of linkMatch) { + if (match.index > 0 && line[match.index - 1] === '!') { + continue; // skip image links + } const linkEndOffset = match.index + match[0].length - 1; // before the parenthesis const linkStartOffset = match.index + match[0].length - match[2].length - 1; const range = new Range(i + 1, linkStartOffset + 1, i + 1, linkEndOffset + 1); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/newPromptsParser.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/newPromptsParser.test.ts index b08976de84b..cb5fed7747b 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/newPromptsParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/newPromptsParser.test.ts @@ -22,7 +22,7 @@ suite('NewPromptsParser', () => { /* 04 */`tools: ['tool1', 'tool2']`, /* 05 */'---', /* 06 */'This is an agent test.', - /* 07 */'Here is a #tool:tool1 variable (and one with closing parenthesis after: #tool:tool-2) and a #file:./reference1.md as well as a [reference](./reference2.md).', + /* 07 */'Here is a #tool:tool1 variable (and one with closing parenthesis after: #tool:tool-2) and a #file:./reference1.md as well as a [reference](./reference2.md) and an image ![image](./image.png).', ].join('\n'); const result = new PromptFileParser().parse(uri, content); assert.deepEqual(result.uri, uri); @@ -42,7 +42,7 @@ suite('NewPromptsParser', () => { ]); assert.deepEqual(result.body.range, { startLineNumber: 6, startColumn: 1, endLineNumber: 8, endColumn: 1 }); assert.equal(result.body.offset, 75); - assert.equal(result.body.getContent(), 'This is an agent test.\nHere is a #tool:tool1 variable (and one with closing parenthesis after: #tool:tool-2) and a #file:./reference1.md as well as a [reference](./reference2.md).'); + assert.equal(result.body.getContent(), 'This is an agent test.\nHere is a #tool:tool1 variable (and one with closing parenthesis after: #tool:tool-2) and a #file:./reference1.md as well as a [reference](./reference2.md) and an image ![image](./image.png).'); assert.deepEqual(result.body.fileReferences, [ { range: new Range(7, 99, 7, 114), content: './reference1.md', isMarkdownLink: false }, From b61f70bf6eb3eec61b260b2e980d7ac22b782462 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 16 Jan 2026 15:17:07 -0800 Subject: [PATCH 133/387] finish up --- .../agentSessions/agentSessionHoverWidget.ts | 175 ++++++++++++++---- .../agentSessions/agentSessionsViewer.ts | 8 +- .../media/agentSessionHoverWidget.css | 91 +++++++++ .../widget/chatContentParts/codeBlockPart.ts | 2 +- .../chat/browser/widget/chatListWidget.ts | 18 +- .../contrib/chat/browser/widget/chatWidget.ts | 10 +- .../chat/common/model/chatViewModel.ts | 15 +- 7 files changed, 265 insertions(+), 54 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionHoverWidget.css diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts index efe969bde8d..4d8130cd370 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts @@ -4,71 +4,116 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../../base/browser/dom.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { IAgentSession, getAgentChangesSummary, hasValidDiff, AgentSessionStatus } from './agentSessionsModel.js'; -import { IChatService } from '../../common/chatService/chatService.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { ChatListWidget } from '../widget/chatListWidget.js'; -import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; -import { ChatViewModel } from '../../common/model/chatViewModel.js'; -import { ChatModeKind } from '../../common/constants.js'; -import { IChatWidgetService } from '../chat.js'; -import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { localize } from '../../../../../nls.js'; +import { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { RunOnceScheduler } from '../../../../../base/common/async.js'; +import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; import { fromNow, getDurationString } from '../../../../../base/common/date.js'; +import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { localize } from '../../../../../nls.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IChatService } from '../../common/chatService/chatService.js'; +import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js'; import { IChatModel } from '../../common/model/chatModel.js'; +import { ChatViewModel } from '../../common/model/chatViewModel.js'; +import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; +import { IChatWidgetService } from '../chat.js'; +import { ChatListWidget } from '../widget/chatListWidget.js'; +import { AgentSessionStatus, getAgentChangesSummary, hasValidDiff, IAgentSession } from './agentSessionsModel.js'; +import './media/agentSessionHoverWidget.css'; + +const HEADER_HEIGHT = 60; +const CHAT_LIST_HEIGHT = 240; +const CHAT_HOVER_WIDTH = 500; export class AgentSessionHoverWidget extends Disposable { public readonly domNode: HTMLElement; - private modelRef: Promise; + private modelRef?: Promise; + private readonly contentElement: HTMLElement; + private readonly loadingElement: HTMLElement; + private readonly renderScheduler: RunOnceScheduler; + private hasRendered = false; + private readonly cts: CancellationTokenSource; constructor( - private readonly session: IAgentSession, + public readonly session: IAgentSession, @IChatService private readonly chatService: IChatService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, ) { super(); this.domNode = dom.$('.agent-session-hover.interactive-session'); - this.domNode.style.width = '500px'; - this.domNode.style.height = '300px'; + this.domNode.style.width = `${CHAT_HOVER_WIDTH}px`; + this.domNode.style.height = `${HEADER_HEIGHT + CHAT_LIST_HEIGHT}px`; this.domNode.style.overflow = 'hidden'; - this.modelRef = this.chatService.getOrRestoreSession(session.resource).then(modelRef => { - if (this._store.isDisposed) { - modelRef?.dispose(); - return; - } + this.cts = new CancellationTokenSource(); + this._register(toDisposable(() => this.cts.cancel())); - if (!modelRef) { - // Show fallback tooltip text - const tooltip = this.buildFallbackTooltip(this.session); - this.domNode.textContent = typeof tooltip === 'string' ? tooltip : tooltip.value; - return; - } + // Build header immediately + this.buildHeader(); - this._register(modelRef); - return modelRef.object; - }); + // Create content container with loading state + this.contentElement = dom.append(this.domNode, dom.$('.agent-session-hover-content')); + this.loadingElement = dom.append(this.contentElement, dom.$('.agent-session-hover-loading')); + dom.append(this.loadingElement, renderIcon(ThemeIcon.modify(Codicon.loading, 'spin'))); + + // Delay rendering by 200ms to avoid expensive rendering for brief hovers + this.renderScheduler = this._register(new RunOnceScheduler(() => this.render(), 200)); } - public async onRendered() { + public onRendered() { + this.modelRef ??= this.loadModel(); + + if (!this.hasRendered) { + this.hasRendered = true; + this.renderScheduler.schedule(); + } + } + + private async loadModel() { + const modelRef = await this.chatService.loadSessionForResource(this.session.resource, ChatAgentLocation.Chat, this.cts.token); + if (this._store.isDisposed) { + modelRef?.dispose(); + return; + } + + if (!modelRef) { + // Show fallback tooltip text + this.loadingElement.remove(); + const tooltip = this.buildFallbackTooltip(this.session); + this.domNode.textContent = typeof tooltip === 'string' ? tooltip : tooltip.value; + return; + } + + this._register(modelRef); + return modelRef.object; + } + + private async render() { + this.modelRef ??= this.loadModel(); const model = await this.modelRef; if (!model || this._store.isDisposed) { return; } - // Create view model + // Remove loading state + this.loadingElement.remove(); + + // Create view model - only show last request+response pair const codeBlockCollection = this._register(this.instantiationService.createInstance(CodeBlockModelCollection, 'agentSessionHover')); const viewModel = this._register(this.instantiationService.createInstance( ChatViewModel, model, - codeBlockCollection + codeBlockCollection, + { maxVisibleItems: 2 } )); // Create the chat list widget - const container = dom.append(this.domNode, dom.$('.interactive-list')); + const container = dom.append(this.contentElement, dom.$('.interactive-list')); const listWidget = this._register(this.instantiationService.createInstance( ChatListWidget, container, @@ -81,8 +126,16 @@ export class AgentSessionHoverWidget extends Disposable { currentChatMode: () => ChatModeKind.Ask, } )); + listWidget.layout(CHAT_LIST_HEIGHT, CHAT_HOVER_WIDTH); + listWidget.setScrollLock(true); listWidget.setViewModel(viewModel); - listWidget.layout(300, 500); + + const viewModelScheudler = this._register(new RunOnceScheduler(() => listWidget.refresh(), 500)); + this._register(viewModel.onDidChange(() => { + if (!viewModelScheudler.isScheduled()) { + viewModelScheudler.schedule(); + } + })); // Handle followup clicks - open the session and accept input this._register(listWidget.onDidClickFollowup(async (followup) => { @@ -93,6 +146,60 @@ export class AgentSessionHoverWidget extends Disposable { })); } + private buildHeader(): void { + const session = this.session; + const header = dom.append(this.domNode, dom.$('.agent-session-hover-header')); + + // Title row + const titleRow = dom.append(header, dom.$('.agent-session-hover-title')); + dom.append(titleRow, dom.$('span', undefined, session.label)); + + // Details row: Status • Provider • Duration/Time • Diff + const detailsRow = dom.append(header, dom.$('.agent-session-hover-details')); + + // Status + dom.append(detailsRow, dom.$('span', undefined, this.toStatusLabel(session.status))); + dom.append(detailsRow, dom.$('span.separator', undefined, '•')); + + // Provider + dom.append(detailsRow, dom.$('span', undefined, session.providerLabel)); + dom.append(detailsRow, dom.$('span.separator', undefined, '•')); + + // Duration or start time + if (session.timing.finishedOrFailedTime && session.timing.inProgressTime) { + const duration = this.toDuration(session.timing.inProgressTime, session.timing.finishedOrFailedTime, true); + if (duration) { + dom.append(detailsRow, dom.$('span', undefined, duration)); + } + } else { + const startTime = session.timing.lastRequestStarted ?? session.timing.created; + dom.append(detailsRow, dom.$('span', undefined, fromNow(startTime, true, true))); + } + + // Diff information + const diff = getAgentChangesSummary(session.changes); + if (diff && hasValidDiff(session.changes)) { + dom.append(detailsRow, dom.$('span.separator', undefined, '•')); + const diffContainer = dom.append(detailsRow, dom.$('.agent-session-hover-diff')); + if (diff.files > 0) { + dom.append(diffContainer, dom.$('span', undefined, diff.files === 1 ? localize('tooltip.file', "1 file") : localize('tooltip.files', "{0} files", diff.files))); + } + if (diff.insertions > 0) { + dom.append(diffContainer, dom.$('span.insertions', undefined, `+${diff.insertions}`)); + } + if (diff.deletions > 0) { + dom.append(diffContainer, dom.$('span.deletions', undefined, `-${diff.deletions}`)); + } + } + + // Archived indicator + if (session.isArchived()) { + dom.append(detailsRow, dom.$('span.separator', undefined, '•')); + dom.append(detailsRow, renderIcon(Codicon.archive)); + dom.append(detailsRow, dom.$('span', undefined, localize('tooltip.archived', "Archived"))); + } + } + private buildFallbackTooltip(session: IAgentSession): IMarkdownString { const lines: string[] = []; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 44e476a7444..0da219dd0a6 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -354,9 +354,15 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre } private buildHoverContent(session: IAgentSession): IDelayedHoverOptions { - const widget = this._sessionHover.value = this.instantiationService.createInstance(AgentSessionHoverWidget, session); + // note: hover service use mouseover which triggers again if the mouse moves + // within the element. Only recreate the hover widget if the session changed. + if (this._sessionHover.value?.session.resource.toString() !== session.resource.toString()) { + this._sessionHover.value = this.instantiationService.createInstance(AgentSessionHoverWidget, session); + } + const widget = this._sessionHover.value; return { + id: 'agent.session.hover.' + session.resource.toString(), content: widget.domNode, style: HoverStyle.Pointer, onDidShow: () => { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionHoverWidget.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionHoverWidget.css new file mode 100644 index 00000000000..e26d04537b0 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionHoverWidget.css @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.agent-session-hover { + display: flex; + flex-direction: column; +} + +.agent-session-hover-loading { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--vscode-descriptionForeground); +} + +/* Header section with session details */ +.agent-session-hover-header { + height: 60px; + padding: 0 12px; + border-bottom: 1px solid var(--vscode-widget-border); + flex-shrink: 0; + display: flex; + flex-direction: column; + justify-content: center; +} + +.agent-session-hover-title { + font-weight: 600; + font-size: 13px; + margin-bottom: 4px; + color: var(--vscode-foreground); + display: flex; + align-items: center; + gap: 6px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.agent-session-hover-details { + font-size: 11px; + color: var(--vscode-descriptionForeground); + display: flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; +} + +.agent-session-hover-details .separator { + opacity: 0.5; +} + +.agent-session-hover-diff { + display: flex; + align-items: center; + gap: 4px; +} + +.agent-session-hover-diff .insertions { + color: var(--vscode-chat-linesAddedForeground); +} + +.agent-session-hover-diff .deletions { + color: var(--vscode-chat-linesRemovedForeground); +} + +/* Content area with chat list */ +.agent-session-hover-content { + flex: 1; + min-height: 0; + opacity: 0; + animation: agentSessionHoverFadeIn 0.2s ease-out forwards; + + .interactive-session .interactive-item-container { + padding: 0; + } +} + +@keyframes agentSessionHoverFadeIn { + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/codeBlockPart.ts index 8b38d206e06..d19dd33ecf4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/codeBlockPart.ts @@ -635,7 +635,7 @@ export class CodeCompareBlockPart extends Disposable { this.resourceLabel = this._register(scopedInstantiationService.createInstance(ResourceLabel, editorHeader, { supportIcons: true })); - const editorScopedService = this.diffEditor.getModifiedEditor().contextKeyService.createScoped(editorHeader); + const editorScopedService = this._register(this.diffEditor.getModifiedEditor().contextKeyService.createScoped(editorHeader)); const editorScopedInstantiationService = this._register(scopedInstantiationService.createChild(new ServiceCollection([IContextKeyService, editorScopedService]))); this.toolbar = this._register(editorScopedInstantiationService.createInstance(MenuWorkbenchToolBar, editorHeader, menuId, { menuOptions: { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts index 140800b88e7..25c204b139b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts @@ -26,7 +26,7 @@ import { ChatAccessibilityProvider } from '../accessibility/chatAccessibilityPro import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions } from '../chat.js'; import { katexContainerClassName } from '../../../markdown/common/markedKatexExtension.js'; import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; -import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js'; import { IChatRequestModeInfo } from '../../common/model/chatModel.js'; @@ -34,6 +34,7 @@ import { IChatFollowup, IChatSendRequestOptions, IChatService } from '../../comm import { ChatEditorOptions } from './chatOptions.js'; import { ChatListDelegate, ChatListItemRenderer, IChatListItemTemplate, IChatRendererDelegate } from './chatListRenderer.js'; import { CodeBlockPart } from './chatContentParts/codeBlockPart.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; export interface IChatListWidgetStyles { listForeground?: string; @@ -258,6 +259,7 @@ export class ChatListWidget extends Disposable { @IChatService private readonly chatService: IChatService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @ILogService private readonly logService: ILogService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(); @@ -440,6 +442,13 @@ export class ChatListWidget extends Disposable { this._register(this._tree.onContextMenu(e => { this.handleContextMenu(e); })); + + this._register(this.configurationService.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration(ChatConfiguration.EditRequests) || e.affectsConfiguration(ChatConfiguration.CheckpointsEnabled)) { + this._settingChangeCounter++; + this.refresh(); + } + })); } //#region Internal event handlers @@ -575,13 +584,6 @@ export class ChatListWidget extends Disposable { return this._scrollLock; } - /** - * Set the setting change counter (forces refresh). - */ - setSettingChangeCounter(value: number): void { - this._settingChangeCounter = value; - } - /** * Set the visible change count (for diff identity). */ diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 0fbf9f49a17..416e6c7edf7 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -228,8 +228,6 @@ export class ChatWidget extends Disposable implements IChatWidget { private recentlyRestoredCheckpoint: boolean = false; - private settingChangeCounter = 0; - private welcomeMessageContainer!: HTMLElement; private readonly welcomePart: MutableDisposable = this._register(new MutableDisposable()); @@ -441,11 +439,6 @@ export class ChatWidget extends Disposable implements IChatWidget { if (e.affectsConfiguration('chat.renderRelatedFiles')) { this.input.renderChatRelatedFiles(); } - - if (e.affectsConfiguration(ChatConfiguration.EditRequests) || e.affectsConfiguration(ChatConfiguration.CheckpointsEnabled)) { - this.settingChangeCounter++; - this.onDidChangeItems(); - } })); this._register(autorun(r => { @@ -777,7 +770,6 @@ export class ChatWidget extends Disposable implements IChatWidget { // Update list widget state and refresh this.listWidget.setVisibleChangeCount(this.visibleChangeCount); - this.listWidget.setSettingChangeCounter(this.settingChangeCounter); this.listWidget.refresh(); if (!skipDynamicLayout && this._dynamicMessageLayoutData) { @@ -1803,7 +1795,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this._codeBlockModelCollection.clear(); this.container.setAttribute('data-session-id', model.sessionId); - this.viewModel = this.instantiationService.createInstance(ChatViewModel, model, this._codeBlockModelCollection); + this.viewModel = this.instantiationService.createInstance(ChatViewModel, model, this._codeBlockModelCollection, undefined); // Pass input model reference to input part for state syncing this.inputPart.setInputModel(model.inputModel, model.getRequests().length === 0); diff --git a/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts index c31571559ba..2c9db5c2e6d 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts @@ -225,6 +225,14 @@ export interface IChatResponseViewModel { readonly shouldBeBlocked: IObservable; } +export interface IChatViewModelOptions { + /** + * Maximum number of items to return from getItems(). + * When set, only the last N items are returned (most recent request/response pairs). + */ + readonly maxVisibleItems?: number; +} + export class ChatViewModel extends Disposable implements IChatViewModel { private readonly _onDidDisposeModel = this._register(new Emitter()); @@ -261,6 +269,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel { constructor( private readonly _model: IChatModel, public readonly codeBlockModelCollection: CodeBlockModelCollection, + private readonly _options: IChatViewModelOptions | undefined, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -319,7 +328,11 @@ export class ChatViewModel extends Disposable implements IChatViewModel { } getItems(): (IChatRequestViewModel | IChatResponseViewModel)[] { - return this._items.filter((item) => !item.shouldBeRemovedOnSend || item.shouldBeRemovedOnSend.afterUndoStop); + const items = this._items.filter((item) => !item.shouldBeRemovedOnSend || item.shouldBeRemovedOnSend.afterUndoStop); + if (this._options?.maxVisibleItems !== undefined && items.length > this._options.maxVisibleItems) { + return items.slice(-this._options.maxVisibleItems); + } + return items; } From 37d9d32ef9934006b8338c33e908478fbc316f2e Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Fri, 16 Jan 2026 15:19:12 -0800 Subject: [PATCH 134/387] Open chats from welcome view in maximized chat --- .../browser/agentSessions/agentSessionsControl.ts | 13 +++++++++++-- .../browser/agentSessions/agentSessionsOpener.ts | 7 ++++--- .../chat/browser/widget/chatWidgetService.ts | 5 +++++ .../chat/browser/widgetHosts/editor/chatEditor.ts | 1 + .../browser/widgetHosts/viewPane/chatViewPane.ts | 3 ++- .../browser/agentSessionsWelcome.ts | 3 ++- 6 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 9a1b6e8d992..f99a9b8de6c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -37,19 +37,27 @@ import { IMouseEvent } from '../../../../../base/browser/mouseEvent.js'; export interface IAgentSessionsControlOptions extends IAgentSessionsSorterOptions { readonly overrideStyles: IStyleOverride; readonly filter: IAgentSessionsFilter; + readonly source: AgentSessionsControlSource; getHoverPosition(): HoverPosition; trackActiveEditorSession(): boolean; } +export const enum AgentSessionsControlSource { + ChatViewPane = 'chatViewPane', + WelcomeView = 'welcomeView' +} + type AgentSessionOpenedClassification = { owner: 'bpasero'; providerType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider type of the opened agent session.' }; + source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the opened agent session.' }; comment: 'Event fired when a agent session is opened from the agent sessions control.'; }; type AgentSessionOpenedEvent = { providerType: string; + source: AgentSessionsControlSource; }; export class AgentSessionsControl extends Disposable implements IAgentSessionsControl { @@ -196,10 +204,11 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo } this.telemetryService.publicLog2('agentSessionOpened', { - providerType: element.providerType + providerType: element.providerType, + source: this.options.source }); - await this.instantiationService.invokeFunction(openSession, element, e); + await this.instantiationService.invokeFunction(openSession, element, { ...e, expanded: this.options.source === AgentSessionsControlSource.WelcomeView }); } private async showContextMenu({ element, anchor, browserEvent }: ITreeContextMenuEvent): Promise { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts index 7fe2e56a977..7220766b9df 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts @@ -15,7 +15,7 @@ import { IAgentSessionProjectionService } from './agentSessionProjectionService. import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ChatConfiguration } from '../../common/constants.js'; -export async function openSession(accessor: ServicesAccessor, session: IAgentSession, openOptions?: { sideBySide?: boolean; editorOptions?: IEditorOptions }): Promise { +export async function openSession(accessor: ServicesAccessor, session: IAgentSession, openOptions?: { sideBySide?: boolean; editorOptions?: IEditorOptions; expanded?: boolean }): Promise { const configurationService = accessor.get(IConfigurationService); const projectionService = accessor.get(IAgentSessionProjectionService); @@ -35,7 +35,7 @@ export async function openSession(accessor: ServicesAccessor, session: IAgentSes * Opens a session in the traditional chat widget (side panel or editor). * Use this when you explicitly want to open in the chat widget rather than agent session projection mode. */ -export async function openSessionInChatWidget(accessor: ServicesAccessor, session: IAgentSession, openOptions?: { sideBySide?: boolean; editorOptions?: IEditorOptions }): Promise { +export async function openSessionInChatWidget(accessor: ServicesAccessor, session: IAgentSession, openOptions?: { sideBySide?: boolean; editorOptions?: IEditorOptions; expanded?: boolean }): Promise { const chatSessionsService = accessor.get(IChatSessionsService); const chatWidgetService = accessor.get(IChatWidgetService); @@ -51,7 +51,8 @@ export async function openSessionInChatWidget(accessor: ServicesAccessor, sessio let options: IChatEditorOptions = { ...sessionOptions, ...openOptions?.editorOptions, - revealIfOpened: true // always try to reveal if already opened + revealIfOpened: true, // always try to reveal if already opened + expanded: openOptions?.expanded }; await chatSessionsService.activateChatSessionItemProvider(session.providerType); // ensure provider is activated before trying to open diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts index 2deb9859f16..f8b867d9a49 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts @@ -19,6 +19,7 @@ import { ChatViewId, ChatViewPaneTarget, IChatWidget, IChatWidgetService, IQuick import { ChatEditor, IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js'; import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js'; import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; +import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; export class ChatWidgetService extends Disposable implements IChatWidgetService { @@ -40,6 +41,7 @@ export class ChatWidgetService extends Disposable implements IChatWidgetService @ILayoutService private readonly layoutService: ILayoutService, @IEditorService private readonly editorService: IEditorService, @IChatService private readonly chatService: IChatService, + @IWorkbenchLayoutService private readonly workbenchLayoutService: IWorkbenchLayoutService, ) { super(); } @@ -116,6 +118,9 @@ export class ChatWidgetService extends Disposable implements IChatWidgetService if (!options?.preserveFocus) { chatView.focusInput(); } + if (options?.expanded) { + this.workbenchLayoutService.setAuxiliaryBarMaximized(true); + } } return chatView?.widget; } diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts index fffb5a27aa8..3f8e0e9d7a9 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts @@ -43,6 +43,7 @@ export interface IChatEditorOptions extends IEditorOptions { preferred?: string; fallback?: string; }; + expanded?: boolean; } export class ChatEditor extends EditorPane { diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 82e01d19a97..2b91c4897da 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -44,7 +44,7 @@ import { IChatModelReference, IChatService } from '../../../common/chatService/c import { IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; import { LocalChatSessionUri, getChatSessionType } from '../../../common/model/chatUri.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../../common/constants.js'; -import { AgentSessionsControl } from '../../agentSessions/agentSessionsControl.js'; +import { AgentSessionsControl, AgentSessionsControlSource } from '../../agentSessions/agentSessionsControl.js'; import { AgentSessionsListDelegate } from '../../agentSessions/agentSessionsViewer.js'; import { ChatWidget } from '../../widget/chatWidget.js'; import { ChatViewWelcomeController, IViewWelcomeDelegate } from '../../viewsWelcome/chatViewWelcomeController.js'; @@ -382,6 +382,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { // Sessions Control this.sessionsControlContainer = append(sessionsContainer, $('.agent-sessions-control-container')); const sessionsControl = this.sessionsControl = this._register(this.instantiationService.createInstance(AgentSessionsControl, this.sessionsControlContainer, { + source: AgentSessionsControlSource.ChatViewPane, filter: sessionsFilter, overrideStyles: this.getLocationBasedColors().listOverrideStyles, getHoverPosition: () => { diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index b820d539d1f..1c3677380f4 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -40,7 +40,7 @@ import { AgentSessionsWelcomeEditorOptions, AgentSessionsWelcomeInput } from './ import { IChatService } from '../../chat/common/chatService/chatService.js'; import { IChatModel } from '../../chat/common/model/chatModel.js'; import { ISessionTypePickerDelegate } from '../../chat/browser/chat.js'; -import { AgentSessionsControl, IAgentSessionsControlOptions } from '../../chat/browser/agentSessions/agentSessionsControl.js'; +import { AgentSessionsControl, AgentSessionsControlSource, IAgentSessionsControlOptions } from '../../chat/browser/agentSessions/agentSessionsControl.js'; import { IAgentSessionsFilter } from '../../chat/browser/agentSessions/agentSessionsViewer.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { IResolvedWalkthrough, IWalkthroughsService } from '../../welcomeGettingStarted/browser/gettingStartedService.js'; @@ -275,6 +275,7 @@ export class AgentSessionsWelcomePage extends EditorPane { filter, getHoverPosition: () => HoverPosition.BELOW, trackActiveEditorSession: () => false, + source: AgentSessionsControlSource.WelcomeView, }; this.sessionsControl = this.sessionsControlDisposables.add(this.instantiationService.createInstance( From d9e80f63e4f1312bbadf63a8df079ece87c8a6b2 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 16 Jan 2026 15:43:28 -0800 Subject: [PATCH 135/387] Update src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts index 25c204b139b..cb1c783a2a5 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts @@ -399,7 +399,7 @@ export class ChatListWidget extends Disposable { supportIcons: true, })); this._scrollDownButton.element.classList.add('chat-scroll-down'); - this._scrollDownButton.label = `$(${Codicon.arrowDown.id}) ${localize('scrollDown', "Scroll down")}`; + this._scrollDownButton.label = `$(${Codicon.chevronDown.id})`; this._scrollDownButton.element.style.display = 'none'; // Hidden by default this._register(this._scrollDownButton.onDidClick(() => { From 126a04baa76816580d8dc8824a52f42658879a9a Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 16 Jan 2026 16:03:50 -0800 Subject: [PATCH 136/387] fix --- src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts index cb1c783a2a5..ef99f4cb3b4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts @@ -19,7 +19,6 @@ import { WorkbenchObjectTree } from '../../../../../platform/list/browser/listSe import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { MenuId } from '../../../../../platform/actions/common/actions.js'; import { asCssVariable, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground } from '../../../../../platform/theme/common/colorRegistry.js'; -import { localize } from '../../../../../nls.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { ChatAccessibilityProvider } from '../accessibility/chatAccessibilityProvider.js'; From 7e01c148b5b345bc33f786f3a20446ab53a710bc Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Sat, 17 Jan 2026 08:30:47 +0800 Subject: [PATCH 137/387] fix mermaid output (#288510) --- .../workbench/contrib/chat/browser/widget/chatListRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index a1ef74cebd9..285aade3b1f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -1289,7 +1289,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer Date: Fri, 16 Jan 2026 16:32:54 -0800 Subject: [PATCH 138/387] Do not disable target picker in welcome view --- .../contrib/chat/browser/actions/chatExecuteActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 0d297efe2ff..bf0ff52a063 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -440,7 +440,7 @@ export class OpenSessionTargetPickerAction extends Action2 { tooltip: localize('setSessionTarget', "Set Session Target"), category: CHAT_CATEGORY, f1: false, - precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.hasCanDelegateProviders, ChatContextKeys.chatSessionIsEmpty), + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.hasCanDelegateProviders, ContextKeyExpr.or(ChatContextKeys.chatSessionIsEmpty, ChatContextKeys.inAgentSessionsWelcome)), menu: [ { id: MenuId.ChatInput, From f5eddbcf6eb8073e0270212f2692e2e6fd707a31 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:40:19 -0800 Subject: [PATCH 139/387] mirror default icon display behavior for chatSession optionGroups --- .../browser/chatSessions/chatSessionPickerActionItem.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.ts index 9a6f9ac8a1f..a5f36c602f3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.ts @@ -122,10 +122,16 @@ export class ChatSessionPickerActionItem extends ActionWidgetDropdownActionViewI const domChildren = []; element.classList.add('chat-session-option-picker'); + // If the current option is the default and has an icon, collapse the text and show only the icon + const isDefaultWithIcon = this.currentOption?.default && this.currentOption?.icon; + if (this.currentOption?.icon) { domChildren.push(renderIcon(this.currentOption.icon)); } - domChildren.push(dom.$('span.chat-session-option-label', undefined, this.currentOption?.name ?? localize('chat.sessionPicker.label', "Pick Option"))); + + if (!isDefaultWithIcon) { + domChildren.push(dom.$('span.chat-session-option-label', undefined, this.currentOption?.name ?? localize('chat.sessionPicker.label', "Pick Option"))); + } domChildren.push(...renderLabelWithIcons(`$(chevron-down)`)); From fee4635063c19f10c8048c0c413611c9f14a9d23 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Fri, 16 Jan 2026 16:45:21 -0800 Subject: [PATCH 140/387] Agents welcome page layout fix --- .../browser/agentSessionsWelcome.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index b820d539d1f..d8bfb8c937e 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -222,6 +222,11 @@ export class AgentSessionsWelcomePage extends EditorPane { this.chatWidget.render(chatWidgetContainer); this.chatWidget.setVisible(true); + // Schedule initial layout at next animation frame to ensure proper input sizing + this.contentDisposables.add(scheduleAtNextAnimationFrame(getWindow(chatWidgetContainer), () => { + this.layoutChatWidget(); + })); + // Start a chat session so the widget has a viewModel // This is necessary for actions like mode switching to work properly this.chatModelRef = this.chatService.startSession(ChatAgentLocation.Chat); @@ -381,13 +386,8 @@ export class AgentSessionsWelcomePage extends EditorPane { this.container.style.height = `${dimension.height}px`; this.container.style.width = `${dimension.width}px`; - // Layout chat widget with height for input area - if (this.chatWidget) { - const chatWidth = Math.min(800, dimension.width - 80); - // Use a reasonable height for the input part - the CSS will hide the list area - const inputHeight = 150; - this.chatWidget.layout(inputHeight, chatWidth); - } + // Layout chat widget + this.layoutChatWidget(); // Layout sessions control this.layoutSessionsControl(); @@ -395,6 +395,17 @@ export class AgentSessionsWelcomePage extends EditorPane { this.scrollableElement?.scanDomNode(); } + private layoutChatWidget(): void { + if (!this.chatWidget || !this.lastDimension) { + return; + } + + const chatWidth = Math.min(800, this.lastDimension.width - 80); + // Use a reasonable height for the input part - the CSS will hide the list area + const inputHeight = 150; + this.chatWidget.layout(inputHeight, chatWidth); + } + private layoutSessionsControl(): void { if (!this.sessionsControl || !this.sessionsControlContainer || !this.lastDimension) { return; From a465d265a48314d07d71253d528e56004ca0e871 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Fri, 16 Jan 2026 16:52:53 -0800 Subject: [PATCH 141/387] Open Agent Sessions button should maximize chat --- .../welcomeAgentSessions/browser/agentSessionsWelcome.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index 1c3677380f4..e2d73bfca55 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -292,7 +292,12 @@ export class AgentSessionsWelcomePage extends EditorPane { // "Open Agent Sessions" link const openButton = append(container, $('button.agentSessionsWelcome-openSessionsButton')); openButton.textContent = localize('openAgentSessions', "Open Agent Sessions"); - openButton.onclick = () => this.commandService.executeCommand('workbench.action.chat.open'); + openButton.onclick = () => { + this.commandService.executeCommand('workbench.action.chat.open'); + if (!this.layoutService.isAuxiliaryBarMaximized()) { + this.layoutService.toggleMaximizedAuxiliaryBar(); + } + }; } private buildWalkthroughs(container: HTMLElement): void { From c945bef6b589b05a9a9b980aeece776c2dc67b71 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 16 Jan 2026 17:57:56 -0800 Subject: [PATCH 142/387] Increase max number of persisted sessions (#288528) Fix #283123 --- src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts b/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts index 4363f93ea7c..531c838fe04 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts @@ -30,7 +30,7 @@ import { ChatModel, ISerializableChatData, ISerializableChatDataIn, ISerializabl import { ChatSessionOperationLog } from './chatSessionOperationLog.js'; import { LocalChatSessionUri } from './chatUri.js'; -const maxPersistedSessions = 25; +const maxPersistedSessions = 50; const ChatIndexStorageKey = 'chat.ChatSessionStore.index'; const ChatTransferIndexStorageKey = 'ChatSessionStore.transferIndex'; From a149ab4bd919cb6c24ae6ba5d8e55c63643d542a Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Sat, 17 Jan 2026 13:37:15 +0800 Subject: [PATCH 143/387] fix padding in thinking part (#288545) --- .../widget/chatContentParts/media/chatThinkingContent.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css index bec837f2640..4d6484b28e5 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css @@ -30,7 +30,7 @@ overflow: hidden; .chat-tool-invocation-part { - padding: 3px 12px 4px 18px; + padding: 4px 12px 4px 18px; position: relative; .chat-used-context { @@ -79,7 +79,7 @@ } .chat-thinking-item.markdown-content { - padding: 5px 12px 6px 24px; + padding: 6px 12px 6px 24px; position: relative; font-size: var(--vscode-chat-font-size-body-s); From 8278a158318ef026965a929abc1083d5760310ad Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 17 Jan 2026 01:59:52 -0800 Subject: [PATCH 144/387] Add dir to default auto approve rules Fixes #288431 --- .../chatAgentTools/common/terminalChatAgentToolsConfiguration.ts | 1 + .../test/electron-browser/runInTerminalTool.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index be29b27e9e6..89e4ada38b8 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -163,6 +163,7 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary { 'echo "abc"', 'echo \'abc\'', 'ls -la', + 'dir', 'pwd', 'cat file.txt', 'head -n 10 file.txt', From c93e674e1c3beac8fca98fc85e2171fa29193b80 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 17 Jan 2026 02:42:05 -0800 Subject: [PATCH 145/387] Optimize initial hint at low widths Fixes #288118 --- .../browser/media/terminalInitialHint.css | 22 ++++++++- .../terminal.initialHint.contribution.ts | 45 ++++++++++++++----- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/media/terminalInitialHint.css b/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/media/terminalInitialHint.css index 047e72f58da..c169a9d60bb 100644 --- a/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/media/terminalInitialHint.css +++ b/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/media/terminalInitialHint.css @@ -8,7 +8,27 @@ container-type: inline-size; } -@container (max-width: 300px) { +.monaco-workbench .terminal-initial-hint .terminal-initial-hint-separator { + display: none; +} + +.monaco-workbench .terminal-initial-hint .terminal-initial-hint-compact { + display: none !important; +} + +@container (max-width: 500px) { + .monaco-workbench .terminal-initial-hint .terminal-initial-hint-prose { + display: none !important; + } + .monaco-workbench .terminal-initial-hint .terminal-initial-hint-separator { + display: block; + } + .monaco-workbench .terminal-initial-hint .terminal-initial-hint-compact { + display: inline !important; + } +} + +@container (max-width: 240px) { .monaco-workbench .terminal-initial-hint > * { display: none !important; } diff --git a/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/terminal.initialHint.contribution.ts b/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/terminal.initialHint.contribution.ts index 1953144be29..0b3db8501b9 100644 --- a/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/terminal.initialHint.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/terminal.initialHint.contribution.ts @@ -282,11 +282,13 @@ class TerminalInitialHintWidget extends Disposable { if (terminalAgents?.length) { const actionPart = localize('emptyHintText', 'Open chat {0}. ', keybindingHintLabel); - const [before, after] = actionPart.split(keybindingHintLabel).map((fragment) => { - const hintPart = $('a', undefined, fragment); - this._toDispose.add(dom.addDisposableListener(hintPart, dom.EventType.CLICK, handleClick)); - return hintPart; - }); + const [beforeText, afterText] = actionPart.split(keybindingHintLabel); + const before = $('a', undefined, beforeText); + this._toDispose.add(dom.addDisposableListener(before, dom.EventType.CLICK, handleClick)); + const after = $('span.terminal-initial-hint-prose', undefined); + const afterLink = $('a', undefined, afterText); + this._toDispose.add(dom.addDisposableListener(afterLink, dom.EventType.CLICK, handleClick)); + after.appendChild(afterLink); hintElement.appendChild(before); @@ -299,6 +301,7 @@ class TerminalInitialHintWidget extends Disposable { this._toDispose.add(dom.addDisposableListener(label.element, dom.EventType.CLICK, handleClick)); hintElement.appendChild(after); + hintElement.appendChild($('span.terminal-initial-hint-separator')); ariaLabelParts.push(actionPart); } @@ -327,11 +330,13 @@ class TerminalInitialHintWidget extends Disposable { this._commandService.executeCommand(TerminalSuggestCommandId.TriggerSuggest); }; - const [suggestBefore, suggestAfter] = suggestActionPart.split(suggestKeybindingLabel).map((fragment) => { - const hintPart = $('a', undefined, fragment); - this._toDispose.add(dom.addDisposableListener(hintPart, dom.EventType.CLICK, handleSuggestClick)); - return hintPart; - }); + const [suggestBeforeText, suggestAfterText] = suggestActionPart.split(suggestKeybindingLabel); + const suggestBefore = $('a', undefined, suggestBeforeText); + this._toDispose.add(dom.addDisposableListener(suggestBefore, dom.EventType.CLICK, handleSuggestClick)); + const suggestAfter = $('span.terminal-initial-hint-prose', undefined); + const suggestAfterLink = $('a', undefined, suggestAfterText); + this._toDispose.add(dom.addDisposableListener(suggestAfterLink, dom.EventType.CLICK, handleSuggestClick)); + suggestAfter.appendChild(suggestAfterLink); hintElement.appendChild(suggestBefore); @@ -343,6 +348,7 @@ class TerminalInitialHintWidget extends Disposable { this._toDispose.add(dom.addDisposableListener(suggestLabel.element, dom.EventType.CLICK, handleSuggestClick)); hintElement.appendChild(suggestAfter); + hintElement.appendChild($('span.terminal-initial-hint-separator')); ariaLabelParts.push(suggestActionPart); } @@ -352,15 +358,30 @@ class TerminalInitialHintWidget extends Disposable { return undefined; } + // Dismiss hint - normal mode version const typeToDismiss = localize({ key: 'hintTextDismiss', comment: [ 'Preserve double-square brackets and their order', ] - }, ' Start typing to dismiss or [[don\'t show]] this again.'); + }, '[[don\'t show]] this again.'); const typeToDismissRendered = renderFormattedText(typeToDismiss, { actionHandler: dontShowHintHandler }); - typeToDismissRendered.classList.add('detail'); + typeToDismissRendered.classList.add('detail', 'terminal-initial-hint-prose'); + + const proseBefore = $('span.terminal-initial-hint-prose', undefined, ' Start typing to dismiss or '); + hintElement.appendChild(proseBefore); hintElement.appendChild(typeToDismissRendered); + + // Dismiss hint - compact mode version + const typeToDismissCompact = localize({ + key: 'hintTextDismissCompact', + comment: [ + 'Preserve double-square brackets and their order', + ] + }, '[[Don\'t show this again]]'); + const typeToDismissCompactRendered = renderFormattedText(typeToDismissCompact, { actionHandler: dontShowHintHandler }); + typeToDismissCompactRendered.classList.add('detail', 'terminal-initial-hint-compact'); + hintElement.appendChild(typeToDismissCompactRendered); ariaLabelParts.push(localize('hintTextDismissAriaLabel', 'Start typing to dismiss or don\'t show this again.')); return { ariaLabel: ariaLabelParts.join(' '), hintHandler, hintElement }; From 80553c1ac8f85b305c957f282ae160de4ad578df Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 17 Jan 2026 03:18:30 -0800 Subject: [PATCH 146/387] Make sed /e and /w rule more specific Fixes #288589 --- .../common/terminalChatAgentToolsConfiguration.ts | 3 ++- .../test/electron-browser/runInTerminalTool.test.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index 89e4ada38b8..8bad0a2dc13 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -328,7 +328,8 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary { 'rg -i --color=never "TODO" src/', 'sed "s/foo/bar/g"', 'sed -n "1,10p" file.txt', + 'sed -n \'45,80p\' /foo/bar/Example.java', 'sort file.txt', 'tree directory', From 7eec263c7cc28e7c1a0f46881a24b3c8e92f3d06 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 17 Jan 2026 03:25:48 -0800 Subject: [PATCH 147/387] Address feedback --- .../terminal.initialHint.contribution.ts | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/terminal.initialHint.contribution.ts b/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/terminal.initialHint.contribution.ts index 0b3db8501b9..e3a1c39329d 100644 --- a/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/terminal.initialHint.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/terminal.initialHint.contribution.ts @@ -234,6 +234,21 @@ class TerminalInitialHintWidget extends Disposable { })); } + /** + * Creates wrapped hint elements with click listeners for responsive hint layouts. + * Returns a before link and an after prose span containing a link. + */ + private _createWrappedHintElements(text: string, keybindingLabel: string, clickHandler: () => void): { before: HTMLAnchorElement; after: HTMLSpanElement } { + const [beforeText, afterText] = text.split(keybindingLabel); + const before = $('a', undefined, beforeText) as HTMLAnchorElement; + this._toDispose.add(dom.addDisposableListener(before, dom.EventType.CLICK, clickHandler)); + const after = $('span.terminal-initial-hint-prose', undefined) as HTMLSpanElement; + const afterLink = $('a', undefined, afterText); + this._toDispose.add(dom.addDisposableListener(afterLink, dom.EventType.CLICK, clickHandler)); + after.appendChild(afterLink); + return { before, after }; + } + private _getHintInlineChat() { const ariaLabelParts: string[] = []; @@ -282,13 +297,7 @@ class TerminalInitialHintWidget extends Disposable { if (terminalAgents?.length) { const actionPart = localize('emptyHintText', 'Open chat {0}. ', keybindingHintLabel); - const [beforeText, afterText] = actionPart.split(keybindingHintLabel); - const before = $('a', undefined, beforeText); - this._toDispose.add(dom.addDisposableListener(before, dom.EventType.CLICK, handleClick)); - const after = $('span.terminal-initial-hint-prose', undefined); - const afterLink = $('a', undefined, afterText); - this._toDispose.add(dom.addDisposableListener(afterLink, dom.EventType.CLICK, handleClick)); - after.appendChild(afterLink); + const { before, after } = this._createWrappedHintElements(actionPart, keybindingHintLabel, handleClick); hintElement.appendChild(before); @@ -330,13 +339,7 @@ class TerminalInitialHintWidget extends Disposable { this._commandService.executeCommand(TerminalSuggestCommandId.TriggerSuggest); }; - const [suggestBeforeText, suggestAfterText] = suggestActionPart.split(suggestKeybindingLabel); - const suggestBefore = $('a', undefined, suggestBeforeText); - this._toDispose.add(dom.addDisposableListener(suggestBefore, dom.EventType.CLICK, handleSuggestClick)); - const suggestAfter = $('span.terminal-initial-hint-prose', undefined); - const suggestAfterLink = $('a', undefined, suggestAfterText); - this._toDispose.add(dom.addDisposableListener(suggestAfterLink, dom.EventType.CLICK, handleSuggestClick)); - suggestAfter.appendChild(suggestAfterLink); + const { before: suggestBefore, after: suggestAfter } = this._createWrappedHintElements(suggestActionPart, suggestKeybindingLabel, handleSuggestClick); hintElement.appendChild(suggestBefore); @@ -348,6 +351,7 @@ class TerminalInitialHintWidget extends Disposable { this._toDispose.add(dom.addDisposableListener(suggestLabel.element, dom.EventType.CLICK, handleSuggestClick)); hintElement.appendChild(suggestAfter); + // Layout-only separator; visibility and spacing are controlled via CSS (including responsive breakpoints). hintElement.appendChild($('span.terminal-initial-hint-separator')); ariaLabelParts.push(suggestActionPart); @@ -368,7 +372,7 @@ class TerminalInitialHintWidget extends Disposable { const typeToDismissRendered = renderFormattedText(typeToDismiss, { actionHandler: dontShowHintHandler }); typeToDismissRendered.classList.add('detail', 'terminal-initial-hint-prose'); - const proseBefore = $('span.terminal-initial-hint-prose', undefined, ' Start typing to dismiss or '); + const proseBefore = $('span.terminal-initial-hint-prose', undefined, localize('hintTextDismissProse', " Start typing to dismiss or ")); hintElement.appendChild(proseBefore); hintElement.appendChild(typeToDismissRendered); From 33501abd3f76fb43809148795e57c23f8432e047 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 17 Jan 2026 14:00:11 +0100 Subject: [PATCH 148/387] reduce diff --- .../agentSessions/agentSessionsViewer.ts | 49 ++++++++++--------- .../media/agentSessionHoverWidget.css | 2 +- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 244031fa10c..449be7de51e 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -3,43 +3,44 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDragAndDropData } from '../../../../../base/browser/dnd.js'; +import './media/agentsessionsviewer.css'; import { h } from '../../../../../base/browser/dom.js'; -import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; -import { HoverStyle, IDelayedHoverOptions } from '../../../../../base/browser/ui/hover/hover.js'; -import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js'; -import { IconLabel } from '../../../../../base/browser/ui/iconLabel/iconLabel.js'; +import { localize } from '../../../../../nls.js'; import { IIdentityProvider, IListVirtualDelegate } from '../../../../../base/browser/ui/list/list.js'; -import { ListViewTargetSector } from '../../../../../base/browser/ui/list/listView.js'; import { IListAccessibilityProvider } from '../../../../../base/browser/ui/list/listWidget.js'; import { ITreeCompressionDelegate } from '../../../../../base/browser/ui/tree/asyncDataTree.js'; import { ICompressedTreeNode } from '../../../../../base/browser/ui/tree/compressedObjectTreeModel.js'; import { ICompressibleKeyboardNavigationLabelProvider, ICompressibleTreeRenderer } from '../../../../../base/browser/ui/tree/objectTree.js'; -import { IAsyncDataSource, ITreeDragAndDrop, ITreeDragOverReaction, ITreeElementRenderDetails, ITreeNode, ITreeSorter } from '../../../../../base/browser/ui/tree/tree.js'; -import { coalesce } from '../../../../../base/common/arrays.js'; -import { IntervalTimer } from '../../../../../base/common/async.js'; +import { ITreeNode, ITreeElementRenderDetails, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction } from '../../../../../base/browser/ui/tree/tree.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { AgentSessionSection, AgentSessionStatus, getAgentChangesSummary, hasValidDiff, IAgentSession, IAgentSessionSection, IAgentSessionsModel, isAgentSession, isAgentSessionSection, isAgentSessionsModel, isSessionInProgressStatus } from './agentSessionsModel.js'; +import { IconLabel } from '../../../../../base/browser/ui/iconLabel/iconLabel.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { fromNow, getDurationString } from '../../../../../base/common/date.js'; -import { Event } from '../../../../../base/common/event.js'; -import { createMatches, FuzzyScore } from '../../../../../base/common/filters.js'; -import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { localize } from '../../../../../nls.js'; +import { FuzzyScore, createMatches } from '../../../../../base/common/filters.js'; +import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js'; +import { allowedChatMarkdownHtmlTags } from '../widget/chatContentMarkdownRenderer.js'; +import { IProductService } from '../../../../../platform/product/common/productService.js'; +import { IDragAndDropData } from '../../../../../base/browser/dnd.js'; +import { ListViewTargetSector } from '../../../../../base/browser/ui/list/listView.js'; +import { coalesce } from '../../../../../base/common/arrays.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { fillEditorsDragData } from '../../../../browser/dnd.js'; +import { HoverStyle, IDelayedHoverOptions } from '../../../../../base/browser/ui/hover/hover.js'; +import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js'; +import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; +import { IntervalTimer } from '../../../../../base/common/async.js'; import { MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; import { MenuId } from '../../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; -import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js'; -import { IProductService } from '../../../../../platform/product/common/productService.js'; -import { fillEditorsDragData } from '../../../../browser/dnd.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; -import { allowedChatMarkdownHtmlTags } from '../widget/chatContentMarkdownRenderer.js'; +import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; +import { Event } from '../../../../../base/common/event.js'; +import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; +import { MarkdownString, IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { AgentSessionHoverWidget } from './agentSessionHoverWidget.js'; -import { AgentSessionSection, AgentSessionStatus, getAgentChangesSummary, hasValidDiff, IAgentSession, IAgentSessionSection, IAgentSessionsModel, isAgentSession, isAgentSessionSection, isAgentSessionsModel, isSessionInProgressStatus } from './agentSessionsModel.js'; -import './media/agentsessionsviewer.css'; + export type AgentSessionListItem = IAgentSession | IAgentSessionSection; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionHoverWidget.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionHoverWidget.css index e26d04537b0..02c57324818 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionHoverWidget.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionHoverWidget.css @@ -74,7 +74,7 @@ opacity: 0; animation: agentSessionHoverFadeIn 0.2s ease-out forwards; - .interactive-session .interactive-item-container { + .interactive-session .interactive-item-container { padding: 0; } } From 58fc5968439fda44061b53d0e4410586ce0e20db Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 17 Jan 2026 14:05:20 +0100 Subject: [PATCH 149/387] :lipstick: --- .../agentSessions/agentSessionHoverWidget.ts | 5 +- .../agentSessions/agentSessionsViewer.ts | 19 ++- .../media/agentSessionHoverWidget.css | 121 +++++++++--------- 3 files changed, 72 insertions(+), 73 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts index 4d8130cd370..8a1b4c1ce39 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts @@ -30,7 +30,7 @@ const CHAT_HOVER_WIDTH = 500; export class AgentSessionHoverWidget extends Disposable { - public readonly domNode: HTMLElement; + readonly domNode: HTMLElement; private modelRef?: Promise; private readonly contentElement: HTMLElement; private readonly loadingElement: HTMLElement; @@ -45,6 +45,7 @@ export class AgentSessionHoverWidget extends Disposable { @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, ) { super(); + this.domNode = dom.$('.agent-session-hover.interactive-session'); this.domNode.style.width = `${CHAT_HOVER_WIDTH}px`; this.domNode.style.height = `${HEADER_HEIGHT + CHAT_LIST_HEIGHT}px`; @@ -65,7 +66,7 @@ export class AgentSessionHoverWidget extends Disposable { this.renderScheduler = this._register(new RunOnceScheduler(() => this.render(), 200)); } - public onRendered() { + onRendered() { this.modelRef ??= this.loadModel(); if (!this.hasRendered) { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 449be7de51e..b7a0dabfac9 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -41,7 +41,6 @@ import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer. import { MarkdownString, IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { AgentSessionHoverWidget } from './agentSessionHoverWidget.js'; - export type AgentSessionListItem = IAgentSession | IAgentSessionSection; //#region Agent Session Renderer @@ -84,7 +83,7 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre readonly templateId = AgentSessionRenderer.TEMPLATE_ID; - private readonly _sessionHover = this._register(new MutableDisposable()); + private readonly sessionHover = this._register(new MutableDisposable()); constructor( private readonly options: IAgentSessionRendererOptions, @@ -351,20 +350,18 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre } private buildHoverContent(session: IAgentSession): IDelayedHoverOptions { - // note: hover service use mouseover which triggers again if the mouse moves - // within the element. Only recreate the hover widget if the session changed. - if (this._sessionHover.value?.session.resource.toString() !== session.resource.toString()) { - this._sessionHover.value = this.instantiationService.createInstance(AgentSessionHoverWidget, session); + if (this.sessionHover.value?.session.resource.toString() !== session.resource.toString()) { + // note: hover service use mouseover which triggers again if the mouse moves + // within the element. Only recreate the hover widget if the session changed. + this.sessionHover.value = this.instantiationService.createInstance(AgentSessionHoverWidget, session); } - const widget = this._sessionHover.value; + const widget = this.sessionHover.value; return { - id: 'agent.session.hover.' + session.resource.toString(), + id: `agent.session.hover.${session.resource.toString()}`, content: widget.domNode, style: HoverStyle.Pointer, - onDidShow: () => { - widget.onRendered(); - }, + onDidShow: () => widget.onRendered(), position: { hoverPosition: this.options.getHoverPosition() } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionHoverWidget.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionHoverWidget.css index 02c57324818..dc51631c560 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionHoverWidget.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionHoverWidget.css @@ -6,76 +6,76 @@ .agent-session-hover { display: flex; flex-direction: column; -} -.agent-session-hover-loading { - display: flex; - align-items: center; - justify-content: center; - height: 100%; - color: var(--vscode-descriptionForeground); -} + .agent-session-hover-loading { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--vscode-descriptionForeground); + } -/* Header section with session details */ -.agent-session-hover-header { - height: 60px; - padding: 0 12px; - border-bottom: 1px solid var(--vscode-widget-border); - flex-shrink: 0; - display: flex; - flex-direction: column; - justify-content: center; -} + /* Header section with session details */ + .agent-session-hover-header { + height: 60px; + padding: 0 12px; + border-bottom: 1px solid var(--vscode-widget-border); + flex-shrink: 0; + display: flex; + flex-direction: column; + justify-content: center; + } -.agent-session-hover-title { - font-weight: 600; - font-size: 13px; - margin-bottom: 4px; - color: var(--vscode-foreground); - display: flex; - align-items: center; - gap: 6px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} + .agent-session-hover-title { + font-weight: 600; + font-size: 13px; + margin-bottom: 4px; + color: var(--vscode-foreground); + display: flex; + align-items: center; + gap: 6px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } -.agent-session-hover-details { - font-size: 11px; - color: var(--vscode-descriptionForeground); - display: flex; - align-items: center; - gap: 4px; - flex-wrap: wrap; -} + .agent-session-hover-details { + font-size: 11px; + color: var(--vscode-descriptionForeground); + display: flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; + } -.agent-session-hover-details .separator { - opacity: 0.5; -} + .agent-session-hover-details .separator { + opacity: 0.5; + } -.agent-session-hover-diff { - display: flex; - align-items: center; - gap: 4px; -} + .agent-session-hover-diff { + display: flex; + align-items: center; + gap: 4px; + } -.agent-session-hover-diff .insertions { - color: var(--vscode-chat-linesAddedForeground); -} + .agent-session-hover-diff .insertions { + color: var(--vscode-chat-linesAddedForeground); + } -.agent-session-hover-diff .deletions { - color: var(--vscode-chat-linesRemovedForeground); -} + .agent-session-hover-diff .deletions { + color: var(--vscode-chat-linesRemovedForeground); + } -/* Content area with chat list */ -.agent-session-hover-content { - flex: 1; - min-height: 0; - opacity: 0; - animation: agentSessionHoverFadeIn 0.2s ease-out forwards; + /* Content area with chat list */ + .agent-session-hover-content { + flex: 1; + min-height: 0; + opacity: 0; + animation: agentSessionHoverFadeIn 0.2s ease-out forwards; - .interactive-session .interactive-item-container { - padding: 0; + .interactive-session .interactive-item-container { + padding: 0; + } } } @@ -84,6 +84,7 @@ opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: translateY(0); From 72eab05d19645c054165b948329ab0509f7cf906 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 17 Jan 2026 09:28:29 -0800 Subject: [PATCH 150/387] Don't show initial hint for hideFromUser and terms with content Fixes #286269 --- .../inlineHint/browser/terminal.initialHint.contribution.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/terminal.initialHint.contribution.ts b/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/terminal.initialHint.contribution.ts index e3a1c39329d..113ae54e1b4 100644 --- a/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/terminal.initialHint.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/terminal.initialHint.contribution.ts @@ -98,7 +98,7 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm xtermOpen(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { // Don't show is the terminal was launched by an extension or a feature like debug - if (hasKey(this._ctx.instance, { shellLaunchConfig: true }) && (this._ctx.instance.shellLaunchConfig.isExtensionOwnedTerminal || this._ctx.instance.shellLaunchConfig.isFeatureTerminal)) { + if (hasKey(this._ctx.instance, { shellLaunchConfig: true }) && (this._ctx.instance.shellLaunchConfig.isExtensionOwnedTerminal || this._ctx.instance.shellLaunchConfig.isFeatureTerminal || this._ctx.instance.shellLaunchConfig.hideFromUser)) { return; } // Don't show if disabled @@ -124,7 +124,7 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm private _createHint(): void { const instance = this._ctx.instance instanceof TerminalInstance ? this._ctx.instance : undefined; const commandDetectionCapability = instance?.capabilities.get(TerminalCapability.CommandDetection); - if (!instance || !this._xterm || this._hintWidget || !commandDetectionCapability || commandDetectionCapability.promptInputModel.value || !!instance.shellLaunchConfig.attachPersistentProcess) { + if (!instance || !this._xterm || this._hintWidget || !commandDetectionCapability || commandDetectionCapability.promptInputModel.value || !!instance.shellLaunchConfig.attachPersistentProcess || commandDetectionCapability.commands.length > 0) { return; } From 1fd19078339c73ee2eccd52164f401f521aa130d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 17 Jan 2026 09:41:36 -0800 Subject: [PATCH 151/387] Update src/vs/workbench/contrib/terminalContrib/inlineHint/browser/terminal.initialHint.contribution.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../inlineHint/browser/terminal.initialHint.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/terminal.initialHint.contribution.ts b/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/terminal.initialHint.contribution.ts index 113ae54e1b4..659d9d8d736 100644 --- a/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/terminal.initialHint.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/inlineHint/browser/terminal.initialHint.contribution.ts @@ -97,7 +97,7 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm } xtermOpen(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { - // Don't show is the terminal was launched by an extension or a feature like debug + // Don't show if the terminal was launched by an extension or a feature like debug if (hasKey(this._ctx.instance, { shellLaunchConfig: true }) && (this._ctx.instance.shellLaunchConfig.isExtensionOwnedTerminal || this._ctx.instance.shellLaunchConfig.isFeatureTerminal || this._ctx.instance.shellLaunchConfig.hideFromUser)) { return; } From 141b5fe8ddc4549608a58c4a53a81313d901ac78 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sat, 17 Jan 2026 10:55:32 -0800 Subject: [PATCH 152/387] Add StaticResourceContextKey to skip global event listeners (#288641) Fix leak warnings from inline anchor widgets - not real leaks, we just create a lot of these and don't dispose when you might expect them to be --- src/vs/workbench/common/contextkeys.ts | 106 ++++++++++-------- .../chatInlineAnchorWidget.ts | 4 +- 2 files changed, 59 insertions(+), 51 deletions(-) diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index c0528feb6d1..1b33b025fdd 100644 --- a/src/vs/workbench/common/contextkeys.ts +++ b/src/vs/workbench/common/contextkeys.ts @@ -175,12 +175,7 @@ export function getVisbileViewContextKey(viewId: string): string { return `view. //#region < --- Resources --- > -export class ResourceContextKey { - - // NOTE: DO NOT CHANGE THE DEFAULT VALUE TO ANYTHING BUT - // UNDEFINED! IT IS IMPORTANT THAT DEFAULTS ARE INHERITED - // FROM THE PARENT CONTEXT AND ONLY UNDEFINED DOES THIS - +abstract class AbstractResourceContextKey { static readonly Scheme = new RawContextKey('resourceScheme', undefined, { type: 'string', description: localize('resourceScheme', "The scheme of the resource") }); static readonly Filename = new RawContextKey('resourceFilename', undefined, { type: 'string', description: localize('resourceFilename', "The file name of the resource") }); static readonly Dirname = new RawContextKey('resourceDirname', undefined, { type: 'string', description: localize('resourceDirname', "The folder name the resource is contained in") }); @@ -191,57 +186,41 @@ export class ResourceContextKey { static readonly HasResource = new RawContextKey('resourceSet', undefined, { type: 'boolean', description: localize('resourceSet', "Whether a resource is present or not") }); static readonly IsFileSystemResource = new RawContextKey('isFileSystemResource', undefined, { type: 'boolean', description: localize('isFileSystemResource', "Whether the resource is backed by a file system provider") }); - private readonly _disposables = new DisposableStore(); + protected readonly _disposables = new DisposableStore(); - private _value: URI | undefined; - private readonly _resourceKey: IContextKey; - private readonly _schemeKey: IContextKey; - private readonly _filenameKey: IContextKey; - private readonly _dirnameKey: IContextKey; - private readonly _pathKey: IContextKey; - private readonly _langIdKey: IContextKey; - private readonly _extensionKey: IContextKey; - private readonly _hasResource: IContextKey; - private readonly _isFileSystemResource: IContextKey; + protected _value: URI | undefined; + protected readonly _resourceKey: IContextKey; + protected readonly _schemeKey: IContextKey; + protected readonly _filenameKey: IContextKey; + protected readonly _dirnameKey: IContextKey; + protected readonly _pathKey: IContextKey; + protected readonly _langIdKey: IContextKey; + protected readonly _extensionKey: IContextKey; + protected readonly _hasResource: IContextKey; + protected readonly _isFileSystemResource: IContextKey; constructor( - @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IFileService private readonly _fileService: IFileService, - @ILanguageService private readonly _languageService: ILanguageService, - @IModelService private readonly _modelService: IModelService + @IContextKeyService protected readonly _contextKeyService: IContextKeyService, + @IFileService protected readonly _fileService: IFileService, + @ILanguageService protected readonly _languageService: ILanguageService, + @IModelService protected readonly _modelService: IModelService ) { - this._schemeKey = ResourceContextKey.Scheme.bindTo(this._contextKeyService); - this._filenameKey = ResourceContextKey.Filename.bindTo(this._contextKeyService); - this._dirnameKey = ResourceContextKey.Dirname.bindTo(this._contextKeyService); - this._pathKey = ResourceContextKey.Path.bindTo(this._contextKeyService); - this._langIdKey = ResourceContextKey.LangId.bindTo(this._contextKeyService); - this._resourceKey = ResourceContextKey.Resource.bindTo(this._contextKeyService); - this._extensionKey = ResourceContextKey.Extension.bindTo(this._contextKeyService); - this._hasResource = ResourceContextKey.HasResource.bindTo(this._contextKeyService); - this._isFileSystemResource = ResourceContextKey.IsFileSystemResource.bindTo(this._contextKeyService); - - this._disposables.add(_fileService.onDidChangeFileSystemProviderRegistrations(() => { - const resource = this.get(); - this._isFileSystemResource.set(Boolean(resource && _fileService.hasProvider(resource))); - })); - - this._disposables.add(_modelService.onModelAdded(model => { - if (isEqual(model.uri, this.get())) { - this._setLangId(); - } - })); - this._disposables.add(_modelService.onModelLanguageChanged(e => { - if (isEqual(e.model.uri, this.get())) { - this._setLangId(); - } - })); + this._schemeKey = AbstractResourceContextKey.Scheme.bindTo(this._contextKeyService); + this._filenameKey = AbstractResourceContextKey.Filename.bindTo(this._contextKeyService); + this._dirnameKey = AbstractResourceContextKey.Dirname.bindTo(this._contextKeyService); + this._pathKey = AbstractResourceContextKey.Path.bindTo(this._contextKeyService); + this._langIdKey = AbstractResourceContextKey.LangId.bindTo(this._contextKeyService); + this._resourceKey = AbstractResourceContextKey.Resource.bindTo(this._contextKeyService); + this._extensionKey = AbstractResourceContextKey.Extension.bindTo(this._contextKeyService); + this._hasResource = AbstractResourceContextKey.HasResource.bindTo(this._contextKeyService); + this._isFileSystemResource = AbstractResourceContextKey.IsFileSystemResource.bindTo(this._contextKeyService); } dispose(): void { this._disposables.dispose(); } - private _setLangId(): void { + protected _setLangId(): void { const value = this.get(); if (!value) { this._langIdKey.set(null); @@ -270,11 +249,10 @@ export class ResourceContextKey { }); } - private uriToPath(uri: URI): string { + protected uriToPath(uri: URI): string { if (uri.scheme === Schemas.file) { return uri.fsPath; } - return uri.path; } @@ -298,6 +276,36 @@ export class ResourceContextKey { } } +export class ResourceContextKey extends AbstractResourceContextKey { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @IFileService fileService: IFileService, + @ILanguageService languageService: ILanguageService, + @IModelService modelService: IModelService + ) { + super(contextKeyService, fileService, languageService, modelService); + this._disposables.add(fileService.onDidChangeFileSystemProviderRegistrations(() => { + const resource = this.get(); + this._isFileSystemResource.set(Boolean(resource && fileService.hasProvider(resource))); + })); + this._disposables.add(modelService.onModelAdded(model => { + if (isEqual(model.uri, this.get())) { + this._setLangId(); + } + })); + this._disposables.add(modelService.onModelLanguageChanged(e => { + if (isEqual(e.model.uri, this.get())) { + this._setLangId(); + } + })); + } +} + +export class StaticResourceContextKey extends AbstractResourceContextKey { + // No event listeners +} + + //#endregion export function applyAvailableEditorIds(contextKey: IContextKey, editor: EditorInput | undefined | null, editorResolverService: IEditorResolverService): void { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts index 91d6ad46898..2030a6f5638 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts @@ -35,7 +35,7 @@ import { ILabelService } from '../../../../../../platform/label/common/label.js' import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; import { FolderThemeIcon, IThemeService } from '../../../../../../platform/theme/common/themeService.js'; import { fillEditorsDragData } from '../../../../../browser/dnd.js'; -import { ResourceContextKey } from '../../../../../common/contextkeys.js'; +import { StaticResourceContextKey } from '../../../../../common/contextkeys.js'; import { IEditorService, SIDE_GROUP } from '../../../../../services/editor/common/editorService.js'; import { INotebookDocumentService } from '../../../../../services/notebook/common/notebookDocumentService.js'; import { ExplorerFolderContext } from '../../../../files/common/files.js'; @@ -236,7 +236,7 @@ export class InlineAnchorWidget extends Disposable { } } - const resourceContextKey = this._register(new ResourceContextKey(contextKeyService, fileService, languageService, modelService)); + const resourceContextKey = this._register(new StaticResourceContextKey(contextKeyService, fileService, languageService, modelService)); resourceContextKey.set(location.uri); this._chatResourceContext.set(location.uri.toString()); From 40552e3bce9cba82766dc267c9103fb6324d5005 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sat, 17 Jan 2026 11:01:49 -0800 Subject: [PATCH 153/387] Prevent calling updateElementHeight while rendering the element (#288644) There are too many ways this can happen on accident so just applying an overall check --- .../contrib/chat/browser/widget/chatListRenderer.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 285aade3b1f..76d10acb2ad 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -198,6 +198,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer()); /** @@ -525,7 +526,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer, index: number, templateData: IChatListItemTemplate): void { - this.renderChatTreeItem(node.element, index, templateData); + this._elementBeingRendered = node.element; + try { + this.renderChatTreeItem(node.element, index, templateData); + } finally { + this._elementBeingRendered = undefined; + } } private clearRenderedParts(templateData: IChatListItemTemplate): void { @@ -536,7 +542,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer Date: Sat, 17 Jan 2026 20:14:11 +0100 Subject: [PATCH 154/387] feat - add retry and report issue commands in `SetupAgent` on timeout (#288645) --- .../browser/chatSetup/chatSetupProviders.ts | 32 ++++++++++++++++--- .../chatCommandContentPart.ts | 22 ++++++++++--- .../chat/browser/widget/media/chat.css | 2 ++ .../chat/common/chatService/chatService.ts | 1 + 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts index f13d8facbad..429a6d99bf0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts @@ -167,7 +167,8 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { private static readonly SETUP_NEEDED_MESSAGE = new MarkdownString(localize('settingUpCopilotNeeded', "You need to set up GitHub Copilot and be signed in to use Chat.")); private static readonly TRUST_NEEDED_MESSAGE = new MarkdownString(localize('trustNeeded', "You need to trust this workspace to use Chat.")); - private static CHAT_REPORT_ISSUE_WITH_OUTPUT_ID = 'workbench.action.chat.reportIssueWithOutput'; + private static readonly CHAT_RETRY_COMMAND_ID = 'workbench.action.chat.retrySetup'; + private static readonly CHAT_REPORT_ISSUE_WITH_OUTPUT_COMMAND_ID = 'workbench.action.chat.reportIssueWithOutput'; private readonly _onUnresolvableError = this._register(new Emitter()); readonly onUnresolvableError = this._onUnresolvableError.event; @@ -192,7 +193,9 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { } private registerCommands(): void { - this._register(CommandsRegistry.registerCommand(SetupAgent.CHAT_REPORT_ISSUE_WITH_OUTPUT_ID, async accessor => { + + // Report issue with output command + this._register(CommandsRegistry.registerCommand(SetupAgent.CHAT_REPORT_ISSUE_WITH_OUTPUT_COMMAND_ID, async accessor => { const outputService = accessor.get(IOutputService); const textModelService = accessor.get(ITextModelService); const issueService = accessor.get(IWorkbenchIssueService); @@ -234,6 +237,22 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { data: outputData || localize('chatOutputChannelUnavailable', "GitHub Copilot Chat output channel not available. Please ensure the GitHub Copilot Chat extension is active and try again. If the issue persists, you can manually include relevant information from the Output panel (View > Output > GitHub Copilot Chat).") }); })); + + // Retry chat command + this._register(CommandsRegistry.registerCommand(SetupAgent.CHAT_RETRY_COMMAND_ID, async (accessor, sessionResource: URI) => { + const chatService = accessor.get(IChatService); + const chatWidgetService = accessor.get(IChatWidgetService); + + const widget = chatWidgetService.getWidgetBySessionResource(sessionResource); + const lastRequest = widget?.viewModel?.model.getRequests().at(-1); + if (lastRequest) { + await chatService.resendRequest(lastRequest, { + ...widget?.getModeRequestOptions(), + modeInfo: widget?.input.currentModeInfo, + userSelectedModelId: widget?.input.currentLanguageModel + }); + } + })); } async invoke(request: IChatAgentRequest, progress: (parts: IChatProgress[]) => void): Promise { @@ -397,9 +416,14 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { progress({ kind: 'command', command: { - id: SetupAgent.CHAT_REPORT_ISSUE_WITH_OUTPUT_ID, + id: SetupAgent.CHAT_RETRY_COMMAND_ID, + title: localize('retryChat', "Retry"), + arguments: [requestModel.session.sessionResource] + }, + additionalCommands: [{ + id: SetupAgent.CHAT_REPORT_ISSUE_WITH_OUTPUT_COMMAND_ID, title: localize('reportChatIssue', "Report Issue"), - } + }] }); // This means Chat is unhealthy and we cannot retry the diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCommandContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCommandContentPart.ts index 0ec3904a72f..a7e0af1cf70 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCommandContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCommandContentPart.ts @@ -13,6 +13,7 @@ import { IChatContentPart, IChatContentPartRenderContext } from './chatContentPa import { IChatProgressRenderableResponseContent } from '../../../common/model/chatModel.js'; import { IChatCommandButton } from '../../../common/chatService/chatService.js'; import { isResponseVM } from '../../../common/model/chatViewModel.js'; +import { Command } from '../../../../../../editor/common/languages.js'; const $ = dom.$; @@ -28,15 +29,28 @@ export class ChatCommandButtonContentPart extends Disposable implements IChatCon this.domNode = $('.chat-command-button'); const enabled = !isResponseVM(context.element) || !context.element.isStale; + + // Render the primary button + this.renderButton(this.domNode, commandButton.command, enabled); + + // Render additional buttons if any + if (commandButton.additionalCommands) { + for (const command of commandButton.additionalCommands) { + this.renderButton(this.domNode, command, enabled, true); + } + } + } + + private renderButton(container: HTMLElement, command: Command, enabled: boolean, secondary?: boolean): void { const tooltip = enabled ? - commandButton.command.tooltip : + command.tooltip : localize('commandButtonDisabled', "Button not available in restored chat"); - const button = this._register(new Button(this.domNode, { ...defaultButtonStyles, supportIcons: true, title: tooltip })); - button.label = commandButton.command.title; + const button = this._register(new Button(container, { ...defaultButtonStyles, supportIcons: true, title: tooltip, secondary })); + button.label = command.title; button.enabled = enabled; // TODO still need telemetry for command buttons - this._register(button.onDidClick(() => this.commandService.executeCommand(commandButton.command.id, ...(commandButton.command.arguments ?? [])))); + this._register(button.onDidClick(() => this.commandService.executeCommand(command.id, ...(command.arguments ?? [])))); } hasSameContent(other: IChatProgressRenderableResponseContent): boolean { diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index b28b64bd950..b8339e70596 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -2239,6 +2239,8 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-item-container .chat-command-button { display: flex; + flex-wrap: wrap; + gap: 8px; margin-bottom: 16px; } diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index f135d3353eb..cc22f71b3f3 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -282,6 +282,7 @@ export interface IChatAgentMarkdownContentWithVulnerability { export interface IChatCommandButton { command: Command; kind: 'command'; + additionalCommands?: Command[]; // rendered as secondary buttons } export interface IChatMoveMessage { From 2e7a9e070cf6e5cebe4b266f2299c4f66efa8175 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sat, 17 Jan 2026 17:18:08 -0800 Subject: [PATCH 155/387] Avoid hashing variables for dataId (#288650) * Avoid hashing variables for dataId This can be extremely slow when loading large models. Instead we increment a version number each time they change (which isn't really necessary anyway today, the vars used to be updated async but now they are just set when the request is created, but set this up correctly for changes later) * Actually increment version --- .../workbench/api/common/extHostTypeConverters.ts | 4 ++-- .../contrib/chat/common/model/chatModel.ts | 9 ++++++++- .../contrib/chat/common/model/chatViewModel.ts | 14 ++++---------- .../contrib/chat/common/model/objectMutationLog.ts | 2 +- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 6cfbb62d07c..ea078db10b6 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -3126,8 +3126,8 @@ export namespace ChatResponsePart { export namespace ChatAgentRequest { export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][], tools: Map, extension: IRelaxedExtensionDescription, logService: ILogService): vscode.ChatRequest { - const toolReferences: typeof request.variables.variables = []; - const variableReferences: typeof request.variables.variables = []; + const toolReferences: IChatRequestVariableEntry[] = []; + const variableReferences: IChatRequestVariableEntry[] = []; for (const v of request.variables.variables) { if (v.kind === 'tool') { toolReferences.push(v); diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index c32f835af68..b752e95e0ae 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -52,7 +52,7 @@ export function getAttachableImageExtension(mimeType: string): string | undefine } export interface IChatRequestVariableData { - variables: IChatRequestVariableEntry[]; + variables: readonly IChatRequestVariableEntry[]; } export namespace IChatRequestVariableData { @@ -64,6 +64,7 @@ export namespace IChatRequestVariableData { export interface IChatRequestModel { readonly id: string; readonly timestamp: number; + readonly version: number; readonly modeInfo?: IChatRequestModeInfo; readonly session: IChatModel; readonly message: IParsedChatRequest; @@ -329,6 +330,7 @@ export class ChatRequestModel implements IChatRequestModel { } public set variableData(v: IChatRequestVariableData) { + this._version++; this._variableData = v; } @@ -348,6 +350,11 @@ export class ChatRequestModel implements IChatRequestModel { return this._editedFileEvents; } + private _version = 0; + public get version(): number { + return this._version; + } + constructor(params: IChatRequestModelParameters) { this._session = params.session; this.message = params.message; diff --git a/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts index c31571559ba..12464c1fad8 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts @@ -5,7 +5,6 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; -import { hash } from '../../../../../base/common/hash.js'; import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable, dispose } from '../../../../../base/common/lifecycle.js'; import { IObservable } from '../../../../../base/common/observable.js'; @@ -342,21 +341,16 @@ export class ChatViewModel extends Disposable implements IChatViewModel { } } -const variablesHash = new WeakMap(); - export class ChatRequestViewModel implements IChatRequestViewModel { get id() { return this._model.id; } + /** + * An ID that changes when the request should be re-rendered. + */ get dataId() { - let varsHash = variablesHash.get(this.variables); - if (typeof varsHash !== 'number') { - varsHash = hash(this.variables); - variablesHash.set(this.variables, varsHash); - } - - return `${this.id}_${this.isComplete ? '1' : '0'}_${varsHash}`; + return `${this.id}_${this._model.version + (this._model.response?.isComplete ? 1 : 0)}`; } /** @deprecated */ diff --git a/src/vs/workbench/contrib/chat/common/model/objectMutationLog.ts b/src/vs/workbench/contrib/chat/common/model/objectMutationLog.ts index 657400cd9d3..01ce16ae67a 100644 --- a/src/vs/workbench/contrib/chat/common/model/objectMutationLog.ts +++ b/src/vs/workbench/contrib/chat/common/model/objectMutationLog.ts @@ -92,7 +92,7 @@ export function value(comparator?: (a: R, b: R) => boolean): TransformValu } /** An array that will use the schema to compare items positionally. */ -export function array(schema: TransformObject | TransformValue): TransformArray { +export function array(schema: TransformObject | TransformValue): TransformArray { return { kind: TransformKind.Array, itemSchema: schema, From 9a414036ba9c1a21261977697cfd25cce4caef40 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sat, 17 Jan 2026 17:18:57 -0800 Subject: [PATCH 156/387] Change chat input layout to be based on ResizeObserver (#288653) Simplify the logic and avoid layout thrashing when we would layout multiple times --- src/vs/base/browser/dom.ts | 22 ++++ .../contrib/chat/browser/widget/chatWidget.ts | 21 +-- .../browser/widget/input/chatInputPart.ts | 122 +++++------------- .../widgetHosts/viewPane/chatViewPane.ts | 14 +- .../inlineChat/browser/inlineChatWidget.ts | 2 +- 5 files changed, 72 insertions(+), 109 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 0e792265805..11862c1b299 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1940,6 +1940,28 @@ export class DragAndDropObserver extends Disposable { } } +/** + * A wrapper around ResizeObserver that is disposable. + */ +export class DisposableResizeObserver extends Disposable { + + private readonly observer: ResizeObserver; + + constructor(callback: ResizeObserverCallback) { + super(); + this.observer = new ResizeObserver(callback); + this._register(toDisposable(() => this.observer.disconnect())); + } + + observe(target: Element, options?: ResizeObserverOptions): void { + this.observer.observe(target, options); + } + + unobserve(target: Element): void { + this.observer.unobserve(target); + } +} + type HTMLElementAttributeKeys = Partial<{ [K in keyof T]: T[K] extends Function ? never : T[K] extends object ? HTMLElementAttributeKeys : T[K] }>; type ElementAttributes = HTMLElementAttributeKeys & Record; type RemoveHTMLElement = T extends HTMLElement ? never : T; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 2298702f2b7..9aa2abdfe71 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -612,7 +612,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } get contentHeight(): number { - return this.input.contentHeight + this.tree.contentHeight + this.chatSuggestNextWidget.height; + return this.input.inputPartHeight.get() + this.tree.contentHeight + this.chatSuggestNextWidget.height; } get attachmentModel(): ChatAttachmentModel { @@ -1921,7 +1921,9 @@ export class ChatWidget extends Disposable implements IChatWidget { }, }); })); - this._register(this.input.onDidChangeHeight(() => { + this._register(autorun(reader => { + this.input.inputPartHeight.read(reader); + const editedRequest = this.renderer.getTemplateDataForRequestId(this.viewModel?.editing?.id); if (isRequestVM(editedRequest?.currentElement) && this.viewModel?.editing) { this.renderer.updateItemHeightOnRender(editedRequest?.currentElement, editedRequest); @@ -2422,14 +2424,13 @@ export class ChatWidget extends Disposable implements IChatWidget { const heightUpdated = this.bodyDimension && this.bodyDimension.height !== height; this.bodyDimension = new dom.Dimension(width, height); - const layoutHeight = this._dynamicMessageLayoutData?.enabled ? this._dynamicMessageLayoutData.maxHeight : height; if (this.viewModel?.editing) { - this.inlineInputPart?.layout(layoutHeight, width); + this.inlineInputPart?.layout(width); } - this.inputPart.layout(layoutHeight, width); + this.inputPart.layout(width); - const inputHeight = this.inputPart.inputPartHeight; + const inputHeight = this.inputPart.inputPartHeight.get(); const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height; const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight - 2; const lastItem = this.viewModel?.getItems().at(-1); @@ -2487,8 +2488,8 @@ export class ChatWidget extends Disposable implements IChatWidget { const possibleMaxHeight = (this._dynamicMessageLayoutData?.maxHeight ?? maxHeight); const width = this.bodyDimension?.width ?? this.container.offsetWidth; - this.input.layout(possibleMaxHeight, width); - const inputPartHeight = this.input.inputPartHeight; + this.input.layout(width); + const inputPartHeight = this.input.inputPartHeight.get(); const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height; const newHeight = Math.min(renderHeight + diff, possibleMaxHeight - inputPartHeight - chatSuggestNextWidgetHeight); this.layout(newHeight + inputPartHeight + chatSuggestNextWidgetHeight, width); @@ -2532,8 +2533,8 @@ export class ChatWidget extends Disposable implements IChatWidget { } const width = this.bodyDimension?.width ?? this.container.offsetWidth; - this.input.layout(this._dynamicMessageLayoutData.maxHeight, width); - const inputHeight = this.input.inputPartHeight; + this.input.layout(width); + const inputHeight = this.input.inputPartHeight.get(); const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height; const totalMessages = this.viewModel.getItems(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 33d2fc18eb4..2db1c584e4b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -187,9 +187,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private _onDidLoadInputState: Emitter = this._register(new Emitter()); readonly onDidLoadInputState: Event = this._onDidLoadInputState.event; - private _onDidChangeHeight = this._register(new Emitter()); - readonly onDidChangeHeight: Event = this._onDidChangeHeight.event; - private _onDidFocus = this._register(new Emitter()); readonly onDidFocus: Event = this._onDidFocus.event; @@ -272,32 +269,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private chatInputWidgetsContainer!: HTMLElement; private readonly _widgetController = this._register(new MutableDisposable()); - private _inputPartHeight: number = 0; - get inputPartHeight() { - return this._inputPartHeight; - } - - private _followupsHeight: number = 0; - get followupsHeight() { - return this._followupsHeight; - } - - private _editSessionWidgetHeight: number = 0; - get editSessionWidgetHeight() { - return this._editSessionWidgetHeight; - } - - get todoListWidgetHeight() { - return this.chatInputTodoListWidgetContainer.offsetHeight; - } - - get inputWidgetsHeight() { - return this.chatInputWidgetsContainer?.offsetHeight ?? 0; - } - - get attachmentsHeight() { - return this.attachmentsContainer.offsetHeight + (this.attachmentsContainer.checkVisibility() ? 6 : 0); - } + readonly inputPartHeight = observableValue(this, 0); private _inputEditor!: CodeEditorWidget; private _inputEditorElement!: HTMLElement; @@ -408,7 +380,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge }; } - private cachedDimensions: dom.Dimension | undefined; + private cachedWidth: number | undefined; private cachedExecuteToolbarWidth: number | undefined; private cachedInputToolbarWidth: number | undefined; @@ -935,9 +907,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge public setCurrentLanguageModel(model: ILanguageModelChatMetadataAndIdentifier) { this._currentLanguageModel = model; - if (this.cachedDimensions) { + if (this.cachedWidth) { // For quick chat and editor chat, relayout because the input may need to shrink to accomodate the model name - this.layout(this.cachedDimensions.height, this.cachedDimensions.width); + this.layout(this.cachedWidth); } // Store as global user preference (session-specific state is in the model's inputModel) @@ -1621,7 +1593,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (!this._widgetController.value) { this._widgetController.value = this.instantiationService.createInstance(ChatInputPartWidgetController, this.chatInputWidgetsContainer); - this._register(this._widgetController.value.onDidChangeHeight(() => this._onDidChangeHeight.fire())); } } @@ -1803,7 +1774,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const currentHeight = Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight); if (currentHeight !== this.inputEditorHeight) { this.inputEditorHeight = currentHeight; - this._onDidChangeHeight.fire(); + // Directly update editor layout - ResizeObserver will notify parent about height change + if (this.cachedWidth) { + this._layout(this.cachedWidth); + } } const model = this._inputEditor.getModel(); @@ -1816,7 +1790,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._register(this._inputEditor.onDidContentSizeChange(e => { if (e.contentHeightChanged) { this.inputEditorHeight = !this.inline ? e.contentHeight : this.inputEditorHeight; - this._onDidChangeHeight.fire(); + // Directly update editor layout - ResizeObserver will notify parent about height change + if (this.cachedWidth) { + this._layout(this.cachedWidth); + } } })); this._register(this._inputEditor.onDidFocusEditorText(() => { @@ -1907,8 +1884,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const container = toolbarElement.querySelector('.chat-sessionPicker-container'); this.chatSessionPickerContainer = container as HTMLElement | undefined; - if (this.cachedDimensions && typeof this.cachedInputToolbarWidth === 'number' && this.cachedInputToolbarWidth !== this.inputActionsToolbar.getItemsWidth()) { - this.layout(this.cachedDimensions.height, this.cachedDimensions.width); + if (this.cachedWidth && typeof this.cachedInputToolbarWidth === 'number' && this.cachedInputToolbarWidth !== this.inputActionsToolbar.getItemsWidth()) { + this.layout(this.cachedWidth); } })); this.executeToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, toolbarsContainer, this.options.menus.executeToolbar, { @@ -1928,8 +1905,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.executeToolbar.getElement().classList.add('chat-execute-toolbar'); this.executeToolbar.context = { widget } satisfies IChatExecuteActionContext; this._register(this.executeToolbar.onDidChangeMenuItems(() => { - if (this.cachedDimensions && typeof this.cachedExecuteToolbarWidth === 'number' && this.cachedExecuteToolbarWidth !== this.executeToolbar.getItemsWidth()) { - this.layout(this.cachedDimensions.height, this.cachedDimensions.width); + if (this.cachedWidth && typeof this.cachedExecuteToolbarWidth === 'number' && this.cachedExecuteToolbarWidth !== this.executeToolbar.getItemsWidth()) { + this.layout(this.cachedWidth); } })); if (this.options.menus.inputSideToolbar) { @@ -2012,12 +1989,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } })); this.addFilesToolbar.context = { widget, placeholder: localize('chatAttachFiles', 'Search for files and context to add to your request') }; - this._register(this.addFilesToolbar.onDidChangeMenuItems(() => { - if (this.cachedDimensions) { - this._onDidChangeHeight.fire(); - } - })); this.renderAttachedContext(); + + const inputResizeObserver = this._register(new dom.DisposableResizeObserver(() => { + const newHeight = this.container.offsetHeight; + this.inputPartHeight.set(newHeight, undefined); + })); + inputResizeObserver.observe(this.container); } public toggleChatInputOverlay(editing: boolean): void { @@ -2035,8 +2013,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge public renderAttachedContext() { const container = this.attachedContextContainer; - // Note- can't measure attachedContextContainer, because it has `display: contents`, so measure the parent to check for height changes - const oldHeight = this.attachmentsContainer.offsetHeight; const store = new DisposableStore(); this.attachedContextDisposables.value = store; @@ -2128,10 +2104,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - if (oldHeight !== this.attachmentsContainer.offsetHeight) { - this._onDidChangeHeight.fire(); - } - this.addFilesButton?.setShowLabel(this._attachmentModel.size === 0 && !this.hasImplicitContextBlock()); this._indexOfLastOpenedContext = -1; @@ -2271,11 +2243,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Add the widget's DOM node to the dedicated todo list container dom.clearNode(this.chatInputTodoListWidgetContainer); dom.append(this.chatInputTodoListWidgetContainer, widget.domNode); - - // Listen to height changes - this._chatEditingTodosDisposables.add(widget.onDidChangeHeight(() => { - this._onDidChangeHeight.fire(); - })); } this._chatInputTodoListWidget.value.render(chatSessionResource); @@ -2387,8 +2354,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.clearNode(this.chatEditingSessionWidgetContainer); this._chatEditsDisposables.clear(); this._chatEditList = undefined; - - this._onDidChangeHeight.fire(); } }); } @@ -2500,9 +2465,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._workingSetLinesRemovedSpan.value.textContent = `-${removed}`; dom.setVisibility(shouldShowEditingSession, this.chatEditingSessionWidgetContainer); - if (!shouldShowEditingSession) { - this._onDidChangeHeight.fire(); - } })); const countsContainer = dom.$('.working-set-line-counts'); @@ -2530,7 +2492,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const collapsed = this._workingSetCollapsed.read(reader); button.icon = collapsed ? Codicon.chevronRight : Codicon.chevronDown; workingSetContainer.classList.toggle('collapsed', collapsed); - this._onDidChangeHeight.fire(); })); if (!this._chatEditList) { @@ -2592,7 +2553,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge list.layout(height); list.getHTMLElement().style.height = `${height}px`; list.splice(0, list.length, allEntries); - this._onDidChangeHeight.fire(); })); } @@ -2650,7 +2610,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge group.remove(); })); } - this._onDidChangeHeight.fire(); } async renderFollowups(items: IChatFollowup[] | undefined, response: IChatResponseViewModel | undefined): Promise { @@ -2663,34 +2622,28 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (items && items.length > 0) { this.followupsDisposables.add(this.instantiationService.createInstance, ChatFollowups>(ChatFollowups, this.followupsContainer, items, this.location, undefined, followup => this._onDidAcceptFollowup.fire({ followup, response }))); } - this._onDidChangeHeight.fire(); } - get contentHeight(): number { - const data = this.getLayoutData(); - return data.followupsHeight + data.inputPartEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.attachmentsHeight + data.toolbarsHeight + data.chatEditingStateHeight + data.todoListWidgetContainerHeight + data.inputWidgetsContainerHeight; - } + /** + * Layout the input part with the given width. Height is intrinsic - determined by content + * and detected via ResizeObserver, which updates `inputPartHeight` for the parent to observe. + */ + layout(width: number) { + this.cachedWidth = width; - layout(height: number, width: number) { - this.cachedDimensions = new dom.Dimension(width, height); - - return this._layout(height, width); + return this._layout(width); } private previousInputEditorDimension: IDimension | undefined; - private _layout(height: number, width: number, allowRecurse = true): void { + private _layout(width: number, allowRecurse = true): void { const data = this.getLayoutData(); - const inputEditorHeight = Math.min(data.inputPartEditorHeight, height - data.followupsHeight - data.attachmentsHeight - data.inputPartVerticalPadding - data.toolbarsHeight - data.chatEditingStateHeight - data.todoListWidgetContainerHeight - data.inputWidgetsContainerHeight); const followupsWidth = width - data.inputPartHorizontalPadding; this.followupsContainer.style.width = `${followupsWidth}px`; - this._inputPartHeight = data.inputPartVerticalPadding + data.followupsHeight + inputEditorHeight + data.inputEditorBorder + data.attachmentsHeight + data.toolbarsHeight + data.chatEditingStateHeight + data.todoListWidgetContainerHeight + data.inputWidgetsContainerHeight; - this._followupsHeight = data.followupsHeight; - this._editSessionWidgetHeight = data.chatEditingStateHeight; - const initialEditorScrollWidth = this._inputEditor.getScrollWidth(); const newEditorWidth = width - data.inputPartHorizontalPadding - data.editorBorder - data.inputPartHorizontalPaddingInside - data.toolbarsWidth - data.sideToolbarWidth; + const inputEditorHeight = Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight); const newDimension = { width: newEditorWidth, height: inputEditorHeight }; if (!this.previousInputEditorDimension || (this.previousInputEditorDimension.width !== newDimension.width || this.previousInputEditorDimension.height !== newDimension.height)) { // This layout call has side-effects that are hard to understand. eg if we are calling this inside a onDidChangeContent handler, this can trigger the next onDidChangeContent handler @@ -2701,7 +2654,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (allowRecurse && initialEditorScrollWidth < 10) { // This is probably the initial layout. Now that the editor is layed out with its correct width, it should report the correct contentHeight - return this._layout(height, width, false); + return this._layout(width, false); } } @@ -2728,20 +2681,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge }; return { - inputEditorBorder: 2, - followupsHeight: this.followupsContainer.offsetHeight, - inputPartEditorHeight: Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight), - inputPartHorizontalPadding: this.options.renderStyle === 'compact' ? 16 : 32, - inputPartVerticalPadding: this.options.renderStyle === 'compact' ? 12 : (16 /* entire part */ + 6 /* input container */ + (2 * 4) /* flex gap: todo|edits|input */), - attachmentsHeight: this.attachmentsHeight, editorBorder: 2, + inputPartHorizontalPadding: this.options.renderStyle === 'compact' ? 16 : 32, inputPartHorizontalPaddingInside: 12, toolbarsWidth: this.options.renderStyle === 'compact' ? getToolbarsWidthCompact() : 0, - toolbarsHeight: this.options.renderStyle === 'compact' ? 0 : 22, - chatEditingStateHeight: this.chatEditingSessionWidgetContainer.offsetHeight, sideToolbarWidth: inputSideToolbarWidth > 0 ? inputSideToolbarWidth + 4 /*gap*/ : 0, - todoListWidgetContainerHeight: this.chatInputTodoListWidgetContainer.offsetHeight, - inputWidgetsContainerHeight: this.inputWidgetsHeight, }; } } diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 7824c4fcf27..52bfac795e0 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -548,7 +548,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { //#region Chat Control - private static readonly MIN_CHAT_WIDGET_HEIGHT = 120; + private static readonly MIN_CHAT_WIDGET_HEIGHT = 116; private _widget!: ChatWidget; get widget(): ChatWidget { return this._widget; } @@ -650,14 +650,10 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { })); // When showing sessions stacked, adjust the height of the sessions list to make room for chat input - let lastChatInputHeight: number | undefined; - this._register(chatWidget.input.onDidChangeHeight(() => { + this._register(autorun(reader => { + chatWidget.input.inputPartHeight.read(reader); if (this.sessionsViewerVisible && this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { - const chatInputHeight = this._widget?.input?.contentHeight; - if (chatInputHeight && chatInputHeight !== lastChatInputHeight) { // ensure we only layout on actual height changes - lastChatInputHeight = chatInputHeight; - this.relayout(); - } + this.relayout(); } })); } @@ -937,7 +933,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { let availableSessionsHeight = height - this.sessionsTitleContainer.offsetHeight; if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { - availableSessionsHeight -= Math.max(ChatViewPane.MIN_CHAT_WIDGET_HEIGHT, this._widget?.input?.contentHeight ?? 0); + availableSessionsHeight -= Math.max(ChatViewPane.MIN_CHAT_WIDGET_HEIGHT, this._widget?.input?.inputPartHeight.get() ?? 0); } else { availableSessionsHeight -= this.sessionsNewButtonContainer?.offsetHeight ?? 0; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index c866191d832..d88b121fadd 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -393,7 +393,7 @@ export class InlineChatWidget { let value = this.contentHeight; value -= this._chatWidget.contentHeight; - value += Math.min(this._chatWidget.input.contentHeight + maxWidgetOutputHeight, this._chatWidget.contentHeight); + value += Math.min(this._chatWidget.input.inputPartHeight.get() + maxWidgetOutputHeight, this._chatWidget.contentHeight); return value; } From 50532b11cbad18fc26c5dbb35a511f3847b01a57 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sat, 17 Jan 2026 18:39:35 -0800 Subject: [PATCH 157/387] StaticResourceContextKey followup (#288673) Address copilot comments in https://github.com/microsoft/vscode/pull/288641 --- src/vs/workbench/common/contextkeys.ts | 26 ++++++++++++------- .../chatInlineAnchorWidget.ts | 2 +- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index 1b33b025fdd..3a687ad965a 100644 --- a/src/vs/workbench/common/contextkeys.ts +++ b/src/vs/workbench/common/contextkeys.ts @@ -176,6 +176,11 @@ export function getVisbileViewContextKey(viewId: string): string { return `view. //#region < --- Resources --- > abstract class AbstractResourceContextKey { + + // NOTE: DO NOT CHANGE THE DEFAULT VALUE TO ANYTHING BUT + // UNDEFINED! IT IS IMPORTANT THAT DEFAULTS ARE INHERITED + // FROM THE PARENT CONTEXT AND ONLY UNDEFINED DOES THIS + static readonly Scheme = new RawContextKey('resourceScheme', undefined, { type: 'string', description: localize('resourceScheme', "The scheme of the resource") }); static readonly Filename = new RawContextKey('resourceFilename', undefined, { type: 'string', description: localize('resourceFilename', "The file name of the resource") }); static readonly Dirname = new RawContextKey('resourceDirname', undefined, { type: 'string', description: localize('resourceDirname', "The folder name the resource is contained in") }); @@ -186,8 +191,6 @@ abstract class AbstractResourceContextKey { static readonly HasResource = new RawContextKey('resourceSet', undefined, { type: 'boolean', description: localize('resourceSet', "Whether a resource is present or not") }); static readonly IsFileSystemResource = new RawContextKey('isFileSystemResource', undefined, { type: 'boolean', description: localize('isFileSystemResource', "Whether the resource is backed by a file system provider") }); - protected readonly _disposables = new DisposableStore(); - protected _value: URI | undefined; protected readonly _resourceKey: IContextKey; protected readonly _schemeKey: IContextKey; @@ -216,10 +219,6 @@ abstract class AbstractResourceContextKey { this._isFileSystemResource = AbstractResourceContextKey.IsFileSystemResource.bindTo(this._contextKeyService); } - dispose(): void { - this._disposables.dispose(); - } - protected _setLangId(): void { const value = this.get(); if (!value) { @@ -277,6 +276,9 @@ abstract class AbstractResourceContextKey { } export class ResourceContextKey extends AbstractResourceContextKey { + + private readonly _disposables = new DisposableStore(); + constructor( @IContextKeyService contextKeyService: IContextKeyService, @IFileService fileService: IFileService, @@ -299,11 +301,17 @@ export class ResourceContextKey extends AbstractResourceContextKey { } })); } + + dispose(): void { + this._disposables.dispose(); + } } -export class StaticResourceContextKey extends AbstractResourceContextKey { - // No event listeners -} +/** + * This is a version of ResourceContextKey that is not disposable and has no listeners for model change events. + * It will configure itself for the state/presence of a model only when created and not update. + */ +export class StaticResourceContextKey extends AbstractResourceContextKey { } //#endregion diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts index 2030a6f5638..0a95042c83c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts @@ -236,7 +236,7 @@ export class InlineAnchorWidget extends Disposable { } } - const resourceContextKey = this._register(new StaticResourceContextKey(contextKeyService, fileService, languageService, modelService)); + const resourceContextKey = new StaticResourceContextKey(contextKeyService, fileService, languageService, modelService); resourceContextKey.set(location.uri); this._chatResourceContext.set(location.uri.toString()); From 2d7335fead330ac952d2bd33ee00d6de519d7d61 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sat, 17 Jan 2026 21:06:39 -0800 Subject: [PATCH 158/387] Lazy render collapsed thinking group parts (#288535) * Lazy render collapsed thinking group parts Fix #287176 * re-comment * Update src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts Co-authored-by: Justin Chen <54879025+justschen@users.noreply.github.com> * - In fixedScrolling mode, need to make sure we know that the part is not streaming before rendering it, so that we don't render the tool parts then immediate finalize and collapse the group - Diffing with lazily created tool parts is a bit weird, led to rendering extra tool parts at the bottom of the response. One thing I added to help with this is clearing out all rendered parts when a new model is swapped in. I wanted to do this anyway because keeping all those parts around can lead to leaks. --------- Co-authored-by: Justin Chen <54879025+justschen@users.noreply.github.com> --- .../chatThinkingContentPart.ts | 170 +++++++++++++----- .../chat/browser/widget/chatListRenderer.ts | 127 ++++++++----- .../contrib/chat/browser/widget/chatWidget.ts | 2 +- 3 files changed, 209 insertions(+), 90 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index ccec61f72f8..66a6558bf95 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -21,6 +21,8 @@ import { ChatCollapsibleContentPart } from './chatCollapsibleContentPart.js'; import { localize } from '../../../../../../nls.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { Lazy } from '../../../../../../base/common/lazy.js'; +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; import { autorun } from '../../../../../../base/common/observable.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { IChatMarkdownAnchorService } from './chatMarkdownAnchorService.js'; @@ -86,6 +88,13 @@ function extractTitleFromThinkingContent(content: string): string | undefined { return headerMatch ? headerMatch[1] : undefined; } +interface ILazyItem { + lazy: Lazy<{ domNode: HTMLElement; disposable?: IDisposable }>; + toolInvocationId?: string; + toolInvocationOrMarkdown?: IChatToolInvocation | IChatToolInvocationSerialized | IChatMarkdownContent; + originalParent?: HTMLElement; +} + export class ChatThinkingContentPart extends ChatCollapsibleContentPart implements IChatContentPart { public readonly codeblocks: undefined; public readonly codeblocksPartId: undefined; @@ -103,15 +112,17 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen private extractedTitles: string[] = []; private toolInvocationCount: number = 0; private appendedItemCount: number = 0; - private streamingCompleted: boolean = false; private isActive: boolean = true; private toolInvocations: (IChatToolInvocation | IChatToolInvocationSerialized)[] = []; private singleItemInfo: { element: HTMLElement; originalParent: HTMLElement; originalNextSibling: Node | null } | undefined; + private lazyItems: ILazyItem[] = []; + private hasExpandedOnce: boolean = false; constructor( content: IChatThinkingPart, context: IChatContentPartRenderContext, private readonly chatContentMarkdownRenderer: IMarkdownRenderer, + private streamingCompleted: boolean, @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService private readonly configurationService: IConfigurationService, @IChatMarkdownAnchorService private readonly chatMarkdownAnchorService: IChatMarkdownAnchorService, @@ -141,11 +152,10 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen if (configuredMode === ThinkingDisplayMode.Collapsed) { this.setExpanded(false); + } else if (configuredMode === ThinkingDisplayMode.CollapsedPreview) { + // Start expanded if still in progress + this.setExpanded(!this.element.isComplete); } else { - this.setExpanded(true); - } - - if (this.fixedScrollingMode) { this.setExpanded(false); } @@ -173,6 +183,17 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen } })); + // Materialize lazy items when first expanded + this._register(autorun(r => { + if (this._isExpanded.read(r) && !this.hasExpandedOnce && this.lazyItems.length > 0) { + this.hasExpandedOnce = true; + for (const item of this.lazyItems) { + this.materializeLazyItem(item); + } + this._onDidChangeHeight.fire(); + } + })); + if (this._collapseButton && !this.streamingCompleted && !this.element.isComplete) { this._collapseButton.icon = ThemeIcon.modify(Codicon.loading, 'spin'); } @@ -204,7 +225,10 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen // @TODO: @justschen Convert to template for each setting? protected override initContent(): HTMLElement { this.wrapper = $('.chat-used-context-list.chat-thinking-collapsible'); - this.wrapper.classList.add('chat-thinking-streaming'); + if (!this.streamingCompleted) { + this.wrapper.classList.add('chat-thinking-streaming'); + } + if (this.currentThinkingValue) { this.textContainer = $('.chat-thinking-item.markdown-content'); this.wrapper.appendChild(this.textContainer); @@ -508,13 +532,93 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen this.updateDropdownClickability(); } - public appendItem(content: HTMLElement, toolInvocationId?: string, toolInvocationOrMarkdown?: IChatToolInvocation | IChatToolInvocationSerialized | IChatMarkdownContent, originalParent?: HTMLElement): void { + /** + * Appends a tool invocation or content item to the thinking group. + * The factory is called lazily - only when the thinking section is expanded. + * If already expanded, the factory is called immediately. + */ + public appendItem( + factory: () => { domNode: HTMLElement; disposable?: IDisposable }, + toolInvocationId?: string, + toolInvocationOrMarkdown?: IChatToolInvocation | IChatToolInvocationSerialized | IChatMarkdownContent, + originalParent?: HTMLElement + ): void { + // Track tool invocation metadata immediately (for title generation) + this.trackToolMetadata(toolInvocationId, toolInvocationOrMarkdown); + this.appendedItemCount++; + + // If expanded or has been expanded once, render immediately + if (this.isExpanded() || this.hasExpandedOnce || (this.fixedScrollingMode && !this.streamingCompleted)) { + const result = factory(); + this.appendItemToDOM(result.domNode, toolInvocationId, toolInvocationOrMarkdown, originalParent); + if (result.disposable) { + this._register(result.disposable); + } + } else { + // Defer rendering until expanded + const item: ILazyItem = { + lazy: new Lazy(factory), + toolInvocationId, + toolInvocationOrMarkdown, + originalParent + }; + this.lazyItems.push(item); + } + + this.updateDropdownClickability(); + } + + private trackToolMetadata( + toolInvocationId?: string, + toolInvocationOrMarkdown?: IChatToolInvocation | IChatToolInvocationSerialized | IChatMarkdownContent + ): void { + if (!toolInvocationId) { + return; + } + + this.toolInvocationCount++; + let toolCallLabel: string; + + const isToolInvocation = toolInvocationOrMarkdown && (toolInvocationOrMarkdown.kind === 'toolInvocation' || toolInvocationOrMarkdown.kind === 'toolInvocationSerialized'); + if (isToolInvocation && toolInvocationOrMarkdown.invocationMessage) { + const message = typeof toolInvocationOrMarkdown.invocationMessage === 'string' ? toolInvocationOrMarkdown.invocationMessage : toolInvocationOrMarkdown.invocationMessage.value; + toolCallLabel = message; + + this.toolInvocations.push(toolInvocationOrMarkdown); + } else if (toolInvocationOrMarkdown?.kind === 'markdownContent') { + const codeblockInfo = extractCodeblockUrisFromText(toolInvocationOrMarkdown.content.value); + if (codeblockInfo?.uri) { + const filename = basename(codeblockInfo.uri); + toolCallLabel = localize('chat.thinking.editedFile', 'Edited {0}', filename); + } else { + toolCallLabel = localize('chat.thinking.editingFile', 'Edited file'); + } + } else { + toolCallLabel = `Invoked \`${toolInvocationId}\``; + } + + // Add tool call to extracted titles for LLM title generation + if (!this.extractedTitles.includes(toolCallLabel)) { + this.extractedTitles.push(toolCallLabel); + } + + if (!this.fixedScrollingMode && !this._isExpanded.get()) { + this.setTitle(toolCallLabel); + } + } + + private appendItemToDOM( + content: HTMLElement, + toolInvocationId?: string, + toolInvocationOrMarkdown?: IChatToolInvocation | IChatToolInvocationSerialized | IChatMarkdownContent, + originalParent?: HTMLElement + ): void { if (!content.hasChildNodes() || content.textContent?.trim() === '') { return; } - // save the first item info for potential restoration later - if (this.appendedItemCount === 0 && originalParent) { + // Save the first item info for potential restoration later + if (this.appendedItemCount === 1 && originalParent) { this.singleItemInfo = { element: content, originalParent, @@ -524,8 +628,6 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen this.singleItemInfo = undefined; } - this.appendedItemCount++; - const itemWrapper = $('.chat-thinking-tool-wrapper'); const isMarkdownEdit = toolInvocationOrMarkdown?.kind === 'markdownContent'; const isTerminalTool = toolInvocationOrMarkdown && (toolInvocationOrMarkdown.kind === 'toolInvocation' || toolInvocationOrMarkdown.kind === 'toolInvocationSerialized') && toolInvocationOrMarkdown.toolSpecificData?.kind === 'terminal'; @@ -546,45 +648,27 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen itemWrapper.appendChild(content); this.wrapper.appendChild(itemWrapper); - if (toolInvocationId) { - this.toolInvocationCount++; - let toolCallLabel: string; - const isToolInvocation = toolInvocationOrMarkdown && (toolInvocationOrMarkdown.kind === 'toolInvocation' || toolInvocationOrMarkdown.kind === 'toolInvocationSerialized'); - if (isToolInvocation && toolInvocationOrMarkdown.invocationMessage) { - const message = typeof toolInvocationOrMarkdown.invocationMessage === 'string' ? toolInvocationOrMarkdown.invocationMessage : toolInvocationOrMarkdown.invocationMessage.value; - toolCallLabel = message; - - this.toolInvocations.push(toolInvocationOrMarkdown); - } else if (toolInvocationOrMarkdown?.kind === 'markdownContent') { - const codeblockInfo = extractCodeblockUrisFromText(toolInvocationOrMarkdown.content.value); - if (codeblockInfo?.uri) { - const filename = basename(codeblockInfo.uri); - toolCallLabel = localize('chat.thinking.editedFile', 'Edited {0}', filename); - } else { - toolCallLabel = localize('chat.thinking.editingFile', 'Edited file'); - } - } else { - toolCallLabel = `Invoked \`${toolInvocationId}\``; - } - - // Add tool call to extracted titles for LLM title generation - if (!this.extractedTitles.includes(toolCallLabel)) { - this.extractedTitles.push(toolCallLabel); - } - - if (!this.fixedScrollingMode && !this._isExpanded.get()) { - this.setTitle(toolCallLabel); - } - } if (this.fixedScrollingMode && this.wrapper) { this.wrapper.scrollTop = this.wrapper.scrollHeight; } - this.updateDropdownClickability(); + } + + private materializeLazyItem(item: ILazyItem): void { + if (item.lazy.hasValue) { + return; // Already materialized + } + + const result = item.lazy.value; + this.appendItemToDOM(result.domNode, item.toolInvocationId, item.toolInvocationOrMarkdown, item.originalParent); + + if (result.disposable) { + this._register(result.disposable); + } } // makes a new text container. when we update, we now update this container. - public setupThinkingContainer(content: IChatThinkingPart, context: IChatContentPartRenderContext) { + public setupThinkingContainer(content: IChatThinkingPart) { // Avoid creating new containers after disposal if (this._store.isDisposed) { return; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 76d10acb2ad..db61991dc8b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -96,6 +96,7 @@ import { ChatEditorOptions } from './chatOptions.js'; import { ChatCodeBlockContentProvider, CodeBlockPart } from './chatContentParts/codeBlockPart.js'; import { autorun, observableValue } from '../../../../../base/common/observable.js'; import { RunSubagentTool } from '../../common/tools/builtinTools/runSubagentTool.js'; +import { isEqual } from '../../../../../base/common/resources.js'; const $ = dom.$; @@ -190,6 +191,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer()); readonly onDidChangeItemHeight: Event = this._onDidChangeItemHeight.event; + private readonly _onDidUpdateViewModel = this._register(new Emitter()); + private readonly _editorPool: EditorPool; private readonly _toolEditorPool: EditorPool; private readonly _diffEditorPool: DiffEditorPool; @@ -303,6 +306,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + if (!template.currentElement || !this.viewModel?.sessionResource || !isEqual(template.currentElement.sessionResource, this.viewModel.sessionResource)) { + this.clearRenderedParts(template); + } + })); + templateDisposables.add(dom.addDisposableListener(disabledOverlay, dom.EventType.CLICK, e => { if (!this.viewModel?.editing) { return; @@ -1558,7 +1568,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer other.kind === content.kind && other.id === content.id); } - private renderNoContent(equals: (otherContent: IChatRendererContent) => boolean): IChatContentPart { + private renderNoContent(equals: (other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem) => boolean): IChatContentPart { return { dispose: () => { }, domNode: undefined, @@ -1664,33 +1674,54 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this._currentLayoutWidth.get(), this._toolInvocationCodeBlockCollection, this._announcedToolProgressKeys, codeBlockStartIndex); - part.addDisposable(part.onDidChangeHeight(() => { - this.updateItemHeight(templateData); - })); - this.handleRenderedCodeblocks(context.element, part, codeBlockStartIndex); - const subagentId = toolInvocation.toolId === RunSubagentTool.Id ? toolInvocation.toolCallId : toolInvocation.subAgentInvocationId; + // Factory that creates the tool invocation part with all necessary setup + let lazilyCreatedPart: ChatToolInvocationPart | undefined = undefined; + const createToolPart = (): { domNode: HTMLElement; disposable: ChatToolInvocationPart } => { + lazilyCreatedPart = this.instantiationService.createInstance(ChatToolInvocationPart, toolInvocation, context, this.chatContentMarkdownRenderer, this._contentReferencesListPool, this._toolEditorPool, () => this._currentLayoutWidth.get(), this._toolInvocationCodeBlockCollection, this._announcedToolProgressKeys, codeBlockStartIndex); + lazilyCreatedPart.addDisposable(lazilyCreatedPart.onDidChangeHeight(() => { + this.updateItemHeight(templateData); + })); + this.handleRenderedCodeblocks(context.element, lazilyCreatedPart, codeBlockStartIndex); - // Handle subagent tool grouping - group them together similar to thinking blocks - if (subagentId && isResponseVM(context.element) && part?.domNode && toolInvocation.presentation !== 'hidden') { - return this.handleSubagentToolGrouping(toolInvocation, part, subagentId, context, templateData); - } + // watch for streaming -> confirmation transition to finalize thinking + if (toolInvocation.kind === 'toolInvocation' && IChatToolInvocation.isStreaming(toolInvocation)) { + let wasStreaming = true; + lazilyCreatedPart.addDisposable(autorun(reader => { + const state = toolInvocation.state.read(reader); + if (wasStreaming && state.type !== IChatToolInvocation.StateKind.Streaming) { + wasStreaming = false; + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) { + if (lazilyCreatedPart!.domNode) { + const wrapper = lazilyCreatedPart!.domNode.parentElement; + if (wrapper?.classList.contains('chat-thinking-tool-wrapper')) { + wrapper.remove(); + } + templateData.value.appendChild(lazilyCreatedPart!.domNode); + } + this.finalizeCurrentThinkingPart(context, templateData); + } + } + })); + } + + return { domNode: lazilyCreatedPart.domNode, disposable: lazilyCreatedPart }; + }; // handling for when we want to put tool invocations inside a thinking part const collapsedToolsMode = this.configService.getValue('chat.agent.thinking.collapsedTools'); if (isResponseVM(context.element) && collapsedToolsMode !== CollapsedToolsDisplayMode.Off) { + const lastThinking = this.getLastThinkingPart(templateData.renderedParts); // create thinking part if it doesn't exist yet - const lastThinking = this.getLastThinkingPart(templateData.renderedParts); - if (!lastThinking && part?.domNode && toolInvocation.presentation !== 'hidden' && this.shouldPinPart(toolInvocation, context.element) && collapsedToolsMode === CollapsedToolsDisplayMode.Always) { + if (!lastThinking && toolInvocation.presentation !== 'hidden' && this.shouldPinPart(toolInvocation, context.element) && collapsedToolsMode === CollapsedToolsDisplayMode.Always) { const thinkingPart = this.renderThinkingPart({ kind: 'thinking', }, context, templateData); if (thinkingPart instanceof ChatThinkingContentPart) { - thinkingPart.appendItem(part?.domNode, toolInvocation.toolId, toolInvocation, templateData.value); - thinkingPart.addDisposable(part); + // Append using factory - thinking part decides whether to render lazily + thinkingPart.appendItem(createToolPart, toolInvocation.toolId, toolInvocation, templateData.value); thinkingPart.addDisposable(thinkingPart.onDidChangeHeight(() => { this.updateItemHeight(templateData); })); @@ -1700,36 +1731,28 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer confirmation transition to finalize thinking - if (toolInvocation.kind === 'toolInvocation' && IChatToolInvocation.isStreaming(toolInvocation)) { - let wasStreaming = true; - part.addDisposable(autorun(reader => { - const state = toolInvocation.state.read(reader); - if (wasStreaming && state.type !== IChatToolInvocation.StateKind.Streaming) { - wasStreaming = false; - if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) { - if (part.domNode) { - const wrapper = part.domNode.parentElement; - if (wrapper?.classList.contains('chat-thinking-tool-wrapper')) { - wrapper.remove(); - } - templateData.value.appendChild(part.domNode); - } - this.finalizeCurrentThinkingPart(context, templateData); - } - } - })); - } + if (lastThinking && toolInvocation.presentation !== 'hidden') { + // Append using factory - thinking part decides whether to render lazily + lastThinking.appendItem(createToolPart, toolInvocation.toolId, toolInvocation, templateData.value); + return this.renderNoContent((other, followingContent, element) => lazilyCreatedPart ? + lazilyCreatedPart.hasSameContent(other, followingContent, element) : + toolInvocation.kind === other.kind); } } else { this.finalizeCurrentThinkingPart(context, templateData); } } + // For cases not handled above (subagent grouping, no thinking part, etc.), create the part now + const { domNode, disposable: part } = createToolPart(); + + const subagentId = toolInvocation.toolId === RunSubagentTool.Id ? toolInvocation.toolCallId : toolInvocation.subAgentInvocationId; + + // Handle subagent tool grouping - group them together similar to thinking blocks + if (subagentId && isResponseVM(context.element) && domNode && toolInvocation.presentation !== 'hidden') { + return this.handleSubagentToolGrouping(toolInvocation, part, subagentId, context, templateData); + } + return part; } @@ -1874,8 +1897,14 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + // Factory wrapping already-created markdown part + thinkingPart.appendItem( + () => ({ domNode: markdownPart.domNode, disposable: markdownPart }), + markdownPart.codeblocksPartId, + markdown, + templateData.value + ); + thinkingPart.addDisposable(thinkingPart.onDidChangeHeight(() => { this.updateItemHeight(templateData); })); } @@ -1885,7 +1914,13 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer ({ domNode: markdownPart.domNode, disposable: markdownPart }), + markdownPart.codeblocksPartId, + markdown, + templateData.value + ); } } else if (!this.shouldPinPart(markdown, context.element) && !isFinalAnswerPart) { this.finalizeCurrentThinkingPart(context, templateData); @@ -1913,10 +1948,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.updateItemHeight(templateData))); lastPart = itemPart; } @@ -1927,10 +1962,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.updateItemHeight(templateData))); return part; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 9aa2abdfe71..441002acd4d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -2010,6 +2010,7 @@ export class ChatWidget extends Disposable implements IChatWidget { // Pass input model reference to input part for state syncing this.inputPart.setInputModel(model.inputModel, model.getRequests().length === 0); + this.renderer.updateViewModel(this.viewModel); if (this._lockedAgent) { let placeholder = this.chatSessionsService.getInputPlaceholderForSessionType(this._lockedAgent.id); @@ -2088,7 +2089,6 @@ export class ChatWidget extends Disposable implements IChatWidget { this.scrollToEnd(); } - this.renderer.updateViewModel(this.viewModel); this.updateChatInputContext(); this.input.renderChatTodoListWidget(this.viewModel.sessionResource); } From 7710bbee04c89f2db8afaceba9fa3d6144d07ee3 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sat, 17 Jan 2026 21:07:48 -0800 Subject: [PATCH 159/387] Adjust chat-current-response-min-height behavior (#288674) Only show the request/response, don't show any of the previous response. This looks cleaner, part of #274099. Also, measuring from the top means that we don't shift the response up and down when expanding the list of edited files (if the response is shorter than the minimum) --- .../contrib/chat/browser/widget/chatWidget.ts | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 441002acd4d..d8d90edf38d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -1570,6 +1570,12 @@ export class ChatWidget extends Disposable implements IChatWidget { if (this.tree.hasElement(e.element) && this.visible) { this.tree.updateElementHeight(e.element, e.height); } + + // If the second-to-last item's height changed, update the last item's min height + const secondToLastItem = this.viewModel?.getItems().at(-2); + if (e.element.id === secondToLastItem?.id) { + this.updateLastItemMinHeight(); + } })); this._register(this.tree.onDidFocus(() => { this._onDidFocus.fire(); @@ -2418,10 +2424,30 @@ export class ChatWidget extends Disposable implements IChatWidget { this.tree.domFocus(); } + private previousLastItemMinHeight: number = 0; + + private updateLastItemMinHeight(): void { + const contentHeight = this.bodyDimension ? Math.max(0, this.bodyDimension.height - this.inputPart.inputPartHeight.get() - this.chatSuggestNextWidget.height) : 0; + if (this.viewOptions.renderStyle === 'compact' || this.viewOptions.renderStyle === 'minimal') { + this.listContainer.style.removeProperty('--chat-current-response-min-height'); + } else { + const secondToLastItem = this.viewModel?.getItems().at(-2); + const secondToLastItemHeight = Math.min(secondToLastItem?.currentRenderedHeight ?? 150, 150); + const lastItemMinHeight = Math.max(contentHeight - (secondToLastItemHeight + 10), 0); + this.listContainer.style.setProperty('--chat-current-response-min-height', lastItemMinHeight + 'px'); + if (lastItemMinHeight !== this.previousLastItemMinHeight) { + this.previousLastItemMinHeight = lastItemMinHeight; + const lastItem = this.viewModel?.getItems().at(-1); + if (lastItem && this.visible && this.tree.hasElement(lastItem)) { + this.tree.updateElementHeight(lastItem, undefined); + } + } + } + } + layout(height: number, width: number): void { width = Math.min(width, this.viewOptions.renderStyle === 'minimal' ? width : 950); // no min width of inline chat - const heightUpdated = this.bodyDimension && this.bodyDimension.height !== height; this.bodyDimension = new dom.Dimension(width, height); if (this.viewModel?.editing) { @@ -2436,14 +2462,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const lastItem = this.viewModel?.getItems().at(-1); const contentHeight = Math.max(0, height - inputHeight - chatSuggestNextWidgetHeight); - if (this.viewOptions.renderStyle === 'compact' || this.viewOptions.renderStyle === 'minimal') { - this.listContainer.style.removeProperty('--chat-current-response-min-height'); - } else { - this.listContainer.style.setProperty('--chat-current-response-min-height', contentHeight * .75 + 'px'); - if (heightUpdated && lastItem && this.visible && this.tree.hasElement(lastItem)) { - this.tree.updateElementHeight(lastItem, undefined); - } - } + this.updateLastItemMinHeight(); this.tree.layout(contentHeight, width); this.welcomeMessageContainer.style.height = `${contentHeight}px`; From 2ffa47e52b5e33d0ba47c77994dffed657ae6c77 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sat, 17 Jan 2026 21:08:13 -0800 Subject: [PATCH 160/387] Fix error in chatwidget setup (#288680) * Fix error in chatwidget setup Leftover from #288653 * Also remove this unneeded layout call that runs too often --- .../workbench/contrib/chat/browser/widget/chatWidget.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index d8d90edf38d..09879aec3a6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -1217,10 +1217,6 @@ export class ChatWidget extends Disposable implements IChatWidget { } else { this.input.renderFollowups(undefined, undefined); } - - if (this.bodyDimension) { - this.layout(this.bodyDimension.height, this.bodyDimension.width); - } } private renderChatSuggestNextWidget(): void { @@ -1929,6 +1925,10 @@ export class ChatWidget extends Disposable implements IChatWidget { })); this._register(autorun(reader => { this.input.inputPartHeight.read(reader); + if (!this.renderer) { + // This is set up before the list/renderer are created + return; + } const editedRequest = this.renderer.getTemplateDataForRequestId(this.viewModel?.editing?.id); if (isRequestVM(editedRequest?.currentElement) && this.viewModel?.editing) { From 97b81ef02327ac993823cbb7cb6b4c1db9b33091 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 06:04:12 +0000 Subject: [PATCH 161/387] Agent sessions: support multi-select for mark read/unread and archive/unarchive (#288449) * Initial plan * Add multi-select support for agent session mark read/unread and archive/unarchive operations Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> * polish --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> Co-authored-by: Benjamin Pasero Co-authored-by: Benjamin Pasero --- .../agentSessions/agentSessionsActions.ts | 121 ++++++++++++------ .../agentSessions/agentSessionsControl.ts | 21 ++- .../agentSessions/agentSessionsModel.ts | 4 +- .../chat/common/actions/chatContextKeys.ts | 1 + 4 files changed, 103 insertions(+), 44 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts index 1510faf82ec..5154b6dcd23 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts @@ -19,7 +19,7 @@ import { getPartByLocation } from '../../../../services/views/browser/viewsServi import { IWorkbenchLayoutService, Position } from '../../../../services/layout/browser/layoutService.js'; import { IAgentSessionsService } from './agentSessionsService.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; -import { ChatEditorInput, showClearEditingSessionConfirmation } from '../widgetHosts/editor/chatEditorInput.js'; +import { ChatEditorInput, shouldShowClearEditingSessionConfirmation } from '../widgetHosts/editor/chatEditorInput.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ChatConfiguration } from '../../common/constants.js'; @@ -33,6 +33,7 @@ import { ActiveEditorContext, AuxiliaryBarMaximizedContext } from '../../../../c import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; +import { coalesce } from '../../../../../base/common/arrays.js'; //#region Chat View @@ -381,24 +382,27 @@ abstract class BaseAgentSessionAction extends Action2 { const agentSessionsService = accessor.get(IAgentSessionsService); const viewsService = accessor.get(IViewsService); - let session: IAgentSession | undefined; + let sessions: IAgentSession[] = []; if (isMarshalledAgentSessionContext(context)) { - session = agentSessionsService.getSession(context.session.resource); - } else { - session = context; + sessions = coalesce((context.sessions ?? [context.session]).map(session => agentSessionsService.getSession(session.resource))); + } else if (context) { + sessions = [context]; } - if (!session) { + if (sessions.length === 0) { const chatView = viewsService.getActiveViewWithId(ChatViewId); - session = chatView?.getFocusedSessions().at(0); + const focused = chatView?.getFocusedSessions().at(0); + if (focused) { + sessions = [focused]; + } } - if (session) { - await this.runWithSession(session, accessor); + if (sessions.length > 0) { + await this.runWithSessions(sessions, accessor); } } - abstract runWithSession(session: IAgentSession, accessor: ServicesAccessor): Promise | void; + abstract runWithSessions(sessions: IAgentSession[], accessor: ServicesAccessor): Promise | void; } export class MarkAgentSessionUnreadAction extends BaseAgentSessionAction { @@ -419,8 +423,10 @@ export class MarkAgentSessionUnreadAction extends BaseAgentSessionAction { }); } - runWithSession(session: IAgentSession): void { - session.setRead(false); + runWithSessions(sessions: IAgentSession[]): void { + for (const session of sessions) { + session.setRead(false); + } } } @@ -442,8 +448,10 @@ export class MarkAgentSessionReadAction extends BaseAgentSessionAction { }); } - runWithSession(session: IAgentSession): void { - session.setRead(true); + runWithSessions(sessions: IAgentSession[]): void { + for (const session of sessions) { + session.setRead(true); + } } } @@ -477,20 +485,37 @@ export class ArchiveAgentSessionAction extends BaseAgentSessionAction { }); } - async runWithSession(session: IAgentSession, accessor: ServicesAccessor): Promise { + async runWithSessions(sessions: IAgentSession[], accessor: ServicesAccessor): Promise { const chatService = accessor.get(IChatService); - const chatModel = chatService.getSession(session.resource); const dialogService = accessor.get(IDialogService); - if (chatModel && !await showClearEditingSessionConfirmation(chatModel, dialogService, { - isArchiveAction: true, - titleOverride: localize('archiveSession', "Archive chat with pending edits?"), - messageOverride: localize('archiveSessionDescription', "You have pending changes in this chat session.") - })) { - return; + // Count sessions with pending changes + let sessionsWithPendingChangesCount = 0; + for (const session of sessions) { + const chatModel = chatService.getSession(session.resource); + if (chatModel && shouldShowClearEditingSessionConfirmation(chatModel, { isArchiveAction: true })) { + sessionsWithPendingChangesCount++; + } } - session.setArchived(true); + // If there are sessions with pending changes, ask for confirmation once + if (sessionsWithPendingChangesCount > 0) { + const confirmed = await dialogService.confirm({ + message: sessionsWithPendingChangesCount === 1 + ? localize('archiveSessionWithPendingEdits', "One session has pending edits. Are you sure you want to archive?") + : localize('archiveSessionsWithPendingEdits', "{0} sessions have pending edits. Are you sure you want to archive?", sessionsWithPendingChangesCount), + primaryButton: localize('archiveSession.archive', "Archive") + }); + + if (!confirmed.confirmed) { + return; + } + } + + // Archive all sessions + for (const session of sessions) { + session.setArchived(true); + } } } @@ -526,8 +551,10 @@ export class UnarchiveAgentSessionAction extends BaseAgentSessionAction { }); } - runWithSession(session: IAgentSession): void { - session.setArchived(false); + runWithSessions(sessions: IAgentSession[]): void { + for (const session of sessions) { + session.setArchived(false); + } } } @@ -537,6 +564,7 @@ export class RenameAgentSessionAction extends BaseAgentSessionAction { super({ id: AGENT_SESSION_RENAME_ACTION_ID, title: localize2('rename', "Rename..."), + precondition: ChatContextKeys.hasMultipleAgentSessionsSelected.negate(), keybinding: { primary: KeyCode.F2, mac: { @@ -557,7 +585,12 @@ export class RenameAgentSessionAction extends BaseAgentSessionAction { }); } - async runWithSession(session: IAgentSession, accessor: ServicesAccessor): Promise { + async runWithSessions(sessions: IAgentSession[], accessor: ServicesAccessor): Promise { + const session = sessions.at(0); + if (!session) { + return; + } + const quickInputService = accessor.get(IQuickInputService); const chatService = accessor.get(IChatService); @@ -583,13 +616,19 @@ export class DeleteAgentSessionAction extends BaseAgentSessionAction { }); } - async runWithSession(session: IAgentSession, accessor: ServicesAccessor): Promise { + async runWithSessions(sessions: IAgentSession[], accessor: ServicesAccessor): Promise { + if (sessions.length === 0) { + return; + } + const chatService = accessor.get(IChatService); const dialogService = accessor.get(IDialogService); const widgetService = accessor.get(IChatWidgetService); const confirmed = await dialogService.confirm({ - message: localize('deleteSession.confirm', "Are you sure you want to delete this chat session?"), + message: sessions.length === 1 + ? localize('deleteSession.confirm', "Are you sure you want to delete this chat session?") + : localize('deleteSessions.confirm', "Are you sure you want to delete {0} chat sessions?", sessions.length), detail: localize('deleteSession.detail', "This action cannot be undone."), primaryButton: localize('deleteSession.delete', "Delete") }); @@ -598,11 +637,14 @@ export class DeleteAgentSessionAction extends BaseAgentSessionAction { return; } - // Clear chat widget - await widgetService.getWidgetBySessionResource(session.resource)?.clear(); + for (const session of sessions) { - // Remove from storage - await chatService.removeHistoryEntry(session.resource); + // Clear chat widget + await widgetService.getWidgetBySessionResource(session.resource)?.clear(); + + // Remove from storage + await chatService.removeHistoryEntry(session.resource); + } } } @@ -651,15 +693,18 @@ export class DeleteAllLocalSessionsAction extends Action2 { abstract class BaseOpenAgentSessionAction extends BaseAgentSessionAction { - async runWithSession(session: IAgentSession, accessor: ServicesAccessor): Promise { + async runWithSessions(sessions: IAgentSession[], accessor: ServicesAccessor): Promise { const chatWidgetService = accessor.get(IChatWidgetService); - const uri = session.resource; + const targetGroup = this.getTargetGroup(); + for (const session of sessions) { + const uri = session.resource; - await chatWidgetService.openSession(uri, this.getTargetGroup(), { - ...this.getOptions(), - pinned: true - }); + await chatWidgetService.openSession(uri, targetGroup, { + ...this.getOptions(), + pinned: true + }); + } } protected abstract getTargetGroup(): PreferredGroup; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index f99a9b8de6c..3ca1f18b287 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -70,6 +70,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo private focusedAgentSessionArchivedContextKey: IContextKey; private focusedAgentSessionReadContextKey: IContextKey; private focusedAgentSessionTypeContextKey: IContextKey; + private hasMultipleAgentSessionsSelectedContextKey: IContextKey; constructor( private readonly container: HTMLElement, @@ -89,6 +90,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo this.focusedAgentSessionArchivedContextKey = ChatContextKeys.isArchivedAgentSession.bindTo(this.contextKeyService); this.focusedAgentSessionReadContextKey = ChatContextKeys.isReadAgentSession.bindTo(this.contextKeyService); this.focusedAgentSessionTypeContextKey = ChatContextKeys.agentSessionType.bindTo(this.contextKeyService); + this.hasMultipleAgentSessionsSelectedContextKey = ChatContextKeys.hasMultipleAgentSessionsSelected.bindTo(this.contextKeyService); this.createList(this.container); @@ -143,7 +145,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo dnd: this.instantiationService.createInstance(AgentSessionsDragAndDrop), identityProvider: new AgentSessionsIdentityProvider(), horizontalScrolling: false, - multipleSelectionSupport: false, + multipleSelectionSupport: true, findWidgetEnabled: true, defaultFindMode: TreeFindMode.Filter, keyboardNavigationLabelProvider: new AgentSessionsKeyboardNavigationLabelProvider(), @@ -183,7 +185,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo } })); - this._register(Event.any(list.onDidChangeFocus, model.onDidChangeSessions)(() => { + this._register(Event.any(list.onDidChangeFocus, list.onDidChangeSelection, model.onDidChangeSessions)(() => { const focused = list.getFocus().at(0); if (focused && isAgentSession(focused)) { this.focusedAgentSessionArchivedContextKey.set(focused.isArchived()); @@ -194,6 +196,9 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo this.focusedAgentSessionReadContextKey.reset(); this.focusedAgentSessionTypeContextKey.reset(); } + + const selection = list.getSelection().filter(isAgentSession); + this.hasMultipleAgentSessionsSelectedContextKey.set(selection.length > 1); })); } @@ -250,11 +255,17 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo const menu = this.menuService.createMenu(MenuId.AgentSessionsContext, this.contextKeyService.createOverlay(contextOverlay)); - const marshalledSession: IMarshalledAgentSessionContext = { session, $mid: MarshalledId.AgentSessionContext }; + const selection = this.sessionsList?.getSelection().filter(isAgentSession) ?? []; + const marshalledContext: IMarshalledAgentSessionContext = { + session, + sessions: selection.length > 1 && selection.includes(session) ? selection : [session], + $mid: MarshalledId.AgentSessionContext + }; + this.contextMenuService.showContextMenu({ - getActions: () => Separator.join(...menu.getActions({ arg: marshalledSession, shouldForwardArgs: true }).map(([, actions]) => actions)), + getActions: () => Separator.join(...menu.getActions({ arg: marshalledContext, shouldForwardArgs: true }).map(([, actions]) => actions)), getAnchor: () => anchor, - getActionsContext: () => marshalledSession, + getActionsContext: () => marshalledContext, }); menu.dispose(); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index 6196447cb8d..29b7f2d8714 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -165,7 +165,9 @@ export function isAgentSessionSection(obj: unknown): obj is IAgentSessionSection export interface IMarshalledAgentSessionContext { readonly $mid: MarshalledId.AgentSessionContext; + readonly session: IAgentSession; + readonly sessions: IAgentSession[]; // support for multi-selection } export function isMarshalledAgentSessionContext(thing: unknown): thing is IMarshalledAgentSessionContext { @@ -370,7 +372,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode providerType: chatSessionType, providerLabel, resource: session.resource, - label: session.label, + label: session.label.split('\n')[0], // protect against weird multi-line labels that break our layout description: session.description, icon, badge: session.badge, diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index 736f3689420..7da745d17d2 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -104,6 +104,7 @@ export namespace ChatContextKeys { export const agentSessionSection = new RawContextKey('agentSessionSection', '', { type: 'string', description: localize('agentSessionSection', "The section of the current agent session section item.") }); export const isArchivedAgentSession = new RawContextKey('agentSessionIsArchived', false, { type: 'boolean', description: localize('agentSessionIsArchived', "True when the agent session item is archived.") }); export const isReadAgentSession = new RawContextKey('agentSessionIsRead', false, { type: 'boolean', description: localize('agentSessionIsRead', "True when the agent session item is read.") }); + export const hasMultipleAgentSessionsSelected = new RawContextKey('agentSessionHasMultipleSelected', false, { type: 'boolean', description: localize('agentSessionHasMultipleSelected', "True when multiple agent sessions are selected.") }); export const hasAgentSessionChanges = new RawContextKey('agentSessionHasChanges', false, { type: 'boolean', description: localize('agentSessionHasChanges', "True when the current agent session item has changes.") }); export const isKatexMathElement = new RawContextKey('chatIsKatexMathElement', false, { type: 'boolean', description: localize('chatIsKatexMathElement', "True when focusing a KaTeX math element.") }); From a8aad7d100ef59e6b2109ab166b14cdc745895dd Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 18 Jan 2026 09:05:08 +0100 Subject: [PATCH 162/387] chat - better alignment for chat input (#288690) --- src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index b8339e70596..2e46db95e90 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1566,7 +1566,7 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .interactive-input-part { margin: 0px 16px; - padding: 4px 0 12px 0px; + padding: 4px 0 8px 0px; display: flex; flex-direction: column; gap: 4px; From 434f92ed0a0720813a90dc9a8198edf3efa3889d Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Sun, 18 Jan 2026 14:53:30 +0100 Subject: [PATCH 163/387] Chat - fix working set secondary text button height (#288704) --- src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 2e46db95e90..7fb771d460d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1020,14 +1020,13 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .chat-editing-session .chat-editing-session-actions .monaco-button.secondary.monaco-text-button.codicon { cursor: pointer; - padding: 3px; + padding: 2px; border-radius: 4px; display: inline-flex; } .interactive-session .chat-editing-session .chat-editing-session-actions .monaco-button.secondary.monaco-text-button { background-color: var(--vscode-button-secondaryBackground); - border: 1px solid var(--vscode-button-border); color: var(--vscode-button-secondaryForeground); } From df2c07422b4ef91f910ca280a2667cdc7eaf0726 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Sun, 18 Jan 2026 14:54:13 +0100 Subject: [PATCH 164/387] fix skipping models from other vendors (#288713) --- src/vs/workbench/contrib/chat/common/languageModels.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index c3d0baa61ad..6127745702a 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -553,8 +553,14 @@ export class LanguageModelsService implements ILanguageModelsService { allModels.push(...models); const modelIdentifiers = []; for (const m of models) { - // Special case for copilot models - they are all user selectable unless marked otherwise - if (vendorId === 'copilot' && (m.metadata.isUserSelectable || this._modelPickerUserPreferences[m.identifier] === true)) { + if (vendorId === 'copilot') { + // Special case for copilot models - they are all user selectable unless marked otherwise + if (m.metadata.isUserSelectable || this._modelPickerUserPreferences[m.identifier] === true) { + modelIdentifiers.push(m.identifier); + } else { + this._logService.trace(`[LM] Skipping model ${m.identifier} from model picker as it is not user selectable.`); + } + } else { modelIdentifiers.push(m.identifier); } } From 61644682e92a61a70011ce6cf7fc60e59941982b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 17:09:46 +0000 Subject: [PATCH 165/387] Fix extension-specific default log levels not being applied (#287729) * Initial plan * Fix default log level not applied to extensions correctly The issue was in extHostOutput.ts where getLogLevel() always returns a value (never undefined) because it falls back to the global default. This was overriding the extension-specific default log level. Changed to only override if there's an explicitly set log level for that logger resource using getRegisteredLogger() which returns the ILoggerResource with logLevel as undefined when using default. Co-authored-by: ulugbekna <16353531+ulugbekna@users.noreply.github.com> * Improve comment clarity based on code review feedback Co-authored-by: ulugbekna <16353531+ulugbekna@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ulugbekna <16353531+ulugbekna@users.noreply.github.com> --- src/vs/workbench/api/common/extHostOutput.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHostOutput.ts b/src/vs/workbench/api/common/extHostOutput.ts index b9520f3c2ab..1c5a4b91585 100644 --- a/src/vs/workbench/api/common/extHostOutput.ts +++ b/src/vs/workbench/api/common/extHostOutput.ts @@ -152,7 +152,12 @@ export class ExtHostOutputService implements ExtHostOutputServiceShape { if (existingOutputChannel) { return existingOutputChannel; } - logLevel = this.loggerService.getLogLevel(logFile) ?? logLevel; + // Only override the extension-specific default log level if the user has explicitly configured a level for this logger. + // Note: registeredLogger.logLevel is undefined when using defaults, and a LogLevel value when explicitly set by the user. + const registeredLogger = this.loggerService.getRegisteredLogger(logFile); + if (registeredLogger?.logLevel !== undefined) { + logLevel = registeredLogger.logLevel; + } extHostOutputChannelPromise = this.doCreateLogOutputChannel(name, logFile, logLevel, extension, channelDisposables); } else { extHostOutputChannelPromise = this.doCreateOutputChannel(name, languageId, extension, channelDisposables); From 7b5ef178cd10d751f57e1a7c561ace4a7f5ae6ce Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 17:35:57 +0000 Subject: [PATCH 166/387] Reveal setting in JSON editor when query looks like a setting key (#288427) * Initial plan * Fix: convert query to revealSetting when opening JSON settings editor Co-authored-by: sandy081 <10746682+sandy081@users.noreply.github.com> * Check for @id: prefix before using query as setting key Co-authored-by: sandy081 <10746682+sandy081@users.noreply.github.com> * Remove build-output.log and add to gitignore Co-authored-by: sandy081 <10746682+sandy081@users.noreply.github.com> * clean up --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sandy081 <10746682+sandy081@users.noreply.github.com> Co-authored-by: Sandeep Somavarapu --- .../preferences/browser/preferencesService.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index 3d422ed4348..93914590d96 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -248,6 +248,21 @@ export class PreferencesService extends Disposable implements IPreferencesServic jsonEditor: options.jsonEditor ?? this.shouldOpenJsonByDefault() }; + if (options.jsonEditor && options.query && !options.revealSetting) { + const query = options.query.trim(); + const idMatch = query.match(/^@id:(.+)$/); + let key: string | undefined; + if (idMatch) { + key = idMatch[1]; + } else if (Registry.as(Extensions.Configuration).getConfigurationProperties()[query]) { + key = query; + } + options.query = undefined; + if (key) { + options.revealSetting = { key }; + } + } + return options.jsonEditor ? this.openSettingsJson(settingsResource, options) : this.openSettings2(options); From 6b921dda5c679c6ca1928a4505567c533ede72f9 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Sun, 18 Jan 2026 12:50:21 -0800 Subject: [PATCH 167/387] remove todo item description field and make it readonly (#288760) --- .../chatResponseAccessibleView.ts | 2 +- .../contrib/chat/browser/chat.contribution.ts | 18 --- .../chatContentParts/chatTodoListWidget.ts | 23 +-- .../chat/common/chatService/chatService.ts | 1 - .../tools/builtinTools/manageTodoListTool.ts | 136 +++++------------- .../chat/common/tools/builtinTools/tools.ts | 11 +- .../chat/common/tools/chatTodoListService.ts | 1 - .../chatResponseAccessibleView.test.ts | 4 +- .../chatTodoListWidget.test.ts | 12 +- .../builtinTools/manageTodoListTool.test.ts | 49 ++++--- 10 files changed, 80 insertions(+), 177 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts index d7cd83cd4bf..3de7e243cb1 100644 --- a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts @@ -92,7 +92,7 @@ export function getToolSpecificDataDescription(toolSpecificData: ToolSpecificDat return ''; } const todoDescriptions = todos.map(t => - localize('todoItem', "{0} ({1}): {2}", t.title, t.status, t.description) + localize('todoItem', "{0} ({1})", t.title, t.status) ); return localize('todoListCount', "{0} items: {1}", todos.length, todoDescriptions.join('; ')); } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 511abdebc43..91a886107c5 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -804,24 +804,6 @@ configurationRegistry.registerConfiguration({ mode: 'auto' } }, - 'chat.todoListTool.writeOnly': { - type: 'boolean', - default: false, - description: nls.localize('chat.todoListTool.writeOnly', "When enabled, the todo tool operates in write-only mode, requiring the agent to remember todos in context."), - tags: ['experimental'], - experiment: { - mode: 'auto' - } - }, - 'chat.todoListTool.descriptionField': { - type: 'boolean', - default: true, - description: nls.localize('chat.todoListTool.descriptionField', "When enabled, todo items include detailed descriptions for implementation context. This provides more information but uses additional tokens and may slow down responses."), - tags: ['experimental'], - experiment: { - mode: 'auto' - } - }, [ChatConfiguration.ThinkingStyle]: { type: 'string', default: 'collapsedPreview', diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTodoListWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTodoListWidget.ts index 7a5b9912b84..57585c981b5 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTodoListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTodoListWidget.ts @@ -11,13 +11,11 @@ import { Codicon } from '../../../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { localize } from '../../../../../../nls.js'; -import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { WorkbenchList } from '../../../../../../platform/list/browser/listService.js'; import { IChatTodoListService, IChatTodo } from '../../../common/tools/chatTodoListService.js'; import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; -import { TodoListToolDescriptionFieldSettingId } from '../../../common/tools/builtinTools/manageTodoListTool.js'; import { URI } from '../../../../../../base/common/uri.js'; import { isEqual } from '../../../../../../base/common/resources.js'; @@ -42,10 +40,6 @@ class TodoListRenderer implements IListRenderer { static TEMPLATE_ID = 'todoListRenderer'; readonly templateId: string = TodoListRenderer.TEMPLATE_ID; - constructor( - private readonly configurationService: IConfigurationService - ) { } - renderTemplate(container: HTMLElement): ITodoListTemplate { const templateDisposables = new DisposableStore(); const todoElement = dom.append(container, dom.$('li.todo-item')); @@ -67,16 +61,11 @@ class TodoListRenderer implements IListRenderer { statusIcon.className = `todo-status-icon codicon ${this.getStatusIconClass(todo.status)}`; statusIcon.style.color = this.getStatusIconColor(todo.status); - // Update title with tooltip if description exists and description field is enabled - const includeDescription = this.configurationService.getValue(TodoListToolDescriptionFieldSettingId) !== false; - const title = includeDescription && todo.description && todo.description.trim() ? todo.description : undefined; - iconLabel.setLabel(todo.title, undefined, { title }); + iconLabel.setLabel(todo.title); // Update aria-label const statusText = this.getStatusText(todo.status); - const ariaLabel = includeDescription && todo.description && todo.description.trim() - ? localize('chat.todoList.itemWithDescription', '{0}, {1}, {2}', todo.title, statusText, todo.description) - : localize('chat.todoList.item', '{0}, {1}', todo.title, statusText); + const ariaLabel = localize('chat.todoList.item', '{0}, {1}', todo.title, statusText); todoElement.setAttribute('aria-label', ariaLabel); } @@ -140,7 +129,6 @@ export class ChatTodoListWidget extends Disposable { constructor( @IChatTodoListService private readonly chatTodoListService: IChatTodoListService, - @IConfigurationService private readonly configurationService: IConfigurationService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService ) { @@ -287,16 +275,13 @@ export class ChatTodoListWidget extends Disposable { 'ChatTodoListRenderer', this.todoListContainer, new TodoListDelegate(), - [new TodoListRenderer(this.configurationService)], + [new TodoListRenderer()], { alwaysConsumeMouseWheel: false, accessibilityProvider: { getAriaLabel: (todo: IChatTodo) => { const statusText = this.getStatusText(todo.status); - const includeDescription = this.configurationService.getValue(TodoListToolDescriptionFieldSettingId) !== false; - return includeDescription && todo.description && todo.description.trim() - ? localize('chat.todoList.itemWithDescription', '{0}, {1}, {2}', todo.title, statusText, todo.description) - : localize('chat.todoList.item', '{0}, {1}', todo.title, statusText); + return localize('chat.todoList.item', '{0}, {1}', todo.title, statusText); }, getWidgetAriaLabel: () => localize('chatTodoList', 'Chat Todo List') } diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index cc22f71b3f3..92041da7586 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -777,7 +777,6 @@ export interface IChatTodoListContent { todoList: Array<{ id: string; title: string; - description: string; status: 'not-started' | 'in-progress' | 'completed'; }>; } diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts index 4196cc92735..88638500cd3 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts @@ -6,6 +6,7 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; +import { IJSONSchema, IJSONSchemaMap } from '../../../../../../base/common/jsonSchema.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { IToolData, @@ -24,60 +25,39 @@ import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { URI } from '../../../../../../base/common/uri.js'; import { chatSessionResourceToId, LocalChatSessionUri } from '../../model/chatUri.js'; -export const TodoListToolWriteOnlySettingId = 'chat.todoListTool.writeOnly'; -export const TodoListToolDescriptionFieldSettingId = 'chat.todoListTool.descriptionField'; - export const ManageTodoListToolToolId = 'manage_todo_list'; -export function createManageTodoListToolData(writeOnly: boolean, includeDescription: boolean = true): IToolData { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const baseProperties: any = { - todoList: { - type: 'array', - description: writeOnly - ? 'Complete array of all todo items. Must include ALL items - both existing and new.' - : 'Complete array of all todo items (required for write operation, ignored for read). Must include ALL items - both existing and new.', - items: { - type: 'object', - properties: { - id: { - type: 'number', - description: 'Unique identifier for the todo. Use sequential numbers starting from 1.' - }, - title: { - type: 'string', - description: 'Concise action-oriented todo label (3-7 words). Displayed in UI.' - }, - ...(includeDescription && { - description: { +export function createManageTodoListToolData(): IToolData { + const inputSchema: IJSONSchema & { properties: IJSONSchemaMap } = { + type: 'object', + properties: { + todoList: { + type: 'array', + description: 'Complete array of all todo items. Must include ALL items - both existing and new.', + items: { + type: 'object', + properties: { + id: { + type: 'number', + description: 'Unique identifier for the todo. Use sequential numbers starting from 1.' + }, + title: { type: 'string', - description: 'Detailed context, requirements, or implementation notes. Include file paths, specific methods, or acceptance criteria.' - } - }), - status: { - type: 'string', - enum: ['not-started', 'in-progress', 'completed'], - description: 'not-started: Not begun | in-progress: Currently working (max 1) | completed: Fully finished with no blockers' + description: 'Concise action-oriented todo label (3-7 words). Displayed in UI.' + }, + status: { + type: 'string', + enum: ['not-started', 'in-progress', 'completed'], + description: 'not-started: Not begun | in-progress: Currently working (max 1) | completed: Fully finished with no blockers' + }, }, - }, - required: includeDescription ? ['id', 'title', 'description', 'status'] : ['id', 'title', 'status'] + required: ['id', 'title', 'status'] + } } - } + }, + required: ['todoList'] }; - // Only require the full todoList when operating in write-only mode. - // In read/write mode, the write path validates todoList at runtime, so it's not schema-required. - const requiredFields = writeOnly ? ['todoList'] : [] as string[]; - - if (!writeOnly) { - baseProperties.operation = { - type: 'string', - enum: ['write', 'read'], - description: 'write: Replace entire todo list with new content. read: Retrieve current todo list. ALWAYS provide complete list when writing - partial updates not supported.' - }; - requiredFields.unshift('operation'); - } - return { id: ManageTodoListToolToolId, toolReferenceName: 'todo', @@ -88,22 +68,17 @@ export function createManageTodoListToolData(writeOnly: boolean, includeDescript userDescription: localize('tool.manageTodoList.userDescription', 'Manage and track todo items for task planning'), modelDescription: 'Manage a structured todo list to track progress and plan tasks throughout your coding session. Use this tool VERY frequently to ensure task visibility and proper planning.\n\nWhen to use this tool:\n- Complex multi-step work requiring planning and tracking\n- When user provides multiple tasks or requests (numbered/comma-separated)\n- After receiving new instructions that require multiple steps\n- BEFORE starting work on any todo (mark as in-progress)\n- IMMEDIATELY after completing each todo (mark completed individually)\n- When breaking down larger tasks into smaller actionable steps\n- To give users visibility into your progress and planning\n\nWhen NOT to use:\n- Single, trivial tasks that can be completed in one step\n- Purely conversational/informational requests\n- When just reading files or performing simple searches\n\nCRITICAL workflow:\n1. Plan tasks by writing todo list with specific, actionable items\n2. Mark ONE todo as in-progress before starting work\n3. Complete the work for that specific todo\n4. Mark that todo as completed IMMEDIATELY\n5. Move to next todo and repeat\n\nTodo states:\n- not-started: Todo not yet begun\n- in-progress: Currently working (limit ONE at a time)\n- completed: Finished successfully\n\nIMPORTANT: Mark todos completed as soon as they are done. Do not batch completions.', source: ToolDataSource.Internal, - inputSchema: { - type: 'object', - properties: baseProperties, - required: requiredFields - } + inputSchema: inputSchema }; } -export const ManageTodoListToolData: IToolData = createManageTodoListToolData(false); +export const ManageTodoListToolData: IToolData = createManageTodoListToolData(); interface IManageTodoListToolInputParams { - operation?: 'write' | 'read'; // Optional in write-only mode + operation?: 'write' | 'read'; // Optional, defaults to 'write' todoList: Array<{ id: number; title: string; - description?: string; status: 'not-started' | 'in-progress' | 'completed'; }>; chatSessionId?: string; @@ -112,8 +87,6 @@ interface IManageTodoListToolInputParams { export class ManageTodoListTool extends Disposable implements IToolImpl { constructor( - private readonly writeOnly: boolean, - private readonly includeDescription: boolean, @IChatTodoListService private readonly chatTodoListService: IChatTodoListService, @ILogService private readonly logService: ILogService, @ITelemetryService private readonly telemetryService: ITelemetryService @@ -131,29 +104,10 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { this.logService.debug(`ManageTodoListTool: Invoking with options ${JSON.stringify(args)}`); try { - // Determine operation: in writeOnly mode, always write; otherwise use args.operation - const operation = this.writeOnly ? 'write' : args.operation; - - if (!operation) { - return { - content: [{ - kind: 'text', - value: 'Error: operation parameter is required' - }] - }; - } - - if (operation === 'read') { + if (args.operation === 'read') { return this.handleReadOperation(LocalChatSessionUri.forSession(chatSessionId)); - } else if (operation === 'write') { - return this.handleWriteOperation(args, LocalChatSessionUri.forSession(chatSessionId)); } else { - return { - content: [{ - kind: 'text', - value: 'Error: Unknown operation' - }] - }; + return this.handleWriteOperation(args, LocalChatSessionUri.forSession(chatSessionId)); } } catch (error) { @@ -176,28 +130,16 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { const currentTodoItems = this.chatTodoListService.getTodos(LocalChatSessionUri.forSession(chatSessionId)); let message: string | undefined; - - const operation = this.writeOnly ? 'write' : args.operation; - switch (operation) { - case 'write': { - if (args.todoList) { - message = this.generatePastTenseMessage(currentTodoItems, args.todoList); - } - break; - } - case 'read': { - message = localize('todo.readOperation', "Read todo list"); - break; - } - default: - break; + if (args.operation === 'read') { + message = localize('todo.readOperation', "Read todo list"); + } else if (args.todoList) { + message = this.generatePastTenseMessage(currentTodoItems, args.todoList); } const items = args.todoList ?? currentTodoItems; const todoList = items.map(todo => ({ id: todo.id.toString(), title: todo.title, - description: todo.description || '', status: todo.status })); @@ -306,7 +248,6 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { const todoList: IChatTodo[] = args.todoList.map((parsedTodo) => ({ id: parsedTodo.id, title: parsedTodo.title, - description: parsedTodo.description || '', status: parsedTodo.status })); @@ -379,9 +320,6 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { } const lines = [`- ${checkbox} ${todo.title}`]; - if (this.includeDescription && todo.description && todo.description.trim()) { - lines.push(` - ${todo.description.trim()}`); - } return lines.join('\n'); }).join('\n'); @@ -394,7 +332,7 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { for (let i = 0; i < minLen; i++) { const o = oldList[i]; const n = newList[i]; - if (o.title !== n.title || (o.description ?? '') !== (n.description ?? '') || o.status !== n.status) { + if (o.title !== n.title || o.status !== n.status) { modified++; } } diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts index 38eaedc5d9a..11b67eb3bec 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts @@ -7,13 +7,12 @@ import { Codicon } from '../../../../../../base/common/codicons.js'; import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { localize } from '../../../../../../nls.js'; -import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContribution } from '../../../../../common/contributions.js'; import { ILanguageModelToolsService, SpecedToolAliases, ToolDataSource } from '../languageModelToolsService.js'; import { ConfirmationTool, ConfirmationToolData } from './confirmationTool.js'; import { EditTool, EditToolData } from './editFileTool.js'; -import { createManageTodoListToolData, ManageTodoListTool, TodoListToolDescriptionFieldSettingId, TodoListToolWriteOnlySettingId } from './manageTodoListTool.js'; +import { createManageTodoListToolData, ManageTodoListTool } from './manageTodoListTool.js'; import { RunSubagentTool } from './runSubagentTool.js'; export class BuiltinToolsContribution extends Disposable implements IWorkbenchContribution { @@ -23,18 +22,14 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo constructor( @ILanguageModelToolsService toolsService: ILanguageModelToolsService, @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(); const editTool = instantiationService.createInstance(EditTool); this._register(toolsService.registerTool(EditToolData, editTool)); - // Check if write-only mode is enabled for the todo tool - const writeOnlyMode = this.configurationService.getValue(TodoListToolWriteOnlySettingId) === true; - const includeDescription = this.configurationService.getValue(TodoListToolDescriptionFieldSettingId) !== false; - const todoToolData = createManageTodoListToolData(writeOnlyMode, includeDescription); - const manageTodoListTool = this._register(instantiationService.createInstance(ManageTodoListTool, writeOnlyMode, includeDescription)); + const todoToolData = createManageTodoListToolData(); + const manageTodoListTool = this._register(instantiationService.createInstance(ManageTodoListTool)); this._register(toolsService.registerTool(todoToolData, manageTodoListTool)); // Register the confirmation tool diff --git a/src/vs/workbench/contrib/chat/common/tools/chatTodoListService.ts b/src/vs/workbench/contrib/chat/common/tools/chatTodoListService.ts index b950bf3ccdc..6b7c0af31a1 100644 --- a/src/vs/workbench/contrib/chat/common/tools/chatTodoListService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/chatTodoListService.ts @@ -14,7 +14,6 @@ import { chatSessionResourceToId } from '../model/chatUri.js'; export interface IChatTodo { id: number; title: string; - description?: string; status: 'not-started' | 'in-progress' | 'completed'; } diff --git a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts index f8710991564..dcb88c2a6ec 100644 --- a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts @@ -102,8 +102,8 @@ suite('ChatResponseAccessibleView', () => { kind: 'todoList', sessionId: 'session-1', todoList: [ - { id: '1', title: 'Task 1', description: 'Do something', status: 'in-progress' }, - { id: '2', title: 'Task 2', description: 'Do something else', status: 'completed' } + { id: '1', title: 'Task 1', status: 'in-progress' }, + { id: '2', title: 'Task 2', status: 'completed' } ] }; const result = getToolSpecificDataDescription(todoData); diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatTodoListWidget.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatTodoListWidget.test.ts index 55e3022ca7b..0f685f734dd 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatTodoListWidget.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatTodoListWidget.test.ts @@ -25,7 +25,7 @@ suite('ChatTodoListWidget Accessibility', () => { const sampleTodos: IChatTodo[] = [ { id: 1, title: 'First task', status: 'not-started' }, - { id: 2, title: 'Second task', status: 'in-progress', description: 'This is a task description' }, + { id: 2, title: 'Second task', status: 'in-progress' }, { id: 3, title: 'Third task', status: 'completed' } ]; @@ -39,7 +39,7 @@ suite('ChatTodoListWidget Accessibility', () => { }; // Mock the configuration service - const mockConfigurationService = new TestConfigurationService({ 'chat.todoListTool.descriptionField': true }); + const mockConfigurationService = new TestConfigurationService(); const instantiationService = workbenchInstantiationService(undefined, store); instantiationService.stub(IChatTodoListService, mockTodoListService); @@ -84,11 +84,10 @@ suite('ChatTodoListWidget Accessibility', () => { assert.ok(firstItem.getAttribute('aria-label')?.includes('First task')); assert.ok(firstItem.getAttribute('aria-label')?.includes('not started')); - // Check second item (in-progress with description) + // Check second item (in-progress) const secondItem = todoItems[1] as HTMLElement; assert.ok(secondItem.getAttribute('aria-label')?.includes('Second task')); assert.ok(secondItem.getAttribute('aria-label')?.includes('in progress')); - assert.ok(secondItem.getAttribute('aria-label')?.includes('This is a task description')); // Check third item (completed) const thirdItem = todoItems[2] as HTMLElement; @@ -139,12 +138,11 @@ suite('ChatTodoListWidget Accessibility', () => { assert.ok(firstAriaLabel?.includes('First task'), 'First item aria-label should include title'); assert.ok(firstAriaLabel?.includes('not started'), 'First item aria-label should include status'); - // Check second item (in-progress with description) - aria-label should include title, status, and description + // Check second item (in-progress) - aria-label should include title and status const secondItem = todoItems[1] as HTMLElement; const secondAriaLabel = secondItem.getAttribute('aria-label'); assert.ok(secondAriaLabel?.includes('Second task'), 'Second item aria-label should include title'); assert.ok(secondAriaLabel?.includes('in progress'), 'Second item aria-label should include status'); - assert.ok(secondAriaLabel?.includes('This is a task description'), 'Second item aria-label should include description'); // Check third item (completed) - aria-label should include title and status const thirdItem = todoItems[2] as HTMLElement; @@ -162,7 +160,7 @@ suite('ChatTodoListWidget Accessibility', () => { setTodos: (sessionResource: URI, todos: IChatTodo[]) => { } }; - const emptyConfigurationService = new TestConfigurationService({ 'chat.todoListTool.descriptionField': true }); + const emptyConfigurationService = new TestConfigurationService(); const instantiationService = workbenchInstantiationService(undefined, store); instantiationService.stub(IChatTodoListService, emptyTodoListService); diff --git a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/manageTodoListTool.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/manageTodoListTool.test.ts index 477f81e5505..5b8ad6895d5 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/manageTodoListTool.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/manageTodoListTool.test.ts @@ -9,14 +9,14 @@ import { createManageTodoListToolData } from '../../../../common/tools/builtinTo import { IToolData } from '../../../../common/tools/languageModelToolsService.js'; import { IJSONSchema } from '../../../../../../../base/common/jsonSchema.js'; -suite('ManageTodoListTool Description Field Setting', () => { +suite('ManageTodoListTool Schema', () => { ensureNoDisposablesAreLeakedInTestSuite(); - function getSchemaProperties(toolData: IToolData): { properties: any; required: string[] } { + function getSchemaProperties(toolData: IToolData): { properties: Record; required: string[] } { assert.ok(toolData.inputSchema); const schema = toolData.inputSchema; const todolistItems = schema?.properties?.todoList?.items as IJSONSchema | undefined; - const properties = todolistItems?.properties; + const properties = todolistItems?.properties as Record | undefined; const required = todolistItems?.required; assert.ok(properties, 'Schema properties should be defined'); @@ -25,30 +25,37 @@ suite('ManageTodoListTool Description Field Setting', () => { return { properties, required }; } - test('createManageTodoListToolData should include description field when enabled', () => { - const toolData = createManageTodoListToolData(false, true); - const { properties, required } = getSchemaProperties(toolData); + test('createManageTodoListToolData returns valid tool data with proper schema', () => { + const toolData = createManageTodoListToolData(); - assert.strictEqual('description' in properties, true); - assert.strictEqual(required.includes('description'), true); - assert.deepStrictEqual(required, ['id', 'title', 'description', 'status']); + assert.ok(toolData.id, 'Tool should have an id'); + assert.ok(toolData.inputSchema, 'Tool should have an input schema'); + assert.strictEqual(toolData.inputSchema?.type, 'object', 'Schema should be an object type'); }); - test('createManageTodoListToolData should exclude description field when disabled', () => { - const toolData = createManageTodoListToolData(false, false); - const { properties, required } = getSchemaProperties(toolData); + test('createManageTodoListToolData schema has required todoList field', () => { + const toolData = createManageTodoListToolData(); - assert.strictEqual('description' in properties, false); - assert.strictEqual(required.includes('description'), false); - assert.deepStrictEqual(required, ['id', 'title', 'status']); + assert.ok(toolData.inputSchema?.required?.includes('todoList'), 'todoList should be required'); + assert.ok(toolData.inputSchema?.properties?.todoList, 'todoList property should exist'); }); - test('createManageTodoListToolData should use default value for includeDescription', () => { - const toolDataDefault = createManageTodoListToolData(false); - const { properties, required } = getSchemaProperties(toolDataDefault); + test('createManageTodoListToolData todoList items have correct required fields', () => { + const toolData = createManageTodoListToolData(); + const { properties, required } = getSchemaProperties(toolData); - // Default should be true (includes description) - assert.strictEqual('description' in properties, true); - assert.strictEqual(required.includes('description'), true); + assert.ok('id' in properties, 'Schema should have id property'); + assert.ok('title' in properties, 'Schema should have title property'); + assert.ok('status' in properties, 'Schema should have status property'); + assert.deepStrictEqual(required, ['id', 'title', 'status'], 'Required fields should be id, title, status'); + }); + + test('createManageTodoListToolData status has correct enum values', () => { + const toolData = createManageTodoListToolData(); + const { properties } = getSchemaProperties(toolData); + + const statusProperty = properties['status']; + assert.ok(statusProperty, 'Status property should exist'); + assert.deepStrictEqual(statusProperty.enum, ['not-started', 'in-progress', 'completed'], 'Status should have correct enum values'); }); }); From aa79fe464111135cbf4b8bf7f6726c92563d1770 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Sun, 18 Jan 2026 23:36:30 -0800 Subject: [PATCH 168/387] pr comment --- .../chat/browser/agentSessions/agentSessionHoverWidget.ts | 1 + .../chat/browser/agentSessions/media/agentSessionHoverWidget.css | 1 + 2 files changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts index 8a1b4c1ce39..01cc9633797 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts @@ -130,6 +130,7 @@ export class AgentSessionHoverWidget extends Disposable { listWidget.layout(CHAT_LIST_HEIGHT, CHAT_HOVER_WIDTH); listWidget.setScrollLock(true); listWidget.setViewModel(viewModel); + listWidget.refresh(); const viewModelScheudler = this._register(new RunOnceScheduler(() => listWidget.refresh(), 500)); this._register(viewModel.onDidChange(() => { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionHoverWidget.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionHoverWidget.css index dc51631c560..3c0043830f0 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionHoverWidget.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionHoverWidget.css @@ -72,6 +72,7 @@ min-height: 0; opacity: 0; animation: agentSessionHoverFadeIn 0.2s ease-out forwards; + margin: 0 -8px; .interactive-session .interactive-item-container { padding: 0; From a0929aabec977dcb49c816142803cf6c7a4aee13 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 19 Jan 2026 09:57:30 +0100 Subject: [PATCH 169/387] trim the key (#288819) --- .../services/preferences/browser/preferencesService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index 93914590d96..4557cd63aac 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -253,9 +253,9 @@ export class PreferencesService extends Disposable implements IPreferencesServic const idMatch = query.match(/^@id:(.+)$/); let key: string | undefined; if (idMatch) { - key = idMatch[1]; - } else if (Registry.as(Extensions.Configuration).getConfigurationProperties()[query]) { - key = query; + key = idMatch[1].trim(); + } else if (Registry.as(Extensions.Configuration).getConfigurationProperties()[query.trim()]) { + key = query.trim(); } options.query = undefined; if (key) { From 9d307cff50f36d82692394009046dc5eb95f6247 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 19 Jan 2026 10:04:54 +0100 Subject: [PATCH 170/387] use default account in chat entitlements service (#288165) #269294 use default account in chat entitlements service --- product.json | 4 +- src/vs/base/common/defaultAccount.ts | 65 +- src/vs/base/common/product.ts | 17 +- .../browser/model/inlineCompletionsModel.ts | 4 +- .../test/browser/suggestWidgetModel.test.ts | 2 +- .../inlineCompletions/test/browser/utils.ts | 5 +- .../standalone/browser/standaloneServices.ts | 16 +- .../defaultAccount/common/defaultAccount.ts | 18 +- src/vs/platform/product/common/product.ts | 17 +- src/vs/workbench/browser/web.main.ts | 2 +- .../contrib/chat/browser/chat.contribution.ts | 10 +- .../chatSetup/chatSetupContributions.ts | 2 +- .../browser/chatSetup/chatSetupController.ts | 47 +- .../browser/chatSetup/chatSetupProviders.ts | 16 +- .../chat/browser/chatSetup/chatSetupRunner.ts | 12 +- .../contrib/chat/common/languageModels.ts | 6 +- .../service/promptsService.test.ts | 260 +------- .../electron-browser/desktop.main.ts | 2 +- .../accounts/common/defaultAccount.ts | 554 ++++++++++++------ .../chat/common/chatEntitlementService.ts | 331 +++-------- .../extensionGalleryManifestService.ts | 4 +- .../test/common/accountPolicyService.test.ts | 52 +- .../common/multiplexPolicyService.test.ts | 55 +- 23 files changed, 684 insertions(+), 817 deletions(-) diff --git a/product.json b/product.json index 3eeae17135a..e3c6fbb58e5 100644 --- a/product.json +++ b/product.json @@ -142,7 +142,9 @@ "resolveMergeConflictsCommand": "github.copilot.git.resolveMergeConflicts", "completionsAdvancedSetting": "github.copilot.advanced", "completionsEnablementSetting": "github.copilot.enable", - "nextEditSuggestionsSetting": "github.copilot.nextEditSuggestions.enabled" + "nextEditSuggestionsSetting": "github.copilot.nextEditSuggestions.enabled", + "tokenEntitlementUrl": "https://api.github.com/copilot_internal/v2/token", + "mcpRegistryDataUrl": "https://api.github.com/copilot/mcp_registry" }, "trustedExtensionAuthAccess": { "github": [ diff --git a/src/vs/base/common/defaultAccount.ts b/src/vs/base/common/defaultAccount.ts index dd412d4de0b..7eb9803f3b5 100644 --- a/src/vs/base/common/defaultAccount.ts +++ b/src/vs/base/common/defaultAccount.ts @@ -3,19 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export interface IDefaultAccount { - readonly sessionId: string; - readonly enterprise: boolean; - readonly access_type_sku?: string; - readonly copilot_plan?: string; - readonly assigned_date?: string; - readonly can_signup_for_limited?: boolean; - readonly chat_enabled?: boolean; - readonly chat_preview_features_enabled?: boolean; - readonly mcp?: boolean; - readonly mcpRegistryUrl?: string; - readonly mcpAccess?: 'allow_all' | 'registry_only'; - readonly analytics_tracking_id?: string; +export interface IQuotaSnapshotData { + readonly entitlement: number; + readonly overage_count: number; + readonly overage_permitted: boolean; + readonly percent_remaining: number; + readonly remaining: number; + readonly unlimited: boolean; +} + +export interface ILegacyQuotaSnapshotData { readonly limited_user_quotas?: { readonly chat: number; readonly completions: number; @@ -24,6 +21,44 @@ export interface IDefaultAccount { readonly chat: number; readonly completions: number; }; - readonly limited_user_reset_date?: string; - readonly chat_agent_enabled?: boolean; +} + +export interface IEntitlementsData extends ILegacyQuotaSnapshotData { + readonly access_type_sku: string; + readonly assigned_date: string; + readonly can_signup_for_limited: boolean; + readonly chat_enabled: boolean; + readonly copilot_plan: string; + readonly organization_login_list: string[]; + readonly analytics_tracking_id: string; + readonly limited_user_reset_date?: string; // for Copilot Free + readonly quota_reset_date?: string; // for all other Copilot SKUs + readonly quota_reset_date_utc?: string; // for all other Copilot SKUs (includes time) + readonly quota_snapshots?: { + chat?: IQuotaSnapshotData; + completions?: IQuotaSnapshotData; + premium_interactions?: IQuotaSnapshotData; + }; +} + +export interface IPolicyData { + readonly mcp?: boolean; + readonly chat_preview_features_enabled?: boolean; + readonly chat_agent_enabled?: boolean; + readonly mcpRegistryUrl?: string; + readonly mcpAccess?: 'allow_all' | 'registry_only'; +} + +export interface IDefaultAccountAuthenticationProvider { + readonly id: string; + readonly name: string; + readonly enterprise: boolean; +} + +export interface IDefaultAccount { + readonly authenticationProvider: IDefaultAccountAuthenticationProvider; + readonly sessionId: string; + readonly enterprise: boolean; + readonly entitlementsData?: IEntitlementsData | null; + readonly policyData?: IPolicyData; } diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index f8d394dfafe..7820be2a1a4 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -208,7 +208,6 @@ export interface IProductConfiguration { readonly msftInternalDomains?: string[]; readonly linkProtectionTrustedDomains?: readonly string[]; - readonly defaultAccount?: IDefaultAccountConfig; readonly authClientIdMetadataUrl?: string; readonly 'configurationSync.store'?: ConfigurationSyncStore; @@ -231,20 +230,6 @@ export interface IProductConfiguration { readonly extensionConfigurationPolicy?: IStringDictionary; } -export interface IDefaultAccountConfig { - readonly preferredExtensions: string[]; - readonly authenticationProvider: { - readonly id: string; - readonly enterpriseProviderId: string; - readonly enterpriseProviderConfig: string; - readonly enterpriseProviderUriSetting: string; - readonly scopes: string[][]; - }; - readonly tokenEntitlementUrl: string; - readonly chatEntitlementUrl: string; - readonly mcpRegistryDataUrl: string; -} - export interface ITunnelApplicationConfig { authenticationProviders: IStringDictionary<{ scopes: string[] }>; editorWebUrl: string; @@ -377,6 +362,8 @@ export interface IDefaultChatAgent { readonly entitlementUrl: string; readonly entitlementSignupLimitedUrl: string; + readonly tokenEntitlementUrl: string; + readonly mcpRegistryDataUrl: string; readonly chatQuotaExceededContext: string; readonly completionsQuotaExceededContext: string; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index 222e76c5e51..eee28998bb6 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -1279,8 +1279,8 @@ export function isSuggestionInViewport(editor: ICodeEditor, suggestion: InlineSu } function skuFromAccount(account: IDefaultAccount | null): InlineSuggestSku | undefined { - if (account?.access_type_sku && account?.copilot_plan) { - return { type: account.access_type_sku, plan: account.copilot_plan }; + if (account?.entitlementsData?.access_type_sku && account?.entitlementsData?.copilot_plan) { + return { type: account.entitlementsData.access_type_sku, plan: account.entitlementsData.copilot_plan }; } return undefined; } diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts index f9b51241aa3..6743ca65a9c 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts @@ -171,7 +171,7 @@ async function withAsyncTestCodeEditorAndInlineCompletionsModel( [IDefaultAccountService, new class extends mock() { override onDidChangeDefaultAccount = Event.None; override getDefaultAccount = async () => null; - override setDefaultAccount = () => { }; + override setDefaultAccountProvider = () => { }; }], ); diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts index bbd453dcaf5..cb1adfefdbe 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts @@ -267,7 +267,10 @@ export async function withAsyncTestCodeEditorAndInlineCompletionsModel( _serviceBrand: undefined, onDidChangeDefaultAccount: Event.None, getDefaultAccount: async () => null, - setDefaultAccount: () => { }, + setDefaultAccountProvider: () => { }, + getDefaultAccountAuthenticationProvider: () => { return { id: 'mockProvider', name: 'Mock Provider', enterprise: false }; }, + refresh: async () => { return null; }, + signIn: async () => { return null; }, }); const d = languageFeaturesService.inlineCompletionsProvider.register({ pattern: '**' }, options.provider); diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 44e4b54d1b7..196514a540e 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -99,7 +99,7 @@ import { IDataChannelService, NullDataChannelService } from '../../../platform/d import { IWebWorkerService } from '../../../platform/webWorker/browser/webWorkerService.js'; import { StandaloneWebWorkerService } from './services/standaloneWebWorkerService.js'; import { IDefaultAccountService } from '../../../platform/defaultAccount/common/defaultAccount.js'; -import { IDefaultAccount } from '../../../base/common/defaultAccount.js'; +import { IDefaultAccount, IDefaultAccountAuthenticationProvider } from '../../../base/common/defaultAccount.js'; class SimpleModel implements IResolvedTextEditorModel { @@ -1118,9 +1118,21 @@ class StandaloneDefaultAccountService implements IDefaultAccountService { return null; } - setDefaultAccount(account: IDefaultAccount | null): void { + setDefaultAccountProvider(): void { // no-op } + + async refresh(): Promise { + return null; + } + + getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider { + return { id: 'default', name: 'Default', enterprise: false }; + } + + async signIn(): Promise { + return null; + } } export interface IEditorOverrideServices { diff --git a/src/vs/platform/defaultAccount/common/defaultAccount.ts b/src/vs/platform/defaultAccount/common/defaultAccount.ts index c9db5b22555..d3bee567a79 100644 --- a/src/vs/platform/defaultAccount/common/defaultAccount.ts +++ b/src/vs/platform/defaultAccount/common/defaultAccount.ts @@ -5,16 +5,24 @@ import { createDecorator } from '../../instantiation/common/instantiation.js'; import { Event } from '../../../base/common/event.js'; -import { IDefaultAccount } from '../../../base/common/defaultAccount.js'; +import { IDefaultAccount, IDefaultAccountAuthenticationProvider } from '../../../base/common/defaultAccount.js'; + +export interface IDefaultAccountProvider { + readonly defaultAccount: IDefaultAccount | null; + readonly onDidChangeDefaultAccount: Event; + getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider; + refresh(): Promise; + signIn(options?: { additionalScopes?: readonly string[];[key: string]: unknown }): Promise; +} export const IDefaultAccountService = createDecorator('defaultAccountService'); export interface IDefaultAccountService { - readonly _serviceBrand: undefined; - readonly onDidChangeDefaultAccount: Event; - getDefaultAccount(): Promise; - setDefaultAccount(account: IDefaultAccount | null): void; + getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider; + setDefaultAccountProvider(provider: IDefaultAccountProvider): void; + refresh(): Promise; + signIn(options?: { additionalScopes?: readonly string[];[key: string]: unknown }): Promise; } diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 6f093e9be94..3af87ba6493 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -70,7 +70,22 @@ else { reportIssueUrl: 'https://github.com/microsoft/vscode/issues/new', licenseName: 'MIT', licenseUrl: 'https://github.com/microsoft/vscode/blob/main/LICENSE.txt', - serverLicenseUrl: 'https://github.com/microsoft/vscode/blob/main/LICENSE.txt' + serverLicenseUrl: 'https://github.com/microsoft/vscode/blob/main/LICENSE.txt', + defaultChatAgent: { + extensionId: 'GitHub.copilot', + chatExtensionId: 'GitHub.copilot-chat', + provider: { + default: { + id: 'github', + name: 'GitHub', + }, + enterprise: { + id: 'github-enterprise', + name: 'GitHub Enterprise', + } + }, + providerScopes: [] + } }); } } diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 55929182a6b..1586cb4ca82 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -349,7 +349,7 @@ export class BrowserMain extends Disposable { this._register(RemoteFileSystemProviderClient.register(remoteAgentService, fileService, logService)); // Default Account - const defaultAccountService = this._register(new DefaultAccountService()); + const defaultAccountService = this._register(new DefaultAccountService(productService)); serviceCollection.set(IDefaultAccountService, defaultAccountService); // Policies diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 91a886107c5..21001e6947f 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -287,7 +287,7 @@ configurationRegistry.registerConfiguration({ name: 'ChatToolsAutoApprove', category: PolicyCategory.InteractiveSession, minimumVersion: '1.99', - value: (account) => account.chat_preview_features_enabled === false ? false : undefined, + value: (account) => account.policyData?.chat_preview_features_enabled === false ? false : undefined, localization: { description: { key: 'autoApprove2.description', @@ -445,10 +445,10 @@ configurationRegistry.registerConfiguration({ category: PolicyCategory.InteractiveSession, minimumVersion: '1.99', value: (account) => { - if (account.mcp === false) { + if (account.policyData?.mcp === false) { return McpAccessValue.None; } - if (account.mcpAccess === 'registry_only') { + if (account.policyData?.mcpAccess === 'registry_only') { return McpAccessValue.Registry; } return undefined; @@ -559,7 +559,7 @@ configurationRegistry.registerConfiguration({ name: 'ChatAgentMode', category: PolicyCategory.InteractiveSession, minimumVersion: '1.99', - value: (account) => account.chat_agent_enabled === false ? false : undefined, + value: (account) => account.policyData?.chat_agent_enabled === false ? false : undefined, localization: { description: { key: 'chat.agent.enabled.description', @@ -619,7 +619,7 @@ configurationRegistry.registerConfiguration({ name: 'McpGalleryServiceUrl', category: PolicyCategory.InteractiveSession, minimumVersion: '1.101', - value: (account) => account.mcpRegistryUrl, + value: (account) => account.policyData?.mcpRegistryUrl, localization: { description: { key: 'mcp.gallery.serviceUrl', diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupContributions.ts b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupContributions.ts index 1d3485df87b..73adc06cf30 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupContributions.ts @@ -371,7 +371,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr if (focus) { windowFocusListener.clear(); - const entitlements = await requests.forceResolveEntitlement(undefined); + const entitlements = await requests.forceResolveEntitlement(); if (entitlements?.entitlement && isProUser(entitlements?.entitlement)) { refreshTokens(commandService); } diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupController.ts b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupController.ts index 39b4ccd15f9..0379543f996 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupController.ts @@ -9,7 +9,7 @@ import { Emitter } from '../../../../../base/common/event.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import Severity from '../../../../../base/common/severity.js'; import { StopWatch } from '../../../../../base/common/stopwatch.js'; -import { isObject } from '../../../../../base/common/types.js'; +import { isObject, isUndefined } from '../../../../../base/common/types.js'; import { localize } from '../../../../../nls.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ConfigurationTarget, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; @@ -23,13 +23,14 @@ import { IQuickInputService } from '../../../../../platform/quickinput/common/qu import { Registry } from '../../../../../platform/registry/common/platform.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { IActivityService, ProgressBadge } from '../../../../services/activity/common/activity.js'; -import { AuthenticationSession, IAuthenticationService } from '../../../../services/authentication/common/authentication.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; import { IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js'; import { ChatEntitlement, ChatEntitlementContext, ChatEntitlementRequests, isProUser } from '../../../../services/chat/common/chatEntitlementService.js'; import { CHAT_OPEN_ACTION_ID } from '../actions/chatActions.js'; import { ChatViewId, ChatViewContainerId } from '../chat.js'; import { ChatSetupAnonymous, ChatSetupStep, ChatSetupResultValue, InstallChatEvent, InstallChatClassification, refreshTokens } from './chatSetup.js'; +import { IDefaultAccount } from '../../../../../base/common/defaultAccount.js'; +import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; const defaultChat = { chatExtensionId: product.defaultChatAgent?.chatExtensionId ?? '', @@ -58,7 +59,6 @@ export class ChatSetupController extends Disposable { private readonly context: ChatEntitlementContext, private readonly requests: ChatEntitlementRequests, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IProductService private readonly productService: IProductService, @ILogService private readonly logService: ILogService, @@ -69,6 +69,7 @@ export class ChatSetupController extends Disposable { @IConfigurationService private readonly configurationService: IConfigurationService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IQuickInputService private readonly quickInputService: IQuickInputService, + @IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService, ) { super(); @@ -111,8 +112,6 @@ export class ChatSetupController extends Disposable { let success: ChatSetupResultValue = false; try { - const providerId = ChatEntitlementRequests.providerId(this.configurationService); - let session: AuthenticationSession | undefined; let entitlement: ChatEntitlement | undefined; let signIn: boolean; @@ -131,7 +130,7 @@ export class ChatSetupController extends Disposable { if (signIn) { this.setStep(ChatSetupStep.SigningIn); const result = await this.signIn(options); - if (!result.session) { + if (!result.defaultAccount) { this.doInstall(); // still install the extension in the background to remind the user to sign-in eventually const provider = options.useSocialProvider ?? (options.useEnterpriseProvider ? defaultChat.provider.enterprise.id : defaultChat.provider.default.id); @@ -139,13 +138,12 @@ export class ChatSetupController extends Disposable { return undefined; // treat as cancelled because signing in already triggers an error dialog } - session = result.session; entitlement = result.entitlement; } // Await Install this.setStep(ChatSetupStep.Installing); - success = await this.install(session, entitlement ?? this.context.state.entitlement, providerId, watch, options); + success = await this.install(entitlement ?? this.context.state.entitlement, watch, options); } finally { this.setStep(ChatSetupStep.Initial); this.context.resume(); @@ -154,19 +152,19 @@ export class ChatSetupController extends Disposable { return success; } - private async signIn(options: IChatSetupControllerOptions): Promise<{ session: AuthenticationSession | undefined; entitlement: ChatEntitlement | undefined }> { - let session: AuthenticationSession | undefined; + private async signIn(options: IChatSetupControllerOptions): Promise<{ defaultAccount: IDefaultAccount | undefined; entitlement: ChatEntitlement | undefined }> { let entitlements; + let defaultAccount; try { - ({ session, entitlements } = await this.requests.signIn(options)); + ({ defaultAccount, entitlements } = await this.requests.signIn(options)); } catch (e) { this.logService.error(`[chat setup] signIn: error ${e}`); } - if (!session && !this.lifecycleService.willShutdown) { + if (!defaultAccount && !this.lifecycleService.willShutdown) { const { confirmed } = await this.dialogService.confirm({ type: Severity.Error, - message: localize('unknownSignInError', "Failed to sign in to {0}. Would you like to try again?", ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.provider.enterprise.id ? defaultChat.provider.enterprise.name : defaultChat.provider.default.name), + message: localize('unknownSignInError', "Failed to sign in to {0}. Would you like to try again?", this.defaultAccountService.getDefaultAccountAuthenticationProvider().name), detail: localize('unknownSignInErrorDetail', "You must be signed in to use AI features."), primaryButton: localize('retry', "Retry") }); @@ -176,10 +174,10 @@ export class ChatSetupController extends Disposable { } } - return { session, entitlement: entitlements?.entitlement }; + return { defaultAccount, entitlement: entitlements?.entitlement }; } - private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, providerId: string, watch: StopWatch, options: IChatSetupControllerOptions): Promise { + private async install(entitlement: ChatEntitlement, watch: StopWatch, options: IChatSetupControllerOptions): Promise { const wasRunning = this.context.state.installed && !this.context.state.disabled; let signUpResult: boolean | { errorCode: number } | undefined = undefined; @@ -190,7 +188,6 @@ export class ChatSetupController extends Disposable { provider = options.useSocialProvider ?? (options.useEnterpriseProvider ? defaultChat.provider.enterprise.id : defaultChat.provider.default.id); } - let sessions = session ? [session] : undefined; try { if ( !options.forceAnonymous && // User is not asking for anonymous access @@ -198,23 +195,13 @@ export class ChatSetupController extends Disposable { !isProUser(entitlement) && // User is not signed up for a Copilot subscription entitlement !== ChatEntitlement.Unavailable // User is eligible for Copilot Free ) { - if (!sessions) { - try { - // Consider all sessions for the provider to be suitable for signing up - const existingSessions = await this.authenticationService.getSessions(providerId); - sessions = existingSessions.length > 0 ? [...existingSessions] : undefined; - } catch (error) { - // ignore - errors can throw if a provider is not registered - } + signUpResult = await this.requests.signUpFree(); - if (!sessions || sessions.length === 0) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNoSession', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider }); - return false; // unexpected - } + if (isUndefined(signUpResult)) { + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNoSession', installDuration: watch.elapsed(), signUpErrorCode: undefined, provider }); + return false; // unexpected } - signUpResult = await this.requests.signUpFree(sessions); - if (typeof signUpResult !== 'boolean' /* error */) { this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedSignUp', installDuration: watch.elapsed(), signUpErrorCode: signUpResult.errorCode, provider }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts index 429a6d99bf0..e417344be53 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts @@ -14,7 +14,6 @@ import { Lazy } from '../../../../../base/common/lazy.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize, localize2 } from '../../../../../nls.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; @@ -25,7 +24,7 @@ import { IWorkbenchEnvironmentService } from '../../../../services/environment/c import { nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolDataSource, ToolProgress } from '../../common/tools/languageModelToolsService.js'; import { IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../../common/participants/chatAgents.js'; -import { ChatEntitlement, ChatEntitlementContext, ChatEntitlementRequests, IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; +import { ChatEntitlement, ChatEntitlementContext, IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; import { ChatModel, ChatRequestModel, IChatRequestModel, IChatRequestVariableData } from '../../common/model/chatModel.js'; import { ChatMode } from '../../common/chatModes.js'; import { ChatRequestAgentPart, ChatRequestToolPart } from '../../common/requestParser/chatParserTypes.js'; @@ -52,6 +51,7 @@ import { CommandsRegistry } from '../../../../../platform/commands/common/comman import { IOutputService } from '../../../../services/output/common/output.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { IWorkbenchIssueService } from '../../../issue/common/issue.js'; +import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -181,7 +181,6 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { private readonly location: ChatAgentLocation, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILogService private readonly logService: ILogService, - @IConfigurationService private readonly configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @@ -262,12 +261,13 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { const chatWidgetService = accessor.get(IChatWidgetService); const chatAgentService = accessor.get(IChatAgentService); const languageModelToolsService = accessor.get(ILanguageModelToolsService); + const defaultAccountService = accessor.get(IDefaultAccountService); - return this.doInvoke(request, part => progress([part]), chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService); + return this.doInvoke(request, part => progress([part]), chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService, defaultAccountService); }); } - private async doInvoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService): Promise { + private async doInvoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService, defaultAccountService: IDefaultAccountService): Promise { if ( !this.context.state.installed || // Extension not installed: run setup to install this.context.state.disabled || // Extension disabled: run setup to enable @@ -278,7 +278,7 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { !this.chatEntitlementService.anonymous // unless anonymous access is enabled ) ) { - return this.doInvokeWithSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService); + return this.doInvokeWithSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService, defaultAccountService); } return this.doInvokeWithoutSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService); @@ -510,7 +510,7 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { } } - private async doInvokeWithSetup(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService): Promise { + private async doInvokeWithSetup(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService, defaultAccountService: IDefaultAccountService): Promise { this.telemetryService.publicLog2('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'chat' }); const widget = chatWidgetService.getWidgetBySessionResource(request.sessionResource); @@ -521,7 +521,7 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { case ChatSetupStep.SigningIn: progress({ kind: 'progressMessage', - content: new MarkdownString(localize('setupChatSignIn2', "Signing in to {0}...", ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.provider.enterprise.id ? defaultChat.provider.enterprise.name : defaultChat.provider.default.name)), + content: new MarkdownString(localize('setupChatSignIn2', "Signing in to {0}...", defaultAccountService.getDefaultAccountAuthenticationProvider().name)), }); break; case ChatSetupStep.Installing: diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupRunner.ts b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupRunner.ts index e03d3616ea2..4c51c01f5ad 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupRunner.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupRunner.ts @@ -16,7 +16,6 @@ import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js'; import { localize } from '../../../../../nls.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { createWorkbenchDialogOptions } from '../../../../../platform/dialogs/browser/dialog.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; @@ -26,10 +25,11 @@ import product from '../../../../../platform/product/common/product.js'; import { ITelemetryService, TelemetryLevel } from '../../../../../platform/telemetry/common/telemetry.js'; import { IWorkspaceTrustRequestService } from '../../../../../platform/workspace/common/workspaceTrust.js'; import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; -import { ChatEntitlement, ChatEntitlementContext, ChatEntitlementRequests, ChatEntitlementService, IChatEntitlementService, isProUser } from '../../../../services/chat/common/chatEntitlementService.js'; +import { ChatEntitlement, ChatEntitlementContext, ChatEntitlementService, IChatEntitlementService, isProUser } from '../../../../services/chat/common/chatEntitlementService.js'; import { IChatWidgetService } from '../chat.js'; import { ChatSetupController } from './chatSetupController.js'; import { IChatSetupResult, ChatSetupAnonymous, InstallChatEvent, InstallChatClassification, ChatSetupStrategy, ChatSetupResultValue } from './chatSetup.js'; +import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; const defaultChat = { publicCodeMatchesUrl: product.defaultChatAgent?.publicCodeMatchesUrl ?? '', @@ -48,7 +48,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(IConfigurationService), accessor.get(IChatWidgetService), accessor.get(IWorkspaceTrustRequestService), accessor.get(IMarkdownRendererService)); + 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)); }); } @@ -67,10 +67,10 @@ export class ChatSetup { @IKeybindingService private readonly keybindingService: IKeybindingService, @IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService, @ILogService private readonly logService: ILogService, - @IConfigurationService private readonly configurationService: IConfigurationService, @IChatWidgetService private readonly widgetService: IChatWidgetService, @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, @IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService, + @IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService, ) { } skipDialog(): void { @@ -116,7 +116,7 @@ export class ChatSetup { setupStrategy = await this.showDialog(options); } - if (setupStrategy === ChatSetupStrategy.DefaultSetup && ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.provider.enterprise.id) { + if (setupStrategy === ChatSetupStrategy.DefaultSetup && this.defaultAccountService.getDefaultAccountAuthenticationProvider().enterprise) { setupStrategy = ChatSetupStrategy.SetupWithEnterpriseProvider; // users with a configured provider go through provider setup } @@ -200,7 +200,7 @@ export class ChatSetup { const googleProviderButton: ContinueWithButton = [localize('continueWith', "Continue with {0}", defaultChat.provider.google.name), ChatSetupStrategy.SetupWithGoogleProvider, styleButton('continue-button', 'google')]; const appleProviderButton: ContinueWithButton = [localize('continueWith', "Continue with {0}", defaultChat.provider.apple.name), ChatSetupStrategy.SetupWithAppleProvider, styleButton('continue-button', 'apple')]; - if (ChatEntitlementRequests.providerId(this.configurationService) !== defaultChat.provider.enterprise.id) { + if (!this.defaultAccountService.getDefaultAccountAuthenticationProvider().enterprise) { buttons = coalesce([ defaultProviderButton, googleProviderButton, diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 6127745702a..a612296c3d6 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -222,15 +222,13 @@ export interface ILanguageModelChatResponse { export interface ILanguageModelChatProvider { readonly onDidChange: Event; provideLanguageModelChatInfo(options: ILanguageModelChatInfoOptions, token: CancellationToken): Promise; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - sendChatRequest(modelId: string, messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: any }, token: CancellationToken): Promise; + sendChatRequest(modelId: string, messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: unknown }, token: CancellationToken): Promise; provideTokenCount(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise; } export interface ILanguageModelChat { metadata: ILanguageModelChatMetadata; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - sendChatRequest(messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: any }, token: CancellationToken): Promise; + sendChatRequest(messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: unknown }, token: CancellationToken): Promise; provideTokenCount(message: string | IChatMessage, token: CancellationToken): Promise; } 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 e24a22e0802..dd32cdedb95 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 @@ -1301,265 +1301,7 @@ suite('PromptsService', () => { }); }); - suite('listPromptFiles - skills', () => { - teardown(() => { - sinon.restore(); - }); - - test('should list skill files from workspace', async () => { - testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); - testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); - - const rootFolderName = 'list-skills-workspace'; - const rootFolder = `/${rootFolderName}`; - const rootFolderUri = URI.file(rootFolder); - - workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); - - await mockFiles(fileService, [ - { - path: `${rootFolder}/.github/skills/skill1/SKILL.md`, - contents: [ - '---', - 'name: "Skill 1"', - 'description: "First skill"', - '---', - 'Skill 1 content', - ], - }, - { - path: `${rootFolder}/.claude/skills/skill2/SKILL.md`, - contents: [ - '---', - 'name: "Skill 2"', - 'description: "Second skill"', - '---', - 'Skill 2 content', - ], - }, - ]); - - const result = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); - - assert.strictEqual(result.length, 2, 'Should find 2 skills'); - - const skill1 = result.find(s => s.uri.path.includes('skill1')); - assert.ok(skill1, 'Should find skill1'); - assert.strictEqual(skill1.type, PromptsType.skill); - assert.strictEqual(skill1.storage, PromptsStorage.local); - - const skill2 = result.find(s => s.uri.path.includes('skill2')); - assert.ok(skill2, 'Should find skill2'); - assert.strictEqual(skill2.type, PromptsType.skill); - assert.strictEqual(skill2.storage, PromptsStorage.local); - }); - - test('should list skill files from user home', async () => { - testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); - testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); - - const rootFolderName = 'list-skills-user-home'; - const rootFolder = `/${rootFolderName}`; - const rootFolderUri = URI.file(rootFolder); - - workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); - - await mockFiles(fileService, [ - { - path: '/home/user/.copilot/skills/personal-skill/SKILL.md', - contents: [ - '---', - 'name: "Personal Skill"', - 'description: "A personal skill"', - '---', - 'Personal skill content', - ], - }, - { - path: '/home/user/.claude/skills/claude-personal/SKILL.md', - contents: [ - '---', - 'name: "Claude Personal Skill"', - 'description: "A Claude personal skill"', - '---', - 'Claude personal skill content', - ], - }, - ]); - - const result = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); - - const personalSkills = result.filter(s => s.storage === PromptsStorage.user); - assert.strictEqual(personalSkills.length, 2, 'Should find 2 personal skills'); - - const copilotSkill = personalSkills.find(s => s.uri.path.includes('.copilot')); - assert.ok(copilotSkill, 'Should find copilot personal skill'); - - const claudeSkill = personalSkills.find(s => s.uri.path.includes('.claude')); - assert.ok(claudeSkill, 'Should find claude personal skill'); - }); - - test('should not list skills when not in skill folder structure', async () => { - testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); - - const rootFolderName = 'no-skills'; - const rootFolder = `/${rootFolderName}`; - const rootFolderUri = URI.file(rootFolder); - - workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); - - // Create files in non-skill locations - await mockFiles(fileService, [ - { - path: `${rootFolder}/.github/prompts/SKILL.md`, - contents: [ - '---', - 'name: "Not a skill"', - '---', - 'This is in prompts folder, not skills', - ], - }, - { - path: `${rootFolder}/SKILL.md`, - contents: [ - '---', - 'name: "Root skill"', - '---', - 'This is in root, not skills folder', - ], - }, - ]); - - const result = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); - - assert.strictEqual(result.length, 0, 'Should not find any skills in non-skill locations'); - }); - - test('should handle mixed workspace and user home skills', async () => { - testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); - testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); - - const rootFolderName = 'mixed-skills'; - const rootFolder = `/${rootFolderName}`; - const rootFolderUri = URI.file(rootFolder); - - workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); - - await mockFiles(fileService, [ - // Workspace skills - { - path: `${rootFolder}/.github/skills/workspace-skill/SKILL.md`, - contents: [ - '---', - 'name: "Workspace Skill"', - 'description: "A workspace skill"', - '---', - 'Workspace skill content', - ], - }, - // User home skills - { - path: '/home/user/.copilot/skills/personal-skill/SKILL.md', - contents: [ - '---', - 'name: "Personal Skill"', - 'description: "A personal skill"', - '---', - 'Personal skill content', - ], - }, - ]); - - const result = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); - - const workspaceSkills = result.filter(s => s.storage === PromptsStorage.local); - const userSkills = result.filter(s => s.storage === PromptsStorage.user); - - assert.strictEqual(workspaceSkills.length, 1, 'Should find 1 workspace skill'); - assert.strictEqual(userSkills.length, 1, 'Should find 1 user skill'); - }); - - test('should respect disabled default paths via config', async () => { - testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); - // Disable .github/skills, only .claude/skills should be searched - testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, { - '.github/skills': false, - '.claude/skills': true, - }); - - const rootFolderName = 'disabled-default-test'; - const rootFolder = `/${rootFolderName}`; - const rootFolderUri = URI.file(rootFolder); - - workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); - - await mockFiles(fileService, [ - { - path: `${rootFolder}/.github/skills/github-skill/SKILL.md`, - contents: [ - '---', - 'name: "GitHub Skill"', - 'description: "Should NOT be found"', - '---', - 'This skill is in a disabled folder', - ], - }, - { - path: `${rootFolder}/.claude/skills/claude-skill/SKILL.md`, - contents: [ - '---', - 'name: "Claude Skill"', - 'description: "Should be found"', - '---', - 'This skill is in an enabled folder', - ], - }, - ]); - - const result = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); - - assert.strictEqual(result.length, 1, 'Should find only 1 skill (from enabled folder)'); - assert.ok(result[0].uri.path.includes('.claude/skills'), 'Should only find skill from .claude/skills'); - assert.ok(!result[0].uri.path.includes('.github/skills'), 'Should not find skill from disabled .github/skills'); - }); - - test('should expand tilde paths in custom locations', async () => { - testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); - // Add a tilde path as custom location - testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, { - '.github/skills': false, - '.claude/skills': false, - '~/my-custom-skills': true, - }); - - const rootFolderName = 'tilde-test'; - const rootFolder = `/${rootFolderName}`; - const rootFolderUri = URI.file(rootFolder); - - workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); - - // The mock user home is /home/user, so ~/my-custom-skills should resolve to /home/user/my-custom-skills - await mockFiles(fileService, [ - { - path: '/home/user/my-custom-skills/custom-skill/SKILL.md', - contents: [ - '---', - 'name: "Custom Skill"', - 'description: "A skill from tilde path"', - '---', - 'Skill content from ~/my-custom-skills', - ], - }, - ]); - - const result = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); - - assert.strictEqual(result.length, 1, 'Should find 1 skill from tilde-expanded path'); - assert.ok(result[0].uri.path.includes('/home/user/my-custom-skills'), 'Path should be expanded from tilde'); - }); - }); - - suite('listPromptFiles - skills', () => { + suite('listPromptFiles - skills ', () => { teardown(() => { sinon.restore(); }); diff --git a/src/vs/workbench/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index 11e037e48f7..03bba5c4b30 100644 --- a/src/vs/workbench/electron-browser/desktop.main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -210,7 +210,7 @@ export class DesktopMain extends Disposable { } // Default Account - const defaultAccountService = this._register(new DefaultAccountService()); + const defaultAccountService = this._register(new DefaultAccountService(productService)); serviceCollection.set(IDefaultAccountService, defaultAccountService); // Policies diff --git a/src/vs/workbench/services/accounts/common/defaultAccount.ts b/src/vs/workbench/services/accounts/common/defaultAccount.ts index 83d7e3ddb57..3bd289f8f31 100644 --- a/src/vs/workbench/services/accounts/common/defaultAccount.ts +++ b/src/vs/workbench/services/accounts/common/defaultAccount.ts @@ -12,21 +12,41 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { IExtensionService } from '../../extensions/common/extensions.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { localize } from '../../../../nls.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; -import { Barrier, timeout } from '../../../../base/common/async.js'; +import { Barrier, ThrottledDelayer, timeout } from '../../../../base/common/async.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { getErrorMessage } from '../../../../base/common/errors.js'; -import { IDefaultAccount } from '../../../../base/common/defaultAccount.js'; +import { IDefaultAccount, IDefaultAccountAuthenticationProvider, IEntitlementsData } from '../../../../base/common/defaultAccount.js'; import { isString } from '../../../../base/common/types.js'; import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; import { isWeb } from '../../../../base/common/platform.js'; -import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js'; +import { IDefaultAccountProvider, IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { distinct } from '../../../../base/common/arrays.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IDefaultAccountConfig } from '../../../../base/common/product.js'; +import { equals } from '../../../../base/common/objects.js'; +import { IDefaultChatAgent } from '../../../../base/common/product.js'; +import { IRequestContext } from '../../../../base/parts/request/common/request.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; + +interface IDefaultAccountConfig { + readonly preferredExtensions: string[]; + readonly authenticationProvider: { + readonly default: { + readonly id: string; + readonly name: string; + }; + readonly enterprise: { + readonly id: string; + readonly name: string; + }; + readonly enterpriseProviderConfig: string; + readonly enterpriseProviderUriSetting: string; + readonly scopes: string[][]; + }; + readonly tokenEntitlementUrl: string; + readonly entitlementUrl: string; + readonly mcpRegistryDataUrl: string; +} export const DEFAULT_ACCOUNT_SIGN_IN_COMMAND = 'workbench.actions.accounts.signIn'; @@ -38,24 +58,6 @@ const enum DefaultAccountStatus { const CONTEXT_DEFAULT_ACCOUNT_STATE = new RawContextKey('defaultAccountStatus', DefaultAccountStatus.Uninitialized); -interface IChatEntitlementsResponse { - readonly access_type_sku: string; - readonly copilot_plan: string; - readonly assigned_date: string; - readonly can_signup_for_limited: boolean; - readonly chat_enabled: boolean; - readonly analytics_tracking_id: string; - readonly limited_user_quotas?: { - readonly chat: number; - readonly completions: number; - }; - readonly monthly_quotas?: { - readonly chat: number; - readonly completions: number; - }; - readonly limited_user_reset_date: string; -} - interface ITokenEntitlementsResponse { token: string; } @@ -76,43 +78,129 @@ interface IMcpRegistryResponse { readonly mcp_registries: ReadonlyArray; } +function toDefaultAccountConfig(defaultChatAgent: IDefaultChatAgent): IDefaultAccountConfig { + return { + preferredExtensions: [ + defaultChatAgent.chatExtensionId, + defaultChatAgent.extensionId, + ], + authenticationProvider: { + default: { + id: defaultChatAgent.provider.default.id, + name: defaultChatAgent.provider.default.name, + }, + enterprise: { + id: defaultChatAgent.provider.enterprise.id, + name: defaultChatAgent.provider.enterprise.name, + }, + enterpriseProviderConfig: `${defaultChatAgent.completionsAdvancedSetting}.authProvider`, + enterpriseProviderUriSetting: defaultChatAgent.providerUriSetting, + scopes: defaultChatAgent.providerScopes, + }, + entitlementUrl: defaultChatAgent.entitlementUrl, + tokenEntitlementUrl: defaultChatAgent.tokenEntitlementUrl, + mcpRegistryDataUrl: defaultChatAgent.mcpRegistryDataUrl, + }; +} + export class DefaultAccountService extends Disposable implements IDefaultAccountService { declare _serviceBrand: undefined; - private _defaultAccount: IDefaultAccount | null | undefined = undefined; - get defaultAccount(): IDefaultAccount | null { return this._defaultAccount ?? null; } + private defaultAccount: IDefaultAccount | null = null; private readonly initBarrier = new Barrier(); private readonly _onDidChangeDefaultAccount = this._register(new Emitter()); readonly onDidChangeDefaultAccount = this._onDidChangeDefaultAccount.event; + private readonly defaultAccountConfig: IDefaultAccountConfig; + private defaultAccountProvider: IDefaultAccountProvider | null = null; + + constructor( + @IProductService productService: IProductService, + ) { + super(); + this.defaultAccountConfig = toDefaultAccountConfig(productService.defaultChatAgent); + } + async getDefaultAccount(): Promise { await this.initBarrier.wait(); return this.defaultAccount; } - setDefaultAccount(account: IDefaultAccount | null): void { - const oldAccount = this._defaultAccount; - this._defaultAccount = account; - - if (oldAccount !== this._defaultAccount) { - this._onDidChangeDefaultAccount.fire(this._defaultAccount); + getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider { + if (this.defaultAccountProvider) { + return this.defaultAccountProvider.getDefaultAccountAuthenticationProvider(); } - - this.initBarrier.open(); + return { + ...this.defaultAccountConfig.authenticationProvider.default, + enterprise: false + }; } + setDefaultAccountProvider(provider: IDefaultAccountProvider): void { + if (this.defaultAccountProvider) { + throw new Error('Default account provider is already set'); + } + + this.defaultAccountProvider = provider; + provider.refresh().then(account => { + this.defaultAccount = account; + }).finally(() => { + this.initBarrier.open(); + this._register(provider.onDidChangeDefaultAccount(account => this.setDefaultAccount(account))); + }); + } + + async refresh(): Promise { + await this.initBarrier.wait(); + + const account = await this.defaultAccountProvider?.refresh(); + this.setDefaultAccount(account ?? null); + return this.defaultAccount; + } + + async signIn(options?: { additionalScopes?: readonly string[];[key: string]: unknown }): Promise { + await this.initBarrier.wait(); + return this.defaultAccountProvider?.signIn(options) ?? null; + } + + private setDefaultAccount(account: IDefaultAccount | null): void { + if (equals(this.defaultAccount, account)) { + return; + } + this.defaultAccount = account; + this._onDidChangeDefaultAccount.fire(this.defaultAccount); + } } -class DefaultAccountSetup extends Disposable { +type DefaultAccountStatusTelemetry = { + status: string; + initial: boolean; +}; + +type DefaultAccountStatusTelemetryClassification = { + owner: 'sandy081'; + comment: 'Log default account availability status'; + status: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Indicates whether default account is available or not.' }; + initial: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Indicates whether this is the initial status report.' }; +}; + +class DefaultAccountProvider extends Disposable implements IDefaultAccountProvider { + + private _defaultAccount: IDefaultAccount | null = null; + get defaultAccount(): IDefaultAccount | null { return this._defaultAccount ?? null; } + + private readonly _onDidChangeDefaultAccount = this._register(new Emitter()); + readonly onDidChangeDefaultAccount = this._onDidChangeDefaultAccount.event; - private defaultAccount: IDefaultAccount | null = null; private readonly accountStatusContext: IContextKey; + private initialized = false; + private readonly initPromise: Promise; + private readonly updateThrottler = this._register(new ThrottledDelayer(100)); constructor( private readonly defaultAccountConfig: IDefaultAccountConfig, - @IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService, @IConfigurationService private readonly configurationService: IConfigurationService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IAuthenticationExtensionsService private readonly authenticationExtensionsService: IAuthenticationExtensionsService, @@ -125,84 +213,121 @@ class DefaultAccountSetup extends Disposable { ) { super(); this.accountStatusContext = CONTEXT_DEFAULT_ACCOUNT_STATE.bindTo(contextKeyService); + this.initPromise = this.init() + .finally(() => { + this.telemetryService.publicLog2('defaultaccount:status', { status: this.defaultAccount ? 'available' : 'unavailable', initial: true }); + this.initialized = true; + }); } - async setup(): Promise { - this.logService.debug('[DefaultAccount] Starting initialization'); - let defaultAccount: IDefaultAccount | null = null; - try { - defaultAccount = await this.fetchDefaultAccount(); - } catch (error) { - this.logService.error('[DefaultAccount] Error during initialization', getErrorMessage(error)); + private async init(): Promise { + if (isWeb && !this.environmentService.remoteAuthority) { + this.logService.debug('[DefaultAccount] Running in web without remote, skipping initialization'); + return; } - this.setDefaultAccount(defaultAccount); + try { + await this.extensionService.whenInstalledExtensionsRegistered(); + this.logService.debug('[DefaultAccount] Installed extensions registered.'); + } catch (error) { + this.logService.error('[DefaultAccount] Error while waiting for installed extensions to be registered', getErrorMessage(error)); + } + + this.logService.debug('[DefaultAccount] Starting initialization'); + await this.doUpdateDefaultAccount(); this.logService.debug('[DefaultAccount] Initialization complete'); - type DefaultAccountStatusTelemetry = { - status: string; - initial: boolean; - }; - type DefaultAccountStatusTelemetryClassification = { - owner: 'sandy081'; - comment: 'Log default account availability status'; - status: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Indicates whether default account is available or not.' }; - initial: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Indicates whether this is the initial status report.' }; - }; - this.telemetryService.publicLog2('defaultaccount:status', { status: this.defaultAccount ? 'available' : 'unavailable', initial: true }); - - this._register(this.defaultAccountService.onDidChangeDefaultAccount(account => { + this._register(this.onDidChangeDefaultAccount(account => { this.telemetryService.publicLog2('defaultaccount:status', { status: account ? 'available' : 'unavailable', initial: false }); })); - this._register(this.authenticationService.onDidChangeSessions(async e => { - if (e.providerId !== this.getDefaultAccountProviderId()) { + this._register(this.authenticationService.onDidChangeSessions(e => { + const defaultAccountProvider = this.getDefaultAccountAuthenticationProvider(); + if (e.providerId !== defaultAccountProvider.id) { return; } if (this.defaultAccount && e.event.removed?.some(session => session.id === this.defaultAccount?.sessionId)) { this.setDefaultAccount(null); } else { - this.setDefaultAccount(await this.getDefaultAccountFromAuthenticatedSessions(e.providerId, this.defaultAccountConfig.authenticationProvider.scopes)); + this.logService.debug('[DefaultAccount] Sessions changed for default account provider, updating default account'); + this.updateDefaultAccount(); } })); this._register(this.authenticationExtensionsService.onDidChangeAccountPreference(async e => { - if (e.providerId !== this.getDefaultAccountProviderId()) { + const defaultAccountProvider = this.getDefaultAccountAuthenticationProvider(); + if (e.providerId !== defaultAccountProvider.id) { return; } - this.setDefaultAccount(await this.getDefaultAccountFromAuthenticatedSessions(e.providerId, this.defaultAccountConfig.authenticationProvider.scopes)); + this.logService.debug('[DefaultAccount] Account preference changed for default account provider, updating default account'); + this.updateDefaultAccount(); + })); + + this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => { + const defaultAccountProvider = this.getDefaultAccountAuthenticationProvider(); + if (e.id !== defaultAccountProvider.id) { + return; + } + this.logService.debug('[DefaultAccount] Default account provider registered, updating default account'); + this.updateDefaultAccount(); + })); + + this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => { + const defaultAccountProvider = this.getDefaultAccountAuthenticationProvider(); + if (e.id !== defaultAccountProvider.id) { + return; + } + this.logService.debug('[DefaultAccount] Default account provider unregistered, updating default account'); + this.updateDefaultAccount(); })); } + async refresh(): Promise { + if (!this.initialized) { + await this.initPromise; + return this.defaultAccount; + } + + this.logService.debug('[DefaultAccount] Refreshing default account'); + await this.updateDefaultAccount(); + return this.defaultAccount; + } + + private async updateDefaultAccount(): Promise { + await this.updateThrottler.trigger(() => this.doUpdateDefaultAccount()); + } + + private async doUpdateDefaultAccount(): Promise { + try { + const defaultAccount = await this.fetchDefaultAccount(); + this.setDefaultAccount(defaultAccount); + } catch (error) { + this.logService.error('[DefaultAccount] Error while updating default account', getErrorMessage(error)); + } + } + private async fetchDefaultAccount(): Promise { - if (isWeb && !this.environmentService.remoteAuthority) { - this.logService.debug('[DefaultAccount] Running in web without remote, skipping initialization'); - return null; - } + const defaultAccountProvider = this.getDefaultAccountAuthenticationProvider(); + this.logService.debug('[DefaultAccount] Default account provider ID:', defaultAccountProvider.id); - const defaultAccountProviderId = this.getDefaultAccountProviderId(); - this.logService.debug('[DefaultAccount] Default account provider ID:', defaultAccountProviderId); - if (!defaultAccountProviderId) { - return null; - } - - await this.extensionService.whenInstalledExtensionsRegistered(); - this.logService.debug('[DefaultAccount] Installed extensions registered.'); - - const declaredProvider = this.authenticationService.declaredProviders.find(provider => provider.id === defaultAccountProviderId); + const declaredProvider = this.authenticationService.declaredProviders.find(provider => provider.id === defaultAccountProvider.id); if (!declaredProvider) { - this.logService.info(`[DefaultAccount] Authentication provider is not declared.`, defaultAccountProviderId); + this.logService.info(`[DefaultAccount] Authentication provider is not declared.`, defaultAccountProvider); return null; } - this.registerSignInAction(this.defaultAccountConfig.authenticationProvider.scopes[0]); - return await this.getDefaultAccountFromAuthenticatedSessions(defaultAccountProviderId, this.defaultAccountConfig.authenticationProvider.scopes); + return await this.getDefaultAccountFromAuthenticatedSessions(defaultAccountProvider, this.defaultAccountConfig.authenticationProvider.scopes); } private setDefaultAccount(account: IDefaultAccount | null): void { - this.defaultAccount = account; - this.defaultAccountService.setDefaultAccount(this.defaultAccount); - if (this.defaultAccount) { + if (equals(this._defaultAccount, account)) { + return; + } + + this.logService.trace('[DefaultAccount] Updating default account:', account); + this._defaultAccount = account; + this._onDidChangeDefaultAccount.fire(this._defaultAccount); + if (this._defaultAccount) { this.accountStatusContext.set(DefaultAccountStatus.Available); this.logService.debug('[DefaultAccount] Account status set to Available'); } else { @@ -223,50 +348,56 @@ class DefaultAccountSetup extends Disposable { return result; } - private async getDefaultAccountFromAuthenticatedSessions(authProviderId: string, scopes: string[][]): Promise { + private async getDefaultAccountFromAuthenticatedSessions(authenticationProvider: IDefaultAccountAuthenticationProvider, scopes: string[][]): Promise { try { - this.logService.debug('[DefaultAccount] Getting Default Account from authenticated sessions for provider:', authProviderId); - const session = await this.findMatchingProviderSession(authProviderId, scopes); + this.logService.debug('[DefaultAccount] Getting Default Account from authenticated sessions for provider:', authenticationProvider.id); + const sessions = await this.findMatchingProviderSession(authenticationProvider.id, scopes); - if (!session) { - this.logService.debug('[DefaultAccount] No matching session found for provider:', authProviderId); + if (!sessions) { + this.logService.debug('[DefaultAccount] No matching session found for provider:', authenticationProvider.id); return null; } - const [chatEntitlements, tokenEntitlements] = await Promise.all([ - this.getChatEntitlements(session.accessToken), - this.getTokenEntitlements(session.accessToken), + const [entitlementsData, policyData] = await Promise.all([ + this.getEntitlements(sessions), + this.getTokenEntitlements(sessions), ]); - const mcpRegistryProvider = tokenEntitlements.mcp ? await this.getMcpRegistryProvider(session.accessToken) : undefined; + const mcpRegistryProvider = policyData.mcp ? await this.getMcpRegistryProvider(sessions) : undefined; - const account = { - sessionId: session.id, - enterprise: this.isEnterpriseAuthenticationProvider(authProviderId) || session.account.label.includes('_'), - ...chatEntitlements, - ...tokenEntitlements, - mcpRegistryUrl: mcpRegistryProvider?.url, - mcpAccess: mcpRegistryProvider?.registry_access, + const account: IDefaultAccount = { + authenticationProvider, + sessionId: sessions[0].id, + enterprise: authenticationProvider.enterprise || sessions[0].account.label.includes('_'), + entitlementsData, + policyData: { + chat_agent_enabled: policyData.chat_agent_enabled, + chat_preview_features_enabled: policyData.chat_preview_features_enabled, + mcp: policyData.mcp, + mcpRegistryUrl: mcpRegistryProvider?.url, + mcpAccess: mcpRegistryProvider?.registry_access, + } }; - this.logService.debug('[DefaultAccount] Successfully created default account for provider:', authProviderId); + this.logService.debug('[DefaultAccount] Successfully created default account for provider:', authenticationProvider.id); return account; } catch (error) { - this.logService.error('[DefaultAccount] Failed to create default account for provider:', authProviderId, getErrorMessage(error)); + this.logService.error('[DefaultAccount] Failed to create default account for provider:', authenticationProvider.id, getErrorMessage(error)); return null; } } - private async findMatchingProviderSession(authProviderId: string, allScopes: string[][]): Promise { + private async findMatchingProviderSession(authProviderId: string, allScopes: string[][]): Promise { const sessions = await this.getSessions(authProviderId); + const matchingSessions = []; for (const session of sessions) { this.logService.debug('[DefaultAccount] Checking session with scopes', session.scopes); for (const scopes of allScopes) { if (this.scopesMatch(session.scopes, scopes)) { - return session; + matchingSessions.push(session); } } } - return undefined; + return matchingSessions.length > 0 ? matchingSessions : undefined; } private async getSessions(authProviderId: string): Promise { @@ -303,7 +434,7 @@ class DefaultAccountSetup extends Disposable { return expectedScopes.every(scope => scopes.includes(scope)); } - private async getTokenEntitlements(accessToken: string): Promise> { + private async getTokenEntitlements(sessions: AuthenticationSession[]): Promise<{ mcp?: boolean; chat_preview_features_enabled?: boolean; chat_agent_enabled?: boolean }> { const tokenEntitlementsUrl = this.getTokenEntitlementUrl(); if (!tokenEntitlementsUrl) { this.logService.debug('[DefaultAccount] No token entitlements URL found'); @@ -311,17 +442,18 @@ class DefaultAccountSetup extends Disposable { } this.logService.debug('[DefaultAccount] Fetching token entitlements from:', tokenEntitlementsUrl); - try { - const chatContext = await this.requestService.request({ - type: 'GET', - url: tokenEntitlementsUrl, - disableCache: true, - headers: { - 'Authorization': `Bearer ${accessToken}` - } - }, CancellationToken.None); + const response = await this.request(tokenEntitlementsUrl, 'GET', undefined, sessions, CancellationToken.None); + if (!response) { + return {}; + } - const chatData = await asJson(chatContext); + if (response.res.statusCode && response.res.statusCode !== 200) { + this.logService.trace(`[DefaultAccount] unexpected status code ${response.res.statusCode} while fetching token entitlements`); + return {}; + } + + try { + const chatData = await asJson(response); if (chatData) { const tokenMap = this.extractFromToken(chatData.token); return { @@ -340,53 +472,59 @@ class DefaultAccountSetup extends Disposable { return {}; } - private async getChatEntitlements(accessToken: string): Promise> { - const chatEntitlementsUrl = this.getChatEntitlementUrl(); - if (!chatEntitlementsUrl) { + private async getEntitlements(sessions: AuthenticationSession[]): Promise { + const entitlementUrl = this.getEntitlementUrl(); + if (!entitlementUrl) { this.logService.debug('[DefaultAccount] No chat entitlements URL found'); - return {}; + return undefined; } - this.logService.debug('[DefaultAccount] Fetching chat entitlements from:', chatEntitlementsUrl); - try { - const context = await this.requestService.request({ - type: 'GET', - url: chatEntitlementsUrl, - disableCache: true, - headers: { - 'Authorization': `Bearer ${accessToken}` - } - }, CancellationToken.None); + this.logService.debug('[DefaultAccount] Fetching entitlements from:', entitlementUrl); + const response = await this.request(entitlementUrl, 'GET', undefined, sessions, CancellationToken.None); + if (!response) { + return undefined; + } - const data = await asJson(context); + if (response.res.statusCode && response.res.statusCode !== 200) { + this.logService.trace(`[DefaultAccount] unexpected status code ${response.res.statusCode} while fetching entitlements`); + return ( + response.res.statusCode === 401 || // oauth token being unavailable (expired/revoked) + response.res.statusCode === 404 // missing scopes/permissions, service pretends the endpoint doesn't exist + ) ? null : undefined; + } + + try { + const data = await asJson(response); if (data) { return data; } - this.logService.error('Failed to fetch entitlements', 'No data returned'); + this.logService.error('[DefaultAccount] Failed to fetch entitlements', 'No data returned'); } catch (error) { - this.logService.error('Failed to fetch entitlements', getErrorMessage(error)); + this.logService.error('[DefaultAccount] Failed to fetch entitlements', getErrorMessage(error)); } - return {}; + return undefined; } - private async getMcpRegistryProvider(accessToken: string): Promise { + private async getMcpRegistryProvider(sessions: AuthenticationSession[]): Promise { const mcpRegistryDataUrl = this.getMcpRegistryDataUrl(); if (!mcpRegistryDataUrl) { this.logService.debug('[DefaultAccount] No MCP registry data URL found'); return undefined; } - try { - const context = await this.requestService.request({ - type: 'GET', - url: mcpRegistryDataUrl, - disableCache: true, - headers: { - 'Authorization': `Bearer ${accessToken}` - } - }, CancellationToken.None); + this.logService.debug('[DefaultAccount] Fetching MCP registry data from:', mcpRegistryDataUrl); + const response = await this.request(mcpRegistryDataUrl, 'GET', undefined, sessions, CancellationToken.None); + if (!response) { + return undefined; + } - const data = await asJson(context); + if (response.res.statusCode && response.res.statusCode !== 200) { + this.logService.trace(`[DefaultAccount] unexpected status code ${response.res.statusCode} while fetching MCP registry data`); + return undefined; + } + + try { + const data = await asJson(response); if (data) { this.logService.debug('Fetched MCP registry providers', data.mcp_registries); return data.mcp_registries[0]; @@ -398,8 +536,56 @@ class DefaultAccountSetup extends Disposable { return undefined; } - private getChatEntitlementUrl(): string | undefined { - if (this.isEnterpriseAuthenticationProvider(this.getDefaultAccountProviderId())) { + private async request(url: string, type: 'GET', body: undefined, sessions: AuthenticationSession[], token: CancellationToken): Promise; + private async request(url: string, type: 'POST', body: object, sessions: AuthenticationSession[], token: CancellationToken): Promise; + private async request(url: string, type: 'GET' | 'POST', body: object | undefined, sessions: AuthenticationSession[], token: CancellationToken): Promise { + let lastResponse: IRequestContext | undefined; + + for (const session of sessions) { + if (token.isCancellationRequested) { + return lastResponse; + } + + try { + const response = await this.requestService.request({ + type, + url, + data: type === 'POST' ? JSON.stringify(body) : undefined, + disableCache: true, + headers: { + 'Authorization': `Bearer ${session.accessToken}` + } + }, token); + + const status = response.res.statusCode; + if (status && status !== 200) { + lastResponse = response; + continue; // try next session + } + + return response; + } catch (error) { + if (!token.isCancellationRequested) { + this.logService.error(`[chat entitlement] request: error ${error}`); + } + } + } + + if (!lastResponse) { + this.logService.trace('[DefaultAccount]: No response received for request', url); + return undefined; + } + + if (lastResponse.res.statusCode && lastResponse.res.statusCode !== 200) { + this.logService.trace(`[DefaultAccount]: unexpected status code ${lastResponse.res.statusCode} for request`, url); + return undefined; + } + + return lastResponse; + } + + private getEntitlementUrl(): string | undefined { + if (this.getDefaultAccountAuthenticationProvider().enterprise) { try { const enterpriseUrl = this.getEnterpriseUrl(); if (!enterpriseUrl) { @@ -411,11 +597,11 @@ class DefaultAccountSetup extends Disposable { } } - return this.defaultAccountConfig.chatEntitlementUrl; + return this.defaultAccountConfig.entitlementUrl; } private getTokenEntitlementUrl(): string | undefined { - if (this.isEnterpriseAuthenticationProvider(this.getDefaultAccountProviderId())) { + if (this.getDefaultAccountAuthenticationProvider().enterprise) { try { const enterpriseUrl = this.getEnterpriseUrl(); if (!enterpriseUrl) { @@ -431,7 +617,7 @@ class DefaultAccountSetup extends Disposable { } private getMcpRegistryDataUrl(): string | undefined { - if (this.isEnterpriseAuthenticationProvider(this.getDefaultAccountProviderId())) { + if (this.getDefaultAccountAuthenticationProvider().enterprise) { try { const enterpriseUrl = this.getEnterpriseUrl(); if (!enterpriseUrl) { @@ -446,15 +632,17 @@ class DefaultAccountSetup extends Disposable { return this.defaultAccountConfig.mcpRegistryDataUrl; } - private getDefaultAccountProviderId(): string { - if (this.configurationService.getValue(this.defaultAccountConfig.authenticationProvider.enterpriseProviderConfig) === this.defaultAccountConfig?.authenticationProvider.enterpriseProviderId) { - return this.defaultAccountConfig.authenticationProvider.enterpriseProviderId; + getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider { + if (this.configurationService.getValue(this.defaultAccountConfig.authenticationProvider.enterpriseProviderConfig) === this.defaultAccountConfig.authenticationProvider.enterprise.id) { + return { + ...this.defaultAccountConfig.authenticationProvider.enterprise, + enterprise: true + }; } - return this.defaultAccountConfig.authenticationProvider.id; - } - - private isEnterpriseAuthenticationProvider(providerId: string): boolean { - return providerId === this.defaultAccountConfig.authenticationProvider.enterpriseProviderId; + return { + ...this.defaultAccountConfig.authenticationProvider.default, + enterprise: false + }; } private getEnterpriseUrl(): URL | undefined { @@ -465,35 +653,27 @@ class DefaultAccountSetup extends Disposable { return new URL(value); } - private registerSignInAction(defaultAccountScopes: string[]): void { - const that = this; - this._register(registerAction2(class extends Action2 { - constructor() { - super({ - id: DEFAULT_ACCOUNT_SIGN_IN_COMMAND, - title: localize('sign in', "Sign in"), - }); - } - async run(accessor: ServicesAccessor, options?: { additionalScopes?: readonly string[];[key: string]: unknown }): Promise { - const authProviderId = that.getDefaultAccountProviderId(); - if (!authProviderId) { - throw new Error('No default account provider configured'); - } - const { additionalScopes, ...sessionOptions } = options ?? {}; - const scopes = additionalScopes ? distinct([...defaultAccountScopes, ...additionalScopes]) : defaultAccountScopes; - const session = await that.authenticationService.createSession(authProviderId, scopes, sessionOptions); - for (const preferredExtension of that.defaultAccountConfig.preferredExtensions) { - that.authenticationExtensionsService.updateAccountPreference(preferredExtension, authProviderId, session.account); - } - } - })); + async signIn(options?: { additionalScopes?: readonly string[];[key: string]: unknown }): Promise { + const authProvider = this.getDefaultAccountAuthenticationProvider(); + if (!authProvider) { + throw new Error('No default account provider configured'); + } + const { additionalScopes, ...sessionOptions } = options ?? {}; + const defaultAccountScopes = this.defaultAccountConfig.authenticationProvider.scopes[0]; + const scopes = additionalScopes ? distinct([...defaultAccountScopes, ...additionalScopes]) : defaultAccountScopes; + const session = await this.authenticationService.createSession(authProvider.id, scopes, sessionOptions); + for (const preferredExtension of this.defaultAccountConfig.preferredExtensions) { + this.authenticationExtensionsService.updateAccountPreference(preferredExtension, authProvider.id, session.account); + } + await this.updateDefaultAccount(); + return this.defaultAccount; } } -class DefaultAccountSetupContribution extends Disposable implements IWorkbenchContribution { +class DefaultAccountProviderContribution extends Disposable implements IWorkbenchContribution { - static ID = 'workbench.contributions.defaultAccountSetup'; + static ID = 'workbench.contributions.defaultAccountProvider'; constructor( @IProductService productService: IProductService, @@ -502,13 +682,9 @@ class DefaultAccountSetupContribution extends Disposable implements IWorkbenchCo @ILogService logService: ILogService, ) { super(); - if (productService.defaultAccount) { - this._register(instantiationService.createInstance(DefaultAccountSetup, productService.defaultAccount)).setup(); - } else { - defaultAccountService.setDefaultAccount(null); - logService.debug('[DefaultAccount] No default account configuration in product service, skipping initialization'); - } + const defaultAccountProvider = this._register(instantiationService.createInstance(DefaultAccountProvider, toDefaultAccountConfig(productService.defaultChatAgent))); + defaultAccountService.setDefaultAccountProvider(defaultAccountProvider); } } -registerWorkbenchContribution2('workbench.contributions.defaultAccountManagement', DefaultAccountSetupContribution, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(DefaultAccountProviderContribution.ID, DefaultAccountProviderContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/services/chat/common/chatEntitlementService.ts b/src/vs/workbench/services/chat/common/chatEntitlementService.ts index 482a93f73e0..5bc38d53b2c 100644 --- a/src/vs/workbench/services/chat/common/chatEntitlementService.ts +++ b/src/vs/workbench/services/chat/common/chatEntitlementService.ts @@ -20,7 +20,7 @@ import { IProductService } from '../../../../platform/product/common/productServ import { asText, IRequestService } from '../../../../platform/request/common/request.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; -import { AuthenticationSession, AuthenticationSessionAccount, IAuthenticationExtensionsService, IAuthenticationService } from '../../authentication/common/authentication.js'; +import { AuthenticationSession, IAuthenticationService } from '../../authentication/common/authentication.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { URI } from '../../../../base/common/uri.js'; import Severity from '../../../../base/common/severity.js'; @@ -28,9 +28,10 @@ import { IWorkbenchEnvironmentService } from '../../environment/common/environme import { isWeb } from '../../../../base/common/platform.js'; import { ILifecycleService } from '../../lifecycle/common/lifecycle.js'; import { Mutable } from '../../../../base/common/types.js'; -import { distinct } from '../../../../base/common/arrays.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { IObservable, observableFromEvent } from '../../../../base/common/observable.js'; +import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js'; +import { IDefaultAccount, IEntitlementsData } from '../../../../base/common/defaultAccount.js'; export namespace ChatEntitlementContextKeys { @@ -179,16 +180,10 @@ export function isProUser(chatEntitlement: ChatEntitlement): boolean { //#region Service Implementation -const defaultChat = { - extensionId: product.defaultChatAgent?.extensionId ?? '', - chatExtensionId: product.defaultChatAgent?.chatExtensionId ?? '', +const defaultChatAgent = { upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '', - provider: product.defaultChatAgent?.provider ?? { default: { id: '' }, enterprise: { id: '' } }, providerUriSetting: product.defaultChatAgent?.providerUriSetting ?? '', - providerScopes: product.defaultChatAgent?.providerScopes ?? [[]], - entitlementUrl: product.defaultChatAgent?.entitlementUrl ?? '', entitlementSignupLimitedUrl: product.defaultChatAgent?.entitlementSignupLimitedUrl ?? '', - completionsAdvancedSetting: product.defaultChatAgent?.completionsAdvancedSetting ?? '', chatQuotaExceededContext: product.defaultChatAgent?.chatQuotaExceededContext ?? '', completionsQuotaExceededContext: product.defaultChatAgent?.completionsQuotaExceededContext ?? '' }; @@ -370,8 +365,8 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme private readonly completionsQuotaExceededContextKey: IContextKey; private ExtensionQuotaContextKeys = { - chatQuotaExceeded: defaultChat.chatQuotaExceededContext, - completionsQuotaExceeded: defaultChat.completionsQuotaExceededContext, + chatQuotaExceeded: defaultChatAgent.chatQuotaExceededContext, + completionsQuotaExceeded: defaultChatAgent.completionsQuotaExceededContext, }; private registerListeners(): void { @@ -486,7 +481,7 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme //#endregion async update(token: CancellationToken): Promise { - await this.requests?.value.forceResolveEntitlement(undefined, token); + await this.requests?.value.forceResolveEntitlement(token); } } @@ -516,44 +511,6 @@ type EntitlementEvent = { quotaResetDate: string | undefined; }; -interface IQuotaSnapshotResponse { - readonly entitlement: number; - readonly overage_count: number; - readonly overage_permitted: boolean; - readonly percent_remaining: number; - readonly remaining: number; - readonly unlimited: boolean; -} - -interface ILegacyQuotaSnapshotResponse { - readonly limited_user_quotas?: { - readonly chat: number; - readonly completions: number; - }; - readonly monthly_quotas?: { - readonly chat: number; - readonly completions: number; - }; -} - -interface IEntitlementsResponse extends ILegacyQuotaSnapshotResponse { - readonly access_type_sku: string; - readonly assigned_date: string; - readonly can_signup_for_limited: boolean; - readonly chat_enabled: boolean; - readonly copilot_plan: string; - readonly organization_login_list: string[]; - readonly analytics_tracking_id: string; - readonly limited_user_reset_date?: string; // for Copilot Free - readonly quota_reset_date?: string; // for all other Copilot SKUs - readonly quota_reset_date_utc?: string; // for all other Copilot SKUs (includes time) - readonly quota_snapshots?: { - chat?: IQuotaSnapshotResponse; - completions?: IQuotaSnapshotResponse; - premium_interactions?: IQuotaSnapshotResponse; - }; -} - interface IEntitlements { readonly entitlement: ChatEntitlement; readonly organisations?: string[]; @@ -584,31 +541,21 @@ interface IQuotas { export class ChatEntitlementRequests extends Disposable { - static providerId(configurationService: IConfigurationService): string { - if (configurationService.getValue(`${defaultChat.completionsAdvancedSetting}.authProvider`) === defaultChat.provider.enterprise.id) { - return defaultChat.provider.enterprise.id; - } - - return defaultChat.provider.default.id; - } - private state: IEntitlements; private pendingResolveCts = new CancellationTokenSource(); - private didResolveEntitlements = false; constructor( private readonly context: ChatEntitlementContext, private readonly chatQuotasAccessor: IChatQuotasAccessor, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IAuthenticationService private readonly authenticationService: IAuthenticationService, @ILogService private readonly logService: ILogService, @IRequestService private readonly requestService: IRequestService, @IDialogService private readonly dialogService: IDialogService, @IOpenerService private readonly openerService: IOpenerService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IAuthenticationExtensionsService private readonly authenticationExtensionsService: IAuthenticationExtensionsService, @ILifecycleService private readonly lifecycleService: ILifecycleService, + @IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService, ) { super(); @@ -620,25 +567,7 @@ export class ChatEntitlementRequests extends Disposable { } private registerListeners(): void { - this._register(this.authenticationService.onDidChangeDeclaredProviders(() => this.resolve())); - - this._register(this.authenticationService.onDidChangeSessions(e => { - if (e.providerId === ChatEntitlementRequests.providerId(this.configurationService)) { - this.resolve(); - } - })); - - this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => { - if (e.id === ChatEntitlementRequests.providerId(this.configurationService)) { - this.resolve(); - } - })); - - this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => { - if (e.id === ChatEntitlementRequests.providerId(this.configurationService)) { - this.resolve(); - } - })); + this._register(this.defaultAccountService.onDidChangeDefaultAccount(() => this.resolve())); this._register(this.context.onDidChange(() => { if (!this.context.state.installed || this.context.state.disabled || this.context.state.entitlement === ChatEntitlement.Unknown) { @@ -654,149 +583,65 @@ export class ChatEntitlementRequests extends Disposable { this.pendingResolveCts.dispose(true); const cts = this.pendingResolveCts = new CancellationTokenSource(); - const session = await this.findMatchingProviderSession(cts.token); + const defaultAccount = await this.defaultAccountService.getDefaultAccount(); if (cts.token.isCancellationRequested) { return; } // Immediately signal whether we have a session or not let state: IEntitlements | undefined = undefined; - if (session) { + if (defaultAccount) { // Do not overwrite any state we have already if (this.state.entitlement === ChatEntitlement.Unknown) { state = { entitlement: ChatEntitlement.Unresolved }; } } else { - this.didResolveEntitlements = false; // reset so that we resolve entitlements fresh when signed in again state = { entitlement: ChatEntitlement.Unknown }; } if (state) { this.update(state); } - if (session && !this.didResolveEntitlements) { + if (defaultAccount) { // Afterwards resolve entitlement with a network request // but only unless it was not already resolved before. - await this.resolveEntitlement(session, cts.token); + await this.resolveEntitlement(defaultAccount, cts.token); } } - private async findMatchingProviderSession(token: CancellationToken): Promise { - const sessions = await this.doGetSessions(ChatEntitlementRequests.providerId(this.configurationService)); - if (token.isCancellationRequested) { - return undefined; - } - - const matchingSessions = new Set(); - for (const session of sessions) { - for (const scopes of defaultChat.providerScopes) { - if (this.includesScopes(session.scopes, scopes)) { - matchingSessions.add(session); - } - } - } - - // We intentionally want to return an array of matching sessions and - // not just the first, because it is possible that a matching session - // has an expired token. As such, we want to try them all until we - // succeeded with the request. - return matchingSessions.size > 0 ? Array.from(matchingSessions) : undefined; - } - - private async doGetSessions(providerId: string): Promise { - const preferredAccountName = this.authenticationExtensionsService.getAccountPreference(defaultChat.chatExtensionId, providerId) ?? this.authenticationExtensionsService.getAccountPreference(defaultChat.extensionId, providerId); - let preferredAccount: AuthenticationSessionAccount | undefined; - for (const account of await this.authenticationService.getAccounts(providerId)) { - if (account.label === preferredAccountName) { - preferredAccount = account; - break; - } - } - - try { - return await this.authenticationService.getSessions(providerId, undefined, { account: preferredAccount }); - } catch (error) { - // ignore - errors can throw if a provider is not registered - } - - return []; - } - - private includesScopes(scopes: ReadonlyArray, expectedScopes: string[]): boolean { - return expectedScopes.every(scope => scopes.includes(scope)); - } - - private async resolveEntitlement(sessions: AuthenticationSession[], token: CancellationToken): Promise { - const entitlements = await this.doResolveEntitlement(sessions, token); + private async resolveEntitlement(defaultAccount: IDefaultAccount, token: CancellationToken): Promise { + const entitlements = await this.doResolveEntitlement(defaultAccount, token); if (typeof entitlements?.entitlement === 'number' && !token.isCancellationRequested) { - this.didResolveEntitlements = true; this.update(entitlements); } - return entitlements; } - private async doResolveEntitlement(sessions: AuthenticationSession[], token: CancellationToken): Promise { + private async doResolveEntitlement(defaultAccount: IDefaultAccount, token: CancellationToken): Promise { if (token.isCancellationRequested) { return undefined; } - const response = await this.request(this.getEntitlementUrl(), 'GET', undefined, sessions, token); - if (token.isCancellationRequested) { - return undefined; - } - - if (!response) { - this.logService.trace('[chat entitlement]: no response'); - return { entitlement: ChatEntitlement.Unresolved }; - } - - if (response.res.statusCode && response.res.statusCode !== 200) { - this.logService.trace(`[chat entitlement]: unexpected status code ${response.res.statusCode}`); - return ( - response.res.statusCode === 401 || // oauth token being unavailable (expired/revoked) - response.res.statusCode === 404 // missing scopes/permissions, service pretends the endpoint doesn't exist - ) ? { entitlement: ChatEntitlement.Unknown /* treat as signed out */ } : { entitlement: ChatEntitlement.Unresolved }; - } - - let responseText: string | null = null; - try { - responseText = await asText(response); - } catch (error) { - // ignore - handled below - } - if (token.isCancellationRequested) { - return undefined; - } - - if (!responseText) { - this.logService.trace('[chat entitlement]: response has no content'); - return { entitlement: ChatEntitlement.Unresolved }; - } - - let entitlementsResponse: IEntitlementsResponse; - try { - entitlementsResponse = JSON.parse(responseText); - this.logService.trace(`[chat entitlement]: parsed result is ${JSON.stringify(entitlementsResponse)}`); - } catch (err) { - this.logService.trace(`[chat entitlement]: error parsing response (${err})`); - return { entitlement: ChatEntitlement.Unresolved }; + const entitlementsData = defaultAccount.entitlementsData; + if (!entitlementsData) { + this.logService.trace('[chat entitlement]: no entitlements data available on default account'); + return { entitlement: entitlementsData === null ? ChatEntitlement.Unknown : ChatEntitlement.Unresolved }; } let entitlement: ChatEntitlement; - if (entitlementsResponse.access_type_sku === 'free_limited_copilot') { + if (entitlementsData.access_type_sku === 'free_limited_copilot') { entitlement = ChatEntitlement.Free; - } else if (entitlementsResponse.can_signup_for_limited) { + } else if (entitlementsData.can_signup_for_limited) { entitlement = ChatEntitlement.Available; - } else if (entitlementsResponse.copilot_plan === 'individual') { + } else if (entitlementsData.copilot_plan === 'individual') { entitlement = ChatEntitlement.Pro; - } else if (entitlementsResponse.copilot_plan === 'individual_pro') { + } else if (entitlementsData.copilot_plan === 'individual_pro') { entitlement = ChatEntitlement.ProPlus; - } else if (entitlementsResponse.copilot_plan === 'business') { + } else if (entitlementsData.copilot_plan === 'business') { entitlement = ChatEntitlement.Business; - } else if (entitlementsResponse.copilot_plan === 'enterprise') { + } else if (entitlementsData.copilot_plan === 'enterprise') { entitlement = ChatEntitlement.Enterprise; - } else if (entitlementsResponse.chat_enabled) { + } else if (entitlementsData.chat_enabled) { // This should never happen as we exhaustively list the plans above. But if a new plan is added in the future older clients won't break entitlement = ChatEntitlement.Pro; } else { @@ -805,15 +650,15 @@ export class ChatEntitlementRequests extends Disposable { const entitlements: IEntitlements = { entitlement, - organisations: entitlementsResponse.organization_login_list, - quotas: this.toQuotas(entitlementsResponse), - sku: entitlementsResponse.access_type_sku + organisations: entitlementsData.organization_login_list, + quotas: this.toQuotas(entitlementsData), + sku: entitlementsData.access_type_sku }; this.logService.trace(`[chat entitlement]: resolved to ${entitlements.entitlement}, quotas: ${JSON.stringify(entitlements.quotas)}`); this.telemetryService.publicLog2('chatInstallEntitlement', { entitlement: entitlements.entitlement, - tid: entitlementsResponse.analytics_tracking_id, + tid: entitlementsData.analytics_tracking_id, sku: entitlements.sku, quotaChat: entitlements.quotas?.chat?.remaining, quotaPremiumChat: entitlements.quotas?.premiumChat?.remaining, @@ -824,42 +669,29 @@ export class ChatEntitlementRequests extends Disposable { return entitlements; } - private getEntitlementUrl(): string { - if (ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.provider.enterprise.id) { - try { - const enterpriseUrl = new URL(this.configurationService.getValue(defaultChat.providerUriSetting)); - return `${enterpriseUrl.protocol}//api.${enterpriseUrl.hostname}${enterpriseUrl.port ? ':' + enterpriseUrl.port : ''}/copilot_internal/user`; - } catch (error) { - this.logService.error(error); - } - } - - return defaultChat.entitlementUrl; - } - - private toQuotas(response: IEntitlementsResponse): IQuotas { + private toQuotas(entitlementsData: IEntitlementsData): IQuotas { const quotas: Mutable = { - resetDate: response.quota_reset_date_utc ?? response.quota_reset_date ?? response.limited_user_reset_date, - resetDateHasTime: typeof response.quota_reset_date_utc === 'string', + resetDate: entitlementsData.quota_reset_date_utc ?? entitlementsData.quota_reset_date ?? entitlementsData.limited_user_reset_date, + resetDateHasTime: typeof entitlementsData.quota_reset_date_utc === 'string', }; // Legacy Free SKU Quota - if (response.monthly_quotas?.chat && typeof response.limited_user_quotas?.chat === 'number') { + if (entitlementsData.monthly_quotas?.chat && typeof entitlementsData.limited_user_quotas?.chat === 'number') { quotas.chat = { - total: response.monthly_quotas.chat, - remaining: response.limited_user_quotas.chat, - percentRemaining: Math.min(100, Math.max(0, (response.limited_user_quotas.chat / response.monthly_quotas.chat) * 100)), + total: entitlementsData.monthly_quotas.chat, + remaining: entitlementsData.limited_user_quotas.chat, + percentRemaining: Math.min(100, Math.max(0, (entitlementsData.limited_user_quotas.chat / entitlementsData.monthly_quotas.chat) * 100)), overageEnabled: false, overageCount: 0, unlimited: false }; } - if (response.monthly_quotas?.completions && typeof response.limited_user_quotas?.completions === 'number') { + if (entitlementsData.monthly_quotas?.completions && typeof entitlementsData.limited_user_quotas?.completions === 'number') { quotas.completions = { - total: response.monthly_quotas.completions, - remaining: response.limited_user_quotas.completions, - percentRemaining: Math.min(100, Math.max(0, (response.limited_user_quotas.completions / response.monthly_quotas.completions) * 100)), + total: entitlementsData.monthly_quotas.completions, + remaining: entitlementsData.limited_user_quotas.completions, + percentRemaining: Math.min(100, Math.max(0, (entitlementsData.limited_user_quotas.completions / entitlementsData.monthly_quotas.completions) * 100)), overageEnabled: false, overageCount: 0, unlimited: false @@ -867,9 +699,9 @@ export class ChatEntitlementRequests extends Disposable { } // New Quota Snapshot - if (response.quota_snapshots) { + if (entitlementsData.quota_snapshots) { for (const quotaType of ['chat', 'completions', 'premium_interactions'] as const) { - const rawQuotaSnapshot = response.quota_snapshots[quotaType]; + const rawQuotaSnapshot = entitlementsData.quota_snapshots[quotaType]; if (!rawQuotaSnapshot) { continue; } @@ -947,28 +779,33 @@ export class ChatEntitlementRequests extends Disposable { } } - async forceResolveEntitlement(sessions: AuthenticationSession[] | undefined, token = CancellationToken.None): Promise { - if (!sessions) { - sessions = await this.findMatchingProviderSession(token); - } - - if (!sessions || sessions.length === 0) { + async forceResolveEntitlement(token = CancellationToken.None): Promise { + const defaultAccount = await this.defaultAccountService.refresh(); + if (!defaultAccount) { return undefined; } - return this.resolveEntitlement(sessions, token); + return this.resolveEntitlement(defaultAccount, token); } - async signUpFree(sessions: AuthenticationSession[]): Promise { + async signUpFree(): Promise { + const sessions = await this.getSessions(); + if (sessions.length === 0) { + return undefined; + } + return this.doSignUpFree(sessions); + } + + private async doSignUpFree(sessions: AuthenticationSession[]): Promise { const body = { restricted_telemetry: this.telemetryService.telemetryLevel === TelemetryLevel.NONE ? 'disabled' : 'enabled', public_code_suggestions: 'enabled' }; - const response = await this.request(defaultChat.entitlementSignupLimitedUrl, 'POST', body, sessions, CancellationToken.None); + const response = await this.request(defaultChatAgent.entitlementSignupLimitedUrl, 'POST', body, sessions, CancellationToken.None); if (!response) { const retry = await this.onUnknownSignUpError(localize('signUpNoResponseError', "No response received."), '[chat entitlement] sign-up: no response'); - return retry ? this.signUpFree(sessions) : { errorCode: 1 }; + return retry ? this.doSignUpFree(sessions) : { errorCode: 1 }; } if (response.res.statusCode && response.res.statusCode !== 200) { @@ -987,7 +824,7 @@ export class ChatEntitlementRequests extends Disposable { } } const retry = await this.onUnknownSignUpError(localize('signUpUnexpectedStatusError', "Unexpected status code {0}.", response.res.statusCode), `[chat entitlement] sign-up: unexpected status code ${response.res.statusCode}`); - return retry ? this.signUpFree(sessions) : { errorCode: response.res.statusCode }; + return retry ? this.doSignUpFree(sessions) : { errorCode: response.res.statusCode }; } let responseText: string | null = null; @@ -999,7 +836,7 @@ export class ChatEntitlementRequests extends Disposable { if (!responseText) { const retry = await this.onUnknownSignUpError(localize('signUpNoResponseContentsError', "Response has no contents."), '[chat entitlement] sign-up: response has no content'); - return retry ? this.signUpFree(sessions) : { errorCode: 2 }; + return retry ? this.doSignUpFree(sessions) : { errorCode: 2 }; } let parsedResult: { subscribed: boolean } | undefined = undefined; @@ -1008,7 +845,7 @@ export class ChatEntitlementRequests extends Disposable { this.logService.trace(`[chat entitlement] sign-up: response is ${responseText}`); } catch (err) { const retry = await this.onUnknownSignUpError(localize('signUpInvalidResponseError', "Invalid response contents."), `[chat entitlement] sign-up: error parsing response (${err})`); - return retry ? this.signUpFree(sessions) : { errorCode: 3 }; + return retry ? this.doSignUpFree(sessions) : { errorCode: 3 }; } // We have made it this far, so the user either did sign-up or was signed-up already. @@ -1018,6 +855,18 @@ export class ChatEntitlementRequests extends Disposable { return Boolean(parsedResult?.subscribed); } + private async getSessions(): Promise { + const defaultAccount = await this.defaultAccountService.getDefaultAccount(); + if (defaultAccount) { + const sessions = await this.authenticationService.getSessions(defaultAccount.authenticationProvider.id); + const accountSessions = sessions.filter(s => s.id === defaultAccount.sessionId); + if (accountSessions.length) { + return accountSessions; + } + } + return [...(await this.authenticationService.getSessions(this.defaultAccountService.getDefaultAccountAuthenticationProvider().id))]; + } + private async onUnknownSignUpError(detail: string, logMessage: string): Promise { this.logService.error(logMessage); @@ -1050,31 +899,25 @@ export class ChatEntitlementRequests extends Disposable { }, { label: localize('learnMore', "Learn More"), - run: () => this.openerService.open(URI.parse(defaultChat.upgradePlanUrl)) + run: () => this.openerService.open(URI.parse(defaultChatAgent.upgradePlanUrl)) } ] }); } } - async signIn(options?: { useSocialProvider?: string; additionalScopes?: readonly string[] }) { - const providerId = ChatEntitlementRequests.providerId(this.configurationService); + async signIn(options?: { useSocialProvider?: string; additionalScopes?: readonly string[] }): Promise<{ defaultAccount?: IDefaultAccount; entitlements?: IEntitlements }> { + const defaultAccount = await this.defaultAccountService.signIn({ + additionalScopes: options?.additionalScopes, + extraAuthorizeParameters: { get_started_with: 'copilot-vscode' }, + provider: options?.useSocialProvider + }); + if (!defaultAccount) { + return {}; + } - const scopes = options?.additionalScopes ? distinct([...defaultChat.providerScopes[0], ...options.additionalScopes]) : defaultChat.providerScopes[0]; - const session = await this.authenticationService.createSession( - providerId, - scopes, - { - extraAuthorizeParameters: { get_started_with: 'copilot-vscode' }, - provider: options?.useSocialProvider - }); - - this.authenticationExtensionsService.updateAccountPreference(defaultChat.extensionId, providerId, session.account); - this.authenticationExtensionsService.updateAccountPreference(defaultChat.chatExtensionId, providerId, session.account); - - const entitlements = await this.forceResolveEntitlement([session]); - - return { session, entitlements }; + const entitlements = await this.doResolveEntitlement(defaultAccount, CancellationToken.None); + return { defaultAccount, entitlements }; } override dispose(): void { diff --git a/src/vs/workbench/services/extensionManagement/electron-browser/extensionGalleryManifestService.ts b/src/vs/workbench/services/extensionManagement/electron-browser/extensionGalleryManifestService.ts index ba14adae9a9..603ad3e902c 100644 --- a/src/vs/workbench/services/extensionManagement/electron-browser/extensionGalleryManifestService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-browser/extensionGalleryManifestService.ts @@ -148,8 +148,8 @@ export class WorkbenchExtensionGalleryManifestService extends ExtensionGalleryMa } private checkAccess(account: IDefaultAccount): boolean { - this.logService.debug('[Marketplace] Checking Account SKU access for configured gallery', account.access_type_sku); - if (account.access_type_sku && this.productService.extensionsGallery?.accessSKUs?.includes(account.access_type_sku)) { + this.logService.debug('[Marketplace] Checking Account SKU access for configured gallery', account.entitlementsData?.access_type_sku); + if (account.entitlementsData?.access_type_sku && this.productService.extensionsGallery?.accessSKUs?.includes(account.entitlementsData.access_type_sku)) { this.logService.debug('[Marketplace] Account has access to configured gallery'); return true; } diff --git a/src/vs/workbench/services/policies/test/common/accountPolicyService.test.ts b/src/vs/workbench/services/policies/test/common/accountPolicyService.test.ts index a3af59e5c77..73b8f9eff67 100644 --- a/src/vs/workbench/services/policies/test/common/accountPolicyService.test.ts +++ b/src/vs/workbench/services/policies/test/common/accountPolicyService.test.ts @@ -4,22 +4,50 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { Event } from '../../../../../base/common/event.js'; import { NullLogService } from '../../../../../platform/log/common/log.js'; -import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; +import { IDefaultAccountProvider, IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; import { DefaultAccountService } from '../../../accounts/common/defaultAccount.js'; import { AccountPolicyService } from '../../common/accountPolicyService.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; import { Extensions, IConfigurationNode, IConfigurationRegistry } from '../../../../../platform/configuration/common/configurationRegistry.js'; import { DefaultConfiguration, PolicyConfiguration } from '../../../../../platform/configuration/common/configurations.js'; -import { IDefaultAccount } from '../../../../../base/common/defaultAccount.js'; +import { IDefaultAccount, IDefaultAccountAuthenticationProvider } from '../../../../../base/common/defaultAccount.js'; import { PolicyCategory } from '../../../../../base/common/policy.js'; +import { TestProductService } from '../../../../test/common/workbenchTestServices.js'; const BASE_DEFAULT_ACCOUNT: IDefaultAccount = { - enterprise: false, + authenticationProvider: { + id: 'github', + name: 'GitHub', + enterprise: false, + }, sessionId: 'abc123', + enterprise: false, }; +class DefaultAccountProvider implements IDefaultAccountProvider { + + readonly onDidChangeDefaultAccount = Event.None; + + constructor( + readonly defaultAccount: IDefaultAccount, + ) { } + + getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider { + return this.defaultAccount.authenticationProvider; + } + + async refresh(): Promise { + return this.defaultAccount; + } + + async signIn(): Promise { + return null; + } +} + suite('AccountPolicyService', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -53,7 +81,7 @@ suite('AccountPolicyService', () => { category: PolicyCategory.Extensions, minimumVersion: '1.0.0', localization: { description: { key: '', value: '' } }, - value: account => account.chat_preview_features_enabled === false ? 'policyValueB' : undefined, + value: account => account.policyData?.chat_preview_features_enabled === false ? 'policyValueB' : undefined, } }, 'setting.C': { @@ -64,7 +92,7 @@ suite('AccountPolicyService', () => { category: PolicyCategory.Extensions, minimumVersion: '1.0.0', localization: { description: { key: '', value: '' } }, - value: account => account.chat_preview_features_enabled === false ? JSON.stringify(['policyValueC1', 'policyValueC2']) : undefined, + value: account => account.policyData?.chat_preview_features_enabled === false ? JSON.stringify(['policyValueC1', 'policyValueC2']) : undefined, } }, 'setting.D': { @@ -75,7 +103,7 @@ suite('AccountPolicyService', () => { category: PolicyCategory.Extensions, minimumVersion: '1.0.0', localization: { description: { key: '', value: '' } }, - value: account => account.chat_preview_features_enabled === false ? false : undefined, + value: account => account.policyData?.chat_preview_features_enabled === false ? false : undefined, } }, 'setting.E': { @@ -93,14 +121,15 @@ suite('AccountPolicyService', () => { const defaultConfiguration = disposables.add(new DefaultConfiguration(new NullLogService())); await defaultConfiguration.initialize(); - defaultAccountService = disposables.add(new DefaultAccountService()); + defaultAccountService = disposables.add(new DefaultAccountService(TestProductService)); policyService = disposables.add(new AccountPolicyService(logService, defaultAccountService)); policyConfiguration = disposables.add(new PolicyConfiguration(defaultConfiguration, policyService, new NullLogService())); }); async function assertDefaultBehavior(defaultAccount: IDefaultAccount) { - defaultAccountService.setDefaultAccount(defaultAccount); + defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(defaultAccount)); + await defaultAccountService.refresh(); await policyConfiguration.initialize(); @@ -135,13 +164,14 @@ suite('AccountPolicyService', () => { }); test('should initialize with default account and preview features enabled', async () => { - const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, chat_preview_features_enabled: true }; + const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, policyData: { chat_preview_features_enabled: true } }; await assertDefaultBehavior(defaultAccount); }); test('should initialize with default account and preview features disabled', async () => { - const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, chat_preview_features_enabled: false }; - defaultAccountService.setDefaultAccount(defaultAccount); + const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, policyData: { chat_preview_features_enabled: false } }; + defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(defaultAccount)); + await defaultAccountService.refresh(); await policyConfiguration.initialize(); const actualConfigurationModel = policyConfiguration.configurationModel; diff --git a/src/vs/workbench/services/policies/test/common/multiplexPolicyService.test.ts b/src/vs/workbench/services/policies/test/common/multiplexPolicyService.test.ts index d410478e5b2..481e3a4e795 100644 --- a/src/vs/workbench/services/policies/test/common/multiplexPolicyService.test.ts +++ b/src/vs/workbench/services/policies/test/common/multiplexPolicyService.test.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { Event } from '../../../../../base/common/event.js'; import { NullLogService } from '../../../../../platform/log/common/log.js'; -import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; +import { IDefaultAccountProvider, IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; import { DefaultAccountService } from '../../../accounts/common/defaultAccount.js'; import { AccountPolicyService } from '../../common/accountPolicyService.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; @@ -19,14 +20,41 @@ import { IFileService } from '../../../../../platform/files/common/files.js'; import { InMemoryFileSystemProvider } from '../../../../../platform/files/common/inMemoryFilesystemProvider.js'; import { FileService } from '../../../../../platform/files/common/fileService.js'; import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { IDefaultAccount } from '../../../../../base/common/defaultAccount.js'; +import { IDefaultAccount, IDefaultAccountAuthenticationProvider } from '../../../../../base/common/defaultAccount.js'; import { PolicyCategory } from '../../../../../base/common/policy.js'; +import { TestProductService } from '../../../../test/common/workbenchTestServices.js'; const BASE_DEFAULT_ACCOUNT: IDefaultAccount = { + authenticationProvider: { + id: 'github', + name: 'GitHub', + enterprise: false, + }, enterprise: false, sessionId: 'abc123', }; +class DefaultAccountProvider implements IDefaultAccountProvider { + + readonly onDidChangeDefaultAccount = Event.None; + + constructor( + readonly defaultAccount: IDefaultAccount, + ) { } + + getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider { + return this.defaultAccount.authenticationProvider; + } + + async refresh(): Promise { + return this.defaultAccount; + } + + async signIn(): Promise { + return null; + } +} + suite('MultiplexPolicyService', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -62,7 +90,7 @@ suite('MultiplexPolicyService', () => { category: PolicyCategory.Extensions, minimumVersion: '1.0.0', localization: { description: { key: '', value: '' } }, - value: account => account.chat_preview_features_enabled === false ? 'policyValueB' : undefined, + value: account => account.policyData?.chat_preview_features_enabled === false ? 'policyValueB' : undefined, } }, 'setting.C': { @@ -73,7 +101,7 @@ suite('MultiplexPolicyService', () => { category: PolicyCategory.Extensions, minimumVersion: '1.0.0', localization: { description: { key: '', value: '' } }, - value: account => account.chat_preview_features_enabled === false ? JSON.stringify(['policyValueC1', 'policyValueC2']) : undefined, + value: account => account.policyData?.chat_preview_features_enabled === false ? JSON.stringify(['policyValueC1', 'policyValueC2']) : undefined, } }, 'setting.D': { @@ -84,7 +112,7 @@ suite('MultiplexPolicyService', () => { category: PolicyCategory.Extensions, minimumVersion: '1.0.0', localization: { description: { key: '', value: '' } }, - value: account => account.chat_preview_features_enabled === false ? false : undefined, + value: account => account.policyData?.chat_preview_features_enabled === false ? false : undefined, } }, 'setting.E': { @@ -106,7 +134,7 @@ suite('MultiplexPolicyService', () => { const diskFileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); disposables.add(fileService.registerProvider(policyFile.scheme, diskFileSystemProvider)); - defaultAccountService = disposables.add(new DefaultAccountService()); + defaultAccountService = disposables.add(new DefaultAccountService(TestProductService)); policyService = disposables.add(new MultiplexPolicyService([ disposables.add(new FilePolicyService(policyFile, fileService, new NullLogService())), disposables.add(new AccountPolicyService(logService, defaultAccountService)), @@ -115,8 +143,6 @@ suite('MultiplexPolicyService', () => { }); async function clear() { - // Reset - defaultAccountService.setDefaultAccount({ ...BASE_DEFAULT_ACCOUNT }); await fileService.writeFile(policyFile, VSBuffer.fromString( JSON.stringify({}) @@ -161,7 +187,8 @@ suite('MultiplexPolicyService', () => { await clear(); const defaultAccount = { ...BASE_DEFAULT_ACCOUNT }; - defaultAccountService.setDefaultAccount(defaultAccount); + defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(defaultAccount)); + await defaultAccountService.refresh(); await fileService.writeFile(policyFile, VSBuffer.fromString( @@ -201,8 +228,9 @@ suite('MultiplexPolicyService', () => { test('policy from default account only', async () => { await clear(); - const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, chat_preview_features_enabled: false }; - defaultAccountService.setDefaultAccount(defaultAccount); + const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, policyData: { chat_preview_features_enabled: false } }; + defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(defaultAccount)); + await defaultAccountService.refresh(); await fileService.writeFile(policyFile, VSBuffer.fromString( @@ -241,8 +269,9 @@ suite('MultiplexPolicyService', () => { test('policy from file and default account', async () => { await clear(); - const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, chat_preview_features_enabled: false }; - defaultAccountService.setDefaultAccount(defaultAccount); + const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, policyData: { chat_preview_features_enabled: false } }; + defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(defaultAccount)); + await defaultAccountService.refresh(); await fileService.writeFile(policyFile, VSBuffer.fromString( From 471da7c9b848098f64440c4a4626b3758f24bd03 Mon Sep 17 00:00:00 2001 From: Takashi Tamura Date: Mon, 19 Jan 2026 18:49:17 +0900 Subject: [PATCH 171/387] Optimize rendering performance by scheduling DOM updates at the next animation frame in NativeEditContext and TextAreaEditContext (#285906) Co-authored-by: Alexandru Dima --- .../editContext/native/nativeEditContext.ts | 20 +++++++++++++++++-- .../textArea/textAreaEditContext.ts | 20 ++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index 6334e8bdfe1..f803edeb372 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -5,10 +5,11 @@ import './nativeEditContext.css'; import { isFirefox } from '../../../../../base/browser/browser.js'; -import { addDisposableListener, getActiveElement, getWindow, getWindowId } from '../../../../../base/browser/dom.js'; +import { addDisposableListener, getActiveElement, getWindow, getWindowId, scheduleAtNextAnimationFrame } from '../../../../../base/browser/dom.js'; import { FastDomNode } from '../../../../../base/browser/fastDomNode.js'; import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js'; import { KeyCode } from '../../../../../base/common/keyCodes.js'; +import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { EditorOption } from '../../../../common/config/editorOptions.js'; import { EndOfLinePreference, IModelDeltaDecoration } from '../../../../common/model.js'; @@ -68,6 +69,7 @@ export class NativeEditContext extends AbstractEditContext { private _targetWindowId: number = -1; private _scrollTop: number = 0; private _scrollLeft: number = 0; + private _selectionAndControlBoundsUpdateDisposable: IDisposable | undefined; private readonly _focusTracker: FocusTracker; @@ -233,6 +235,8 @@ export class NativeEditContext extends AbstractEditContext { this.domNode.domNode.blur(); this.domNode.domNode.remove(); this._imeTextArea.domNode.remove(); + this._selectionAndControlBoundsUpdateDisposable?.dispose(); + this._selectionAndControlBoundsUpdateDisposable = undefined; super.dispose(); } @@ -505,7 +509,19 @@ export class NativeEditContext extends AbstractEditContext { } } - private _updateSelectionAndControlBoundsAfterRender() { + private _updateSelectionAndControlBoundsAfterRender(): void { + if (this._selectionAndControlBoundsUpdateDisposable) { + return; + } + // Schedule this work after render so we avoid triggering a layout while still painting. + const targetWindow = getWindow(this.domNode.domNode); + this._selectionAndControlBoundsUpdateDisposable = scheduleAtNextAnimationFrame(targetWindow, () => { + this._selectionAndControlBoundsUpdateDisposable = undefined; + this._applySelectionAndControlBounds(); + }); + } + + private _applySelectionAndControlBounds(): void { const options = this._context.configuration.options; const contentLeft = options.get(EditorOption.layoutInfo).contentLeft; diff --git a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts index b1eab383d05..53988fb113b 100644 --- a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts @@ -6,6 +6,7 @@ import './textAreaEditContext.css'; import * as nls from '../../../../../nls.js'; import * as browser from '../../../../../base/browser/browser.js'; +import { scheduleAtNextAnimationFrame, getWindow } from '../../../../../base/browser/dom.js'; import { FastDomNode, createFastDomNode } from '../../../../../base/browser/fastDomNode.js'; import { IKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js'; import * as platform from '../../../../../base/common/platform.js'; @@ -31,6 +32,7 @@ import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from '../../../../../base/browser/ui import { TokenizationRegistry } from '../../../../common/languages.js'; import { ColorId, ITokenPresentation } from '../../../../common/encodedTokenAttributes.js'; import { Color } from '../../../../../base/common/color.js'; +import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { IME } from '../../../../../base/common/ime.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; @@ -139,6 +141,7 @@ export class TextAreaEditContext extends AbstractEditContext { * This is useful for hit-testing and determining the mouse position. */ private _lastRenderPosition: Position | null; + private _scheduledRender: IDisposable | null = null; public readonly textArea: FastDomNode; public readonly textAreaCover: FastDomNode; @@ -459,6 +462,8 @@ export class TextAreaEditContext extends AbstractEditContext { } public override dispose(): void { + this._scheduledRender?.dispose(); + this._scheduledRender = null; super.dispose(); this.textArea.domNode.remove(); this.textAreaCover.domNode.remove(); @@ -682,7 +687,20 @@ export class TextAreaEditContext extends AbstractEditContext { public render(ctx: RestrictedRenderingContext): void { this._textAreaInput.writeNativeTextAreaContent('render'); - this._render(); + this._scheduleRender(); + } + + // Delay expensive DOM updates until the next animation frame to reduce reflow pressure. + private _scheduleRender(): void { + if (this._scheduledRender) { + return; + } + + const targetWindow = getWindow(this.textArea.domNode); + this._scheduledRender = scheduleAtNextAnimationFrame(targetWindow, () => { + this._scheduledRender = null; + this._render(); + }); } private _render(): void { From 91eeae05dd2c02b7e79c0f27ea1a1437a40611bd Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 19 Jan 2026 10:52:40 +0100 Subject: [PATCH 172/387] Abstract CopyPasteController from DOM clipboard APIs Introduce widget-level onWillCopy/onWillCut/onWillPaste events that bubble up from edit contexts through View to CodeEditorWidget. CopyPasteController now subscribes to these abstracted events instead of directly listening to DOM clipboard events. --- .../controller/editContext/clipboardUtils.ts | 182 ++++++++++++++++++ .../controller/editContext/editContext.ts | 12 ++ .../editContext/native/nativeEditContext.ts | 30 ++- .../textArea/textAreaEditContext.ts | 5 + .../textArea/textAreaEditContextInput.ts | 38 +++- src/vs/editor/browser/editorBrowser.ts | 19 ++ src/vs/editor/browser/view.ts | 29 ++- .../widget/codeEditor/codeEditorWidget.ts | 15 ++ .../contrib/clipboard/browser/clipboard.ts | 4 +- .../browser/copyPasteController.ts | 65 +++---- 10 files changed, 353 insertions(+), 46 deletions(-) diff --git a/src/vs/editor/browser/controller/editContext/clipboardUtils.ts b/src/vs/editor/browser/controller/editContext/clipboardUtils.ts index 09ca8745315..334e62e1385 100644 --- a/src/vs/editor/browser/controller/editContext/clipboardUtils.ts +++ b/src/vs/editor/browser/controller/editContext/clipboardUtils.ts @@ -169,3 +169,185 @@ export const ClipboardEventUtils = { clipboardData.setData('vscode-editor-data', JSON.stringify(metadata)); } }; + +/** + * Abstracted clipboard data that does not directly expose DOM ClipboardEvent/DataTransfer. + * This allows editor contributions to work with clipboard data without DOM dependencies. + */ +export interface IClipboardData { + /** + * The text content from the clipboard. + */ + readonly text: string; + + /** + * The HTML content from the clipboard, if available. + */ + readonly html: string | undefined; + + /** + * VS Code editor metadata associated with this clipboard data. + */ + readonly metadata: ClipboardStoredMetadata | null; + + /** + * All MIME types present in the clipboard. + */ + readonly types: readonly string[]; + + /** + * Files from the clipboard (for paste operations). + */ + readonly files: readonly File[]; + + /** + * Get data for a specific MIME type. + */ + getData(type: string): string; +} + +/** + * Writable clipboard data for copy/cut operations. + */ +export interface IWritableClipboardData extends IClipboardData { + /** + * Set data for a specific MIME type. + */ + setData(type: string, value: string): void; +} + +/** + * Event data for clipboard copy/cut events. + */ +export interface IClipboardCopyEvent { + /** + * Whether this is a cut operation. + */ + readonly isCut: boolean; + + /** + * The clipboard data to write to. + */ + readonly clipboardData: IWritableClipboardData; + + /** + * The underlying DOM event, if available. + * @deprecated Use clipboardData instead. This is provided for backward compatibility. + */ + readonly browserEvent: ClipboardEvent | undefined; + + /** + * Signal that the event has been handled and default processing should be skipped. + */ + setHandled(): void; + + /** + * Whether the event has been marked as handled. + */ + readonly isHandled: boolean; +} + +/** + * Event data for clipboard paste events. + */ +export interface IClipboardPasteEvent { + /** + * The clipboard data being pasted. + */ + readonly clipboardData: IClipboardData; + + /** + * The underlying DOM event, if available. + * @deprecated Use clipboardData instead. This is provided for backward compatibility. + */ + readonly browserEvent: ClipboardEvent | undefined; + + /** + * Signal that the event has been handled and default processing should be skipped. + */ + setHandled(): void; + + /** + * Whether the event has been marked as handled. + */ + readonly isHandled: boolean; +} + +/** + * Creates an IClipboardData from a DOM DataTransfer. + */ +export function createClipboardData(dataTransfer: DataTransfer): IClipboardData { + const [text, metadata] = ClipboardEventUtils.getTextData(dataTransfer); + const html = dataTransfer.getData('text/html') || undefined; + const files: File[] = Array.prototype.slice.call(dataTransfer.files, 0); + + return { + text, + html, + metadata, + types: Array.from(dataTransfer.types), + files, + getData: (type: string) => dataTransfer.getData(type), + }; +} + +/** + * Creates an IWritableClipboardData from a DOM DataTransfer. + */ +export function createWritableClipboardData(dataTransfer: DataTransfer): IWritableClipboardData { + const base = createClipboardData(dataTransfer); + return { + ...base, + setData: (type: string, value: string) => dataTransfer.setData(type, value), + }; +} + +/** + * Creates an IClipboardCopyEvent from a DOM ClipboardEvent. + */ +export function createClipboardCopyEvent(e: ClipboardEvent, isCut: boolean): IClipboardCopyEvent { + let handled = false; + return { + isCut, + clipboardData: e.clipboardData ? createWritableClipboardData(e.clipboardData) : { + text: '', + html: undefined, + metadata: null, + types: [], + files: [], + getData: () => '', + setData: () => { }, + }, + browserEvent: e, + setHandled: () => { + handled = true; + e.preventDefault(); + e.stopImmediatePropagation(); + }, + get isHandled() { return handled; }, + }; +} + +/** + * Creates an IClipboardPasteEvent from a DOM ClipboardEvent. + */ +export function createClipboardPasteEvent(e: ClipboardEvent): IClipboardPasteEvent { + let handled = false; + return { + clipboardData: e.clipboardData ? createClipboardData(e.clipboardData) : { + text: '', + html: undefined, + metadata: null, + types: [], + files: [], + getData: () => '', + }, + browserEvent: e, + setHandled: () => { + handled = true; + e.preventDefault(); + e.stopImmediatePropagation(); + }, + get isHandled() { return handled; }, + }; +} diff --git a/src/vs/editor/browser/controller/editContext/editContext.ts b/src/vs/editor/browser/controller/editContext/editContext.ts index edcf2be3361..c5e677252f9 100644 --- a/src/vs/editor/browser/controller/editContext/editContext.ts +++ b/src/vs/editor/browser/controller/editContext/editContext.ts @@ -4,9 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { FastDomNode } from '../../../../base/browser/fastDomNode.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; import { Position } from '../../../common/core/position.js'; import { IEditorAriaOptions } from '../../editorBrowser.js'; import { ViewPart } from '../../view/viewPart.js'; +import { IClipboardCopyEvent, IClipboardPasteEvent } from './clipboardUtils.js'; export abstract class AbstractEditContext extends ViewPart { abstract domNode: FastDomNode; @@ -16,4 +18,14 @@ export abstract class AbstractEditContext extends ViewPart { abstract setAriaOptions(options: IEditorAriaOptions): void; abstract getLastRenderData(): Position | null; abstract writeScreenReaderContent(reason: string): void; + + // Clipboard events - emitted before the default clipboard handling + protected readonly _onWillCopy = this._register(new Emitter()); + public readonly onWillCopy: Event = this._onWillCopy.event; + + protected readonly _onWillCut = this._register(new Emitter()); + public readonly onWillCut: Event = this._onWillCut.event; + + protected readonly _onWillPaste = this._register(new Emitter()); + public readonly onWillPaste: Event = this._onWillPaste.event; } diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index 6334e8bdfe1..0ccadb7d8bd 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -16,7 +16,7 @@ import { ViewConfigurationChangedEvent, ViewCursorStateChangedEvent, ViewDecorat import { ViewContext } from '../../../../common/viewModel/viewContext.js'; import { RestrictedRenderingContext, RenderingContext, HorizontalPosition } from '../../../view/renderingContext.js'; import { ViewController } from '../../../view/viewController.js'; -import { ClipboardEventUtils, ensureClipboardGetsEditorSelection, InMemoryClipboardMetadataManager } from '../clipboardUtils.js'; +import { ClipboardEventUtils, createClipboardCopyEvent, createClipboardPasteEvent, ensureClipboardGetsEditorSelection, InMemoryClipboardMetadataManager } from '../clipboardUtils.js'; import { AbstractEditContext } from '../editContext.js'; import { editContextAddDisposableListener, FocusTracker, ITypeData } from './nativeEditContextUtils.js'; import { ScreenReaderSupport } from './screenReaderSupport.js'; @@ -114,10 +114,20 @@ export class NativeEditContext extends AbstractEditContext { this._register(addDisposableListener(this.domNode.domNode, 'copy', (e) => { this.logService.trace('NativeEditContext#copy'); + const copyEvent = createClipboardCopyEvent(e, /* isCut */ false); + this._onWillCopy.fire(copyEvent); + if (copyEvent.isHandled) { + return; + } ensureClipboardGetsEditorSelection(e, this._context, this.logService, isFirefox); })); this._register(addDisposableListener(this.domNode.domNode, 'cut', (e) => { this.logService.trace('NativeEditContext#cut'); + const cutEvent = createClipboardCopyEvent(e, /* isCut */ true); + this._onWillCut.fire(cutEvent); + if (cutEvent.isHandled) { + return; + } // Pretend here we touched the text area, as the `cut` event will most likely // result in a `selectionchange` event which we want to ignore this._screenReaderSupport.onWillCut(); @@ -141,6 +151,12 @@ export class NativeEditContext extends AbstractEditContext { })); this._register(addDisposableListener(this.domNode.domNode, 'paste', (e) => { this.logService.trace('NativeEditContext#paste'); + const pasteEvent = createClipboardPasteEvent(e); + this._onWillPaste.fire(pasteEvent); + if (pasteEvent.isHandled) { + e.preventDefault(); + return; + } e.preventDefault(); if (!e.clipboardData) { return; @@ -313,17 +329,17 @@ export class NativeEditContext extends AbstractEditContext { return true; } - public onWillPaste(): void { - this.logService.trace('NativeEditContext#onWillPaste'); - this._onWillPaste(); + public handleWillPaste(): void { + this.logService.trace('NativeEditContext#handleWillPaste'); + this._prepareScreenReaderForPaste(); } - private _onWillPaste(): void { + private _prepareScreenReaderForPaste(): void { this._screenReaderSupport.onWillPaste(); } - public onWillCopy(): void { - this.logService.trace('NativeEditContext#onWillCopy'); + public handleWillCopy(): void { + this.logService.trace('NativeEditContext#handleWillCopy'); this.logService.trace('NativeEditContext#isFocused : ', this.domNode.domNode === getActiveElement()); } diff --git a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts index b1eab383d05..f19cc424373 100644 --- a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts @@ -277,6 +277,11 @@ export class TextAreaEditContext extends AbstractEditContext { isSafari: browser.isSafari, })); + // Relay clipboard events from TextAreaInput + this._register(this._textAreaInput.onWillCopy(e => this._onWillCopy.fire(e))); + this._register(this._textAreaInput.onWillCut(e => this._onWillCut.fire(e))); + this._register(this._textAreaInput.onWillPaste(e => this._onWillPaste.fire(e))); + this._register(this._textAreaInput.onKeyDown((e: IKeyboardEvent) => { this._viewController.emitKeyDown(e); })); diff --git a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts index fa7ecddebff..a3e1d8a5b3c 100644 --- a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts +++ b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts @@ -18,7 +18,7 @@ import { Position } from '../../../../common/core/position.js'; import { Selection } from '../../../../common/core/selection.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; -import { ClipboardEventUtils, ClipboardStoredMetadata, ensureClipboardGetsEditorSelection, InMemoryClipboardMetadataManager } from '../clipboardUtils.js'; +import { ClipboardEventUtils, ClipboardStoredMetadata, createClipboardCopyEvent, createClipboardPasteEvent, ensureClipboardGetsEditorSelection, IClipboardCopyEvent, IClipboardPasteEvent, InMemoryClipboardMetadataManager } from '../clipboardUtils.js'; import { _debugComposition, ITextAreaWrapper, ITypeData, TextAreaState } from './textAreaEditContextState.js'; import { ViewContext } from '../../../../common/viewModel/viewContext.js'; @@ -127,6 +127,15 @@ export class TextAreaInput extends Disposable { private _onPaste = this._register(new Emitter()); public readonly onPaste: Event = this._onPaste.event; + private _onWillCopy = this._register(new Emitter()); + public readonly onWillCopy: Event = this._onWillCopy.event; + + private _onWillCut = this._register(new Emitter()); + public readonly onWillCut: Event = this._onWillCut.event; + + private _onWillPaste = this._register(new Emitter()); + public readonly onWillPaste: Event = this._onWillPaste.event; + private _onType = this._register(new Emitter()); public readonly onType: Event = this._onType.event; @@ -359,6 +368,15 @@ export class TextAreaInput extends Disposable { this._register(this._textArea.onCut((e) => { this._logService.trace(`TextAreaInput#onCut`, e); + + // Fire onWillCut event to allow interception + const cutEvent = createClipboardCopyEvent(e, /* isCut */ true); + this._onWillCut.fire(cutEvent); + if (cutEvent.isHandled) { + // Event was handled externally, skip default processing + return; + } + // Pretend here we touched the text area, as the `cut` event will most likely // result in a `selectionchange` event which we want to ignore this._textArea.setIgnoreSelectionChangeTime('received cut event'); @@ -371,6 +389,15 @@ export class TextAreaInput extends Disposable { this._register(this._textArea.onCopy((e) => { this._logService.trace(`TextAreaInput#onCopy`, e); + + // Fire onWillCopy event to allow interception + const copyEvent = createClipboardCopyEvent(e, /* isCut */ false); + this._onWillCopy.fire(copyEvent); + if (copyEvent.isHandled) { + // Event was handled externally, skip default processing + return; + } + if (this._host.context) { ensureClipboardGetsEditorSelection(e, this._host.context, this._logService, this._browser.isFirefox); } @@ -378,6 +405,15 @@ export class TextAreaInput extends Disposable { this._register(this._textArea.onPaste((e) => { this._logService.trace(`TextAreaInput#onPaste`, e); + + // Fire onWillPaste event to allow interception + const pasteEvent = createClipboardPasteEvent(e); + this._onWillPaste.fire(pasteEvent); + if (pasteEvent.isHandled) { + // Event was handled externally, skip default processing + return; + } + // Pretend here we touched the text area, as the `paste` event will most likely // result in a `selectionchange` event which we want to ignore this._textArea.setIgnoreSelectionChangeTime('received paste event'); diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index f2ac74abf99..cf774c63bba 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -27,6 +27,7 @@ import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguag import { IEditorWhitespace, IViewModel } from '../common/viewModel.js'; import { OverviewRulerZone } from '../common/viewModel/overviewZoneManager.js'; import { IEditorConstructionOptions } from './config/editorConfiguration.js'; +import { IClipboardCopyEvent, IClipboardPasteEvent } from './controller/editContext/clipboardUtils.js'; /** * A view zone is a full horizontal rectangle that 'pushes' text down. @@ -725,6 +726,24 @@ export interface ICodeEditor extends editorCommon.IEditor { * @event */ readonly onDidPaste: Event; + /** + * An event emitted before clipboard copy operation starts. + * @internal + * @event + */ + readonly onWillCopy: Event; + /** + * An event emitted before clipboard cut operation starts. + * @internal + * @event + */ + readonly onWillCut: Event; + /** + * An event emitted before clipboard paste operation starts. + * @internal + * @event + */ + readonly onWillPaste: Event; /** * An event emitted on a "mouseup". * @event diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 913c7c970e2..2521a18eec7 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -9,7 +9,7 @@ import { IMouseWheelEvent } from '../../base/browser/mouseEvent.js'; import { inputLatency } from '../../base/browser/performance.js'; import { CodeWindow } from '../../base/browser/window.js'; import { BugIndicatingError, onUnexpectedError } from '../../base/common/errors.js'; -import { Disposable, IDisposable } from '../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable } from '../../base/common/lifecycle.js'; import { IPointerHandlerHelper } from './controller/mouseHandler.js'; import { PointerHandlerLastRenderData } from './controller/mouseTarget.js'; import { PointerHandler } from './controller/pointerHandler.js'; @@ -58,6 +58,7 @@ import { IColorTheme, getThemeTypeSelector } from '../../platform/theme/common/t import { ViewGpuContext } from './gpu/viewGpuContext.js'; import { ViewLinesGpu } from './viewParts/viewLinesGpu/viewLinesGpu.js'; import { AbstractEditContext } from './controller/editContext/editContext.js'; +import { IClipboardCopyEvent, IClipboardPasteEvent } from './controller/editContext/clipboardUtils.js'; import { IVisibleRangeProvider, TextAreaEditContext } from './controller/editContext/textArea/textAreaEditContext.js'; import { NativeEditContext } from './controller/editContext/native/nativeEditContext.js'; import { RulersGpu } from './viewParts/rulersGpu/rulersGpu.js'; @@ -106,8 +107,19 @@ export class View extends ViewEventHandler { private _editContextEnabled: boolean; private _accessibilitySupport: AccessibilitySupport; private _editContext: AbstractEditContext; + private readonly _editContextClipboardListeners = new DisposableStore(); private readonly _pointerHandler: PointerHandler; + // Clipboard events relayed from editContext + private readonly _onWillCopy = this._register(new Emitter()); + public readonly onWillCopy: Event = this._onWillCopy.event; + + private readonly _onWillCut = this._register(new Emitter()); + public readonly onWillCut: Event = this._onWillCut.event; + + private readonly _onWillPaste = this._register(new Emitter()); + public readonly onWillPaste: Event = this._onWillPaste.event; + // Dom nodes private readonly _linesContent: FastDomNode; public readonly domNode: FastDomNode; @@ -160,6 +172,7 @@ export class View extends ViewEventHandler { this._editContextEnabled = this._context.configuration.options.get(EditorOption.effectiveEditContext); this._accessibilitySupport = this._context.configuration.options.get(EditorOption.accessibilitySupport); this._editContext = this._instantiateEditContext(); + this._connectEditContextClipboardEvents(); this._viewParts.push(this._editContext); @@ -309,6 +322,7 @@ export class View extends ViewEventHandler { const indexOfEditContext = this._viewParts.indexOf(this._editContext); this._editContext.dispose(); this._editContext = this._instantiateEditContext(); + this._connectEditContextClipboardEvents(); if (isEditContextFocused) { this._editContext.focus(); } @@ -317,6 +331,16 @@ export class View extends ViewEventHandler { } } + private _connectEditContextClipboardEvents(): void { + // Dispose old listeners + this._editContextClipboardListeners.clear(); + + // Connect to current edit context's clipboard events + this._editContextClipboardListeners.add(this._editContext.onWillCopy(e => this._onWillCopy.fire(e))); + this._editContextClipboardListeners.add(this._editContext.onWillCut(e => this._onWillCut.fire(e))); + this._editContextClipboardListeners.add(this._editContext.onWillPaste(e => this._onWillPaste.fire(e))); + } + private _computeGlyphMarginLanes(): IGlyphMarginLanesModel { const model = this._context.viewModel.model; const laneModel = this._context.viewModel.glyphLanes; @@ -474,6 +498,9 @@ export class View extends ViewEventHandler { this._renderAnimationFrame = null; } + // Dispose clipboard event listeners + this._editContextClipboardListeners.dispose(); + this._contentWidgets.overflowingContentWidgetsDomNode.domNode.remove(); this._overlayWidgets.overflowingOverlayWidgetsDomNode.domNode.remove(); diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index 753bd958113..a53d266f5f4 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -18,6 +18,7 @@ import { applyFontInfo } from '../../config/domFontInfo.js'; import { EditorConfiguration, IEditorConstructionOptions } from '../../config/editorConfiguration.js'; import { TabFocus } from '../../config/tabFocus.js'; import * as editorBrowser from '../../editorBrowser.js'; +import { IClipboardCopyEvent, IClipboardPasteEvent } from '../../controller/editContext/clipboardUtils.js'; import { EditorExtensionsRegistry, IEditorContributionDescription } from '../../editorExtensions.js'; import { ICodeEditorService } from '../../services/codeEditorService.js'; import { IContentWidgetData, IGlyphMarginWidgetData, IOverlayWidgetData, View } from '../../view.js'; @@ -147,6 +148,15 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE private readonly _onDidPaste: Emitter = this._register(new InteractionEmitter(this._contributions, this._deliveryQueue)); public readonly onDidPaste = this._onDidPaste.event; + private readonly _onWillCopy: Emitter = this._register(new InteractionEmitter(this._contributions, this._deliveryQueue)); + public readonly onWillCopy = this._onWillCopy.event; + + private readonly _onWillCut: Emitter = this._register(new InteractionEmitter(this._contributions, this._deliveryQueue)); + public readonly onWillCut = this._onWillCut.event; + + private readonly _onWillPaste: Emitter = this._register(new InteractionEmitter(this._contributions, this._deliveryQueue)); + public readonly onWillPaste = this._onWillPaste.event; + private readonly _onMouseUp: Emitter = this._register(new InteractionEmitter(this._contributions, this._deliveryQueue)); public readonly onMouseUp: Event = this._onMouseUp.event; @@ -1880,6 +1890,11 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE view.render(false, true); view.domNode.domNode.setAttribute('data-uri', model.uri.toString()); + + // Connect clipboard events from View + listenersToRemove.push(view.onWillCopy(e => this._onWillCopy.fire(e))); + listenersToRemove.push(view.onWillCut(e => this._onWillCut.fire(e))); + listenersToRemove.push(view.onWillPaste(e => this._onWillPaste.fire(e))); } this._modelData = new ModelData(model, viewModel, view, hasRealView, listenersToRemove, attachedView); diff --git a/src/vs/editor/contrib/clipboard/browser/clipboard.ts b/src/vs/editor/contrib/clipboard/browser/clipboard.ts index 59079079e51..c492206c2f2 100644 --- a/src/vs/editor/contrib/clipboard/browser/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/browser/clipboard.ts @@ -266,7 +266,7 @@ function logCopyCommand(editor: ICodeEditor) { if (editContextEnabled) { const nativeEditContext = NativeEditContextRegistry.get(editor.getId()); if (nativeEditContext) { - nativeEditContext.onWillCopy(); + nativeEditContext.handleWillCopy(); } } } @@ -290,7 +290,7 @@ if (PasteAction) { if (editContextEnabled) { const nativeEditContext = NativeEditContextRegistry.get(focusedEditor.getId()); if (nativeEditContext) { - nativeEditContext.onWillPaste(); + nativeEditContext.handleWillPaste(); } } diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index 3d524982981..be555c84b9a 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { addDisposableListener } from '../../../../base/browser/dom.js'; import { IAction } from '../../../../base/common/actions.js'; import { coalesce } from '../../../../base/common/arrays.js'; import { CancelablePromise, createCancelablePromise, DeferredPromise, raceCancellation } from '../../../../base/common/async.js'; @@ -25,7 +24,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; -import { ClipboardEventUtils, InMemoryClipboardMetadataManager } from '../../../browser/controller/editContext/clipboardUtils.js'; +import { ClipboardEventUtils, IClipboardCopyEvent, IClipboardPasteEvent, InMemoryClipboardMetadataManager } from '../../../browser/controller/editContext/clipboardUtils.js'; import { toExternalVSDataTransfer, toVSDataTransfer } from '../../../browser/dataTransfer.js'; import { ICodeEditor, PastePayload } from '../../../browser/editorBrowser.js'; import { IBulkEditService } from '../../../browser/services/bulkEditService.js'; @@ -130,10 +129,9 @@ export class CopyPasteController extends Disposable implements IEditorContributi this._editor = editor; - const container = editor.getContainerDomNode(); - this._register(addDisposableListener(container, 'copy', e => this.handleCopy(e))); - this._register(addDisposableListener(container, 'cut', e => this.handleCopy(e))); - this._register(addDisposableListener(container, 'paste', e => this.handlePaste(e), true)); + this._register(editor.onWillCopy(e => this.handleCopy(e))); + this._register(editor.onWillCut(e => this.handleCopy(e))); + this._register(editor.onWillPaste(e => this.handlePaste(e))); this._pasteProgressManager = this._register(new InlineProgressManager('pasteIntoEditor', editor, instantiationService)); @@ -171,10 +169,11 @@ export class CopyPasteController extends Disposable implements IEditorContributi await this._currentPasteOperation; } - private handleCopy(e: ClipboardEvent) { + private handleCopy(e: IClipboardCopyEvent) { + const clipboardData = e.browserEvent?.clipboardData; let id: string | null = null; - if (e.clipboardData) { - const [text, metadata] = ClipboardEventUtils.getTextData(e.clipboardData); + if (clipboardData) { + const [text, metadata] = ClipboardEventUtils.getTextData(clipboardData); const storedMetadata = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text); id = storedMetadata?.id || null; this._logService.trace('CopyPasteController#handleCopy for id : ', id, ' with text.length : ', text.length); @@ -190,7 +189,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi // This means the resources clipboard is not properly updated when copying from the editor. this._clipboardService.clearInternalState?.(); - if (!e.clipboardData || !this.isPasteAsEnabled()) { + if (!clipboardData || !this.isPasteAsEnabled()) { return; } @@ -225,16 +224,16 @@ export class CopyPasteController extends Disposable implements IEditorContributi .ordered(model) .filter(x => !!x.prepareDocumentPaste); if (!providers.length) { - this.setCopyMetadata(e.clipboardData, { defaultPastePayload }); + this.setCopyMetadata(clipboardData, { defaultPastePayload }); return; } - const dataTransfer = toVSDataTransfer(e.clipboardData); + const dataTransfer = toVSDataTransfer(clipboardData); const providerCopyMimeTypes = providers.flatMap(x => x.copyMimeTypes ?? []); // Save off a handle pointing to data that VS Code maintains. const handle = id ?? generateUuid(); - this.setCopyMetadata(e.clipboardData, { + this.setCopyMetadata(clipboardData, { id: handle, providerCopyMimeTypes, defaultPastePayload @@ -256,15 +255,16 @@ export class CopyPasteController extends Disposable implements IEditorContributi CopyPasteController._currentCopyOperation = { handle, operations }; } - private async handlePaste(e: ClipboardEvent) { - if (e.clipboardData) { - const [text, metadata] = ClipboardEventUtils.getTextData(e.clipboardData); + private async handlePaste(e: IClipboardPasteEvent) { + const clipboardData = e.browserEvent?.clipboardData; + if (clipboardData) { + const [text, metadata] = ClipboardEventUtils.getTextData(clipboardData); const metadataComputed = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text); this._logService.trace('CopyPasteController#handlePaste for id : ', metadataComputed?.id); } else { this._logService.trace('CopyPasteController#handlePaste'); } - if (!e.clipboardData || !this._editor.hasTextFocus()) { + if (!clipboardData || !this._editor.hasTextFocus()) { return; } @@ -285,15 +285,15 @@ export class CopyPasteController extends Disposable implements IEditorContributi return; } - const metadata = this.fetchCopyMetadata(e); - this._logService.trace('CopyPasteController#handlePaste with metadata : ', metadata?.id, ' and text.length : ', e.clipboardData.getData('text/plain').length); - const dataTransfer = toExternalVSDataTransfer(e.clipboardData); + const metadata = this.fetchCopyMetadata(clipboardData); + this._logService.trace('CopyPasteController#handlePaste with metadata : ', metadata?.id, ' and text.length : ', clipboardData.getData('text/plain').length); + const dataTransfer = toExternalVSDataTransfer(clipboardData); dataTransfer.delete(vscodeClipboardMime); - const fileTypes = Array.from(e.clipboardData.files).map(file => file.type); + const fileTypes = Array.from(clipboardData.files).map(file => file.type); const allPotentialMimeTypes = [ - ...e.clipboardData.types, + ...clipboardData.types, ...fileTypes, ...metadata?.providerCopyMimeTypes ?? [], // TODO: always adds `uri-list` because this get set if there are resources in the system clipboard. @@ -321,8 +321,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi this.showPasteAsNoEditMessage(selections, this._pasteAsActionContext.preferred); // Also prevent default paste from applying - e.preventDefault(); - e.stopImmediatePropagation(); + e.setHandled(); } return; } @@ -330,13 +329,12 @@ export class CopyPasteController extends Disposable implements IEditorContributi // Prevent the editor's default paste handler from running. // Note that after this point, we are fully responsible for handling paste. // If we can't provider a paste for any reason, we need to explicitly delegate pasting back to the editor. - e.preventDefault(); - e.stopImmediatePropagation(); + e.setHandled(); if (this._pasteAsActionContext) { this.showPasteAsPick(this._pasteAsActionContext.preferred, allProviders, selections, dataTransfer, metadata); } else { - this.doPasteInline(allProviders, selections, dataTransfer, metadata, e); + this.doPasteInline(allProviders, selections, dataTransfer, metadata, e.browserEvent); } } @@ -350,7 +348,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi MessageController.get(this._editor)?.showMessage(localize('pasteAsError', "No paste edits for '{0}' found", kindLabel), selections[0].getStartPosition()); } - private doPasteInline(allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, clipboardEvent: ClipboardEvent): void { + private doPasteInline(allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, clipboardEvent: ClipboardEvent | undefined): void { this._logService.trace('CopyPasteController#doPasteInline'); const editor = this._editor; if (!editor.hasModel()) { @@ -562,14 +560,11 @@ export class CopyPasteController extends Disposable implements IEditorContributi dataTransfer.setData(vscodeClipboardMime, JSON.stringify(metadata)); } - private fetchCopyMetadata(e: ClipboardEvent): CopyMetadata | undefined { + private fetchCopyMetadata(clipboardData: DataTransfer): CopyMetadata | undefined { this._logService.trace('CopyPasteController#fetchCopyMetadata'); - if (!e.clipboardData) { - return; - } // Prefer using the clipboard data we saved off - const rawMetadata = e.clipboardData.getData(vscodeClipboardMime); + const rawMetadata = clipboardData.getData(vscodeClipboardMime); if (rawMetadata) { try { return JSON.parse(rawMetadata); @@ -579,7 +574,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi } // Otherwise try to extract the generic text editor metadata - const [_, metadata] = ClipboardEventUtils.getTextData(e.clipboardData); + const [_, metadata] = ClipboardEventUtils.getTextData(clipboardData); if (metadata) { return { defaultPastePayload: { @@ -657,7 +652,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi }; } - private async applyDefaultPasteHandler(dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken, clipboardEvent: ClipboardEvent) { + private async applyDefaultPasteHandler(dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken, clipboardEvent: ClipboardEvent | undefined) { const textDataTransfer = dataTransfer.get(Mimes.text) ?? dataTransfer.get('text'); const text = (await textDataTransfer?.asString()) ?? ''; if (token.isCancellationRequested) { From 2ef69332792a300ee276120c0533ecbab8981e11 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Mon, 19 Jan 2026 11:24:46 +0100 Subject: [PATCH 173/387] Move continue chat in into agent type picker --- .../browser/actions/chatContinueInAction.ts | 8 +- .../browser/actions/chatExecuteActions.ts | 73 ++++++++++++++++++- .../browser/agentSessions/agentSessions.ts | 11 +++ .../contrib/chat/browser/chat.contribution.ts | 2 - src/vs/workbench/contrib/chat/browser/chat.ts | 10 +++ .../browser/widget/input/chatInputPart.ts | 52 +++++++++---- .../delegationSessionPickerActionItem.ts | 39 ++++++++++ .../input/sessionTargetPickerActionItem.ts | 54 +++++++++----- 8 files changed, 208 insertions(+), 41 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts index 4ba1db28fbf..23132be452d 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts @@ -36,7 +36,7 @@ import { IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/ import { ChatAgentLocation } from '../../common/constants.js'; import { PROMPT_LANGUAGE_ID } from '../../common/promptSyntax/promptTypes.js'; import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../agentSessions/agentSessions.js'; -import { IChatWidgetService } from '../chat.js'; +import { IChatWidget, IChatWidgetService } from '../chat.js'; import { ctxHasEditorModification } from '../chatEditing/chatEditingEditorContextKeys.js'; import { CHAT_SETUP_ACTION_ID } from './chatActions.js'; import { PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/attachments/chatVariableEntries.js'; @@ -202,14 +202,14 @@ export class ChatContinueInSessionActionItem extends ActionWidgetDropdownActionV const NEW_CHAT_SESSION_ACTION_ID = 'workbench.action.chat.openNewSessionEditor'; -class CreateRemoteAgentJobAction { +export class CreateRemoteAgentJobAction { constructor() { } private openUntitledEditor(commandService: ICommandService, continuationTarget: IChatSessionsExtensionPoint) { commandService.executeCommand(`${NEW_CHAT_SESSION_ACTION_ID}.${continuationTarget.type}`); } - async run(accessor: ServicesAccessor, continuationTarget: IChatSessionsExtensionPoint) { + async run(accessor: ServicesAccessor, continuationTarget: IChatSessionsExtensionPoint, _widget?: IChatWidget) { const contextKeyService = accessor.get(IContextKeyService); const commandService = accessor.get(ICommandService); const widgetService = accessor.get(IChatWidgetService); @@ -222,7 +222,7 @@ class CreateRemoteAgentJobAction { try { remoteJobCreatingKey.set(true); - const widget = widgetService.lastFocusedWidget; + const widget = _widget ?? widgetService.lastFocusedWidget; if (!widget || !widget.viewModel) { return this.openUntitledEditor(commandService, continuationTarget); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 80e6d09bce6..ba1da5abcf3 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -27,11 +27,13 @@ import { IChatService } from '../../common/chatService/chatService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind, } from '../../common/constants.js'; import { ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { ILanguageModelToolsService } from '../../common/tools/languageModelToolsService.js'; +import { IChatSessionsService } from '../../common/chatSessionsService.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; +import { getAgentSessionProvider, AgentSessionProviders } from '../agentSessions/agentSessions.js'; import { getEditingSessionContext } from '../chatEditing/chatEditingActions.js'; import { ctxHasEditorModification } from '../chatEditing/chatEditingEditorContextKeys.js'; import { ACTION_ID_NEW_CHAT, CHAT_CATEGORY, handleCurrentEditingSession, handleModeSwitch } from './chatActions.js'; -import { ContinueChatInSessionAction } from './chatContinueInAction.js'; +import { CreateRemoteAgentJobAction } from './chatContinueInAction.js'; export interface IVoiceChatExecuteActionContext { readonly disableTimeout?: boolean; @@ -49,6 +51,13 @@ abstract class SubmitAction extends Action2 { const telemetryService = accessor.get(ITelemetryService); const widgetService = accessor.get(IChatWidgetService); const widget = context?.widget ?? widgetService.lastFocusedWidget; + + // Check if there's a pending delegation target + const pendingDelegationTarget = widget?.input.pendingDelegationTarget; + if (pendingDelegationTarget && pendingDelegationTarget !== AgentSessionProviders.Local) { + return await this.handleDelegation(accessor, widget, pendingDelegationTarget); + } + if (widget?.viewModel?.editing) { const configurationService = accessor.get(IConfigurationService); const dialogService = accessor.get(IDialogService); @@ -144,6 +153,27 @@ abstract class SubmitAction extends Action2 { } widget?.acceptInput(context?.inputValue); } + + private async handleDelegation(accessor: ServicesAccessor, widget: IChatWidget, delegationTarget: Exclude): Promise { + const chatSessionsService = accessor.get(IChatSessionsService); + + // Find the contribution for the delegation target + const contributions = chatSessionsService.getAllChatSessionContributions(); + const targetContribution = contributions.find(contrib => { + const providerType = getAgentSessionProvider(contrib.type); + return providerType === delegationTarget; + }); + + if (!targetContribution) { + throw new Error(`No contribution found for delegation target: ${delegationTarget}`); + } + + if (targetContribution.canDelegate === false) { + throw new Error(`The contribution for delegation target: ${delegationTarget} does not support delegation.`); + } + + return new CreateRemoteAgentJobAction().run(accessor, targetContribution, widget); + } } const whenNotInProgress = ChatContextKeys.requestInProgress.negate(); @@ -449,7 +479,8 @@ export class OpenSessionTargetPickerAction extends Action2 { ChatContextKeys.enabled, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), ChatContextKeys.inQuickChat.negate(), - ChatContextKeys.hasCanDelegateProviders), + ChatContextKeys.hasCanDelegateProviders, + ContextKeyExpr.or(ChatContextKeys.chatSessionIsEmpty, ChatContextKeys.hasCanDelegateProviders.negate())), group: 'navigation', }, ] @@ -465,6 +496,42 @@ export class OpenSessionTargetPickerAction extends Action2 { } } +export class OpenDelegationPickerAction extends Action2 { + static readonly ID = 'workbench.action.chat.openDelegationPicker'; + + constructor() { + super({ + id: OpenDelegationPickerAction.ID, + title: localize2('interactive.openDelegationPicker.label', "Open Delegation Picker"), + tooltip: localize('delegateSession', "Delegate Session"), + category: CHAT_CATEGORY, + f1: false, + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.hasCanDelegateProviders, ChatContextKeys.chatSessionIsEmpty.negate()), + menu: [ + { + id: MenuId.ChatInput, + order: 0.5, + when: ContextKeyExpr.and( + ChatContextKeys.enabled, + ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), + ChatContextKeys.inQuickChat.negate(), + ChatContextKeys.hasCanDelegateProviders, + ContextKeyExpr.and(ChatContextKeys.chatSessionIsEmpty.negate(), ChatContextKeys.hasCanDelegateProviders)), + group: 'navigation', + }, + ] + }); + } + + override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + const widgetService = accessor.get(IChatWidgetService); + const widget = widgetService.lastFocusedWidget; + if (widget) { + widget.input.openDelegationPicker(); + } + } +} + export class ChatSessionPrimaryPickerAction extends Action2 { static readonly ID = 'workbench.action.chat.chatSessionPrimaryPicker'; constructor() { @@ -793,12 +860,12 @@ export function registerChatExecuteActions() { registerAction2(CancelAction); registerAction2(SendToNewChatAction); registerAction2(ChatSubmitWithCodebaseAction); - registerAction2(ContinueChatInSessionAction); registerAction2(ToggleChatModeAction); registerAction2(SwitchToNextModelAction); registerAction2(OpenModelPickerAction); registerAction2(OpenModePickerAction); registerAction2(OpenSessionTargetPickerAction); + registerAction2(OpenDelegationPickerAction); registerAction2(ChatSessionPrimaryPickerAction); registerAction2(ChangeChatModelAction); registerAction2(CancelEdit); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts index a72ab1cd2e6..682eeb42f20 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts @@ -57,6 +57,17 @@ export function getAgentSessionProviderIcon(provider: AgentSessionProviders): Th } } +export function isFirstPartyAgentSessionProvider(provider: AgentSessionProviders): boolean { + switch (provider) { + case AgentSessionProviders.Local: + case AgentSessionProviders.Background: + case AgentSessionProviders.Cloud: + return true; + case AgentSessionProviders.ClaudeCode: + return false; + } +} + export enum AgentSessionsViewerOrientation { Stacked = 1, SideBySide, diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 91a886107c5..7ef4c651b6d 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -69,7 +69,6 @@ import { ACTION_ID_NEW_CHAT, CopilotTitleBarMenuRendering, ModeOpenChatGlobalAct import { CodeBlockActionRendering, registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from './actions/chatCodeblockActions.js'; import { ChatContextContributions } from './actions/chatContext.js'; import { registerChatContextActions } from './actions/chatContextActions.js'; -import { ContinueChatInSessionActionRendering } from './actions/chatContinueInAction.js'; import { registerChatCopyActions } from './actions/chatCopyActions.js'; import { registerChatDeveloperActions } from './actions/chatDeveloperActions.js'; import { ChatSubmitAction, registerChatExecuteActions } from './actions/chatExecuteActions.js'; @@ -1214,7 +1213,6 @@ registerWorkbenchContribution2(ChatPromptFilesExtensionPointHandler.ID, ChatProm registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNotifier, WorkbenchPhase.Eventually); registerWorkbenchContribution2(CopilotTitleBarMenuRendering.ID, CopilotTitleBarMenuRendering, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(CodeBlockActionRendering.ID, CodeBlockActionRendering, WorkbenchPhase.BlockRestore); -registerWorkbenchContribution2(ContinueChatInSessionActionRendering.ID, ContinueChatInSessionActionRendering, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatImplicitContextContribution.ID, ChatImplicitContextContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatRelatedFilesContribution.ID, ChatRelatedFilesContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatViewsWelcomeHandler.ID, ChatViewsWelcomeHandler, WorkbenchPhase.BlockStartup); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index d4ac8d70efa..1066c998358 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -41,6 +41,16 @@ export interface ISessionTypePickerDelegate { * This allows the welcome view to maintain independent state from the main chat panel. */ setActiveSessionProvider?(provider: AgentSessionProviders): void; + /** + * Optional getter for the pending delegation target - the target that will be used when submit is pressed. + */ + getPendingDelegationTarget?(): AgentSessionProviders | undefined; + /** + * Optional setter for the pending delegation target. + * When a user selects a different session provider in a non-empty chat, + * this stores the target for delegation on the next submit instead of immediately creating a new session. + */ + setPendingDelegationTarget?(provider: AgentSessionProviders): void; /** * Optional event that fires when the active session provider changes. * When provided, listeners (like chatInputPart) can react to session type changes diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 2db1c584e4b..fe8d666b932 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -92,9 +92,9 @@ import { ChatHistoryNavigator } from '../../../common/widget/chatWidgetHistorySe import { ChatAgentLocation, ChatConfiguration, ChatModeKind, validateChatMode } from '../../../common/constants.js'; import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../../common/languageModels.js'; import { ILanguageModelToolsService } from '../../../common/tools/languageModelToolsService.js'; -import { ActionLocation, ChatContinueInSessionActionItem, ContinueChatInSessionAction } from '../../actions/chatContinueInAction.js'; -import { ChatSessionPrimaryPickerAction, ChatSubmitAction, IChatExecuteActionContext, OpenModelPickerAction, OpenModePickerAction, OpenSessionTargetPickerAction } from '../../actions/chatExecuteActions.js'; +import { ChatSessionPrimaryPickerAction, ChatSubmitAction, IChatExecuteActionContext, OpenDelegationPickerAction, OpenModelPickerAction, OpenModePickerAction, OpenSessionTargetPickerAction } from '../../actions/chatExecuteActions.js'; import { IAgentSessionsService } from '../../agentSessions/agentSessionsService.js'; +import { AgentSessionProviders, getAgentSessionProvider } from '../../agentSessions/agentSessions.js'; import { ImplicitContextAttachmentWidget } from '../../attachments/implicitContextAttachment.js'; import { IChatWidget, ISessionTypePickerDelegate, isIChatResourceViewContext } from '../../chat.js'; import { ChatAttachmentModel } from '../../attachments/chatAttachmentModel.js'; @@ -115,8 +115,8 @@ import { resizeImage } from '../../chatImageUtils.js'; import { IModelPickerDelegate, ModelPickerActionItem } from './modelPickerActionItem.js'; import { IModePickerDelegate, ModePickerActionItem } from './modePickerActionItem.js'; import { SessionTypePickerActionItem } from './sessionTargetPickerActionItem.js'; +import { DelegationSessionPickerActionItem } from './delegationSessionPickerActionItem.js'; import { IChatInputPickerOptions } from './chatInputPickerActionItem.js'; -import { getAgentSessionProvider } from '../../agentSessions/agentSessions.js'; import { SearchableOptionPickerActionItem } from '../../chatSessions/searchableOptionPickerActionItem.js'; import { mixin } from '../../../../../../base/common/objects.js'; @@ -315,6 +315,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private modelWidget: ModelPickerActionItem | undefined; private modeWidget: ModePickerActionItem | undefined; private sessionTargetWidget: SessionTypePickerActionItem | undefined; + private delegationWidget: DelegationSessionPickerActionItem | undefined; private chatSessionPickerWidgets: Map = new Map(); private chatSessionPickerContainer: HTMLElement | undefined; private _lastSessionPickerAction: MenuItemAction | undefined; @@ -416,6 +417,15 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this._attemptedWorkingSetEntriesCount; } + /** + * Gets the pending delegation target if one is set. + * This is used when the user changes the session target picker to a different provider + * but hasn't submitted yet, so the delegation will happen on submit. + */ + public get pendingDelegationTarget(): AgentSessionProviders | undefined { + return this._pendingDelegationTarget; + } + /** * Number consumers holding the 'generating' lock. */ @@ -423,6 +433,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private _emptyInputState: ObservableMemento; private _chatSessionIsEmpty = false; + private _pendingDelegationTarget: AgentSessionProviders | undefined = undefined; constructor( // private readonly editorOptions: ChatEditorOptions, // TODO this should be used @@ -715,6 +726,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.sessionTargetWidget?.show(); } + public openDelegationPicker(): void { + this.delegationWidget?.show(); + } + public openChatSessionPicker(): void { // Open the first available picker widget const firstWidget = this.chatSessionPickerWidgets?.values()?.next().value; @@ -1583,7 +1598,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // - Otherwise, use the actual session's type const delegate = this.options.sessionTypePickerDelegate; const delegateSessionType = delegate?.setActiveSessionProvider && delegate?.getActiveSessionProvider?.(); - const sessionType = delegateSessionType || getChatSessionType(sessionResource); + const sessionType = delegateSessionType || this._pendingDelegationTarget || getChatSessionType(sessionResource); const isLocalSession = sessionType === localChatSessionType; if (!isLocalSession) { @@ -1601,6 +1616,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.computeVisibleOptionGroups(); this._register(widget.onDidChangeViewModel(() => { + this._pendingDelegationTarget = undefined; // Update agentSessionType when view model changes this.updateAgentSessionTypeContextKey(); this.refreshChatSessionPickers(); @@ -1853,16 +1869,30 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge sessionResource: () => this._widget?.viewModel?.sessionResource, }; return this.modeWidget = this.instantiationService.createInstance(ModePickerActionItem, action, delegate, pickerOptions); - } else if (action.id === OpenSessionTargetPickerAction.ID && action instanceof MenuItemAction) { + } else if ((action.id === OpenSessionTargetPickerAction.ID || action.id === OpenDelegationPickerAction.ID) && action instanceof MenuItemAction) { // Use provided delegate if available, otherwise create default delegate + const getActiveSessionType = () => { + const sessionResource = this._widget?.viewModel?.sessionResource; + return sessionResource ? getAgentSessionProvider(sessionResource) : undefined; + }; const delegate: ISessionTypePickerDelegate = this.options.sessionTypePickerDelegate ?? { getActiveSessionProvider: () => { - const sessionResource = this._widget?.viewModel?.sessionResource; - return sessionResource ? getAgentSessionProvider(sessionResource) : undefined; + return getActiveSessionType(); + }, + getPendingDelegationTarget: () => { + return this._pendingDelegationTarget; + }, + setPendingDelegationTarget: (provider: AgentSessionProviders) => { + const isActive = getActiveSessionType() === provider; + this._pendingDelegationTarget = isActive ? undefined : provider; + this.updateWidgetLockStateFromSessionType(provider); + this.updateAgentSessionTypeContextKey(); + this.refreshChatSessionPickers(); }, }; const chatSessionPosition = isIChatResourceViewContext(widget.viewContext) ? 'editor' : 'sidebar'; - return this.sessionTargetWidget = this.instantiationService.createInstance(SessionTypePickerActionItem, action, chatSessionPosition, delegate, pickerOptions); + const Picker = action.id === OpenSessionTargetPickerAction.ID ? SessionTypePickerActionItem : DelegationSessionPickerActionItem; + return this.sessionTargetWidget = this.instantiationService.createInstance(Picker, action, chatSessionPosition, delegate, pickerOptions); } else if (action.id === ChatSessionPrimaryPickerAction.ID && action instanceof MenuItemAction) { // Create all pickers and return a container action view item const widgets = this.createChatSessionPickerWidgets(action); @@ -1895,12 +1925,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge }, hoverDelegate, hiddenItemStrategy: HiddenItemStrategy.NoHide, - actionViewItemProvider: (action, options) => { - if (action.id === ContinueChatInSessionAction.ID && action instanceof MenuItemAction) { - return this.instantiationService.createInstance(ChatContinueInSessionActionItem, action, ActionLocation.ChatWidget); - } - return undefined; - } })); this.executeToolbar.getElement().classList.add('chat-execute-toolbar'); this.executeToolbar.context = { widget } satisfies IChatExecuteActionContext; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts new file mode 100644 index 00000000000..86d444ae395 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AgentSessionProviders, getAgentSessionProvider } from '../../agentSessions/agentSessions.js'; +import { ISessionTypeItem, SessionTypePickerActionItem } from './sessionTargetPickerActionItem.js'; + +/** + * Action view item for delegating to a remote session (Background or Cloud). + * This picker allows switching to remote execution providers when the session is not empty. + */ +export class DelegationSessionPickerActionItem extends SessionTypePickerActionItem { + protected override _run(sessionTypeItem: ISessionTypeItem): void { + if (this.delegate.setPendingDelegationTarget) { + this.delegate.setPendingDelegationTarget(sessionTypeItem.type); + } + if (this.element) { + this.renderLabel(this.element); + } + } + + protected override _getSelectedSessionType(): AgentSessionProviders | undefined { + const delegationTarget = this.delegate.getPendingDelegationTarget ? this.delegate.getPendingDelegationTarget() : undefined; + if (delegationTarget) { + return delegationTarget; + } + return this.delegate.getActiveSessionProvider(); + } + + protected override _isSessionTypeEnabled(type: AgentSessionProviders): boolean { + const allContributions = this.chatSessionsService.getAllChatSessionContributions(); + const contribution = allContributions.find(contribution => getAgentSessionProvider(contribution.type) === type); + if (contribution !== undefined && !!contribution.canDelegate) { + return true; // Session type supports delegation + } + return this.delegate.getActiveSessionProvider() === type; // Always allow switching back to active session + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts index 8e26298839a..5e99e2d96fe 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts @@ -17,11 +17,11 @@ import { IContextKeyService } from '../../../../../../platform/contextkey/common import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; import { IChatSessionsService } from '../../../common/chatSessionsService.js'; -import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../../agentSessions/agentSessions.js'; +import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName, isFirstPartyAgentSessionProvider } from '../../agentSessions/agentSessions.js'; import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; import { ISessionTypePickerDelegate } from '../../chat.js'; -interface ISessionTypeItem { +export interface ISessionTypeItem { type: AgentSessionProviders; label: string; description: string; @@ -30,26 +30,29 @@ interface ISessionTypeItem { /** * Action view item for selecting a session target in the chat interface. - * This picker allows switching between different chat session types contributed via extensions. + * This picker allows switching between different chat session types for new/empty sessions. */ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { private _sessionTypeItems: ISessionTypeItem[] = []; constructor( action: MenuItemAction, - private readonly chatSessionPosition: 'sidebar' | 'editor', - private readonly delegate: ISessionTypePickerDelegate, + protected readonly chatSessionPosition: 'sidebar' | 'editor', + protected readonly delegate: ISessionTypePickerDelegate, pickerOptions: IChatInputPickerOptions, @IActionWidgetService actionWidgetService: IActionWidgetService, @IKeybindingService keybindingService: IKeybindingService, @IContextKeyService contextKeyService: IContextKeyService, - @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, + @IChatSessionsService protected readonly chatSessionsService: IChatSessionsService, @ICommandService private readonly commandService: ICommandService, @IOpenerService openerService: IOpenerService, ) { + const firstPartyCategory = { label: localize('chat.sessionTarget.category.agent', "Agent Types"), order: 1 }; + const otherCategory = { label: localize('chat.sessionTarget.category.other', "Other"), order: 2 }; + const actionProvider: IActionWidgetDropdownActionProvider = { getActions: () => { - const currentType = this.delegate.getActiveSessionProvider(); + const currentType = this._getSelectedSessionType(); const actions: IActionWidgetDropdownAction[] = []; for (const sessionTypeItem of this._sessionTypeItems) { @@ -60,16 +63,10 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { tooltip: sessionTypeItem.description, checked: currentType === sessionTypeItem.type, icon: getAgentSessionProviderIcon(sessionTypeItem.type), - enabled: true, + enabled: this._isSessionTypeEnabled(sessionTypeItem.type), + category: isFirstPartyAgentSessionProvider(sessionTypeItem.type) ? firstPartyCategory : otherCategory, run: async () => { - if (this.delegate.setActiveSessionProvider) { - this.delegate.setActiveSessionProvider(sessionTypeItem.type); - } else { - this.commandService.executeCommand(sessionTypeItem.commandId, this.chatSessionPosition); - } - if (this.element) { - this.renderLabel(this.element); - } + this._run(sessionTypeItem); }, }); } @@ -83,7 +80,7 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { const learnMoreUrl = 'https://code.visualstudio.com/docs/copilot/agents/overview'; actionBarActions.push({ id: 'workbench.action.chat.agentOverview.learnMore', - label: localize('chat.learnMore', "Learn about agent types..."), + label: localize('chat.learnMoreAgentTypes', "Learn about agent types..."), tooltip: learnMoreUrl, class: undefined, enabled: true, @@ -107,6 +104,23 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { })); } + protected _run(sessionTypeItem: ISessionTypeItem): void { + if (this.delegate.setActiveSessionProvider) { + // Use provided setter (for welcome view) + this.delegate.setActiveSessionProvider(sessionTypeItem.type); + } else { + // Execute command to create new session + this.commandService.executeCommand(sessionTypeItem.commandId, this.chatSessionPosition); + } + if (this.element) { + this.renderLabel(this.element); + } + } + + protected _getSelectedSessionType(): AgentSessionProviders | undefined { + return this.delegate.getActiveSessionProvider(); + } + private _updateAgentSessionItems(): void { const localSessionItem = { type: AgentSessionProviders.Local, @@ -134,9 +148,13 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { this._sessionTypeItems = agentSessionItems; } + protected _isSessionTypeEnabled(type: AgentSessionProviders): boolean { + return true; + } + protected override renderLabel(element: HTMLElement): IDisposable | null { this.setAriaLabelAttributes(element); - const currentType = this.delegate.getActiveSessionProvider(); + const currentType = this._getSelectedSessionType(); const label = getAgentSessionProviderName(currentType ?? AgentSessionProviders.Local); const icon = getAgentSessionProviderIcon(currentType ?? AgentSessionProviders.Local); From a2ad5acb046ef375c9f65af1483d4348cdf84836 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 19 Jan 2026 11:41:52 +0100 Subject: [PATCH 174/387] agent sessions - introduce experiments and adopt for projection & status (#288696) --- .github/CODENOTIFY | 2 - src/vs/platform/actions/common/actions.ts | 2 +- .../titlebar/commandCenterControlRegistry.ts | 71 ---------- .../browser/parts/titlebar/titlebarPart.ts | 30 +--- .../chat/browser/actions/chatActions.ts | 4 - .../chat/browser/actions/chatNewActions.ts | 8 -- .../agentSessions.contribution.ts | 92 +----------- .../browser/agentSessions/agentSessions.ts | 4 +- .../agentSessions/agentSessionsControl.ts | 23 ++- .../agentSessions/agentSessionsOpener.ts | 69 ++++++--- .../experiments/agentSessionProjection.ts | 9 ++ .../agentSessionProjectionActions.ts | 33 ++--- .../agentSessionProjectionService.ts | 73 ++++++---- .../agentSessionsExperiments.contribution.ts | 48 +++++++ .../agentTitleBarStatusService.ts} | 12 +- .../agentTitleBarStatusWidget.ts} | 133 ++++++++++++------ .../media/agentsessionprojection.css} | 4 - .../media/agenttitlebarstatuswidget.css} | 4 - .../chat/browser/widget/chatWidgetService.ts | 5 - .../browser/widgetHosts/editor/chatEditor.ts | 1 - .../widgetHosts/viewPane/chatViewPane.ts | 4 +- .../chat/common/actions/chatContextKeys.ts | 4 - .../browser/agentSessionsWelcome.ts | 7 +- 23 files changed, 293 insertions(+), 349 deletions(-) delete mode 100644 src/vs/workbench/browser/parts/titlebar/commandCenterControlRegistry.ts create mode 100644 src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjection.ts rename src/vs/workbench/contrib/chat/browser/agentSessions/{ => experiments}/agentSessionProjectionActions.ts (74%) rename src/vs/workbench/contrib/chat/browser/agentSessions/{ => experiments}/agentSessionProjectionService.ts (81%) create mode 100644 src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts rename src/vs/workbench/contrib/chat/browser/agentSessions/{agentStatusService.ts => experiments/agentTitleBarStatusService.ts} (86%) rename src/vs/workbench/contrib/chat/browser/agentSessions/{agentStatusWidget.ts => experiments/agentTitleBarStatusWidget.ts} (84%) rename src/vs/workbench/contrib/chat/browser/agentSessions/{media/agentSessionProjection.css => experiments/media/agentsessionprojection.css} (94%) rename src/vs/workbench/contrib/chat/browser/agentSessions/{media/agentStatusWidget.css => experiments/media/agenttitlebarstatuswidget.css} (98%) diff --git a/.github/CODENOTIFY b/.github/CODENOTIFY index ac22ac40d26..dc4ef34cf21 100644 --- a/.github/CODENOTIFY +++ b/.github/CODENOTIFY @@ -7,7 +7,6 @@ src/vs/base/common/path.ts @bpasero src/vs/base/common/stream.ts @bpasero src/vs/base/common/uri.ts @jrieken src/vs/base/browser/domSanitize.ts @mjbvz -src/vs/base/browser/** @bpasero src/vs/base/node/pfs.ts @bpasero src/vs/base/node/unc.ts @bpasero src/vs/base/parts/contextmenu/** @bpasero @@ -110,7 +109,6 @@ src/vs/workbench/contrib/chat/browser/media/chatViewTitleControl.css @bpasero src/vs/workbench/contrib/chat/browser/chatManagement/chatUsageWidget.ts @bpasero src/vs/workbench/contrib/chat/browser/chatManagement/media/chatUsageWidget.css @bpasero src/vs/workbench/contrib/chat/browser/agentSessions/** @bpasero -src/vs/workbench/contrib/chat/browser/chatSessions/** @bpasero src/vs/workbench/contrib/localization/** @TylerLeonhardt src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @TylerLeonhardt src/vs/workbench/contrib/scm/** @lszomoru diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index a66127dd044..5c8cd932f8e 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -292,7 +292,7 @@ export class MenuId { static readonly AgentSessionsToolbar = new MenuId('AgentSessionsToolbar'); static readonly AgentSessionItemToolbar = new MenuId('AgentSessionItemToolbar'); static readonly AgentSessionSectionToolbar = new MenuId('AgentSessionSectionToolbar'); - static readonly AgentsControlMenu = new MenuId('AgentsControlMenu'); + static readonly AgentsTitleBarControlMenu = new MenuId('AgentsTitleBarControlMenu'); static readonly ChatViewSessionTitleNavigationToolbar = new MenuId('ChatViewSessionTitleNavigationToolbar'); static readonly ChatViewSessionTitleToolbar = new MenuId('ChatViewSessionTitleToolbar'); diff --git a/src/vs/workbench/browser/parts/titlebar/commandCenterControlRegistry.ts b/src/vs/workbench/browser/parts/titlebar/commandCenterControlRegistry.ts deleted file mode 100644 index 20eeafacdb0..00000000000 --- a/src/vs/workbench/browser/parts/titlebar/commandCenterControlRegistry.ts +++ /dev/null @@ -1,71 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IDisposable } from '../../../../base/common/lifecycle.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; - -/** - * Interface for a command center control that can be registered with the titlebar. - */ -export interface ICommandCenterControl extends IDisposable { - readonly element: HTMLElement; -} - -/** - * A registration for a custom command center control. - */ -export interface ICommandCenterControlRegistration { - /** - * The context key that must be truthy for this control to be shown. - * When this context key is true, this control replaces the default command center. - */ - readonly contextKey: string; - - /** - * Priority for when multiple controls match. Higher priority wins. - */ - readonly priority: number; - - /** - * Factory function to create the control. - */ - create(instantiationService: IInstantiationService): ICommandCenterControl; -} - -class CommandCenterControlRegistryImpl { - private readonly registrations: ICommandCenterControlRegistration[] = []; - - /** - * Register a custom command center control. - */ - register(registration: ICommandCenterControlRegistration): IDisposable { - this.registrations.push(registration); - // Sort by priority descending - this.registrations.sort((a, b) => b.priority - a.priority); - - return { - dispose: () => { - const index = this.registrations.indexOf(registration); - if (index >= 0) { - this.registrations.splice(index, 1); - } - } - }; - } - - /** - * Get all registered command center controls. - */ - getRegistrations(): readonly ICommandCenterControlRegistration[] { - return this.registrations; - } -} - -/** - * Registry for custom command center controls. - * Contrib modules can register controls here, and the titlebar will use them - * when their context key conditions are met. - */ -export const CommandCenterControlRegistry = new CommandCenterControlRegistryImpl(); diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 831c4be2380..743f9e6ee8b 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -30,7 +30,6 @@ import { IContextKey, IContextKeyService } from '../../../../platform/contextkey import { IHostService } from '../../../services/host/browser/host.js'; import { WindowTitle } from './windowTitle.js'; import { CommandCenterControl } from './commandCenterControl.js'; -import { CommandCenterControlRegistry } from './commandCenterControlRegistry.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { ACCOUNTS_ACTIVITY_ID, GLOBAL_ACTIVITY_ID } from '../../../common/activity.js'; @@ -329,14 +328,6 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { this._register(this.hostService.onDidChangeActiveWindow(windowId => windowId === targetWindowId ? this.onFocus() : this.onBlur())); this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChanged(e))); this._register(this.editorGroupsContainer.onDidChangeEditorPartOptions(e => this.onEditorPartConfigurationChange(e))); - - // Re-create title when any registered command center control's context key changes - this._register(this.contextKeyService.onDidChangeContext(e => { - const registeredContextKeys = new Set(CommandCenterControlRegistry.getRegistrations().map(r => r.contextKey)); - if (registeredContextKeys.size > 0 && e.affectsSome(registeredContextKeys)) { - this.createTitle(); - } - })); } private onBlur(): void { @@ -585,24 +576,9 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { // Menu Title else { - // Check if any registered command center control should be shown - let customControlShown = false; - for (const registration of CommandCenterControlRegistry.getRegistrations()) { - if (this.contextKeyService.getContextKeyValue(registration.contextKey)) { - const control = registration.create(this.instantiationService); - reset(this.title, control.element); - this.titleDisposables.add(control); - customControlShown = true; - break; - } - } - - if (!customControlShown) { - // Normal mode - show regular command center - const commandCenter = this.instantiationService.createInstance(CommandCenterControl, this.windowTitle, this.hoverDelegate); - reset(this.title, commandCenter.element); - this.titleDisposables.add(commandCenter); - } + const commandCenter = this.instantiationService.createInstance(CommandCenterControl, this.windowTitle, this.hoverDelegate); + reset(this.title, commandCenter.element); + this.titleDisposables.add(commandCenter); } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index c6b4113cbd4..8b6a6f5f946 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -948,10 +948,6 @@ MenuRegistry.appendMenuItem(MenuId.CommandCenter, { ChatContextKeys.Setup.disabled.negate() ), ContextKeyExpr.has('config.chat.commandCenter.enabled'), - ContextKeyExpr.or( - ContextKeyExpr.has(`config.${ChatConfiguration.AgentStatusEnabled}`).negate(), // Show when agent status is disabled - ChatContextKeys.agentStatusHasNotifications.negate() // Or when agent status has no notifications - ) ), order: 10003 // to the right of agent controls }); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts index 1c525a8eaa0..d662fd086eb 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts @@ -30,7 +30,6 @@ import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; import { ACTION_ID_NEW_CHAT, ACTION_ID_NEW_EDIT_SESSION, CHAT_CATEGORY, handleCurrentEditingSession } from './chatActions.js'; import { clearChatEditor } from './chatClear.js'; import { AgentSessionsViewerOrientation } from '../agentSessions/agentSessions.js'; -import { IAgentSessionProjectionService } from '../agentSessions/agentSessionProjectionService.js'; export interface INewEditSessionActionContext { @@ -121,13 +120,6 @@ export function registerNewChatActions() { async run(accessor: ServicesAccessor, ...args: unknown[]) { const accessibilityService = accessor.get(IAccessibilityService); - const projectionService = accessor.get(IAgentSessionProjectionService); - - // Exit projection mode if active (back button behavior) - if (projectionService.isActive) { - await projectionService.exitProjection(); - return; - } const viewsService = accessor.get(IViewsService); const executeCommandContext = args[0] as INewEditSessionActionContext | undefined; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts index 10c3927eab9..1016c2afb42 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts @@ -3,10 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import './experiments/agentSessionsExperiments.contribution.js'; import { Codicon } from '../../../../../base/common/codicons.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; import { localize, localize2 } from '../../../../../nls.js'; -import { mainWindow } from '../../../../../base/browser/window.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { registerSingleton, InstantiationType } from '../../../../../platform/instantiation/common/extensions.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; @@ -15,20 +14,11 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { AgentSessionsViewerOrientation, AgentSessionsViewerPosition } from './agentSessions.js'; import { IAgentSessionsService, AgentSessionsService } from './agentSessionsService.js'; import { LocalAgentsSessionsProvider } from './localAgentSessionsProvider.js'; -import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js'; -import { ISubmenuItem, MenuId, MenuRegistry, registerAction2, SubmenuItemAction } from '../../../../../platform/actions/common/actions.js'; -import { ArchiveAgentSessionAction, ArchiveAgentSessionSectionAction, UnarchiveAgentSessionSectionAction, UnarchiveAgentSessionAction, OpenAgentSessionInEditorGroupAction, OpenAgentSessionInNewEditorGroupAction, OpenAgentSessionInNewWindowAction, ShowAgentSessionsSidebar, HideAgentSessionsSidebar, ToggleAgentSessionsSidebar, RefreshAgentSessionsViewerAction, FindAgentSessionInViewerAction, MarkAgentSessionUnreadAction, MarkAgentSessionReadAction, MarkAgentSessionSectionReadAction, FocusAgentSessionsAction, SetAgentSessionsOrientationStackedAction, SetAgentSessionsOrientationSideBySideAction, ShowAllAgentSessionsAction, ShowRecentAgentSessionsAction, HideAgentSessionsAction, PickAgentSessionAction, ArchiveAllAgentSessionsAction, RenameAgentSessionAction, DeleteAgentSessionAction, DeleteAllLocalSessionsAction } from './agentSessionsActions.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js'; +import { ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { ArchiveAgentSessionAction, ArchiveAgentSessionSectionAction, UnarchiveAgentSessionAction, OpenAgentSessionInEditorGroupAction, OpenAgentSessionInNewEditorGroupAction, OpenAgentSessionInNewWindowAction, ShowAgentSessionsSidebar, HideAgentSessionsSidebar, ToggleAgentSessionsSidebar, RefreshAgentSessionsViewerAction, FindAgentSessionInViewerAction, MarkAgentSessionUnreadAction, MarkAgentSessionReadAction, FocusAgentSessionsAction, SetAgentSessionsOrientationStackedAction, SetAgentSessionsOrientationSideBySideAction, PickAgentSessionAction, ArchiveAllAgentSessionsAction, RenameAgentSessionAction, DeleteAgentSessionAction, DeleteAllLocalSessionsAction, HideAgentSessionsAction, MarkAgentSessionSectionReadAction, ShowAllAgentSessionsAction, ShowRecentAgentSessionsAction, UnarchiveAgentSessionSectionAction } from './agentSessionsActions.js'; import { AgentSessionsQuickAccessProvider, AGENT_SESSIONS_QUICK_ACCESS_PREFIX } from './agentSessionsQuickAccess.js'; -import { IAgentSessionProjectionService, AgentSessionProjectionService } from './agentSessionProjectionService.js'; -import { EnterAgentSessionProjectionAction, ExitAgentSessionProjectionAction, ToggleAgentStatusAction, ToggleAgentSessionProjectionAction } from './agentSessionProjectionActions.js'; -import { IAgentStatusService, AgentStatusService } from './agentStatusService.js'; -import { AgentStatusWidget } from './agentStatusWidget.js'; -import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { ChatConfiguration } from '../../common/constants.js'; import { AuxiliaryBarMaximizedContext } from '../../../../common/contextkeys.js'; -import { LayoutSettings } from '../../../../services/layout/browser/layoutService.js'; //#region Actions and Menus @@ -59,12 +49,6 @@ registerAction2(HideAgentSessionsAction); registerAction2(SetAgentSessionsOrientationStackedAction); registerAction2(SetAgentSessionsOrientationSideBySideAction); -// Agent Session Projection -registerAction2(EnterAgentSessionProjectionAction); -registerAction2(ExitAgentSessionProjectionAction); -registerAction2(ToggleAgentStatusAction); -registerAction2(ToggleAgentSessionProjectionAction); - // --- Agent Sessions Toolbar MenuRegistry.appendMenuItem(MenuId.AgentSessionsToolbar, { @@ -193,73 +177,7 @@ Registry.as(QuickAccessExtensions.Quickaccess).registerQui //#region Workbench Contributions registerWorkbenchContribution2(LocalAgentsSessionsProvider.ID, LocalAgentsSessionsProvider, WorkbenchPhase.AfterRestored); + registerSingleton(IAgentSessionsService, AgentSessionsService, InstantiationType.Delayed); -registerSingleton(IAgentStatusService, AgentStatusService, InstantiationType.Delayed); -registerSingleton(IAgentSessionProjectionService, AgentSessionProjectionService, InstantiationType.Delayed); - -// Register Agent Status as a menu item in the command center (alongside the search box, not replacing it) -MenuRegistry.appendMenuItem(MenuId.CommandCenter, { - submenu: MenuId.AgentsControlMenu, - title: localize('agentsControl', "Agents"), - icon: Codicon.chatSparkle, - when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentStatusEnabled}`), - order: 10002 // to the right of the chat button -}); - -// Register a placeholder action to the submenu so it appears (required for submenus) -MenuRegistry.appendMenuItem(MenuId.AgentsControlMenu, { - command: { - id: 'workbench.action.chat.toggle', - title: localize('openChat', "Open Chat"), - }, - when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentStatusEnabled}`), -}); - -/** - * Provides custom rendering for the agent status in the command center. - * Uses IActionViewItemService to render a custom AgentStatusWidget - * for the AgentsControlMenu submenu. - * Also adds a CSS class to the workbench when agent status is enabled. - */ -class AgentStatusRendering extends Disposable implements IWorkbenchContribution { - - static readonly ID = 'workbench.contrib.agentStatus.rendering'; - - constructor( - @IActionViewItemService actionViewItemService: IActionViewItemService, - @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService configurationService: IConfigurationService - ) { - super(); - - this._register(actionViewItemService.register(MenuId.CommandCenter, MenuId.AgentsControlMenu, (action, options) => { - if (!(action instanceof SubmenuItemAction)) { - return undefined; - } - return instantiationService.createInstance(AgentStatusWidget, action, options); - }, undefined)); - - // Add/remove CSS class on workbench based on setting - // Also force enable command center when agent status is enabled - const updateClass = () => { - const enabled = configurationService.getValue(ChatConfiguration.AgentStatusEnabled) === true; - mainWindow.document.body.classList.toggle('agent-status-enabled', enabled); - - // Force enable command center when agent status is enabled - if (enabled && configurationService.getValue(LayoutSettings.COMMAND_CENTER) !== true) { - configurationService.updateValue(LayoutSettings.COMMAND_CENTER, true); - } - }; - updateClass(); - this._register(configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(ChatConfiguration.AgentStatusEnabled)) { - updateClass(); - } - })); - } -} - -// Register the workbench contribution that provides custom rendering for the agent status -registerWorkbenchContribution2(AgentStatusRendering.ID, AgentStatusRendering, WorkbenchPhase.AfterRestored); //#endregion diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts index a72ab1cd2e6..1741e73eace 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts @@ -68,10 +68,12 @@ export enum AgentSessionsViewerPosition { } export interface IAgentSessionsControl { + + readonly element: HTMLElement | undefined; + refresh(): void; openFind(): void; reveal(sessionResource: URI): void; - setGridMarginOffset(offset: number): void; } export const agentSessionReadIndicatorForeground = registerColor( diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 3ca1f18b287..a44fd400086 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -33,19 +33,17 @@ import { openSession } from './agentSessionsOpener.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js'; import { IMouseEvent } from '../../../../../base/browser/mouseEvent.js'; +import { IChatWidget } from '../chat.js'; export interface IAgentSessionsControlOptions extends IAgentSessionsSorterOptions { readonly overrideStyles: IStyleOverride; readonly filter: IAgentSessionsFilter; - readonly source: AgentSessionsControlSource; + readonly source: string; getHoverPosition(): HoverPosition; trackActiveEditorSession(): boolean; -} -export const enum AgentSessionsControlSource { - ChatViewPane = 'chatViewPane', - WelcomeView = 'welcomeView' + notifySessionOpened?(resource: URI, widget: IChatWidget): void; } type AgentSessionOpenedClassification = { @@ -57,12 +55,14 @@ type AgentSessionOpenedClassification = { type AgentSessionOpenedEvent = { providerType: string; - source: AgentSessionsControlSource; + source: string; }; export class AgentSessionsControl extends Disposable implements IAgentSessionsControl { private sessionsContainer: HTMLElement | undefined; + get element(): HTMLElement | undefined { return this.sessionsContainer; } + private sessionsList: WorkbenchCompressibleAsyncDataTree | undefined; private visible: boolean = true; @@ -213,7 +213,10 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo source: this.options.source }); - await this.instantiationService.invokeFunction(openSession, element, { ...e, expanded: this.options.source === AgentSessionsControlSource.WelcomeView }); + const widget = await this.instantiationService.invokeFunction(openSession, element, e); + if (widget) { + this.options.notifySessionOpened?.(element.resource, widget); + } } private async showContextMenu({ element, anchor, browserEvent }: ITreeContextMenuEvent): Promise { @@ -358,10 +361,4 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo this.sessionsList.setFocus([session]); this.sessionsList.setSelection([session]); } - - setGridMarginOffset(offset: number): void { - if (this.sessionsContainer) { - this.sessionsContainer.style.marginBottom = `-${offset}px`; - } - } } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts index 7220766b9df..47e10c26ce5 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts @@ -3,39 +3,65 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { IAgentSession, isLocalAgentSessionItem } from './agentSessionsModel.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js'; -import { ChatViewPaneTarget, IChatWidgetService } from '../chat.js'; +import { ChatViewPaneTarget, IChatWidget, IChatWidgetService } from '../chat.js'; import { ACTIVE_GROUP, SIDE_GROUP } from '../../../../services/editor/common/editorService.js'; import { IEditorOptions } from '../../../../../platform/editor/common/editor.js'; import { IChatSessionsService } from '../../common/chatSessionsService.js'; import { Schemas } from '../../../../../base/common/network.js'; -import { IAgentSessionProjectionService } from './agentSessionProjectionService.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { ChatConfiguration } from '../../common/constants.js'; -export async function openSession(accessor: ServicesAccessor, session: IAgentSession, openOptions?: { sideBySide?: boolean; editorOptions?: IEditorOptions; expanded?: boolean }): Promise { - const configurationService = accessor.get(IConfigurationService); - const projectionService = accessor.get(IAgentSessionProjectionService); +//#region Session Opener Registry - session.setRead(true); // mark as read when opened +export interface ISessionOpenerParticipant { + handleOpenSession(accessor: ServicesAccessor, session: IAgentSession, openOptions?: ISessionOpenOptions): Promise; +} - const agentSessionProjectionEnabled = configurationService.getValue(ChatConfiguration.AgentSessionProjectionEnabled) === true; - if (agentSessionProjectionEnabled) { - // Enter Agent Session Projection mode for the session - await projectionService.enterProjection(session); - } else { - // Fall back to opening in chat widget when Agent Session Projection is disabled - await openSessionInChatWidget(accessor, session, openOptions); +export interface ISessionOpenOptions { + readonly sideBySide?: boolean; + readonly editorOptions?: IEditorOptions; +} + +class SessionOpenerRegistry { + + private readonly participants = new Set(); + + registerParticipant(participant: ISessionOpenerParticipant): IDisposable { + this.participants.add(participant); + + return { + dispose: () => { + this.participants.delete(participant); + } + }; + } + + getParticipants(): readonly ISessionOpenerParticipant[] { + return Array.from(this.participants); } } -/** - * Opens a session in the traditional chat widget (side panel or editor). - * Use this when you explicitly want to open in the chat widget rather than agent session projection mode. - */ -export async function openSessionInChatWidget(accessor: ServicesAccessor, session: IAgentSession, openOptions?: { sideBySide?: boolean; editorOptions?: IEditorOptions; expanded?: boolean }): Promise { +export const sessionOpenerRegistry = new SessionOpenerRegistry(); + +//#endregion + +export async function openSession(accessor: ServicesAccessor, session: IAgentSession, openOptions?: ISessionOpenOptions): Promise { + + // First, give registered participants a chance to handle the session + for (const participant of sessionOpenerRegistry.getParticipants()) { + const handled = await participant.handleOpenSession(accessor, session, openOptions); + if (handled) { + return undefined; // Participant handled the session, skip default opening + } + } + + // Default session opening logic + return openSessionDefault(accessor, session, openOptions); +} + +async function openSessionDefault(accessor: ServicesAccessor, session: IAgentSession, openOptions?: ISessionOpenOptions): Promise { const chatSessionsService = accessor.get(IChatSessionsService); const chatWidgetService = accessor.get(IChatWidgetService); @@ -52,7 +78,6 @@ export async function openSessionInChatWidget(accessor: ServicesAccessor, sessio ...sessionOptions, ...openOptions?.editorOptions, revealIfOpened: true, // always try to reveal if already opened - expanded: openOptions?.expanded }; await chatSessionsService.activateChatSessionItemProvider(session.providerType); // ensure provider is activated before trying to open @@ -70,5 +95,5 @@ export async function openSessionInChatWidget(accessor: ServicesAccessor, sessio options = { ...options, revealIfOpened: true }; } - await chatWidgetService.openSession(session.resource, target, options); + return chatWidgetService.openSession(session.resource, target, options); } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjection.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjection.ts new file mode 100644 index 00000000000..1984aa24605 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjection.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from '../../../../../../nls.js'; +import { RawContextKey } from '../../../../../../platform/contextkey/common/contextkey.js'; + +export const inAgentSessionProjection = new RawContextKey('chatInAgentSessionProjection', false, { type: 'boolean', description: localize('chatInAgentSessionProjection', "True when the workbench is in agent session projection mode for reviewing an agent session.") }); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionActions.ts similarity index 74% rename from src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionActions.ts rename to src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionActions.ts index 7571d0f8e50..1e017fc3d96 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionActions.ts @@ -3,20 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize, localize2 } from '../../../../../nls.js'; -import { Action2 } from '../../../../../platform/actions/common/actions.js'; -import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; -import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { KeyCode } from '../../../../../base/common/keyCodes.js'; -import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; -import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; -import { ChatConfiguration } from '../../common/constants.js'; +import { localize, localize2 } from '../../../../../../nls.js'; +import { Action2 } from '../../../../../../platform/actions/common/actions.js'; +import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { KeybindingWeight } from '../../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { KeyCode } from '../../../../../../base/common/keyCodes.js'; +import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; import { IAgentSessionProjectionService } from './agentSessionProjectionService.js'; -import { IAgentSession, isMarshalledAgentSessionContext, IMarshalledAgentSessionContext } from './agentSessionsModel.js'; -import { IAgentSessionsService } from './agentSessionsService.js'; -import { CHAT_CATEGORY } from '../actions/chatActions.js'; -import { ToggleTitleBarConfigAction } from '../../../../browser/parts/titlebar/titlebarActions.js'; -import { IsCompactTitleBarContext } from '../../../../common/contextkeys.js'; +import { IAgentSession, isMarshalledAgentSessionContext, IMarshalledAgentSessionContext } from '../agentSessionsModel.js'; +import { IAgentSessionsService } from '../agentSessionsService.js'; +import { CHAT_CATEGORY } from '../../actions/chatActions.js'; +import { ToggleTitleBarConfigAction } from '../../../../../browser/parts/titlebar/titlebarActions.js'; +import { IsCompactTitleBarContext } from '../../../../../common/contextkeys.js'; +import { inAgentSessionProjection } from './agentSessionProjection.js'; +import { ChatConfiguration } from '../../../common/constants.js'; //#region Enter Agent Session Projection @@ -32,7 +33,7 @@ export class EnterAgentSessionProjectionAction extends Action2 { precondition: ContextKeyExpr.and( ChatContextKeys.enabled, ContextKeyExpr.has(`config.${ChatConfiguration.AgentSessionProjectionEnabled}`), - ChatContextKeys.inAgentSessionProjection.negate() + inAgentSessionProjection.negate() ), }); } @@ -71,12 +72,12 @@ export class ExitAgentSessionProjectionAction extends Action2 { f1: true, precondition: ContextKeyExpr.and( ChatContextKeys.enabled, - ChatContextKeys.inAgentSessionProjection + inAgentSessionProjection ), keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.Escape, - when: ChatContextKeys.inAgentSessionProjection, + when: inAgentSessionProjection, }, }); } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionService.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts similarity index 81% rename from src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionService.ts rename to src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts index 8521dd2ecd7..67e8db416a0 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts @@ -3,28 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './media/agentSessionProjection.css'; - -import { Emitter, Event } from '../../../../../base/common/event.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { localize } from '../../../../../nls.js'; -import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../../platform/log/common/log.js'; -import { IEditorGroupsService, IEditorWorkingSet } from '../../../../services/editor/common/editorGroupsService.js'; -import { IEditorService } from '../../../../services/editor/common/editorService.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; -import { IAgentSession } from './agentSessionsModel.js'; -import { ChatViewPaneTarget, IChatWidgetService } from '../chat.js'; -import { AgentSessionProviders } from './agentSessions.js'; -import { IChatSessionsService } from '../../common/chatSessionsService.js'; -import { ChatConfiguration } from '../../common/constants.js'; -import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; -import { ACTION_ID_NEW_CHAT } from '../actions/chatActions.js'; -import { IChatEditingService, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js'; -import { IAgentStatusService } from './agentStatusService.js'; +import './media/agentsessionprojection.css'; +import { Emitter, Event } from '../../../../../../base/common/event.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { localize } from '../../../../../../nls.js'; +import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { createDecorator } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { IEditorGroupsService, IEditorWorkingSet } from '../../../../../services/editor/common/editorGroupsService.js'; +import { IEditorService } from '../../../../../services/editor/common/editorService.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { IAgentSession } from '../agentSessionsModel.js'; +import { ChatViewPaneTarget, IChatWidgetService } from '../../chat.js'; +import { AgentSessionProviders } from '../agentSessions.js'; +import { IChatSessionsService } from '../../../common/chatSessionsService.js'; +import { IWorkbenchLayoutService } from '../../../../../services/layout/browser/layoutService.js'; +import { ACTION_ID_NEW_CHAT } from '../../actions/chatActions.js'; +import { IChatEditingService, ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; +import { IAgentTitleBarStatusService } from './agentTitleBarStatusService.js'; +import { ISessionOpenerParticipant, ISessionOpenOptions, sessionOpenerRegistry } from '../agentSessionsOpener.js'; +import { ServicesAccessor } from '../../../../../../editor/browser/editorExtensions.js'; +import { inAgentSessionProjection } from './agentSessionProjection.js'; +import { ChatConfiguration } from '../../../common/constants.js'; //#region Configuration @@ -113,14 +114,34 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @ICommandService private readonly commandService: ICommandService, @IChatEditingService private readonly chatEditingService: IChatEditingService, - @IAgentStatusService private readonly agentStatusService: IAgentStatusService, + @IAgentTitleBarStatusService private readonly agentTitleBarStatusService: IAgentTitleBarStatusService, ) { super(); - this._inProjectionModeContextKey = ChatContextKeys.inAgentSessionProjection.bindTo(contextKeyService); + this._inProjectionModeContextKey = inAgentSessionProjection.bindTo(contextKeyService); // Listen for editor close events to exit projection mode when all editors are closed this._register(this.editorService.onDidCloseEditor(() => this._checkForEmptyEditors())); + + // Register as a session opener participant to enter projection mode when sessions are opened + this._register(sessionOpenerRegistry.registerParticipant(this._createSessionOpenerParticipant())); + } + + private _createSessionOpenerParticipant(): ISessionOpenerParticipant { + return { + handleOpenSession: async (_accessor: ServicesAccessor, session: IAgentSession, _openOptions?: ISessionOpenOptions): Promise => { + // Only handle if projection mode is enabled + if (!this._isEnabled()) { + return false; + } + + // Enter projection mode for the session + await this.enterProjection(session); + + // Return true to indicate we handled the session (projection mode opens the chat itself) + return true; + } + }; } private _isEnabled(): boolean { @@ -260,7 +281,7 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS this.layoutService.mainContainer.classList.add('agent-session-projection-active'); // Update the agent status to show session mode - this.agentStatusService.enterSessionMode(session.resource.toString(), session.label); + this.agentTitleBarStatusService.enterSessionMode(session.resource.toString(), session.label); if (!wasActive) { this._onDidChangeProjectionMode.fire(true); @@ -316,7 +337,7 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS this.layoutService.mainContainer.classList.remove('agent-session-projection-active'); // Update the agent status to exit session mode - this.agentStatusService.exitSessionMode(); + this.agentTitleBarStatusService.exitSessionMode(); this._onDidChangeProjectionMode.fire(false); this._onDidChangeActiveSession.fire(undefined); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts new file mode 100644 index 00000000000..f510c88aaca --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerSingleton, InstantiationType } from '../../../../../../platform/instantiation/common/extensions.js'; +import { MenuId, MenuRegistry, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; +import { IAgentSessionProjectionService, AgentSessionProjectionService } from './agentSessionProjectionService.js'; +import { EnterAgentSessionProjectionAction, ExitAgentSessionProjectionAction, ToggleAgentStatusAction, ToggleAgentSessionProjectionAction } from './agentSessionProjectionActions.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../../common/contributions.js'; +import { AgentTitleBarStatusRendering } from './agentTitleBarStatusWidget.js'; +import { AgentTitleBarStatusService, IAgentTitleBarStatusService } from './agentTitleBarStatusService.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { localize } from '../../../../../../nls.js'; +import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { ChatConfiguration } from '../../../common/constants.js'; + +// #region Agent Session Projection & Status + +registerAction2(EnterAgentSessionProjectionAction); +registerAction2(ExitAgentSessionProjectionAction); +registerAction2(ToggleAgentStatusAction); +registerAction2(ToggleAgentSessionProjectionAction); + +registerSingleton(IAgentSessionProjectionService, AgentSessionProjectionService, InstantiationType.Delayed); +registerSingleton(IAgentTitleBarStatusService, AgentTitleBarStatusService, InstantiationType.Delayed); + +registerWorkbenchContribution2(AgentTitleBarStatusRendering.ID, AgentTitleBarStatusRendering, WorkbenchPhase.AfterRestored); + +// Register Agent Status as a menu item in the command center (alongside the search box, not replacing it) +MenuRegistry.appendMenuItem(MenuId.CommandCenter, { + submenu: MenuId.AgentsTitleBarControlMenu, + title: localize('agentsControl', "Agents"), + icon: Codicon.chatSparkle, + when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentStatusEnabled}`), + order: 10002 // to the right of the chat button +}); + +// Register a placeholder action to the submenu so it appears (required for submenus) +MenuRegistry.appendMenuItem(MenuId.AgentsTitleBarControlMenu, { + command: { + id: 'workbench.action.chat.toggle', + title: localize('openChat', "Open Chat"), + }, + when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentStatusEnabled}`), +}); + +//#endregion diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusService.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusService.ts similarity index 86% rename from src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusService.ts rename to src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusService.ts index a6607e468f5..0c8389b4060 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusService.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event } from '../../../../../base/common/event.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { Emitter, Event } from '../../../../../../base/common/event.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { createDecorator } from '../../../../../../platform/instantiation/common/instantiation.js'; //#region Agent Status Mode @@ -25,7 +25,7 @@ export interface IAgentStatusSessionInfo { //#region Agent Status Service Interface -export interface IAgentStatusService { +export interface IAgentTitleBarStatusService { readonly _serviceBrand: undefined; /** @@ -66,13 +66,13 @@ export interface IAgentStatusService { updateSessionTitle(title: string): void; } -export const IAgentStatusService = createDecorator('agentStatusService'); +export const IAgentTitleBarStatusService = createDecorator('agentTitleBarStatusService'); //#endregion //#region Agent Status Service Implementation -export class AgentStatusService extends Disposable implements IAgentStatusService { +export class AgentTitleBarStatusService extends Disposable implements IAgentTitleBarStatusService { declare readonly _serviceBrand: undefined; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts similarity index 84% rename from src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts rename to src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts index 6bfea0a5b27..bb4bdaf6386 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts @@ -3,39 +3,44 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './media/agentStatusWidget.css'; - -import { $, addDisposableListener, EventType, reset } from '../../../../../base/browser/dom.js'; -import { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; -import { localize } from '../../../../../nls.js'; -import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; -import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; -import { AgentStatusMode, IAgentStatusService } from './agentStatusService.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; +import './media/agenttitlebarstatuswidget.css'; +import { $, addDisposableListener, EventType, reset } from '../../../../../../base/browser/dom.js'; +import { renderIcon } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { localize } from '../../../../../../nls.js'; +import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; +import { getDefaultHoverDelegate } from '../../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { AgentStatusMode, IAgentTitleBarStatusService } from './agentTitleBarStatusService.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; import { ExitAgentSessionProjectionAction } from './agentSessionProjectionActions.js'; -import { IAgentSessionsService } from './agentSessionsService.js'; -import { AgentSessionStatus, IAgentSession, isSessionInProgressStatus } from './agentSessionsModel.js'; -import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; -import { IAction, SubmenuAction } from '../../../../../base/common/actions.js'; -import { ILabelService } from '../../../../../platform/label/common/label.js'; -import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { IBrowserWorkbenchEnvironmentService } from '../../../../services/environment/browser/environmentService.js'; -import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; -import { IEditorService } from '../../../../services/editor/common/editorService.js'; -import { Verbosity } from '../../../../common/editor.js'; -import { Schemas } from '../../../../../base/common/network.js'; -import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; -import { openSession } from './agentSessionsOpener.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { HiddenItemStrategy, WorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; -import { createActionViewItem } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; -import { FocusAgentSessionsAction } from './agentSessionsActions.js'; +import { IAgentSessionsService } from '../agentSessionsService.js'; +import { AgentSessionStatus, IAgentSession, isSessionInProgressStatus } from '../agentSessionsModel.js'; +import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { IAction, SubmenuAction } from '../../../../../../base/common/actions.js'; +import { ILabelService } from '../../../../../../platform/label/common/label.js'; +import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; +import { IBrowserWorkbenchEnvironmentService } from '../../../../../services/environment/browser/environmentService.js'; +import { IEditorGroupsService } from '../../../../../services/editor/common/editorGroupsService.js'; +import { IEditorService } from '../../../../../services/editor/common/editorService.js'; +import { Verbosity } from '../../../../../common/editor.js'; +import { Schemas } from '../../../../../../base/common/network.js'; +import { renderAsPlaintext } from '../../../../../../base/browser/markdownRenderer.js'; +import { openSession } from '../agentSessionsOpener.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IMenuService, MenuId, SubmenuItemAction } from '../../../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { HiddenItemStrategy, WorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; +import { createActionViewItem } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; +import { FocusAgentSessionsAction } from '../agentSessionsActions.js'; +import { IWorkbenchContribution } from '../../../../../common/contributions.js'; +import { IActionViewItemService } from '../../../../../../platform/actions/browser/actionViewItemService.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { mainWindow } from '../../../../../../base/browser/window.js'; +import { LayoutSettings } from '../../../../../services/layout/browser/layoutService.js'; +import { ChatConfiguration } from '../../../common/constants.js'; // Action triggered when clicking the main pill - change this to modify the primary action const ACTION_ID = 'workbench.action.quickchat.toggle'; @@ -53,7 +58,7 @@ const TITLE_DIRTY = '\u25cf '; * * The command center search box and navigation controls remain visible alongside this control. */ -export class AgentStatusWidget extends BaseActionViewItem { +export class AgentTitleBarStatusWidget extends BaseActionViewItem { private static readonly _quickOpenCommandId = 'workbench.action.quickOpenWithModes'; @@ -73,7 +78,7 @@ export class AgentStatusWidget extends BaseActionViewItem { action: IAction, options: IBaseActionViewItemOptions | undefined, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IAgentStatusService private readonly agentStatusService: IAgentStatusService, + @IAgentTitleBarStatusService private readonly agentTitleBarStatusService: IAgentTitleBarStatusService, @IHoverService private readonly hoverService: IHoverService, @ICommandService private readonly commandService: ICommandService, @IKeybindingService private readonly keybindingService: IKeybindingService, @@ -93,11 +98,11 @@ export class AgentStatusWidget extends BaseActionViewItem { this._commandCenterMenu = this._register(this.menuService.createMenu(MenuId.CommandCenterCenter, this.contextKeyService)); // Re-render when control mode or session info changes - this._register(this.agentStatusService.onDidChangeMode(() => { + this._register(this.agentTitleBarStatusService.onDidChangeMode(() => { this._render(); })); - this._register(this.agentStatusService.onDidChangeSessionInfo(() => { + this._register(this.agentTitleBarStatusService.onDidChangeSessionInfo(() => { this._render(); })); @@ -140,8 +145,8 @@ export class AgentStatusWidget extends BaseActionViewItem { } // Compute current render state to avoid unnecessary DOM rebuilds - const mode = this.agentStatusService.mode; - const sessionInfo = this.agentStatusService.sessionInfo; + const mode = this.agentTitleBarStatusService.mode; + const sessionInfo = this.agentTitleBarStatusService.sessionInfo; const { activeSessions, unreadSessions, attentionNeededSessions } = this._getSessionStats(); // Get attention session info for state computation @@ -184,7 +189,7 @@ export class AgentStatusWidget extends BaseActionViewItem { // Clear previous disposables for dynamic content this._dynamicDisposables.clear(); - if (this.agentStatusService.mode === AgentStatusMode.Session) { + if (this.agentTitleBarStatusService.mode === AgentStatusMode.Session) { // Agent Session Projection mode - show session title + close button this._renderSessionMode(this._dynamicDisposables); } else { @@ -352,7 +357,7 @@ export class AgentStatusWidget extends BaseActionViewItem { // Session title (center) const titleLabel = $('span.agent-status-title'); - const sessionInfo = this.agentStatusService.sessionInfo; + const sessionInfo = this.agentTitleBarStatusService.sessionInfo; titleLabel.textContent = sessionInfo?.title ?? localize('agentSessionProjection', "Agent Session Projection"); pill.appendChild(titleLabel); @@ -362,7 +367,7 @@ export class AgentStatusWidget extends BaseActionViewItem { // Setup pill hover const hoverDelegate = getDefaultHoverDelegate('mouse'); disposables.add(this.hoverService.setupManagedHover(hoverDelegate, pill, () => { - const sessionInfo = this.agentStatusService.sessionInfo; + const sessionInfo = this.agentTitleBarStatusService.sessionInfo; return sessionInfo ? localize('agentSessionProjectionTooltip', "Agent Session Projection: {0}", sessionInfo.title) : localize('agentSessionProjection', "Agent Session Projection"); })); @@ -389,7 +394,7 @@ export class AgentStatusWidget extends BaseActionViewItem { for (const [, actions] of this._commandCenterMenu.getActions({ shouldForwardArgs: true })) { for (const action of actions) { // Filter out the quick open action - we provide our own search UI - if (action.id === AgentStatusWidget._quickOpenCommandId) { + if (action.id === AgentTitleBarStatusWidget._quickOpenCommandId) { continue; } // For submenus (like debug toolbar), add the submenu actions @@ -824,3 +829,47 @@ export class AgentStatusWidget extends BaseActionViewItem { // #endregion } + +/** + * Provides custom rendering for the agent status in the command center. + * Uses IActionViewItemService to render a custom AgentStatusWidget + * for the AgentsControlMenu submenu. + * Also adds a CSS class to the workbench when agent status is enabled. + */ +export class AgentTitleBarStatusRendering extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.agentStatus.rendering'; + + constructor( + @IActionViewItemService actionViewItemService: IActionViewItemService, + @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService + ) { + super(); + + this._register(actionViewItemService.register(MenuId.CommandCenter, MenuId.AgentsTitleBarControlMenu, (action, options) => { + if (!(action instanceof SubmenuItemAction)) { + return undefined; + } + return instantiationService.createInstance(AgentTitleBarStatusWidget, action, options); + }, undefined)); + + // Add/remove CSS class on workbench based on setting + // Also force enable command center when agent status is enabled + const updateClass = () => { + const enabled = configurationService.getValue(ChatConfiguration.AgentStatusEnabled) === true; + mainWindow.document.body.classList.toggle('agent-status-enabled', enabled); + + // Force enable command center when agent status is enabled + if (enabled && configurationService.getValue(LayoutSettings.COMMAND_CENTER) !== true) { + configurationService.updateValue(LayoutSettings.COMMAND_CENTER, true); + } + }; + updateClass(); + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(ChatConfiguration.AgentStatusEnabled)) { + updateClass(); + } + })); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionProjection.css b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agentsessionprojection.css similarity index 94% rename from src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionProjection.css rename to src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agentsessionprojection.css index d6d3b3c3694..7f64094c2b4 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionProjection.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agentsessionprojection.css @@ -3,10 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* ======================================== -Agent Session Projection Mode - Tab and Editor styling -======================================== */ - /* Style all tabs with the same background as the agent status */ .monaco-workbench.agent-session-projection-active .part.editor > .content .editor-group-container > .title .tabs-container > .tab { background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent) !important; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css similarity index 98% rename from src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css rename to src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css index e1d663108da..e4af6e88de4 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css @@ -3,10 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* ======================================== -Agent Status Widget - Titlebar control -======================================== */ - /* Hide command center search box when agent status enabled */ .agent-status-enabled .command-center .action-item.command-center-center { display: none !important; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts index f8b867d9a49..2deb9859f16 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts @@ -19,7 +19,6 @@ import { ChatViewId, ChatViewPaneTarget, IChatWidget, IChatWidgetService, IQuick import { ChatEditor, IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js'; import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js'; import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; -import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; export class ChatWidgetService extends Disposable implements IChatWidgetService { @@ -41,7 +40,6 @@ export class ChatWidgetService extends Disposable implements IChatWidgetService @ILayoutService private readonly layoutService: ILayoutService, @IEditorService private readonly editorService: IEditorService, @IChatService private readonly chatService: IChatService, - @IWorkbenchLayoutService private readonly workbenchLayoutService: IWorkbenchLayoutService, ) { super(); } @@ -118,9 +116,6 @@ export class ChatWidgetService extends Disposable implements IChatWidgetService if (!options?.preserveFocus) { chatView.focusInput(); } - if (options?.expanded) { - this.workbenchLayoutService.setAuxiliaryBarMaximized(true); - } } return chatView?.widget; } diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts index 3f8e0e9d7a9..fffb5a27aa8 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts @@ -43,7 +43,6 @@ export interface IChatEditorOptions extends IEditorOptions { preferred?: string; fallback?: string; }; - expanded?: boolean; } export class ChatEditor extends EditorPane { diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 52bfac795e0..7fd857ffb22 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -47,7 +47,7 @@ import { IChatModelReference, IChatService } from '../../../common/chatService/c import { IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; import { LocalChatSessionUri, getChatSessionType } from '../../../common/model/chatUri.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../../common/constants.js'; -import { AgentSessionsControl, AgentSessionsControlSource } from '../../agentSessions/agentSessionsControl.js'; +import { AgentSessionsControl } from '../../agentSessions/agentSessionsControl.js'; import { AgentSessionsListDelegate } from '../../agentSessions/agentSessionsViewer.js'; import { ACTION_ID_NEW_CHAT } from '../../actions/chatActions.js'; import { ChatWidget } from '../../widget/chatWidget.js'; @@ -394,7 +394,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { // Sessions Control this.sessionsControlContainer = append(sessionsContainer, $('.agent-sessions-control-container')); const sessionsControl = this.sessionsControl = this._register(this.instantiationService.createInstance(AgentSessionsControl, this.sessionsControlContainer, { - source: AgentSessionsControlSource.ChatViewPane, + source: 'chatViewPane', filter: sessionsFilter, overrideStyles: this.getLocationBasedColors().listOverrideStyles, getHoverPosition: () => { diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index 7da745d17d2..ed0de643267 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -108,10 +108,6 @@ export namespace ChatContextKeys { export const hasAgentSessionChanges = new RawContextKey('agentSessionHasChanges', false, { type: 'boolean', description: localize('agentSessionHasChanges', "True when the current agent session item has changes.") }); export const isKatexMathElement = new RawContextKey('chatIsKatexMathElement', false, { type: 'boolean', description: localize('chatIsKatexMathElement', "True when focusing a KaTeX math element.") }); - - export const inAgentSessionProjection = new RawContextKey('chatInAgentSessionProjection', false, { type: 'boolean', description: localize('chatInAgentSessionProjection', "True when the workbench is in agent session projection mode for reviewing an agent session.") }); - - export const agentStatusHasNotifications = new RawContextKey('agentStatusHasNotifications', false, { type: 'boolean', description: localize('agentStatusHasNotifications', "True when the agent status widget has unread or in-progress sessions.") }); } export namespace ChatContextKeyExprs { diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index ed1689fb2e2..e437e7144ef 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -40,7 +40,7 @@ import { AgentSessionsWelcomeEditorOptions, AgentSessionsWelcomeInput } from './ import { IChatService } from '../../chat/common/chatService/chatService.js'; import { IChatModel } from '../../chat/common/model/chatModel.js'; import { ISessionTypePickerDelegate } from '../../chat/browser/chat.js'; -import { AgentSessionsControl, AgentSessionsControlSource, IAgentSessionsControlOptions } from '../../chat/browser/agentSessions/agentSessionsControl.js'; +import { AgentSessionsControl, IAgentSessionsControlOptions } from '../../chat/browser/agentSessions/agentSessionsControl.js'; import { IAgentSessionsFilter } from '../../chat/browser/agentSessions/agentSessionsViewer.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { IResolvedWalkthrough, IWalkthroughsService } from '../../welcomeGettingStarted/browser/gettingStartedService.js'; @@ -280,7 +280,8 @@ export class AgentSessionsWelcomePage extends EditorPane { filter, getHoverPosition: () => HoverPosition.BELOW, trackActiveEditorSession: () => false, - source: AgentSessionsControlSource.WelcomeView, + source: 'welcomeView', + notifySessionOpened: () => this.layoutService.setAuxiliaryBarMaximized(true) // TODO@osortega what if the session did not open in the 2nd sidebar? }; this.sessionsControl = this.sessionsControlDisposables.add(this.instantiationService.createInstance( @@ -433,7 +434,7 @@ export class AgentSessionsWelcomePage extends EditorPane { // Set margin offset for 2-column layout: actual height - visual height // Visual height = ceil(n/2) * 52, so offset = floor(n/2) * 52 const marginOffset = Math.floor(visibleSessions / 2) * 52; - this.sessionsControl.setGridMarginOffset(marginOffset); + this.sessionsControl.element!.style.marginBottom = `-${marginOffset}px`; } override focus(): void { From 6ba7b2e6caeb9020a4a5b73de641e052722ac154 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:00:58 +0100 Subject: [PATCH 175/387] Chat: Fix excessive top padding in request bubble when user replies with a bullet list (#288113) * Initial plan * Fix excessive top padding in chat request bubble when user replies with a bullet list Co-authored-by: joaomoreno <22350+joaomoreno@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: joaomoreno <22350+joaomoreno@users.noreply.github.com> --- src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 7fb771d460d..f4ba328b3b4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -2476,6 +2476,10 @@ have to be updated for changes to the rules above, or to support more deeply nes border: 1px dotted var(--vscode-focusBorder); } + .interactive-item-container.interactive-request .value .rendered-markdown > :first-child { + margin-top: 0px; + } + .interactive-item-container.interactive-request .value .rendered-markdown > :last-child { margin-bottom: 0px; } From 08132f0222a82169663bac4afdb51113d281079a Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 19 Jan 2026 12:06:19 +0100 Subject: [PATCH 176/387] remote cli: do not open files with openExternal (#288856) --- src/vs/workbench/api/node/extHostCLIServer.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts index 01bc190d579..0353ab3a9de 100644 --- a/src/vs/workbench/api/node/extHostCLIServer.ts +++ b/src/vs/workbench/api/node/extHostCLIServer.ts @@ -153,8 +153,11 @@ export class CLIServerBase { private async openExternal(data: OpenExternalCommandPipeArgs): Promise { for (const uriString of data.uris) { const uri = URI.parse(uriString); - const urioOpen = uri.scheme === 'file' ? uri : uriString; // workaround for #112577 - await this._commands.executeCommand('_remoteCLI.openExternal', urioOpen); + if (uri.scheme === 'file') { + // skip file:// uris, they refer to the file system of the remote that have no meaning on the local machine + continue; + } + await this._commands.executeCommand('_remoteCLI.openExternal', uriString); // always send the string, workaround for #112577 } } From 0971437ee0ca1cd272941a9d0667b97ad196b8dc Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:06:53 +0100 Subject: [PATCH 177/387] Background - fix viewing changes action in working set (#288845) --- .../contrib/chat/browser/chatEditing/chatEditingActions.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 0451c0746d7..a2cde761222 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -339,9 +339,10 @@ export class ViewAllSessionChangesAction extends Action2 { return; } - const resources = changes - .filter(d => d.originalUri) - .map(d => ({ originalUri: d.originalUri!, modifiedUri: d.modifiedUri })); + const resources = changes.map(d => ({ + originalUri: d.originalUri, + modifiedUri: d.modifiedUri + })); if (resources.length > 0) { await commandService.executeCommand('_workbench.openMultiDiffEditor', { From 9a0734e5edfb08ce6bdc44be9fe519237a2cfd9b Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Mon, 19 Jan 2026 12:12:39 +0100 Subject: [PATCH 178/387] Remove chat_enabled flag --- src/vs/base/common/defaultAccount.ts | 1 - .../workbench/services/chat/common/chatEntitlementService.ts | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/vs/base/common/defaultAccount.ts b/src/vs/base/common/defaultAccount.ts index 7eb9803f3b5..6b7f5d76d50 100644 --- a/src/vs/base/common/defaultAccount.ts +++ b/src/vs/base/common/defaultAccount.ts @@ -27,7 +27,6 @@ export interface IEntitlementsData extends ILegacyQuotaSnapshotData { readonly access_type_sku: string; readonly assigned_date: string; readonly can_signup_for_limited: boolean; - readonly chat_enabled: boolean; readonly copilot_plan: string; readonly organization_login_list: string[]; readonly analytics_tracking_id: string; diff --git a/src/vs/workbench/services/chat/common/chatEntitlementService.ts b/src/vs/workbench/services/chat/common/chatEntitlementService.ts index 5bc38d53b2c..fedadf5b1d6 100644 --- a/src/vs/workbench/services/chat/common/chatEntitlementService.ts +++ b/src/vs/workbench/services/chat/common/chatEntitlementService.ts @@ -641,9 +641,6 @@ export class ChatEntitlementRequests extends Disposable { entitlement = ChatEntitlement.Business; } else if (entitlementsData.copilot_plan === 'enterprise') { entitlement = ChatEntitlement.Enterprise; - } else if (entitlementsData.chat_enabled) { - // This should never happen as we exhaustively list the plans above. But if a new plan is added in the future older clients won't break - entitlement = ChatEntitlement.Pro; } else { entitlement = ChatEntitlement.Unavailable; } From 4547b4a3a520cda4c2a7b0de2acd19c7ee15e5bd Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Mon, 19 Jan 2026 11:36:49 +0000 Subject: [PATCH 179/387] Add ChatContextUsageWidget to display token usage in chat input --- .../widget/input/chatContextUsageWidget.ts | 260 ++++++++++++++++++ .../browser/widget/input/chatInputPart.ts | 9 + .../input/media/chatContextUsageWidget.css | 137 +++++++++ 3 files changed, 406 insertions(+) create mode 100644 src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts create mode 100644 src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts new file mode 100644 index 00000000000..e9d319dd4e3 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts @@ -0,0 +1,260 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/chatContextUsageWidget.css'; +import * as dom from '../../../../../../base/browser/dom.js'; +import { Disposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; +import { IChatModel } from '../../../common/model/chatModel.js'; +import { ILanguageModelsService } from '../../../common/languageModels.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; +import { localize } from '../../../../../../nls.js'; +import { RunOnceScheduler } from '../../../../../../base/common/async.js'; + +const $ = dom.$; + +export class ChatContextUsageWidget extends Disposable { + + public readonly domNode: HTMLElement; + private readonly ringProgress: SVGCircleElement; + + private readonly _modelListener = this._register(new MutableDisposable()); + private _currentModel: IChatModel | undefined; + + private readonly _updateScheduler: RunOnceScheduler; + + // Stats + private _totalTokenCount = 0; + private _promptsTokenCount = 0; + private _filesTokenCount = 0; + private _toolsTokenCount = 0; + private _contextTokenCount = 0; + + private _maxTokenCount = 4096; // Default fallback + private _usagePercent = 0; + + constructor( + @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, + @IHoverService private readonly hoverService: IHoverService, + ) { + super(); + + this.domNode = $('.chat-context-usage-widget'); + this.domNode.style.display = 'none'; + + // Create SVG Ring + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('class', 'chat-context-usage-ring'); + svg.setAttribute('width', '16'); + svg.setAttribute('height', '16'); + svg.setAttribute('viewBox', '0 0 16 16'); + + const background = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + background.setAttribute('class', 'chat-context-usage-ring-background'); + background.setAttribute('cx', '8'); + background.setAttribute('cy', '8'); + background.setAttribute('r', '7'); + svg.appendChild(background); + + this.ringProgress = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + this.ringProgress.setAttribute('class', 'chat-context-usage-ring-progress'); + this.ringProgress.setAttribute('cx', '8'); + this.ringProgress.setAttribute('cy', '8'); + this.ringProgress.setAttribute('r', '7'); + svg.appendChild(this.ringProgress); + + this.domNode.appendChild(svg); + + this._updateScheduler = this._register(new RunOnceScheduler(() => this._refreshUsage(), 2000)); + + this._register(this.hoverService.setupDelayedHover(this.domNode, () => ({ + content: this._getHoverDomNode(), + appearance: { + showPointer: true, + skipFadeInAnimation: true + } + }))); + + this._register(dom.addDisposableListener(this.domNode, dom.EventType.CLICK, () => { + this.hoverService.showInstantHover({ + content: this._getHoverDomNode(), + target: this.domNode, + appearance: { + showPointer: true, + skipFadeInAnimation: true + }, + persistence: { + sticky: true + } + }, true); + })); + } + + setModel(model: IChatModel | undefined) { + if (this._currentModel === model) { + return; + } + + this._currentModel = model; + this._modelListener.clear(); + + if (model) { + this._modelListener.value = model.onDidChange(() => { + this._updateScheduler.schedule(); + }); + this._updateScheduler.schedule(0); + this.domNode.style.display = ''; + } else { + this.domNode.style.display = 'none'; + } + } + + private async _refreshUsage() { + if (!this._currentModel) { + return; + } + + this._promptsTokenCount = 0; + this._filesTokenCount = 0; + this._toolsTokenCount = 0; + this._contextTokenCount = 0; + + const requests = this._currentModel.getRequests(); + + let modelId: string | undefined; + + const inputState = this._currentModel.inputModel.state.get(); + if (inputState?.selectedModel) { + modelId = inputState.selectedModel.identifier; + if (inputState.selectedModel.metadata.maxInputTokens) { + this._maxTokenCount = inputState.selectedModel.metadata.maxInputTokens; + } + } + + const countTokens = async (text: string): Promise => { + if (modelId) { + return this.languageModelsService.computeTokenLength(modelId, text, CancellationToken.None); + } + return text.length / 4; + }; + + for (const request of requests) { + // Prompts: User message + const messageText = typeof request.message === 'string' ? request.message : request.message.text; + this._promptsTokenCount += await countTokens(messageText); + + // Variables (Files, Context) + if (request.variableData && request.variableData.variables) { + for (const variable of request.variableData.variables) { + // Estimate usage for variables as getting full content might be expensive/complex async + // Using a safe estimate for now per item type + const defaultEstimate = 500; + + if (variable.kind === 'file') { + this._filesTokenCount += defaultEstimate; + } else { + this._contextTokenCount += defaultEstimate; + } + } + } + + // Tools & Response + if (request.response) { + const responseString = request.response.response.toString(); + this._promptsTokenCount += await countTokens(responseString); + + // Loop through response parts for tool invocations + for (const part of request.response.response.value) { + if (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') { + // Estimate tool invocation cost + this._toolsTokenCount += 200; + } + } + } + } + + this._totalTokenCount = Math.round(this._promptsTokenCount + this._filesTokenCount + this._toolsTokenCount + this._contextTokenCount); + this._usagePercent = Math.min(100, (this._totalTokenCount / this._maxTokenCount) * 100); + + this._updateRing(); + } + + private _updateRing() { + const r = 7; + const c = 2 * Math.PI * r; + const offset = c - (this._usagePercent / 100) * c; + this.ringProgress.style.strokeDashoffset = String(offset); + + this.domNode.classList.remove('warning', 'error'); + if (this._usagePercent > 90) { + this.domNode.classList.add('error'); + } else if (this._usagePercent > 75) { + this.domNode.classList.add('warning'); + } + } + + private _getHoverDomNode(): HTMLElement { + const container = $('.chat-context-usage-hover'); + + const percentStr = `${this._usagePercent.toFixed(0)}%`; + const formatTokens = (value: number) => { + if (value >= 1000) { + const thousands = value / 1000; + return `${thousands >= 10 ? Math.round(thousands) : thousands.toFixed(1)}k`; + } + return `${value}`; + }; + const usageStr = `${formatTokens(this._totalTokenCount)} / ${formatTokens(this._maxTokenCount)}`; + + // Header + // const header = dom.append(container, $('.header')); + // dom.append(header, $('span', undefined, localize('contextUsage', "Context Usage"))); + + // Quota Indicator (Progress Bar) + const quotaIndicator = dom.append(container, $('.quota-indicator')); + const quotaBar = dom.append(quotaIndicator, $('.quota-bar')); + const quotaBit = dom.append(quotaBar, $('.quota-bit')); + quotaBit.style.width = `${this._usagePercent}%`; + + const quotaLabel = dom.append(quotaIndicator, $('.quota-label')); + dom.append(quotaLabel, $('span.quota-title', undefined, localize('totalUsageLabel', "Total usage"))); + dom.append(quotaLabel, $('span.quota-value', undefined, `${usageStr} • ${percentStr}`)); + + + if (this._usagePercent > 90) { + quotaIndicator.classList.add('error'); + } else if (this._usagePercent > 75) { + quotaIndicator.classList.add('warning'); + } + + dom.append(container, $('.chat-context-usage-hover-separator')); + + // List + const list = dom.append(container, $('.chat-context-usage-hover-list')); + + const addItem = (label: string, value: number) => { + const item = dom.append(list, $('.chat-context-usage-hover-item')); + dom.append(item, $('span.label', undefined, label)); + + // Calculate percentage for breakdown + const percent = this._maxTokenCount > 0 ? (value / this._maxTokenCount) * 100 : 0; + const displayValue = `${percent.toFixed(0)}%`; + dom.append(item, $('span.value', undefined, displayValue)); + }; + + addItem(localize('prompts', "Prompts"), Math.round(this._promptsTokenCount)); + addItem(localize('files', "Files"), Math.round(this._filesTokenCount)); + addItem(localize('tools', "Tools"), Math.round(this._toolsTokenCount)); + addItem(localize('context', "Context"), Math.round(this._contextTokenCount)); + + if (this._usagePercent > 80) { + const remaining = Math.max(0, this._maxTokenCount - this._totalTokenCount); + const warning = dom.append(container, $('div', { style: 'margin-top: 8px; color: var(--vscode-editorWarning-foreground);' })); + warning.textContent = localize('contextLimitWarning', "Approaching limit. {0} tokens remaining.", remaining); + } + + return container; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 2db1c584e4b..b76884cd375 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -119,6 +119,7 @@ import { IChatInputPickerOptions } from './chatInputPickerActionItem.js'; import { getAgentSessionProvider } from '../../agentSessions/agentSessions.js'; import { SearchableOptionPickerActionItem } from '../../chatSessions/searchableOptionPickerActionItem.js'; import { mixin } from '../../../../../../base/common/objects.js'; +import { ChatContextUsageWidget } from './chatContextUsageWidget.js'; const $ = dom.$; @@ -268,6 +269,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private chatInputTodoListWidgetContainer!: HTMLElement; private chatInputWidgetsContainer!: HTMLElement; private readonly _widgetController = this._register(new MutableDisposable()); + private readonly _contextUsageWidget = this._register(new MutableDisposable()); readonly inputPartHeight = observableValue(this, 0); @@ -1605,6 +1607,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.updateAgentSessionTypeContextKey(); this.refreshChatSessionPickers(); this.tryUpdateWidgetController(); + this._contextUsageWidget.value?.setModel(widget.viewModel?.model); })); let elements; @@ -1670,6 +1673,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.chatInputTodoListWidgetContainer = elements.chatInputTodoListWidgetContainer; this.chatInputWidgetsContainer = elements.chatInputWidgetsContainer; + this._contextUsageWidget.value = this.instantiationService.createInstance(ChatContextUsageWidget); + elements.editorContainer.appendChild(this._contextUsageWidget.value.domNode); + if (this._widget?.viewModel) { + this._contextUsageWidget.value.setModel(this._widget.viewModel.model); + } + if (this.options.enableImplicitContext && !this._implicitContext) { this._implicitContext = this._register( this.instantiationService.createInstance(ChatImplicitContext), diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css new file mode 100644 index 00000000000..bf663b98959 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css @@ -0,0 +1,137 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-context-usage-widget { + position: absolute; + top: 10px; + right: 10px; + width: 16px; + height: 16px; + z-index: 10; + cursor: pointer; + opacity: 0.6; +} + +.chat-context-usage-widget:hover { + opacity: 1; +} + +.chat-context-usage-ring { + transform: rotate(-90deg); +} + +.chat-context-usage-ring-background { + fill: none; + stroke: var(--vscode-icon-foreground); + stroke-width: 2; + opacity: 0.3; +} + +.chat-context-usage-ring-progress { + fill: none; + stroke: var(--vscode-icon-foreground); + stroke-width: 2; + stroke-dasharray: 44; /* 2 * PI * r (r=7) */ + stroke-dashoffset: 44; + transition: stroke-dashoffset 0.5s ease; +} + +.chat-context-usage-widget.warning .chat-context-usage-ring-progress { + stroke: var(--vscode-editorWarning-foreground); +} + +.chat-context-usage-widget.error .chat-context-usage-ring-progress { + stroke: var(--vscode-editorError-foreground); +} + +/* Hover Content */ + +.chat-context-usage-hover { + min-width: 250px; + padding: 8px; + display: flex; + flex-direction: column; + gap: 4px; + font-size: 12px; +} + +.chat-context-usage-hover .header { + font-weight: 600; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4px; +} + +/* .chat-context-usage-hover .quota-indicator { + margin-bottom: 6px; +} */ + +.chat-context-usage-hover .quota-indicator .quota-label { + display: flex; + justify-content: space-between; + gap: 20px; + margin-bottom: 3px; +} + +.chat-context-usage-hover .quota-indicator .quota-label{ + color: var(--vscode-descriptionForeground); +} + +.chat-context-usage-hover .quota-indicator .quota-label .quota-value{ + color: var(--vscode-foreground); +} + +.chat-context-usage-hover .quota-indicator .quota-bar { + width: 100%; + height: 4px; + background-color: var(--vscode-gauge-background); + border-radius: 4px; + border: 1px solid var(--vscode-gauge-border); + margin: 4px 0; +} + +.chat-context-usage-hover .quota-indicator .quota-bar .quota-bit { + height: 100%; + background-color: var(--vscode-gauge-foreground); + border-radius: 4px; +} + +.chat-context-usage-hover .quota-indicator.warning .quota-bar { + background-color: var(--vscode-gauge-warningBackground); +} + +.chat-context-usage-hover .quota-indicator.warning .quota-bar .quota-bit { + background-color: var(--vscode-gauge-warningForeground); +} + +.chat-context-usage-hover .quota-indicator.error .quota-bar { + background-color: var(--vscode-gauge-errorBackground); +} + +.chat-context-usage-hover .quota-indicator.error .quota-bar .quota-bit { + background-color: var(--vscode-gauge-errorForeground); +} + +.chat-context-usage-hover-separator { + height: 1px; + background-color: var(--vscode-widget-border); + opacity: 0.3; +} + +.chat-context-usage-hover-list { + display: flex; + flex-direction: column; + gap: 2px; +} + +.chat-context-usage-hover-item { + display: flex; + justify-content: space-between; +} + +.chat-context-usage-hover-item .label { + color: var(--vscode-descriptionForeground); +} From e5c481e4affad0c7915f540f768a48b0c3e364fe Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 19 Jan 2026 12:39:32 +0100 Subject: [PATCH 180/387] do not set policy data if not fetched (#288866) --- .../services/accounts/common/defaultAccount.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/services/accounts/common/defaultAccount.ts b/src/vs/workbench/services/accounts/common/defaultAccount.ts index 3bd289f8f31..f9192f16c51 100644 --- a/src/vs/workbench/services/accounts/common/defaultAccount.ts +++ b/src/vs/workbench/services/accounts/common/defaultAccount.ts @@ -16,7 +16,7 @@ import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase import { Barrier, ThrottledDelayer, timeout } from '../../../../base/common/async.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { getErrorMessage } from '../../../../base/common/errors.js'; -import { IDefaultAccount, IDefaultAccountAuthenticationProvider, IEntitlementsData } from '../../../../base/common/defaultAccount.js'; +import { IDefaultAccount, IDefaultAccountAuthenticationProvider, IEntitlementsData, IPolicyData } from '../../../../base/common/defaultAccount.js'; import { isString } from '../../../../base/common/types.js'; import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; import { isWeb } from '../../../../base/common/platform.js'; @@ -363,20 +363,20 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid this.getTokenEntitlements(sessions), ]); - const mcpRegistryProvider = policyData.mcp ? await this.getMcpRegistryProvider(sessions) : undefined; + const mcpRegistryProvider = policyData?.mcp ? await this.getMcpRegistryProvider(sessions) : undefined; const account: IDefaultAccount = { authenticationProvider, sessionId: sessions[0].id, enterprise: authenticationProvider.enterprise || sessions[0].account.label.includes('_'), entitlementsData, - policyData: { + policyData: policyData ? { chat_agent_enabled: policyData.chat_agent_enabled, chat_preview_features_enabled: policyData.chat_preview_features_enabled, mcp: policyData.mcp, mcpRegistryUrl: mcpRegistryProvider?.url, mcpAccess: mcpRegistryProvider?.registry_access, - } + } : undefined, }; this.logService.debug('[DefaultAccount] Successfully created default account for provider:', authenticationProvider.id); return account; @@ -434,22 +434,22 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid return expectedScopes.every(scope => scopes.includes(scope)); } - private async getTokenEntitlements(sessions: AuthenticationSession[]): Promise<{ mcp?: boolean; chat_preview_features_enabled?: boolean; chat_agent_enabled?: boolean }> { + private async getTokenEntitlements(sessions: AuthenticationSession[]): Promise | undefined> { const tokenEntitlementsUrl = this.getTokenEntitlementUrl(); if (!tokenEntitlementsUrl) { this.logService.debug('[DefaultAccount] No token entitlements URL found'); - return {}; + return undefined; } this.logService.debug('[DefaultAccount] Fetching token entitlements from:', tokenEntitlementsUrl); const response = await this.request(tokenEntitlementsUrl, 'GET', undefined, sessions, CancellationToken.None); if (!response) { - return {}; + return undefined; } if (response.res.statusCode && response.res.statusCode !== 200) { this.logService.trace(`[DefaultAccount] unexpected status code ${response.res.statusCode} while fetching token entitlements`); - return {}; + return undefined; } try { From a81e20f4a56a33c239c4cd689bb9fabea8225991 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Mon, 19 Jan 2026 12:05:33 +0000 Subject: [PATCH 181/387] Enhance ChatContextUsageWidget with hover display and token usage updates --- .../widget/input/chatContextUsageWidget.ts | 131 +++++++++++++----- .../input/media/chatContextUsageWidget.css | 15 +- 2 files changed, 106 insertions(+), 40 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts index e9d319dd4e3..cf1bf5d79c2 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts @@ -24,6 +24,7 @@ export class ChatContextUsageWidget extends Disposable { private _currentModel: IChatModel | undefined; private readonly _updateScheduler: RunOnceScheduler; + private readonly _hoverDisplayScheduler: RunOnceScheduler; // Stats private _totalTokenCount = 0; @@ -35,6 +36,10 @@ export class ChatContextUsageWidget extends Disposable { private _maxTokenCount = 4096; // Default fallback private _usagePercent = 0; + private _hoverQuotaBit: HTMLElement | undefined; + private _hoverQuotaValue: HTMLElement | undefined; + private _hoverItemValues: Map = new Map(); + constructor( @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, @IHoverService private readonly hoverService: IHoverService, @@ -67,17 +72,30 @@ export class ChatContextUsageWidget extends Disposable { this.domNode.appendChild(svg); - this._updateScheduler = this._register(new RunOnceScheduler(() => this._refreshUsage(), 2000)); + this._updateScheduler = this._register(new RunOnceScheduler(() => this._refreshUsage(), 1000)); + this._hoverDisplayScheduler = this._register(new RunOnceScheduler(() => { + this._updateScheduler.schedule(0); + this.hoverService.showInstantHover({ + content: this._getHoverDomNode(), + target: this.domNode, + appearance: { + showPointer: true, + skipFadeInAnimation: true + } + }); + }, 600)); - this._register(this.hoverService.setupDelayedHover(this.domNode, () => ({ - content: this._getHoverDomNode(), - appearance: { - showPointer: true, - skipFadeInAnimation: true - } - }))); + this._register(dom.addDisposableListener(this.domNode, 'mouseenter', () => { + this._hoverDisplayScheduler.schedule(); + })); + + this._register(dom.addDisposableListener(this.domNode, 'mouseleave', () => { + this._hoverDisplayScheduler.cancel(); + })); this._register(dom.addDisposableListener(this.domNode, dom.EventType.CLICK, () => { + this._hoverDisplayScheduler.cancel(); + this._updateScheduler.schedule(0); this.hoverService.showInstantHover({ content: this._getHoverDomNode(), target: this.domNode, @@ -105,7 +123,6 @@ export class ChatContextUsageWidget extends Disposable { this._updateScheduler.schedule(); }); this._updateScheduler.schedule(0); - this.domNode.style.display = ''; } else { this.domNode.style.display = 'none'; } @@ -116,13 +133,20 @@ export class ChatContextUsageWidget extends Disposable { return; } - this._promptsTokenCount = 0; - this._filesTokenCount = 0; - this._toolsTokenCount = 0; - this._contextTokenCount = 0; + let promptsTokenCount = 0; + let filesTokenCount = 0; + let toolsTokenCount = 0; + let contextTokenCount = 0; const requests = this._currentModel.getRequests(); + if (requests.length === 0) { + this.domNode.style.display = 'none'; + return; + } + + this.domNode.style.display = ''; + let modelId: string | undefined; const inputState = this._currentModel.inputModel.state.get(); @@ -135,7 +159,11 @@ export class ChatContextUsageWidget extends Disposable { const countTokens = async (text: string): Promise => { if (modelId) { - return this.languageModelsService.computeTokenLength(modelId, text, CancellationToken.None); + try { + return await this.languageModelsService.computeTokenLength(modelId, text, CancellationToken.None); + } catch (error) { + return text.length / 4; + } } return text.length / 4; }; @@ -143,7 +171,7 @@ export class ChatContextUsageWidget extends Disposable { for (const request of requests) { // Prompts: User message const messageText = typeof request.message === 'string' ? request.message : request.message.text; - this._promptsTokenCount += await countTokens(messageText); + promptsTokenCount += await countTokens(messageText); // Variables (Files, Context) if (request.variableData && request.variableData.variables) { @@ -153,9 +181,9 @@ export class ChatContextUsageWidget extends Disposable { const defaultEstimate = 500; if (variable.kind === 'file') { - this._filesTokenCount += defaultEstimate; + filesTokenCount += defaultEstimate; } else { - this._contextTokenCount += defaultEstimate; + contextTokenCount += defaultEstimate; } } } @@ -163,22 +191,28 @@ export class ChatContextUsageWidget extends Disposable { // Tools & Response if (request.response) { const responseString = request.response.response.toString(); - this._promptsTokenCount += await countTokens(responseString); + promptsTokenCount += await countTokens(responseString); // Loop through response parts for tool invocations for (const part of request.response.response.value) { if (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') { // Estimate tool invocation cost - this._toolsTokenCount += 200; + toolsTokenCount += 200; } } } } + this._promptsTokenCount = promptsTokenCount; + this._filesTokenCount = filesTokenCount; + this._toolsTokenCount = toolsTokenCount; + this._contextTokenCount = contextTokenCount; + this._totalTokenCount = Math.round(this._promptsTokenCount + this._filesTokenCount + this._toolsTokenCount + this._contextTokenCount); this._usagePercent = Math.min(100, (this._totalTokenCount / this._maxTokenCount) * 100); this._updateRing(); + this._updateHover(); } private _updateRing() { @@ -195,6 +229,39 @@ export class ChatContextUsageWidget extends Disposable { } } + private _updateHover() { + if (this._hoverQuotaValue) { + const percentStr = `${this._usagePercent.toFixed(0)}%`; + const formatTokens = (value: number) => { + if (value >= 1000) { + const thousands = value / 1000; + return `${thousands >= 10 ? Math.round(thousands) : thousands.toFixed(1)}k`; + } + return `${value}`; + }; + const usageStr = `${formatTokens(this._totalTokenCount)} / ${formatTokens(this._maxTokenCount)}`; + this._hoverQuotaValue.textContent = `${usageStr} • ${percentStr}`; + } + + if (this._hoverQuotaBit) { + this._hoverQuotaBit.style.width = `${this._usagePercent}%`; + } + + const updateItem = (key: string, value: number) => { + const item = this._hoverItemValues.get(key); + if (item) { + const percent = this._maxTokenCount > 0 ? (value / this._maxTokenCount) * 100 : 0; + const displayValue = `${percent.toFixed(0)}%`; + item.textContent = displayValue; + } + }; + + updateItem('prompts', this._promptsTokenCount); + updateItem('files', this._filesTokenCount); + updateItem('tools', this._toolsTokenCount); + updateItem('context', this._contextTokenCount); + } + private _getHoverDomNode(): HTMLElement { const container = $('.chat-context-usage-hover'); @@ -208,20 +275,16 @@ export class ChatContextUsageWidget extends Disposable { }; const usageStr = `${formatTokens(this._totalTokenCount)} / ${formatTokens(this._maxTokenCount)}`; - // Header - // const header = dom.append(container, $('.header')); - // dom.append(header, $('span', undefined, localize('contextUsage', "Context Usage"))); - // Quota Indicator (Progress Bar) const quotaIndicator = dom.append(container, $('.quota-indicator')); - const quotaBar = dom.append(quotaIndicator, $('.quota-bar')); - const quotaBit = dom.append(quotaBar, $('.quota-bit')); - quotaBit.style.width = `${this._usagePercent}%`; const quotaLabel = dom.append(quotaIndicator, $('.quota-label')); dom.append(quotaLabel, $('span.quota-title', undefined, localize('totalUsageLabel', "Total usage"))); - dom.append(quotaLabel, $('span.quota-value', undefined, `${usageStr} • ${percentStr}`)); + this._hoverQuotaValue = dom.append(quotaLabel, $('span.quota-value', undefined, `${usageStr} • ${percentStr}`)); + const quotaBar = dom.append(quotaIndicator, $('.quota-bar')); + this._hoverQuotaBit = dom.append(quotaBar, $('.quota-bit')); + this._hoverQuotaBit.style.width = `${this._usagePercent}%`; if (this._usagePercent > 90) { quotaIndicator.classList.add('error'); @@ -233,21 +296,23 @@ export class ChatContextUsageWidget extends Disposable { // List const list = dom.append(container, $('.chat-context-usage-hover-list')); + this._hoverItemValues.clear(); - const addItem = (label: string, value: number) => { + const addItem = (key: string, label: string, value: number) => { const item = dom.append(list, $('.chat-context-usage-hover-item')); dom.append(item, $('span.label', undefined, label)); // Calculate percentage for breakdown const percent = this._maxTokenCount > 0 ? (value / this._maxTokenCount) * 100 : 0; const displayValue = `${percent.toFixed(0)}%`; - dom.append(item, $('span.value', undefined, displayValue)); + const valueSpan = dom.append(item, $('span.value', undefined, displayValue)); + this._hoverItemValues.set(key, valueSpan); }; - addItem(localize('prompts', "Prompts"), Math.round(this._promptsTokenCount)); - addItem(localize('files', "Files"), Math.round(this._filesTokenCount)); - addItem(localize('tools', "Tools"), Math.round(this._toolsTokenCount)); - addItem(localize('context', "Context"), Math.round(this._contextTokenCount)); + addItem('prompts', localize('prompts', "Prompts"), Math.round(this._promptsTokenCount)); + addItem('files', localize('files', "Files"), Math.round(this._filesTokenCount)); + addItem('tools', localize('tools', "Tools"), Math.round(this._toolsTokenCount)); + addItem('context', localize('context', "Context"), Math.round(this._contextTokenCount)); if (this._usagePercent > 80) { const remaining = Math.max(0, this._maxTokenCount - this._totalTokenCount); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css index bf663b98959..8f262c41e28 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css +++ b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css @@ -49,7 +49,7 @@ /* Hover Content */ .chat-context-usage-hover { - min-width: 250px; + min-width: 200px; padding: 8px; display: flex; flex-direction: column; @@ -65,10 +65,6 @@ margin-bottom: 4px; } -/* .chat-context-usage-hover .quota-indicator { - margin-bottom: 6px; -} */ - .chat-context-usage-hover .quota-indicator .quota-label { display: flex; justify-content: space-between; @@ -77,11 +73,11 @@ } .chat-context-usage-hover .quota-indicator .quota-label{ - color: var(--vscode-descriptionForeground); + color: var(--vscode-foreground); } .chat-context-usage-hover .quota-indicator .quota-label .quota-value{ - color: var(--vscode-foreground); + color: var(--vscode-descriptionForeground); } .chat-context-usage-hover .quota-indicator .quota-bar { @@ -133,5 +129,10 @@ } .chat-context-usage-hover-item .label { + color: var(--vscode-foreground); +} + + +.chat-context-usage-hover-item .value { color: var(--vscode-descriptionForeground); } From c0828d057e2af56af34467cb8c4365c1ee3cca55 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Mon, 19 Jan 2026 12:49:35 +0100 Subject: [PATCH 182/387] Add suspend/resume telemetry for reliability insights --- .../electron-main/nativeHostMainService.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index ee61af05310..f29c5416306 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -47,6 +47,7 @@ import { zip } from '../../../base/node/zip.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { IProxyAuthService } from './auth.js'; import { AuthInfo, Credentials, IRequestService } from '../../request/common/request.js'; +import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { randomPath } from '../../../base/common/extpath.js'; export interface INativeHostMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } @@ -70,7 +71,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain @IConfigurationService private readonly configurationService: IConfigurationService, @IRequestService private readonly requestService: IRequestService, @IProxyAuthService private readonly proxyAuthService: IProxyAuthService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ITelemetryService private readonly telemetryService: ITelemetryService ) { super(); @@ -119,6 +121,18 @@ export class NativeHostMainService extends Disposable implements INativeHostMain this.onDidResumeOS = Event.fromNodeEventEmitter(powerMonitor, 'resume'); + // Telemetry for power events + type PowerEventClassification = { + owner: 'chrmarti'; + comment: 'Tracks OS power suspend and resume events for reliability insights.'; + }; + this._register(Event.fromNodeEventEmitter(powerMonitor, 'suspend')(() => { + this.telemetryService.publicLog2<{}, PowerEventClassification>('power.suspend', {}); + })); + this._register(Event.fromNodeEventEmitter(powerMonitor, 'resume')(() => { + this.telemetryService.publicLog2<{}, PowerEventClassification>('power.resume', {}); + })); + this.onDidChangeColorScheme = this.themeMainService.onDidChangeColorScheme; this.onDidChangeDisplay = Event.debounce(Event.any( From 6543b513279577e1a0e12fe1536a289e158190ab Mon Sep 17 00:00:00 2001 From: Robo Date: Mon, 19 Jan 2026 21:16:20 +0900 Subject: [PATCH 183/387] feat: enabled windows version update for stable (#288126) * feat: enabled windows version update for stable * chore: update setup file * temp: bump distro * chore: fix electron re-download * fix: oss callsite in updateservice * chore: simplify check in tunnel-forwarding --- build/gulpfile.vscode.ts | 20 +- build/gulpfile.vscode.win32.ts | 9 +- build/lib/electron.ts | 6 +- build/win32/code-insider.iss | 1740 ----------------- build/win32/code.iss | 402 ++-- extensions/tunnel-forwarding/src/extension.ts | 30 +- package.json | 2 +- .../win32/{insider => versioned}/bin/code.cmd | 0 .../win32/{insider => versioned}/bin/code.sh | 0 src/bootstrap-node.ts | 2 +- src/vs/base/common/product.ts | 1 + src/vs/code/electron-main/main.ts | 2 +- .../contrib/defaultExtensionsInitializer.ts | 2 +- .../remoteTunnel/node/remoteTunnelService.ts | 2 +- .../electron-main/updateService.win32.ts | 4 +- 15 files changed, 278 insertions(+), 1944 deletions(-) delete mode 100644 build/win32/code-insider.iss rename resources/win32/{insider => versioned}/bin/code.cmd (100%) rename resources/win32/{insider => versioned}/bin/code.sh (100%) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index d3ab651ef2e..cb76ed614f4 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -39,7 +39,8 @@ const glob = promisify(globCallback); const rcedit = promisify(rceditCallback); const root = path.dirname(import.meta.dirname); const commit = getVersion(root); -const versionedResourcesFolder = (product as typeof product & { quality?: string })?.quality === 'insider' ? commit!.substring(0, 10) : ''; +const useVersionedUpdate = process.platform === 'win32' && (product as typeof product & { win32VersionedUpdate?: boolean })?.win32VersionedUpdate; +const versionedResourcesFolder = useVersionedUpdate ? commit!.substring(0, 10) : ''; // Build const vscodeEntryPoints = [ @@ -321,7 +322,6 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d deps ); - let customElectronConfig = {}; if (platform === 'win32') { all = es.merge(all, gulp.src([ 'resources/win32/bower.ico', @@ -354,12 +354,6 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d 'resources/win32/code_70x70.png', 'resources/win32/code_150x150.png' ], { base: '.' })); - if (quality && quality === 'insider') { - customElectronConfig = { - createVersionedResources: true, - productVersionString: `${versionedResourcesFolder}`, - }; - } } else if (platform === 'linux') { const policyDest = gulp.src('.build/policies/linux/**', { base: '.build/policies/linux' }) .pipe(rename(f => f.dirname = `policies/${f.dirname}`)); @@ -377,7 +371,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d .pipe(util.skipDirectories()) .pipe(util.fixWin32DirectoryPermissions()) .pipe(filter(['**', '!**/.github/**'], { dot: true })) // https://github.com/microsoft/vscode/issues/116523 - .pipe(electron({ ...config, platform, arch: arch === 'armhf' ? 'arm' : arch, ffmpegChromium: false, ...customElectronConfig })) + .pipe(electron({ ...config, platform, arch: arch === 'armhf' ? 'arm' : arch, ffmpegChromium: false })) .pipe(filter(['**', '!LICENSE', '!version'], { dot: true })); if (platform === 'linux') { @@ -393,13 +387,13 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d if (platform === 'win32') { result = es.merge(result, gulp.src('resources/win32/bin/code.js', { base: 'resources/win32', allowEmpty: true })); - if (quality && quality === 'insider') { - result = es.merge(result, gulp.src('resources/win32/insider/bin/code.cmd', { base: 'resources/win32/insider' }) + if (useVersionedUpdate) { + result = es.merge(result, gulp.src('resources/win32/versioned/bin/code.cmd', { base: 'resources/win32/versioned' }) .pipe(replace('@@NAME@@', product.nameShort)) .pipe(replace('@@VERSIONFOLDER@@', versionedResourcesFolder)) .pipe(rename(function (f) { f.basename = product.applicationName; }))); - result = es.merge(result, gulp.src('resources/win32/insider/bin/code.sh', { base: 'resources/win32/insider' }) + result = es.merge(result, gulp.src('resources/win32/versioned/bin/code.sh', { base: 'resources/win32/versioned' }) .pipe(replace('@@NAME@@', product.nameShort)) .pipe(replace('@@PRODNAME@@', product.nameLong)) .pipe(replace('@@VERSION@@', version)) @@ -407,7 +401,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d .pipe(replace('@@APPNAME@@', product.applicationName)) .pipe(replace('@@VERSIONFOLDER@@', versionedResourcesFolder)) .pipe(replace('@@SERVERDATAFOLDER@@', product.serverDataFolderName || '.vscode-remote')) - .pipe(replace('@@QUALITY@@', quality)) + .pipe(replace('@@QUALITY@@', quality!)) .pipe(rename(function (f) { f.basename = product.applicationName; f.extname = ''; }))); } else { result = es.merge(result, gulp.src('resources/win32/bin/code.cmd', { base: 'resources/win32' }) diff --git a/build/gulpfile.vscode.win32.ts b/build/gulpfile.vscode.win32.ts index a7b01f0a371..d04e7f1f0e7 100644 --- a/build/gulpfile.vscode.win32.ts +++ b/build/gulpfile.vscode.win32.ts @@ -72,12 +72,9 @@ function buildWin32Setup(arch: string, target: string): task.CallbackTask { fs.mkdirSync(outputPath, { recursive: true }); const quality = (product as typeof product & { quality?: string }).quality || 'dev'; - let versionedResourcesFolder = ''; - let issPath = path.join(import.meta.dirname, 'win32', 'code.iss'); - if (quality && quality === 'insider') { - versionedResourcesFolder = commit!.substring(0, 10); - issPath = path.join(import.meta.dirname, 'win32', 'code-insider.iss'); - } + const useVersionedUpdate = (product as typeof product & { win32VersionedUpdate?: boolean })?.win32VersionedUpdate; + const versionedResourcesFolder = useVersionedUpdate ? commit!.substring(0, 10) : ''; + const issPath = path.join(import.meta.dirname, 'win32', 'code.iss'); const originalProductJsonPath = path.join(sourcePath, versionedResourcesFolder, 'resources/app/product.json'); const productJsonPath = path.join(outputPath, 'product.json'); const productJson = JSON.parse(fs.readFileSync(originalProductJsonPath, 'utf8')); diff --git a/build/lib/electron.ts b/build/lib/electron.ts index 4747ff4a1e0..aadc9b5fbe7 100644 --- a/build/lib/electron.ts +++ b/build/lib/electron.ts @@ -29,6 +29,8 @@ function isDocumentSuffix(str?: string): str is DarwinDocumentSuffix { const root = path.dirname(path.dirname(import.meta.dirname)); const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8')); const commit = getVersion(root); +const useVersionedUpdate = process.platform === 'win32' && (product as typeof product & { win32VersionedUpdate?: boolean })?.win32VersionedUpdate; +const versionedResourcesFolder = useVersionedUpdate ? commit!.substring(0, 10) : ''; function createTemplate(input: string): (params: Record) => string { return (params: Record) => { @@ -203,6 +205,8 @@ export const config = { repo: product.electronRepository || undefined, validateChecksum: true, checksumFile: path.join(root, 'build', 'checksums', 'electron.txt'), + createVersionedResources: useVersionedUpdate, + productVersionString: versionedResourcesFolder, }; function getElectron(arch: string): () => NodeJS.ReadWriteStream { @@ -226,7 +230,7 @@ function getElectron(arch: string): () => NodeJS.ReadWriteStream { async function main(arch: string = process.arch): Promise { const version = electronVersion; const electronPath = path.join(root, '.build', 'electron'); - const versionFile = path.join(electronPath, 'version'); + const versionFile = path.join(electronPath, versionedResourcesFolder, 'version'); const isUpToDate = fs.existsSync(versionFile) && fs.readFileSync(versionFile, 'utf8') === `${version}`; if (!isUpToDate) { diff --git a/build/win32/code-insider.iss b/build/win32/code-insider.iss deleted file mode 100644 index 2cbf252779b..00000000000 --- a/build/win32/code-insider.iss +++ /dev/null @@ -1,1740 +0,0 @@ -#define RootLicenseFileName FileExists(RepoDir + '\LICENSE.rtf') ? 'LICENSE.rtf' : 'LICENSE.txt' -#define LocalizedLanguageFile(Language = "") \ - DirExists(RepoDir + "\licenses") && Language != "" \ - ? ('; LicenseFile: "' + RepoDir + '\licenses\LICENSE-' + Language + '.rtf"') \ - : '; LicenseFile: "' + RepoDir + '\' + RootLicenseFileName + '"' - -[Setup] -AppId={#AppId} -AppName={#NameLong} -AppVerName={#NameVersion} -AppPublisher=Microsoft Corporation -AppPublisherURL=https://code.visualstudio.com/ -AppSupportURL=https://code.visualstudio.com/ -AppUpdatesURL=https://code.visualstudio.com/ -DefaultGroupName={#NameLong} -AllowNoIcons=yes -OutputDir={#OutputDir} -OutputBaseFilename=VSCodeSetup -Compression=lzma -SolidCompression=yes -AppMutex={code:GetAppMutex} -SetupMutex={#AppMutex}setup -WizardImageFile="{#RepoDir}\resources\win32\inno-big-100.bmp,{#RepoDir}\resources\win32\inno-big-125.bmp,{#RepoDir}\resources\win32\inno-big-150.bmp,{#RepoDir}\resources\win32\inno-big-175.bmp,{#RepoDir}\resources\win32\inno-big-200.bmp,{#RepoDir}\resources\win32\inno-big-225.bmp,{#RepoDir}\resources\win32\inno-big-250.bmp" -WizardSmallImageFile="{#RepoDir}\resources\win32\inno-small-100.bmp,{#RepoDir}\resources\win32\inno-small-125.bmp,{#RepoDir}\resources\win32\inno-small-150.bmp,{#RepoDir}\resources\win32\inno-small-175.bmp,{#RepoDir}\resources\win32\inno-small-200.bmp,{#RepoDir}\resources\win32\inno-small-225.bmp,{#RepoDir}\resources\win32\inno-small-250.bmp" -SetupIconFile={#RepoDir}\resources\win32\code.ico -UninstallDisplayIcon={app}\{#ExeBasename}.exe -ChangesEnvironment=true -ChangesAssociations=true -MinVersion=10.0 -SourceDir={#SourceDir} -AppVersion={#Version} -VersionInfoVersion={#RawVersion} -ShowLanguageDialog=auto -ArchitecturesAllowed={#ArchitecturesAllowed} -ArchitecturesInstallIn64BitMode={#ArchitecturesInstallIn64BitMode} -WizardStyle=modern - -// We've seen an uptick on broken installations from updates which were unable -// to shutdown VS Code. We rely on the fact that the update signals -// that VS Code is ready to be shutdown, so we're good to use `force` here. -CloseApplications=force - -#ifdef Sign -SignTool=esrp -#endif - -#if "user" == InstallTarget -DefaultDirName={userpf}\{#DirName} -PrivilegesRequired=lowest -#else -DefaultDirName={pf}\{#DirName} -#endif - -[Languages] -Name: "english"; MessagesFile: "compiler:Default.isl,{#RepoDir}\build\win32\i18n\messages.en.isl" {#LocalizedLanguageFile} -Name: "german"; MessagesFile: "compiler:Languages\German.isl,{#RepoDir}\build\win32\i18n\messages.de.isl" {#LocalizedLanguageFile("deu")} -Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl,{#RepoDir}\build\win32\i18n\messages.es.isl" {#LocalizedLanguageFile("esp")} -Name: "french"; MessagesFile: "compiler:Languages\French.isl,{#RepoDir}\build\win32\i18n\messages.fr.isl" {#LocalizedLanguageFile("fra")} -Name: "italian"; MessagesFile: "compiler:Languages\Italian.isl,{#RepoDir}\build\win32\i18n\messages.it.isl" {#LocalizedLanguageFile("ita")} -Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl,{#RepoDir}\build\win32\i18n\messages.ja.isl" {#LocalizedLanguageFile("jpn")} -Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl,{#RepoDir}\build\win32\i18n\messages.ru.isl" {#LocalizedLanguageFile("rus")} -Name: "korean"; MessagesFile: "{#RepoDir}\build\win32\i18n\Default.ko.isl,{#RepoDir}\build\win32\i18n\messages.ko.isl" {#LocalizedLanguageFile("kor")} -Name: "simplifiedChinese"; MessagesFile: "{#RepoDir}\build\win32\i18n\Default.zh-cn.isl,{#RepoDir}\build\win32\i18n\messages.zh-cn.isl" {#LocalizedLanguageFile("chs")} -Name: "traditionalChinese"; MessagesFile: "{#RepoDir}\build\win32\i18n\Default.zh-tw.isl,{#RepoDir}\build\win32\i18n\messages.zh-tw.isl" {#LocalizedLanguageFile("cht")} -Name: "brazilianPortuguese"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl,{#RepoDir}\build\win32\i18n\messages.pt-br.isl" {#LocalizedLanguageFile("ptb")} -Name: "hungarian"; MessagesFile: "{#RepoDir}\build\win32\i18n\Default.hu.isl,{#RepoDir}\build\win32\i18n\messages.hu.isl" {#LocalizedLanguageFile("hun")} -Name: "turkish"; MessagesFile: "compiler:Languages\Turkish.isl,{#RepoDir}\build\win32\i18n\messages.tr.isl" {#LocalizedLanguageFile("trk")} - -[InstallDelete] -Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\out"; Check: IsNotBackgroundUpdate -Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\plugins"; Check: IsNotBackgroundUpdate -Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\extensions"; Check: IsNotBackgroundUpdate -Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\node_modules"; Check: IsNotBackgroundUpdate -Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\node_modules.asar.unpacked"; Check: IsNotBackgroundUpdate -Type: files; Name: "{app}\{#VersionedResourcesFolder}\resources\app\node_modules.asar"; Check: IsNotBackgroundUpdate -Type: files; Name: "{app}\{#VersionedResourcesFolder}\resources\app\Credits_45.0.2454.85.html"; Check: IsNotBackgroundUpdate - -[UninstallDelete] -Type: filesandordirs; Name: "{app}\_" -Type: filesandordirs; Name: "{app}\bin" -Type: files; Name: "{app}\old_*" -Type: files; Name: "{app}\new_*" -Type: files; Name: "{app}\updating_version" - -[Tasks] -Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked -Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1 -Name: "addcontextmenufiles"; Description: "{cm:AddContextMenuFiles,{#NameShort}}"; GroupDescription: "{cm:Other}"; Flags: unchecked -Name: "addcontextmenufolders"; Description: "{cm:AddContextMenuFolders,{#NameShort}}"; GroupDescription: "{cm:Other}"; Flags: unchecked; Check: not (IsWindows11OrLater and QualityIsInsiders) -Name: "associatewithfiles"; Description: "{cm:AssociateWithFiles,{#NameShort}}"; GroupDescription: "{cm:Other}" -Name: "addtopath"; Description: "{cm:AddToPath}"; GroupDescription: "{cm:Other}" -Name: "runcode"; Description: "{cm:RunAfter,{#NameShort}}"; GroupDescription: "{cm:Other}"; Check: WizardSilent - -[Dirs] -Name: "{app}"; AfterInstall: DisableAppDirInheritance - -[Files] -Source: "*"; Excludes: "\CodeSignSummary*.md,\tools,\tools\*,\policies,\policies\*,\appx,\appx\*,\resources\app\product.json,\{#ExeBasename}.exe,\{#ExeBasename}.VisualElementsManifest.xml,\bin,\bin\*"; DestDir: "{code:GetDestDir}"; Flags: ignoreversion recursesubdirs createallsubdirs -Source: "{#ExeBasename}.exe"; DestDir: "{code:GetDestDir}"; DestName: "{code:GetExeBasename}"; Flags: ignoreversion -Source: "{#ExeBasename}.VisualElementsManifest.xml"; DestDir: "{code:GetDestDir}"; DestName: "{code:GetVisualElementsManifest}"; Flags: ignoreversion -Source: "tools\*"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\tools"; Flags: ignoreversion -Source: "policies\*"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\policies"; Flags: ignoreversion skipifsourcedoesntexist -Source: "bin\{#TunnelApplicationName}.exe"; DestDir: "{code:GetDestDir}\bin"; DestName: "{code:GetBinDirTunnelApplicationFilename}"; Flags: ignoreversion skipifsourcedoesntexist -Source: "bin\{#ApplicationName}.cmd"; DestDir: "{code:GetDestDir}\bin"; DestName: "{code:GetBinDirApplicationCmdFilename}"; Flags: ignoreversion -Source: "bin\{#ApplicationName}"; DestDir: "{code:GetDestDir}\bin"; DestName: "{code:GetBinDirApplicationFilename}"; Flags: ignoreversion -Source: "{#ProductJsonPath}"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\resources\app"; Flags: ignoreversion -#ifdef AppxPackageName -#if "user" == InstallTarget -Source: "appx\{#AppxPackage}"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\appx"; BeforeInstall: RemoveAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater -Source: "appx\{#AppxPackageDll}"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\appx"; AfterInstall: AddAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater -#endif -#endif - -[Icons] -Name: "{group}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; AppUserModelID: "{#AppUserId}" -Name: "{autodesktop}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: desktopicon; AppUserModelID: "{#AppUserId}" -Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: quicklaunchicon; AppUserModelID: "{#AppUserId}" - -[Run] -Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Tasks: runcode; Flags: nowait postinstall; Check: ShouldRunAfterUpdate -Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Flags: nowait postinstall; Check: WizardNotSilent - -[Registry] -#if "user" == InstallTarget -#define SoftwareClassesRootKey "HKCU" -#else -#define SoftwareClassesRootKey "HKLM" -#endif - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ascx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ascx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ascx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASCX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.asp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.asp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.asp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASP}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.aspx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.aspx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.aspx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASPX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_login\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_login\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_login"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Login}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_logout\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_logout\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_logout"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Logout}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_profile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_profile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_profile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bashrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bashrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bashrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bib\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bib\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bib"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,BibTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bowerrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bowerrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bowerrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bower RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\bower.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.c++\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.c++\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.c++"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c++"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c++"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c++\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c++\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.c\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.c\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.c"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\c.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cfg\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cfg\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cfg"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cfg"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Configuration}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cfg"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cfg\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cfg\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cfg\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cjs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cjs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cjs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cjs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cjs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cjs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cjs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cjs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.clj\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.clj\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.clj"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Clojure}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cljs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cljs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cljs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ClojureScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cljx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cljx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cljx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CLJX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.clojure\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.clojure\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.clojure"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Clojure}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cls\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cls\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cls"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cls"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LaTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cls"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cls\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cls\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cls\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.code-workspace\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.code-workspace\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.code-workspace"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Code Workspace}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cmake\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cmake\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cmake"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cmake"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CMake}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cmake"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cmake\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cmake\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cmake\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.coffee\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.coffee\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.coffee"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CoffeeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.config\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.config\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.config"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Configuration}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.containerfile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.containerfile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.containerfile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.containerfile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Containerfile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.containerfile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.containerfile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.containerfile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cpp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cpp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cpp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C#}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\csharp.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cshtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cshtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cshtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CSHTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csproj\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csproj\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csproj"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C# Project}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.css\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.css\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.css"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CSS}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\css.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csv\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csv\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csv"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csv"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Comma Separated Values}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csv"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csv\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csv\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csv\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\csharp.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ctp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ctp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ctp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CakePHP Template}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cxx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cxx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cxx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dart\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dart\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dart"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dart"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dart}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dart"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dart\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dart\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dart\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.diff\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.diff\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.diff"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.diff"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Diff}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.diff"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.diff\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.diff\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.diff\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dockerfile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dockerfile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dockerfile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dockerfile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dot\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dot\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dot"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dot}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dtd\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dtd\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dtd"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Document Type Definition}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.editorconfig\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.editorconfig\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.editorconfig"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Editor Config}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.edn\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.edn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.edn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Extensible Data Notation}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.erb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.erb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.erb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.erb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Ruby}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.erb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.erb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\ruby.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.erb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.erb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.eyaml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.eyaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.eyaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Hiera Eyaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.eyml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.eyml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.eyml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Hiera Eyaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F#}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Signature}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsscript\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsscript\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsscript"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gemspec\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gemspec\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gemspec"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Gemspec}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\ruby.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gitattributes\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gitattributes\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gitattributes"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Attributes}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gitconfig\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gitconfig\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gitconfig"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Config}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gitignore\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gitignore\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gitignore"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Ignore}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.go\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.go\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.go"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Go}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\go.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gradle\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gradle\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gradle"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gradle"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Gradle}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gradle"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gradle\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gradle\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gradle\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.groovy\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.groovy\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.groovy"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.groovy"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Groovy}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.groovy"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.groovy\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.groovy\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.groovy\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.h\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.h\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.h"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\c.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.handlebars\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.handlebars\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.handlebars"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Handlebars}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hbs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hbs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hbs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Handlebars}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.h++\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.h++\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.h++"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h++"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h++"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h++\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h++\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hpp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hpp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hpp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.htm\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.htm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.htm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.html\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.html\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.html"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hxx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hxx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hxx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ini\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ini\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ini"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,INI}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ipynb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ipynb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ipynb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Jupyter}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jade\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jade\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jade"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Jade}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\jade.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jav\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jav\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jav"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\java.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.java\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.java\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.java"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\java.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.js\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.js\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.js"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jsx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\react.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jscsrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jscsrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jscsrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSCS RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jshintrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jshintrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jshintrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSHint RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jshtm\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jshtm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jshtm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript HTML Template}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.json\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.json\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.json"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSON}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\json.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jsp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jsp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jsp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java Server Pages}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.less\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.less\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.less"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LESS}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\less.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.log\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.log\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.log"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.log"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Log file}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.log"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.log\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.log\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.log\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.lua\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.lua\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.lua"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Lua}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.m\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.m\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.m"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Objective C}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.makefile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.makefile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.makefile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Makefile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.markdown\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.markdown\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.markdown"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.md\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.md\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.md"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdoc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdoc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdoc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,MDoc}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdown\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdown\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdown"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdtext\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdtext\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdtext"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdtxt\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdtxt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdtxt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdwn\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdwn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdwn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mk\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mk\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mk"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mk"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Makefile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mk"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mk\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mk\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mk\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mkd\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mkd\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mkd"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mkdn\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mkdn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mkdn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,OCaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mli\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mli\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mli"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,OCaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mjs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mjs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mjs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mjs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mjs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mjs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mjs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mjs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.npmignore\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.npmignore\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.npmignore"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,NPM Ignore}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.php\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.php\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.php"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PHP}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\php.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.phtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.phtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.phtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PHP HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pl\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pl\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pl"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pl6\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pl6\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pl6"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl 6}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.plist\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.plist\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.plist"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Properties file}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pm\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pm6\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pm6\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pm6"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl 6 Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pod\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pod\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pod"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl POD}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.profile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.profile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.profile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.properties\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.properties\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.properties"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Properties}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ps1\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ps1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ps1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\powershell.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psd1\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psd1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psd1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell Module Manifest}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\powershell.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psgi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psgi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psgi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl CGI}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psm1\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psm1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psm1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\powershell.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.py\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.py\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.py"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Python}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\python.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pyi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pyi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pyi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pyi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Python}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pyi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pyi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\python.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pyi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pyi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.r\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.r\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.r"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Ruby}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\ruby.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rhistory\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rhistory\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rhistory"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R History}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rprofile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rprofile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rprofile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Rust}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rst\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rst\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rst"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rst"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Restructured Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rst"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rst\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rst\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rst\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rt\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Rich Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sass\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sass\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sass"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sass"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Sass}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sass"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sass\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\sass.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sass\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sass\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.scss\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.scss\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.scss"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Sass}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\sass.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SH}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.shtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.shtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.shtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SHTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sql\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sql\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sql"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SQL}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\sql.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.svg\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.svg\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.svg"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SVG}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.t\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.t\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.t"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.tex\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.tex\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.tex"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LaTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ts\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ts\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ts"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,TypeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\typescript.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.toml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.toml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.toml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.toml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Toml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.toml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.toml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.toml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.toml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.tsx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.tsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.tsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,TypeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\react.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tsx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.txt\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.txt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.txt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.vb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.vb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.vb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Visual Basic}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.vue\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.vue\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.vue"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vue"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,VUE}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vue"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vue\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\vue.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vue\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vue\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX Include}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxl\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxl\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxl"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX Localization}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xaml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,XAML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xhtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xhtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xhtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xhtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xhtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xhtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xhtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xhtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,XML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.yaml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.yaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.yaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Yaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.yml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.yml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.yml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Yaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.zsh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.zsh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.zsh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ZSH}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,{#NameLong}}"; Flags: uninsdeletekey -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico" -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile\shell\open"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe""" -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1""" - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe"; ValueType: none; ValueName: ""; Flags: uninsdeletekey -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico" -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe""" -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1""" - -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}ContextMenu"; ValueType: expandsz; ValueName: "Title"; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: IsWindows11OrLater and QualityIsInsiders -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufiles; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: addcontextmenufiles; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not (IsWindows11OrLater and QualityIsInsiders) - -; Environment -#if "user" == InstallTarget -#define EnvironmentRootKey "HKCU" -#define EnvironmentKey "Environment" -#define Uninstall64RootKey "HKCU64" -#define Uninstall32RootKey "HKCU32" -#else -#define EnvironmentRootKey "HKLM" -#define EnvironmentKey "System\CurrentControlSet\Control\Session Manager\Environment" -#define Uninstall64RootKey "HKLM64" -#define Uninstall32RootKey "HKLM32" -#endif - -Root: {#EnvironmentRootKey}; Subkey: "{#EnvironmentKey}"; ValueType: expandsz; ValueName: "Path"; ValueData: "{code:AddToPath|{app}\bin}"; Tasks: addtopath; Check: NeedsAddToPath(ExpandConstant('{app}\bin')) - -[Code] -function IsBackgroundUpdate(): Boolean; -begin - Result := ExpandConstant('{param:update|false}') <> 'false'; -end; - -function IsNotBackgroundUpdate(): Boolean; -begin - Result := not IsBackgroundUpdate(); -end; - -// Don't allow installing conflicting architectures -function InitializeSetup(): Boolean; -var - RegKey: String; - ThisArch: String; - AltArch: String; -begin - Result := True; - - #if "user" == InstallTarget - if not WizardSilent() and IsAdmin() then begin - if MsgBox('This User Installer is not meant to be run as an Administrator. If you would like to install VS Code for all users in this system, download the System Installer instead from https://code.visualstudio.com. Are you sure you want to continue?', mbError, MB_OKCANCEL) = IDCANCEL then begin - Result := False; - end; - end; - #endif - - #if "user" == InstallTarget - #if "arm64" == Arch - #define IncompatibleArchRootKey "HKLM32" - #else - #define IncompatibleArchRootKey "HKLM64" - #endif - - if Result and not WizardSilent() then begin - RegKey := 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\' + copy('{#IncompatibleTargetAppId}', 2, 38) + '_is1'; - - if RegKeyExists({#IncompatibleArchRootKey}, RegKey) then begin - if MsgBox('{#NameShort} is already installed on this system for all users. We recommend first uninstalling that version before installing this one. Are you sure you want to continue the installation?', mbConfirmation, MB_YESNO) = IDNO then begin - Result := False; - end; - end; - end; - #endif - -end; - -function WizardNotSilent(): Boolean; -begin - Result := not WizardSilent(); -end; - -// Updates - -var - ShouldRestartTunnelService: Boolean; - -function StopTunnelOtherProcesses(): Boolean; -var - WaitCounter: Integer; - TaskKilled: Integer; -begin - Log('Stopping all tunnel services (at ' + ExpandConstant('"{app}\bin\{#TunnelApplicationName}.exe"') + ')'); - ShellExec('', 'powershell.exe', '-Command "Get-WmiObject Win32_Process | Where-Object { $_.ExecutablePath -eq ' + ExpandConstant('''{app}\bin\{#TunnelApplicationName}.exe''') + ' } | Select @{Name=''Id''; Expression={$_.ProcessId}} | Stop-Process -Force"', '', SW_HIDE, ewWaitUntilTerminated, TaskKilled) - - WaitCounter := 10; - while (WaitCounter > 0) and CheckForMutexes('{#TunnelMutex}') do - begin - Log('Tunnel process is is still running, waiting'); - Sleep(500); - WaitCounter := WaitCounter - 1 - end; - - if CheckForMutexes('{#TunnelMutex}') then - begin - Log('Unable to stop tunnel processes'); - Result := False; - end - else - Result := True; -end; - -procedure StopTunnelServiceIfNeeded(); -var - StopServiceResultCode: Integer; - WaitCounter: Integer; -begin - ShouldRestartTunnelService := False; - if CheckForMutexes('{#TunnelServiceMutex}') then begin - // stop the tunnel service - Log('Stopping the tunnel service using ' + ExpandConstant('"{app}\bin\{#ApplicationName}.cmd"')); - ShellExec('', ExpandConstant('"{app}\bin\{#ApplicationName}.cmd"'), 'tunnel service uninstall', '', SW_HIDE, ewWaitUntilTerminated, StopServiceResultCode); - - Log('Stopping the tunnel service completed with result code ' + IntToStr(StopServiceResultCode)); - - WaitCounter := 10; - while (WaitCounter > 0) and CheckForMutexes('{#TunnelServiceMutex}') do - begin - Log('Tunnel service is still running, waiting'); - Sleep(500); - WaitCounter := WaitCounter - 1 - end; - if CheckForMutexes('{#TunnelServiceMutex}') then - Log('Unable to stop tunnel service') - else - ShouldRestartTunnelService := True; - end -end; - - -// called before the wizard checks for running application -function PrepareToInstall(var NeedsRestart: Boolean): String; -begin - if IsNotBackgroundUpdate() then - StopTunnelServiceIfNeeded(); - - if IsNotBackgroundUpdate() and not StopTunnelOtherProcesses() then - Result := '{#NameShort} is still running a tunnel process. Please stop the tunnel before installing.' - else - Result := ''; -end; - -// VS Code will create a flag file before the update starts (/update=C:\foo\bar) -// - if the file exists at this point, the user quit Code before the update finished, so don't start Code after update -// - otherwise, the user has accepted to apply the update and Code should start -function LockFileExists(): Boolean; -begin - Result := FileExists(ExpandConstant('{param:update}')) -end; - -// Check if VS Code created a session-end flag file to indicate OS is shutting down -// This prevents calling inno_updater.exe during system shutdown -function SessionEndFileExists(): Boolean; -begin - Result := FileExists(ExpandConstant('{param:sessionend}')) -end; - -function ShouldRunAfterUpdate(): Boolean; -begin - if IsBackgroundUpdate() then - Result := not LockFileExists() - else - Result := True; -end; - -function IsWindows11OrLater(): Boolean; -begin - Result := (GetWindowsVersion >= $0A0055F0); -end; - -function GetAppMutex(Value: string): string; -begin - if IsBackgroundUpdate() then - Result := '' - else - Result := '{#AppMutex}'; -end; - -function GetDestDir(Value: string): string; -begin - Result := ExpandConstant('{app}'); -end; - -function GetVisualElementsManifest(Value: string): string; -begin - if IsBackgroundUpdate() then - Result := ExpandConstant('new_{#ExeBasename}.VisualElementsManifest.xml') - else - Result := ExpandConstant('{#ExeBasename}.VisualElementsManifest.xml'); -end; - -function GetExeBasename(Value: string): string; -begin - if IsBackgroundUpdate() then - Result := ExpandConstant('new_{#ExeBasename}.exe') - else - Result := ExpandConstant('{#ExeBasename}.exe'); -end; - -function GetBinDirTunnelApplicationFilename(Value: string): string; -begin - if IsBackgroundUpdate() then - Result := ExpandConstant('new_{#TunnelApplicationName}.exe') - else - Result := ExpandConstant('{#TunnelApplicationName}.exe'); -end; - -function GetBinDirApplicationFilename(Value: string): string; -begin - if IsBackgroundUpdate() then - Result := ExpandConstant('new_{#ApplicationName}') - else - Result := ExpandConstant('{#ApplicationName}'); -end; - -function GetBinDirApplicationCmdFilename(Value: string): string; -begin - if IsBackgroundUpdate() then - Result := ExpandConstant('new_{#ApplicationName}.cmd') - else - Result := ExpandConstant('{#ApplicationName}.cmd'); -end; - -function BoolToStr(Value: Boolean): String; -begin - if Value then - Result := 'true' - else - Result := 'false'; -end; - -function QualityIsInsiders(): boolean; -begin - if '{#Quality}' = 'insider' then - Result := True - else - Result := False; -end; - -#ifdef AppxPackageName -var - AppxPackageFullname: String; - -procedure ExecAndGetFirstLineLog(const S: String; const Error, FirstLine: Boolean); -begin - if not Error and (AppxPackageFullname = '') and (Trim(S) <> '') then - AppxPackageFullname := S; - Log(S); -end; - -function AppxPackageInstalled(const name: String; var ResultCode: Integer): Boolean; -begin - AppxPackageFullname := ''; - try - Log('Get-AppxPackage for package with name: ' + name); - ExecAndLogOutput('powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Get-AppxPackage -Name ''' + name + ''' | Select-Object -ExpandProperty PackageFullName'), '', SW_HIDE, ewWaitUntilTerminated, ResultCode, @ExecAndGetFirstLineLog); - except - Log(GetExceptionMessage); - end; - if (AppxPackageFullname <> '') then - Result := True - else - Result := False -end; - -procedure AddAppxPackage(); -var - AddAppxPackageResultCode: Integer; -begin - if not SessionEndFileExists() and not AppxPackageInstalled(ExpandConstant('{#AppxPackageName}'), AddAppxPackageResultCode) then begin - Log('Installing appx ' + AppxPackageFullname + ' ...'); - ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Add-AppxPackage -Path ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx\{#AppxPackage}') + ''' -ExternalLocation ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); - Log('Add-AppxPackage complete.'); - end; -end; - -procedure RemoveAppxPackage(); -var - RemoveAppxPackageResultCode: Integer; -begin - // Remove the old context menu package - // Following condition can be removed after two versions. - if QualityIsInsiders() and not SessionEndFileExists() and AppxPackageInstalled('Microsoft.VSCodeInsiders', RemoveAppxPackageResultCode) then begin - Log('Deleting old appx ' + AppxPackageFullname + ' installation...'); - ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Remove-AppxPackage -Package ''' + AppxPackageFullname + ''''), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); - DeleteFile(ExpandConstant('{app}\appx\code_insiders_explorer_{#Arch}.appx')); - DeleteFile(ExpandConstant('{app}\appx\code_insiders_explorer_command.dll')); - end; - if not SessionEndFileExists() and AppxPackageInstalled(ExpandConstant('{#AppxPackageName}'), RemoveAppxPackageResultCode) then begin - Log('Removing current ' + AppxPackageFullname + ' appx installation...'); - ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Remove-AppxPackage -Package ''' + AppxPackageFullname + ''''), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); - Log('Remove-AppxPackage for current appx installation complete.'); - end; -end; -#endif - -procedure CurStepChanged(CurStep: TSetupStep); -var - UpdateResultCode: Integer; - StartServiceResultCode: Integer; -begin - if CurStep = ssPostInstall then - begin -#ifdef AppxPackageName - // Remove the old context menu registry keys for insiders - if QualityIsInsiders() and WizardIsTaskSelected('addcontextmenufiles') then begin - RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\*\shell\{#RegValueName}'); - RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\directory\shell\{#RegValueName}'); - RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\directory\background\shell\{#RegValueName}'); - RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\Drive\shell\{#RegValueName}'); - end; -#endif - - if IsBackgroundUpdate() then - begin - SaveStringToFile(ExpandConstant('{app}\updating_version'), '{#Commit}', False); - CreateMutex('{#AppMutex}-ready'); - - Log('Checking whether application is still running...'); - while (CheckForMutexes('{#AppMutex}')) do - begin - Sleep(1000) - end; - Log('Application appears not to be running.'); - - if not SessionEndFileExists() then begin - StopTunnelServiceIfNeeded(); - Log('Invoking inno_updater for background update'); - Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"{app}\{#ExeBasename}.exe" ' + BoolToStr(LockFileExists()) + ' "{cm:UpdatingVisualStudioCode}"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); - DeleteFile(ExpandConstant('{app}\updating_version')); - Log('inno_updater completed successfully'); - #if "system" == InstallTarget - Log('Invoking inno_updater to remove previous installation folder'); - Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"--gc" "{app}\{#ExeBasename}.exe" "{#VersionedResourcesFolder}"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); - Log('inno_updater completed gc successfully'); - #endif - end else begin - Log('Skipping inno_updater.exe call because OS session is ending'); - end; - end else begin - Log('Invoking inno_updater to remove previous installation folder'); - Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"--gc" "{app}\{#ExeBasename}.exe" "{#VersionedResourcesFolder}"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); - Log('inno_updater completed gc successfully'); - end; - - if ShouldRestartTunnelService then - begin - // start the tunnel service - Log('Restarting the tunnel service...'); - ShellExec('', ExpandConstant('"{app}\bin\{#ApplicationName}.cmd"'), 'tunnel service install', '', SW_HIDE, ewWaitUntilTerminated, StartServiceResultCode); - Log('Starting the tunnel service completed with result code ' + IntToStr(StartServiceResultCode)); - ShouldRestartTunnelService := False - end; - end; -end; - -// https://stackoverflow.com/a/23838239/261019 -procedure Explode(var Dest: TArrayOfString; Text: String; Separator: String); -var - i, p: Integer; -begin - i := 0; - repeat - SetArrayLength(Dest, i+1); - p := Pos(Separator,Text); - if p > 0 then begin - Dest[i] := Copy(Text, 1, p-1); - Text := Copy(Text, p + Length(Separator), Length(Text)); - i := i + 1; - end else begin - Dest[i] := Text; - Text := ''; - end; - until Length(Text)=0; -end; - -function NeedsAddToPath(VSCode: string): boolean; -var - OrigPath: string; -begin - if not RegQueryStringValue({#EnvironmentRootKey}, '{#EnvironmentKey}', 'Path', OrigPath) - then begin - Result := True; - exit; - end; - Result := Pos(';' + VSCode + ';', ';' + OrigPath + ';') = 0; -end; - -function AddToPath(VSCode: string): string; -var - OrigPath: string; -begin - RegQueryStringValue({#EnvironmentRootKey}, '{#EnvironmentKey}', 'Path', OrigPath) - - if (Length(OrigPath) > 0) and (OrigPath[Length(OrigPath)] = ';') then - Result := OrigPath + VSCode - else - Result := OrigPath + ';' + VSCode -end; - -procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); -var - Path: string; - VSCodePath: string; - Parts: TArrayOfString; - NewPath: string; - i: Integer; -begin - if not CurUninstallStep = usUninstall then begin - exit; - end; -#ifdef AppxPackageName - #if "user" == InstallTarget - RemoveAppxPackage(); - #endif -#endif - if not RegQueryStringValue({#EnvironmentRootKey}, '{#EnvironmentKey}', 'Path', Path) - then begin - exit; - end; - NewPath := ''; - VSCodePath := ExpandConstant('{app}\bin') - Explode(Parts, Path, ';'); - for i:=0 to GetArrayLength(Parts)-1 do begin - if CompareText(Parts[i], VSCodePath) <> 0 then begin - NewPath := NewPath + Parts[i]; - - if i < GetArrayLength(Parts) - 1 then begin - NewPath := NewPath + ';'; - end; - end; - end; - RegWriteExpandStringValue({#EnvironmentRootKey}, '{#EnvironmentKey}', 'Path', NewPath); -end; - -#ifdef Debug - #expr SaveToFile(AddBackslash(SourcePath) + "code-processed.iss") -#endif - -// https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/icacls -// https://docs.microsoft.com/en-US/windows/security/identity-protection/access-control/security-identifiers -procedure DisableAppDirInheritance(); -var - ResultCode: Integer; - Permissions: string; -begin - Permissions := '/grant:r "*S-1-5-18:(OI)(CI)F" /grant:r "*S-1-5-32-544:(OI)(CI)F" /grant:r "*S-1-5-11:(OI)(CI)RX" /grant:r "*S-1-5-32-545:(OI)(CI)RX"'; - - #if "user" == InstallTarget - Permissions := Permissions + Format(' /grant:r "*S-1-3-0:(OI)(CI)F" /grant:r "%s:(OI)(CI)F"', [GetUserNameString()]); - #endif - - Exec(ExpandConstant('{sys}\icacls.exe'), ExpandConstant('"{app}" /inheritancelevel:r ') + Permissions, '', SW_HIDE, ewWaitUntilTerminated, ResultCode); -end; diff --git a/build/win32/code.iss b/build/win32/code.iss index f8f202f42ee..bc3217e736e 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -67,16 +67,20 @@ Name: "hungarian"; MessagesFile: "{#RepoDir}\build\win32\i18n\Default.hu.isl,{#R Name: "turkish"; MessagesFile: "compiler:Languages\Turkish.isl,{#RepoDir}\build\win32\i18n\messages.tr.isl" {#LocalizedLanguageFile("trk")} [InstallDelete] -Type: filesandordirs; Name: "{app}\resources\app\out"; Check: IsNotBackgroundUpdate -Type: filesandordirs; Name: "{app}\resources\app\plugins"; Check: IsNotBackgroundUpdate -Type: filesandordirs; Name: "{app}\resources\app\extensions"; Check: IsNotBackgroundUpdate -Type: filesandordirs; Name: "{app}\resources\app\node_modules"; Check: IsNotBackgroundUpdate -Type: filesandordirs; Name: "{app}\resources\app\node_modules.asar.unpacked"; Check: IsNotBackgroundUpdate -Type: files; Name: "{app}\resources\app\node_modules.asar"; Check: IsNotBackgroundUpdate -Type: files; Name: "{app}\resources\app\Credits_45.0.2454.85.html"; Check: IsNotBackgroundUpdate +Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\out"; Check: IsNotBackgroundUpdate +Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\plugins"; Check: IsNotBackgroundUpdate +Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\extensions"; Check: IsNotBackgroundUpdate +Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\node_modules"; Check: IsNotBackgroundUpdate +Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\node_modules.asar.unpacked"; Check: IsNotBackgroundUpdate +Type: files; Name: "{app}\{#VersionedResourcesFolder}\resources\app\node_modules.asar"; Check: IsNotBackgroundUpdate +Type: files; Name: "{app}\{#VersionedResourcesFolder}\resources\app\Credits_45.0.2454.85.html"; Check: IsNotBackgroundUpdate [UninstallDelete] Type: filesandordirs; Name: "{app}\_" +Type: filesandordirs; Name: "{app}\bin" +Type: files; Name: "{app}\old_*" +Type: files; Name: "{app}\new_*" +Type: files; Name: "{app}\updating_version" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked @@ -91,12 +95,18 @@ Name: "runcode"; Description: "{cm:RunAfter,{#NameShort}}"; GroupDescription: "{ Name: "{app}"; AfterInstall: DisableAppDirInheritance [Files] -Source: "*"; Excludes: "\CodeSignSummary*.md,\tools,\tools\*,\appx,\appx\*,\resources\app\product.json"; DestDir: "{code:GetDestDir}"; Flags: ignoreversion recursesubdirs createallsubdirs -Source: "tools\*"; DestDir: "{app}\tools"; Flags: ignoreversion -Source: "{#ProductJsonPath}"; DestDir: "{code:GetDestDir}\resources\app"; Flags: ignoreversion +Source: "*"; Excludes: "\CodeSignSummary*.md,\tools,\tools\*,\policies,\policies\*,\appx,\appx\*,\resources\app\product.json,\{#ExeBasename}.exe,\{#ExeBasename}.VisualElementsManifest.xml,\bin,\bin\*"; DestDir: "{code:GetDestDir}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#ExeBasename}.exe"; DestDir: "{code:GetDestDir}"; DestName: "{code:GetExeBasename}"; Flags: ignoreversion +Source: "{#ExeBasename}.VisualElementsManifest.xml"; DestDir: "{code:GetDestDir}"; DestName: "{code:GetVisualElementsManifest}"; Flags: ignoreversion +Source: "tools\*"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\tools"; Flags: ignoreversion +Source: "policies\*"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\policies"; Flags: ignoreversion skipifsourcedoesntexist +Source: "bin\{#TunnelApplicationName}.exe"; DestDir: "{code:GetDestDir}\bin"; DestName: "{code:GetBinDirTunnelApplicationFilename}"; Flags: ignoreversion skipifsourcedoesntexist +Source: "bin\{#ApplicationName}.cmd"; DestDir: "{code:GetDestDir}\bin"; DestName: "{code:GetBinDirApplicationCmdFilename}"; Flags: ignoreversion +Source: "bin\{#ApplicationName}"; DestDir: "{code:GetDestDir}\bin"; DestName: "{code:GetBinDirApplicationFilename}"; Flags: ignoreversion +Source: "{#ProductJsonPath}"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\resources\app"; Flags: ignoreversion #ifdef AppxPackageName -Source: "appx\{#AppxPackage}"; DestDir: "{app}\appx"; BeforeInstall: RemoveAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater -Source: "appx\{#AppxPackageDll}"; DestDir: "{app}\appx"; AfterInstall: AddAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater +Source: "appx\{#AppxPackage}"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\appx"; BeforeInstall: RemoveAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater +Source: "appx\{#AppxPackageDll}"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\appx"; AfterInstall: AddAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater #endif [Icons] @@ -119,7 +129,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ascx\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ascx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ascx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASCX}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -127,7 +137,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.asp\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.asp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.asp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASP}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -135,7 +145,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.aspx\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.aspx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.aspx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASPX}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -143,7 +153,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -151,7 +161,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_login\OpenWithP Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_login\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_login"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Login}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -159,7 +169,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_logout\OpenWith Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_logout\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_logout"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Logout}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -167,7 +177,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_profile\OpenWit Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_profile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_profile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -175,7 +185,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bashrc\OpenWithProgi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bashrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bashrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -183,7 +193,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bib\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bib\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bib"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,BibTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -191,7 +201,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bowerrc\OpenWithProg Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bowerrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bowerrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bower RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\bower.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\bower.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -199,14 +209,14 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.c++\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.c++\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.c++"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c++"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c++"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c++\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c++\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c++\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.c\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.c\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.c"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\c.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\c.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -214,7 +224,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cc\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -222,7 +232,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cfg\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cfg\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cfg"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cfg"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Configuration}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cfg"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cfg\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cfg\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cfg\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cfg\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -230,7 +240,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cjs\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cjs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cjs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cjs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cjs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cjs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cjs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cjs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cjs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -238,7 +248,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.clj\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.clj\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.clj"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Clojure}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -246,7 +256,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cljs\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cljs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cljs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ClojureScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -254,7 +264,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cljx\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cljx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cljx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CLJX}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -262,7 +272,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.clojure\OpenWithProg Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.clojure\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.clojure"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Clojure}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -270,7 +280,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cls\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cls\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cls"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cls"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LaTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cls"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cls\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cls\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cls\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cls\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -278,7 +288,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.code-workspace\OpenW Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.code-workspace\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.code-workspace"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Code Workspace}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -286,7 +296,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cmake\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cmake\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cmake"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cmake"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CMake}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cmake"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cmake\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cmake\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cmake\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cmake\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -294,7 +304,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.coffee\OpenWithProgi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.coffee\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.coffee"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CoffeeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -302,7 +312,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.config\OpenWithProgi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.config\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.config"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Configuration}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -310,14 +320,14 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.containerfile\OpenWi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.containerfile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.containerfile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.containerfile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Containerfile}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.containerfile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.containerfile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.containerfile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.containerfile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cpp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cpp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cpp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -325,7 +335,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cs\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C#}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\csharp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\csharp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -333,7 +343,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cshtml\OpenWithProgi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cshtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cshtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CSHTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -341,7 +351,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csproj\OpenWithProgi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csproj\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csproj"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C# Project}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -349,7 +359,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.css\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.css\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.css"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CSS}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\css.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\css.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -357,7 +367,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csv\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csv\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csv"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csv"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Comma Separated Values}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csv"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csv\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csv\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csv\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csv\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -365,7 +375,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csx\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\csharp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\csharp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -373,7 +383,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ctp\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ctp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ctp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CakePHP Template}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -381,7 +391,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cxx\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cxx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cxx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -389,7 +399,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dart\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dart\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dart"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dart"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dart}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dart"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dart\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dart\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dart\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dart\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -397,7 +407,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.diff\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.diff\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.diff"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.diff"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Diff}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.diff"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.diff\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.diff\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.diff\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.diff\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -405,7 +415,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dockerfile\OpenWithP Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dockerfile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dockerfile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dockerfile}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -413,7 +423,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dot\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dot\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dot"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dot}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -421,7 +431,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dtd\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dtd\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dtd"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Document Type Definition}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -429,7 +439,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.editorconfig\OpenWit Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.editorconfig\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.editorconfig"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Editor Config}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -437,7 +447,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.edn\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.edn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.edn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Extensible Data Notation}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -445,7 +455,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.erb\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.erb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.erb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.erb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Ruby}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.erb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.erb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\ruby.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.erb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\ruby.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.erb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.erb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -453,7 +463,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.eyaml\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.eyaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.eyaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Hiera Eyaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -461,7 +471,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.eyml\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.eyml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.eyml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Hiera Eyaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -469,7 +479,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fs\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F#}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -477,7 +487,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsi\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Signature}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -485,7 +495,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsscript\OpenWithPro Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsscript\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsscript"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -493,7 +503,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsx\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -501,7 +511,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gemspec\OpenWithProg Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gemspec\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gemspec"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Gemspec}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\ruby.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\ruby.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -510,7 +520,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gitattributes\OpenWi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Attributes}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -519,7 +529,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gitconfig\OpenWithPr Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Config}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -528,7 +538,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gitignore\OpenWithPr Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Ignore}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -536,7 +546,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.go\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.go\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.go"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Go}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\go.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\go.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -544,7 +554,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gradle\OpenWithProgi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gradle\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gradle"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gradle"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Gradle}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gradle"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gradle\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gradle\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gradle\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gradle\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -552,7 +562,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.groovy\OpenWithProgi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.groovy\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.groovy"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.groovy"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Groovy}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.groovy"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.groovy\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.groovy\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.groovy\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.groovy\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -560,7 +570,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.h\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.h\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.h"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\c.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\c.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -568,7 +578,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.handlebars\OpenWithP Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.handlebars\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.handlebars"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Handlebars}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -576,7 +586,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hbs\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hbs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hbs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Handlebars}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -584,14 +594,14 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.h++\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.h++\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.h++"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h++"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h++"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h++\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h++\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h++\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -599,7 +609,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hpp\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hpp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hpp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -607,7 +617,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.htm\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.htm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.htm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -615,7 +625,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.html\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.html\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.html"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -623,7 +633,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hxx\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hxx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hxx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -631,7 +641,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ini\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ini\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ini"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,INI}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -639,7 +649,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ipynb\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ipynb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ipynb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Jupyter}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -647,7 +657,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jade\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jade\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jade"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Jade}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\jade.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\jade.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -655,7 +665,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jav\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jav\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jav"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\java.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\java.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -663,7 +673,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.java\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.java\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.java"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\java.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\java.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -671,7 +681,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.js\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.js\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.js"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -679,7 +689,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jsx\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\react.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\react.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -687,7 +697,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jscsrc\OpenWithProgi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jscsrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jscsrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSCS RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -695,7 +705,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jshintrc\OpenWithPro Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jshintrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jshintrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSHint RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -703,7 +713,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jshtm\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jshtm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jshtm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript HTML Template}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -711,7 +721,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.json\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.json\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.json"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSON}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\json.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\json.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -719,7 +729,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jsp\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jsp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jsp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java Server Pages}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -727,7 +737,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.less\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.less\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.less"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LESS}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\less.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\less.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -735,7 +745,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.log\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.log\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.log"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.log"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Log file}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.log"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.log\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.log\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.log\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.log\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -743,7 +753,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.lua\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.lua\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.lua"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Lua}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -751,7 +761,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.m\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.m\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.m"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Objective C}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -759,7 +769,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.makefile\OpenWithPro Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.makefile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.makefile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Makefile}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -767,7 +777,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.markdown\OpenWithPro Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.markdown\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.markdown"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -775,7 +785,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.md\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.md\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.md"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -783,7 +793,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdoc\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdoc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdoc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,MDoc}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -791,7 +801,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdown\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdown\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdown"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -799,7 +809,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdtext\OpenWithProgi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdtext\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdtext"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -807,7 +817,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdtxt\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdtxt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdtxt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -815,7 +825,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdwn\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdwn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdwn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -823,7 +833,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mk\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mk\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mk"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mk"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Makefile}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mk"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mk\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mk\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mk\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mk\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -831,7 +841,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mkd\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mkd\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mkd"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -839,7 +849,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mkdn\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mkdn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mkdn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -847,7 +857,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ml\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,OCaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -855,7 +865,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mli\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mli\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mli"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,OCaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -863,7 +873,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mjs\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mjs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mjs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mjs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mjs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mjs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mjs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mjs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mjs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -872,7 +882,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.npmignore\OpenWithPr Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,NPM Ignore}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -880,7 +890,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.php\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.php\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.php"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PHP}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\php.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\php.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -888,7 +898,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.phtml\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.phtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.phtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PHP HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -896,7 +906,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pl\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pl\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pl"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -904,7 +914,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pl6\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pl6\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pl6"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl 6}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -912,7 +922,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.plist\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.plist\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.plist"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Properties file}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -920,7 +930,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pm\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -928,7 +938,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pm6\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pm6\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pm6"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl 6 Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -936,7 +946,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pod\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pod\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pod"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl POD}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -944,7 +954,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pp\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -952,7 +962,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.profile\OpenWithProg Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.profile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.profile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -960,7 +970,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.properties\OpenWithP Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.properties\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.properties"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Properties}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -968,7 +978,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ps1\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ps1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ps1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\powershell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\powershell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -976,7 +986,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psd1\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psd1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psd1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell Module Manifest}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\powershell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\powershell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -984,7 +994,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psgi\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psgi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psgi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl CGI}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -992,7 +1002,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psm1\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psm1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psm1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\powershell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\powershell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1000,7 +1010,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.py\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.py\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.py"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Python}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\python.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\python.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1008,7 +1018,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pyi\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pyi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pyi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pyi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Python}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pyi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pyi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\python.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pyi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\python.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pyi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pyi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1016,7 +1026,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.r\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.r\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.r"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1024,7 +1034,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rb\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Ruby}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\ruby.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\ruby.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1032,7 +1042,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rhistory\OpenWithPro Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rhistory\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rhistory"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R History}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1040,7 +1050,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rprofile\OpenWithPro Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rprofile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rprofile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1048,7 +1058,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rs\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Rust}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1056,7 +1066,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rst\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rst\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rst"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rst"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Restructured Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rst"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rst\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rst\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rst\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rst\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1064,7 +1074,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rt\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Rich Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1072,7 +1082,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sass\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sass\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sass"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sass"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Sass}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sass"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sass\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\sass.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sass\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\sass.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sass\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sass\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1080,7 +1090,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.scss\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.scss\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.scss"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Sass}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\sass.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\sass.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1088,7 +1098,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sh\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SH}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1096,7 +1106,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.shtml\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.shtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.shtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SHTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1104,7 +1114,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sql\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sql\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sql"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SQL}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\sql.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\sql.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1112,7 +1122,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.svg\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.svg\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.svg"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SVG}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1120,7 +1130,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.t\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.t\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.t"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1128,7 +1138,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.tex\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.tex\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.tex"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LaTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1136,7 +1146,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ts\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ts\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ts"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,TypeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\typescript.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\typescript.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1144,7 +1154,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.toml\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.toml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.toml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.toml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Toml}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.toml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.toml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.toml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.toml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.toml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1152,7 +1162,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.tsx\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.tsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.tsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,TypeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\react.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\react.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tsx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1160,7 +1170,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.txt\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.txt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.txt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1168,7 +1178,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.vb\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.vb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.vb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Visual Basic}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1176,7 +1186,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.vue\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.vue\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.vue"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vue"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,VUE}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vue"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vue\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\vue.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vue\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\vue.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vue\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vue\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1184,7 +1194,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxi\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX Include}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1192,7 +1202,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxl\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxl\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxl"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX Localization}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1200,7 +1210,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxs\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1208,7 +1218,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xaml\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,XAML}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1216,7 +1226,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xhtml\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xhtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xhtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xhtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xhtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xhtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xhtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xhtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xhtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1224,7 +1234,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xml\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,XML}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1232,7 +1242,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.yaml\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.yaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.yaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Yaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1240,7 +1250,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.yml\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.yml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.yml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Yaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1248,17 +1258,17 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.zsh\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.zsh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.zsh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ZSH}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,{#NameLong}}"; Flags: uninsdeletekey -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico" +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile\shell\open"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe""" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1""" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe"; ValueType: none; ValueName: ""; Flags: uninsdeletekey -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico" +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe""" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1""" @@ -1422,6 +1432,13 @@ begin Result := FileExists(ExpandConstant('{param:update}')) end; +// Check if VS Code created a session-end flag file to indicate OS is shutting down +// This prevents calling inno_updater.exe during system shutdown +function SessionEndFileExists(): Boolean; +begin + Result := FileExists(ExpandConstant('{param:sessionend}')) +end; + function ShouldRunAfterUpdate(): Boolean; begin if IsBackgroundUpdate() then @@ -1444,11 +1461,48 @@ begin end; function GetDestDir(Value: string): string; +begin + Result := ExpandConstant('{app}'); +end; + +function GetVisualElementsManifest(Value: string): string; begin if IsBackgroundUpdate() then - Result := ExpandConstant('{app}\_') + Result := ExpandConstant('new_{#ExeBasename}.VisualElementsManifest.xml') else - Result := ExpandConstant('{app}'); + Result := ExpandConstant('{#ExeBasename}.VisualElementsManifest.xml'); +end; + +function GetExeBasename(Value: string): string; +begin + if IsBackgroundUpdate() then + Result := ExpandConstant('new_{#ExeBasename}.exe') + else + Result := ExpandConstant('{#ExeBasename}.exe'); +end; + +function GetBinDirTunnelApplicationFilename(Value: string): string; +begin + if IsBackgroundUpdate() then + Result := ExpandConstant('new_{#TunnelApplicationName}.exe') + else + Result := ExpandConstant('{#TunnelApplicationName}.exe'); +end; + +function GetBinDirApplicationFilename(Value: string): string; +begin + if IsBackgroundUpdate() then + Result := ExpandConstant('new_{#ApplicationName}') + else + Result := ExpandConstant('{#ApplicationName}'); +end; + +function GetBinDirApplicationCmdFilename(Value: string): string; +begin + if IsBackgroundUpdate() then + Result := ExpandConstant('new_{#ApplicationName}.cmd') + else + Result := ExpandConstant('{#ApplicationName}.cmd'); end; function BoolToStr(Value: Boolean): String; @@ -1497,12 +1551,12 @@ procedure AddAppxPackage(); var AddAppxPackageResultCode: Integer; begin - if not AppxPackageInstalled(ExpandConstant('{#AppxPackageName}'), AddAppxPackageResultCode) then begin + if not SessionEndFileExists() and not AppxPackageInstalled(ExpandConstant('{#AppxPackageName}'), AddAppxPackageResultCode) then begin Log('Installing appx ' + AppxPackageFullname + ' ...'); #if "user" == InstallTarget - ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Add-AppxPackage -Path ''' + ExpandConstant('{app}\appx\{#AppxPackage}') + ''' -ExternalLocation ''' + ExpandConstant('{app}\appx') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); + ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Add-AppxPackage -Path ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx\{#AppxPackage}') + ''' -ExternalLocation ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); #else - ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Add-AppxPackage -Stage ''' + ExpandConstant('{app}\appx\{#AppxPackage}') + ''' -ExternalLocation ''' + ExpandConstant('{app}\appx') + '''; Add-AppxProvisionedPackage -Online -SkipLicense -PackagePath ''' + ExpandConstant('{app}\appx\{#AppxPackage}') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); + ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Add-AppxPackage -Stage ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx\{#AppxPackage}') + ''' -ExternalLocation ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx') + '''; Add-AppxProvisionedPackage -Online -SkipLicense -PackagePath ''' + ExpandConstant('{app}\appx\{#AppxPackage}') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); #endif Log('Add-AppxPackage complete.'); end; @@ -1514,7 +1568,7 @@ var begin // Remove the old context menu package // Following condition can be removed after two versions. - if QualityIsInsiders() and AppxPackageInstalled('Microsoft.VSCodeInsiders', RemoveAppxPackageResultCode) then begin + if QualityIsInsiders() and not SessionEndFileExists() and AppxPackageInstalled('Microsoft.VSCodeInsiders', RemoveAppxPackageResultCode) then begin Log('Deleting old appx ' + AppxPackageFullname + ' installation...'); #if "user" == InstallTarget ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Remove-AppxPackage -Package ''' + AppxPackageFullname + ''''), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); @@ -1522,7 +1576,7 @@ begin DeleteFile(ExpandConstant('{app}\appx\code_insiders_explorer_{#Arch}.appx')); DeleteFile(ExpandConstant('{app}\appx\code_insiders_explorer_command.dll')); end; - if AppxPackageInstalled(ExpandConstant('{#AppxPackageName}'), RemoveAppxPackageResultCode) then begin + if not SessionEndFileExists() and AppxPackageInstalled(ExpandConstant('{#AppxPackageName}'), RemoveAppxPackageResultCode) then begin Log('Removing current ' + AppxPackageFullname + ' appx installation...'); #if "user" == InstallTarget ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Remove-AppxPackage -Package ''' + AppxPackageFullname + ''''), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); @@ -1553,6 +1607,7 @@ begin if IsBackgroundUpdate() then begin + SaveStringToFile(ExpandConstant('{app}\updating_version'), '{#Commit}', False); CreateMutex('{#AppMutex}-ready'); Log('Checking whether application is still running...'); @@ -1562,9 +1617,24 @@ begin end; Log('Application appears not to be running.'); - StopTunnelServiceIfNeeded(); - - Exec(ExpandConstant('{app}\tools\inno_updater.exe'), ExpandConstant('"{app}\{#ExeBasename}.exe" ' + BoolToStr(LockFileExists()) + ' "{cm:UpdatingVisualStudioCode}"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); + if not SessionEndFileExists() then begin + StopTunnelServiceIfNeeded(); + Log('Invoking inno_updater for background update'); + Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"{app}\{#ExeBasename}.exe" ' + BoolToStr(LockFileExists()) + ' "{cm:UpdatingVisualStudioCode}"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); + DeleteFile(ExpandConstant('{app}\updating_version')); + Log('inno_updater completed successfully'); + #if "system" == InstallTarget + Log('Invoking inno_updater to remove previous installation folder'); + Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"--gc" "{app}\{#ExeBasename}.exe" "{#VersionedResourcesFolder}"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); + Log('inno_updater completed gc successfully'); + #endif + end else begin + Log('Skipping inno_updater.exe call because OS session is ending'); + end; + end else begin + Log('Invoking inno_updater to remove previous installation folder'); + Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"--gc" "{app}\{#ExeBasename}.exe" "{#VersionedResourcesFolder}"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); + Log('inno_updater completed gc successfully'); end; if ShouldRestartTunnelService then diff --git a/extensions/tunnel-forwarding/src/extension.ts b/extensions/tunnel-forwarding/src/extension.ts index 4752167e6f2..2f71999b4b8 100644 --- a/extensions/tunnel-forwarding/src/extension.ts +++ b/extensions/tunnel-forwarding/src/extension.ts @@ -21,17 +21,25 @@ export const enum TunnelPrivacyId { */ const CLEANUP_TIMEOUT = 10_000; -const cliPath = process.env.VSCODE_FORWARDING_IS_DEV - ? path.join(__dirname, '../../../cli/target/debug/code') - : path.join( - vscode.env.appRoot, - process.platform === 'darwin' - ? 'bin' - : process.platform === 'win32' && vscode.env.appQuality === 'insider' - ? '../../../bin' // TODO: remove as part of https://github.com/microsoft/vscode/issues/282514 - : '../../bin', - vscode.env.appQuality === 'stable' ? 'code-tunnel' : 'code-tunnel-insiders', - ) + (process.platform === 'win32' ? '.exe' : ''); +const versionFolder = vscode.env.appCommit?.substring(0, 10); +let cliPath: string; +if (process.env.VSCODE_FORWARDING_IS_DEV) { + cliPath = path.join(__dirname, '../../../cli/target/debug/code'); +} else { + let binPath: string; + if (process.platform === 'darwin') { + binPath = 'bin'; + } else if (process.platform === 'win32' && versionFolder && vscode.env.appRoot.includes(versionFolder)) { + binPath = '../../../bin'; + } else { + binPath = '../../bin'; + } + + const cliName = vscode.env.appQuality === 'stable' ? 'code-tunnel' : 'code-tunnel-insiders'; + const extension = process.platform === 'win32' ? '.exe' : ''; + + cliPath = path.join(vscode.env.appRoot, binPath, cliName) + extension; +} class Tunnel implements vscode.Tunnel { private readonly disposeEmitter = new vscode.EventEmitter(); diff --git a/package.json b/package.json index b3198745e54..0d102190322 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.109.0", - "distro": "b90415e4e25274537a83463c7af88fca7e9528a7", + "distro": "9f8bbb424a617246ab35c7c2a143a51fa0ba9ef0", "author": { "name": "Microsoft Corporation" }, diff --git a/resources/win32/insider/bin/code.cmd b/resources/win32/versioned/bin/code.cmd similarity index 100% rename from resources/win32/insider/bin/code.cmd rename to resources/win32/versioned/bin/code.cmd diff --git a/resources/win32/insider/bin/code.sh b/resources/win32/versioned/bin/code.sh similarity index 100% rename from resources/win32/insider/bin/code.sh rename to resources/win32/versioned/bin/code.sh diff --git a/src/bootstrap-node.ts b/src/bootstrap-node.ts index 837dcc91424..e0aa65a7589 100644 --- a/src/bootstrap-node.ts +++ b/src/bootstrap-node.ts @@ -143,7 +143,7 @@ export function configurePortable(product: Partial): { po } // appRoot = ..\Microsoft VS Code Insiders\\resources\app - if (process.platform === 'win32' && product.quality === 'insider') { + if (process.platform === 'win32' && product.win32VersionedUpdate) { return path.dirname(path.dirname(path.dirname(appRoot))); } diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 7820be2a1a4..6db227f4d8e 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -76,6 +76,7 @@ export interface IProductConfiguration { readonly win32AppUserModelId?: string; readonly win32MutexName?: string; readonly win32RegValueName?: string; + readonly win32VersionedUpdate?: boolean; readonly applicationName: string; readonly embedderIdentifier?: string; diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 8ea3d44a286..d3ca580a42c 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -496,7 +496,7 @@ class CodeMain { } private async checkInnoSetupMutex(productService: IProductService): Promise { - if (!isWindows || !productService.win32MutexName || productService.quality !== 'insider') { + if (!(isWindows && productService.win32MutexName && productService.win32VersionedUpdate)) { return false; } diff --git a/src/vs/code/electron-utility/sharedProcess/contrib/defaultExtensionsInitializer.ts b/src/vs/code/electron-utility/sharedProcess/contrib/defaultExtensionsInitializer.ts index 04b79ab51fd..f940df7cd09 100644 --- a/src/vs/code/electron-utility/sharedProcess/contrib/defaultExtensionsInitializer.ts +++ b/src/vs/code/electron-utility/sharedProcess/contrib/defaultExtensionsInitializer.ts @@ -72,7 +72,7 @@ export class DefaultExtensionsInitializer extends Disposable { } private getDefaultExtensionVSIXsLocation(): URI { - if (this.productService.quality === 'insider') { + if (this.productService.win32VersionedUpdate) { // appRoot = C:\Users\\AppData\Local\Programs\Microsoft VS Code Insiders\\resources\app // extensionsPath = C:\Users\\AppData\Local\Programs\Microsoft VS Code Insiders\\bootstrap\extensions return URI.file(join(dirname(dirname(dirname(this.environmentService.appRoot))), 'bootstrap', 'extensions')); diff --git a/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts b/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts index 73829f9a556..ac968da53bb 100644 --- a/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts +++ b/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts @@ -176,7 +176,7 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ // bin = /Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin binParentLocation = this.environmentService.appRoot; } else if (isWindows) { - if (this.productService.quality === 'insider') { + if (this.productService.win32VersionedUpdate) { // appRoot = C:\Users\\AppData\Local\Programs\Microsoft VS Code Insiders\\resources\app // bin = C:\Users\\AppData\Local\Programs\Microsoft VS Code Insiders\bin binParentLocation = dirname(dirname(dirname(this.environmentService.appRoot))); diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index ae4fd9cc879..5bf91910dd7 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -92,7 +92,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun } protected override async initialize(): Promise { - if (this.environmentMainService.isBuilt) { + if (this.productService.win32VersionedUpdate) { const cachePath = await this.cachePath; app.setPath('appUpdate', cachePath); try { @@ -110,7 +110,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun } protected override async postInitialize(): Promise { - if (this.productService.quality !== 'insider') { + if (!this.productService.win32VersionedUpdate) { return; } // Check for pending update from previous session From 27c0d63f0f5bfe99f1d3817070fe46e3202351d2 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Mon, 19 Jan 2026 12:38:06 +0000 Subject: [PATCH 184/387] Refactor ChatContextUsageWidget to improve token usage display and enhance hover interactions --- .../widget/input/chatContextUsageWidget.ts | 45 ++++++++++++++----- .../input/media/chatContextUsageWidget.css | 13 +++++- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts index cf1bf5d79c2..33f34ab3ac0 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts @@ -12,6 +12,7 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { localize } from '../../../../../../nls.js'; import { RunOnceScheduler } from '../../../../../../base/common/async.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; const $ = dom.$; @@ -43,6 +44,7 @@ export class ChatContextUsageWidget extends Disposable { constructor( @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, @IHoverService private readonly hoverService: IHoverService, + @ICommandService private readonly commandService: ICommandService, ) { super(); @@ -222,7 +224,7 @@ export class ChatContextUsageWidget extends Disposable { this.ringProgress.style.strokeDashoffset = String(offset); this.domNode.classList.remove('warning', 'error'); - if (this._usagePercent > 90) { + if (this._usagePercent > 95) { this.domNode.classList.add('error'); } else if (this._usagePercent > 75) { this.domNode.classList.add('warning'); @@ -286,13 +288,23 @@ export class ChatContextUsageWidget extends Disposable { this._hoverQuotaBit = dom.append(quotaBar, $('.quota-bit')); this._hoverQuotaBit.style.width = `${this._usagePercent}%`; - if (this._usagePercent > 90) { - quotaIndicator.classList.add('error'); - } else if (this._usagePercent > 75) { - quotaIndicator.classList.add('warning'); + if (this._usagePercent > 75) { + if (this._usagePercent > 95) { + quotaIndicator.classList.add('error'); + } else { + quotaIndicator.classList.add('warning'); + } + + const quotaSubLabel = dom.append(quotaIndicator, $('div.quota-sub-label')); + quotaSubLabel.textContent = this._usagePercent >= 100 + ? localize('contextWindowFull', "Context window full") + : localize('approachingLimit', "Approaching limit"); + quotaSubLabel.style.fontSize = '12px'; + quotaSubLabel.style.textAlign = 'right'; + quotaSubLabel.style.color = 'var(--vscode-descriptionForeground)'; } - dom.append(container, $('.chat-context-usage-hover-separator')); + // dom.append(container, $('.chat-context-usage-hover-separator')); // List const list = dom.append(container, $('.chat-context-usage-hover-list')); @@ -314,10 +326,23 @@ export class ChatContextUsageWidget extends Disposable { addItem('tools', localize('tools', "Tools"), Math.round(this._toolsTokenCount)); addItem('context', localize('context', "Context"), Math.round(this._contextTokenCount)); - if (this._usagePercent > 80) { - const remaining = Math.max(0, this._maxTokenCount - this._totalTokenCount); - const warning = dom.append(container, $('div', { style: 'margin-top: 8px; color: var(--vscode-editorWarning-foreground);' })); - warning.textContent = localize('contextLimitWarning', "Approaching limit. {0} tokens remaining.", remaining); + if (this._usagePercent > 75) { + const warning = dom.append(container, $('.chat-context-usage-warning')); + + const link = dom.append(warning, $('a', { href: '#', class: 'chat-context-usage-action-link' }, localize('startNewSession', "Start a new session"))); + + this._register(dom.addDisposableListener(link, 'click', (e) => { + e.preventDefault(); + this.hoverService.hideHover(); + this.commandService.executeCommand('workbench.action.chat.newChat'); + })); + + const suffix = localize('toIncreaseLimit', " to reset context window."); + dom.append(warning, document.createTextNode(suffix)); + + if (this._usagePercent > 95) { + warning.classList.add('error'); + } } return container; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css index 8f262c41e28..952fce4e3e8 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css +++ b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css @@ -53,7 +53,7 @@ padding: 8px; display: flex; flex-direction: column; - gap: 4px; + gap: 8px; font-size: 12px; } @@ -114,7 +114,6 @@ .chat-context-usage-hover-separator { height: 1px; background-color: var(--vscode-widget-border); - opacity: 0.3; } .chat-context-usage-hover-list { @@ -136,3 +135,13 @@ .chat-context-usage-hover-item .value { color: var(--vscode-descriptionForeground); } + +.chat-context-usage-action-link { + color: var(--vscode-textLink-foreground); + text-decoration: none; + cursor: pointer; +} + +.chat-context-usage-action-link:hover { + text-decoration: underline; +} From ecb744fd3acddf2737ebd3d796dd6b2ff11d0130 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Mon, 19 Jan 2026 12:40:11 +0000 Subject: [PATCH 185/387] Remove unused hover separator from ChatContextUsageWidget styles --- .../chat/browser/widget/input/chatContextUsageWidget.ts | 2 -- .../browser/widget/input/media/chatContextUsageWidget.css | 5 ----- 2 files changed, 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts index 33f34ab3ac0..9b7bb3a034e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts @@ -304,8 +304,6 @@ export class ChatContextUsageWidget extends Disposable { quotaSubLabel.style.color = 'var(--vscode-descriptionForeground)'; } - // dom.append(container, $('.chat-context-usage-hover-separator')); - // List const list = dom.append(container, $('.chat-context-usage-hover-list')); this._hoverItemValues.clear(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css index 952fce4e3e8..7121acd07d2 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css +++ b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css @@ -111,11 +111,6 @@ background-color: var(--vscode-gauge-errorForeground); } -.chat-context-usage-hover-separator { - height: 1px; - background-color: var(--vscode-widget-border); -} - .chat-context-usage-hover-list { display: flex; flex-direction: column; From e9f791745f939b534c85c0a235c27b14b8a7baaf Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:46:14 +0100 Subject: [PATCH 186/387] Add tooltip to chat context item (#288418) * Add tooltip to chat context item Part of #280658 * Copilot feedback --- .../api/common/extHostChatContext.ts | 9 ++++++-- .../attachments/chatAttachmentWidgets.ts | 20 ++++++++++++++++-- .../attachments/implicitContextAttachment.ts | 21 +++++++++++++------ .../contextContrib/chatContextService.ts | 3 +++ .../common/attachments/chatVariableEntries.ts | 3 +++ .../chat/common/contextContrib/chatContext.ts | 3 +++ .../vscode.proposed.chatContextProvider.d.ts | 4 ++++ 7 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/api/common/extHostChatContext.ts b/src/vs/workbench/api/common/extHostChatContext.ts index 74710e0309e..83f8513a0b9 100644 --- a/src/vs/workbench/api/common/extHostChatContext.ts +++ b/src/vs/workbench/api/common/extHostChatContext.ts @@ -7,7 +7,7 @@ import type * as vscode from 'vscode'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { ExtHostChatContextShape, MainContext, MainThreadChatContextShape } from './extHost.protocol.js'; -import { DocumentSelector } from './extHostTypeConverters.js'; +import { DocumentSelector, MarkdownString } from './extHostTypeConverters.js'; import { IExtHostRpcService } from './extHostRpcService.js'; import { IChatContextItem } from '../../contrib/chat/common/contextContrib/chatContext.js'; import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; @@ -48,6 +48,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext icon: item.icon, label: item.label, modelDescription: item.modelDescription, + tooltip: item.tooltip ? MarkdownString.from(item.tooltip) : undefined, value: item.value, command: item.command ? { id: item.command.command } : undefined }); @@ -88,17 +89,19 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext } const itemHandle = this._addTrackedItem(handle, result); - const item: IChatContextItem | undefined = { + const item: IChatContextItem = { handle: itemHandle, icon: result.icon, label: result.label, modelDescription: result.modelDescription, + tooltip: result.tooltip ? MarkdownString.from(result.tooltip) : undefined, value: options.withValue ? result.value : undefined, command: result.command ? { id: result.command.command } : undefined }; if (options.withValue && !item.value && provider.resolveChatContext) { const resolved = await provider.resolveChatContext(result, token); item.value = resolved?.value; + item.tooltip = resolved?.tooltip ? MarkdownString.from(resolved.tooltip) : item.tooltip; } return item; @@ -112,6 +115,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext icon: extResult.icon, label: extResult.label, modelDescription: extResult.modelDescription, + tooltip: extResult.tooltip ? MarkdownString.from(extResult.tooltip) : undefined, value: extResult.value, command: extResult.command ? { id: extResult.command.command } : undefined }; @@ -176,6 +180,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext icon: item.icon, label: item.label, modelDescription: item.modelDescription, + tooltip: item.tooltip ? MarkdownString.from(item.tooltip) : undefined, value: item.value, handle: itemHandle, command: item.command ? { id: item.command.command } : undefined diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts index 478d6c0e848..6e388ab0286 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts @@ -13,10 +13,10 @@ import { createInstantHoverDelegate } from '../../../../../base/browser/ui/hover import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import * as event from '../../../../../base/common/event.js'; -import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../../base/common/iterator.js'; import { KeyCode } from '../../../../../base/common/keyCodes.js'; -import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; import { basename, dirname } from '../../../../../base/common/path.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; @@ -547,6 +547,9 @@ export class PasteAttachmentWidget extends AbstractChatAttachmentWidget { } export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget { + + private readonly _tooltipHover: MutableDisposable = this._register(new MutableDisposable()); + constructor( resource: URI | undefined, range: IRange | undefined, @@ -560,6 +563,7 @@ export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget { @IOpenerService openerService: IOpenerService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @IHoverService private readonly hoverService: IHoverService, ) { super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService); @@ -595,10 +599,22 @@ export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget { })); } + // Setup tooltip hover for string context attachments + if (isStringVariableEntry(attachment) && attachment.tooltip) { + this._setupTooltipHover(attachment.tooltip); + } + if (resource) { this.addResourceOpenHandlers(resource, range); } } + + private _setupTooltipHover(tooltip: IMarkdownString): void { + this._tooltipHover.value = this.hoverService.setupDelayedHover(this.element, { + content: tooltip, + appearance: { showPointer: true }, + }); + } } export class PromptFileAttachmentWidget extends AbstractChatAttachmentWidget { diff --git a/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts index cc939a7ff93..b08372902fc 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts @@ -7,8 +7,8 @@ import * as dom from '../../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js'; import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; import { Button } from '../../../../../base/browser/ui/button/button.js'; -import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { Codicon } from '../../../../../base/common/codicons.js'; +import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { KeyCode } from '../../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; @@ -127,14 +127,21 @@ export class ImplicitContextAttachmentWidget extends Disposable { const label = this.resourceLabels.create(this.domNode, { supportIcons: true }); - let title: string; + let title: string | undefined; + let markdownTooltip: IMarkdownString | undefined; if (isStringImplicitContextValue(this.attachment.value)) { - title = this.renderString(label); + markdownTooltip = this.attachment.value.tooltip; + title = this.renderString(label, markdownTooltip); } else { title = this.renderResource(this.attachment.value, label); } - this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this.domNode, title)); + if (markdownTooltip || title) { + this.renderDisposables.add(this.hoverService.setupDelayedHover(this.domNode, { + content: markdownTooltip! ?? title!, + appearance: { showPointer: true }, + })); + } // Context menu const scopedContextKeyService = this.renderDisposables.add(this.contextKeyService.createScoped(this.domNode)); @@ -157,10 +164,11 @@ export class ImplicitContextAttachmentWidget extends Disposable { })); } - private renderString(resourceLabel: IResourceLabel): string { + private renderString(resourceLabel: IResourceLabel, markdownTooltip: IMarkdownString | undefined): string | undefined { const label = this.attachment.name; const icon = this.attachment.icon; - const title = localize('openFile', "Current file context"); + // Don't set title if we have a markdown tooltip - the hover service will handle it + const title = markdownTooltip ? undefined : localize('openFile', "Current file context"); resourceLabel.setLabel(label, undefined, { iconPath: icon, title }); return title; } @@ -210,6 +218,7 @@ export class ImplicitContextAttachmentWidget extends Disposable { icon: this.attachment.value.icon, modelDescription: this.attachment.value.modelDescription, uri: this.attachment.value.uri, + tooltip: this.attachment.value.tooltip, commandId: this.attachment.value.commandId, handle: this.attachment.value.handle }; diff --git a/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts b/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts index 57850cea5ca..d13e43ef823 100644 --- a/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts +++ b/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts @@ -133,6 +133,7 @@ export class ChatContextService extends Disposable { icon: context.icon, uri: uri, modelDescription: context.modelDescription, + tooltip: context.tooltip, commandId: context.command?.id, handle: context.handle }; @@ -151,12 +152,14 @@ export class ChatContextService extends Disposable { const resolved = await this._contextForResource(context.uri, true); context.value = resolved?.value; context.modelDescription = resolved?.modelDescription; + context.tooltip = resolved?.tooltip; return context; } else if (item.provider.resolveChatContext) { const resolved = await item.provider.resolveChatContext(item.originalItem, CancellationToken.None); if (resolved) { context.value = resolved.value; context.modelDescription = resolved.modelDescription; + context.tooltip = resolved.tooltip; return context; } } diff --git a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts index c45c4899abc..dec115cb277 100644 --- a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts +++ b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../../base/common/codicons.js'; +import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { basename } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -73,6 +74,7 @@ export interface StringChatContextValue { modelDescription?: string; icon: ThemeIcon; uri: URI; + tooltip?: IMarkdownString; /** * Command ID to execute when this context item is clicked. */ @@ -95,6 +97,7 @@ export interface IChatRequestStringVariableEntry extends IBaseChatRequestVariabl readonly modelDescription?: string; readonly icon: ThemeIcon; readonly uri: URI; + readonly tooltip?: IMarkdownString; /** * Command ID to execute when this context item is clicked. */ diff --git a/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts b/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts index 6973240728d..6661e3e5130 100644 --- a/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts +++ b/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts @@ -7,10 +7,13 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; +import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; + export interface IChatContextItem { icon: ThemeIcon; label: string; modelDescription?: string; + tooltip?: IMarkdownString; handle: number; value?: string; command?: { diff --git a/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts index 94681edf174..fb810775142 100644 --- a/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts @@ -39,6 +39,10 @@ declare module 'vscode' { * An optional description of the context item, e.g. to describe the item to the language model. */ modelDescription?: string; + /** + * An optional tooltip to show when hovering over the context item in the UI. + */ + tooltip?: MarkdownString; /** * The value of the context item. Can be omitted when returned from one of the `provide` methods if the provider supports `resolveChatContext`. */ From 658baa7739695f16325460518c9467e9eb36904c Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Mon, 19 Jan 2026 12:49:16 +0000 Subject: [PATCH 187/387] Refactor token counting in ChatContextUsageWidget for improved performance and readability; add styling for quota sub-label in hover display --- .../widget/input/chatContextUsageWidget.ts | 76 +++++++++---------- .../input/media/chatContextUsageWidget.css | 6 ++ 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts index 9b7bb3a034e..1fd11728799 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts @@ -135,11 +135,6 @@ export class ChatContextUsageWidget extends Disposable { return; } - let promptsTokenCount = 0; - let filesTokenCount = 0; - let toolsTokenCount = 0; - let contextTokenCount = 0; - const requests = this._currentModel.getRequests(); if (requests.length === 0) { @@ -170,22 +165,25 @@ export class ChatContextUsageWidget extends Disposable { return text.length / 4; }; - for (const request of requests) { + const requestCounts = await Promise.all(requests.map(async (request) => { + let p = 0; + let f = 0; + let t = 0; + let c = 0; + // Prompts: User message const messageText = typeof request.message === 'string' ? request.message : request.message.text; - promptsTokenCount += await countTokens(messageText); + p += await countTokens(messageText); // Variables (Files, Context) if (request.variableData && request.variableData.variables) { for (const variable of request.variableData.variables) { - // Estimate usage for variables as getting full content might be expensive/complex async - // Using a safe estimate for now per item type + // Estimate usage const defaultEstimate = 500; - if (variable.kind === 'file') { - filesTokenCount += defaultEstimate; + f += defaultEstimate; } else { - contextTokenCount += defaultEstimate; + c += defaultEstimate; } } } @@ -193,22 +191,29 @@ export class ChatContextUsageWidget extends Disposable { // Tools & Response if (request.response) { const responseString = request.response.response.toString(); - promptsTokenCount += await countTokens(responseString); + p += await countTokens(responseString); - // Loop through response parts for tool invocations for (const part of request.response.response.value) { if (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') { - // Estimate tool invocation cost - toolsTokenCount += 200; + t += 200; } } } - } - this._promptsTokenCount = promptsTokenCount; - this._filesTokenCount = filesTokenCount; - this._toolsTokenCount = toolsTokenCount; - this._contextTokenCount = contextTokenCount; + return { p, f, t, c }; + })); + + this._promptsTokenCount = 0; + this._filesTokenCount = 0; + this._toolsTokenCount = 0; + this._contextTokenCount = 0; + + for (const count of requestCounts) { + this._promptsTokenCount += count.p; + this._filesTokenCount += count.f; + this._toolsTokenCount += count.t; + this._contextTokenCount += count.c; + } this._totalTokenCount = Math.round(this._promptsTokenCount + this._filesTokenCount + this._toolsTokenCount + this._contextTokenCount); this._usagePercent = Math.min(100, (this._totalTokenCount / this._maxTokenCount) * 100); @@ -217,6 +222,14 @@ export class ChatContextUsageWidget extends Disposable { this._updateHover(); } + private _formatTokens(value: number): string { + if (value >= 1000) { + const thousands = value / 1000; + return `${thousands >= 10 ? Math.round(thousands) : thousands.toFixed(1)}k`; + } + return `${value}`; + } + private _updateRing() { const r = 7; const c = 2 * Math.PI * r; @@ -234,14 +247,7 @@ export class ChatContextUsageWidget extends Disposable { private _updateHover() { if (this._hoverQuotaValue) { const percentStr = `${this._usagePercent.toFixed(0)}%`; - const formatTokens = (value: number) => { - if (value >= 1000) { - const thousands = value / 1000; - return `${thousands >= 10 ? Math.round(thousands) : thousands.toFixed(1)}k`; - } - return `${value}`; - }; - const usageStr = `${formatTokens(this._totalTokenCount)} / ${formatTokens(this._maxTokenCount)}`; + const usageStr = `${this._formatTokens(this._totalTokenCount)} / ${this._formatTokens(this._maxTokenCount)}`; this._hoverQuotaValue.textContent = `${usageStr} • ${percentStr}`; } @@ -268,14 +274,7 @@ export class ChatContextUsageWidget extends Disposable { const container = $('.chat-context-usage-hover'); const percentStr = `${this._usagePercent.toFixed(0)}%`; - const formatTokens = (value: number) => { - if (value >= 1000) { - const thousands = value / 1000; - return `${thousands >= 10 ? Math.round(thousands) : thousands.toFixed(1)}k`; - } - return `${value}`; - }; - const usageStr = `${formatTokens(this._totalTokenCount)} / ${formatTokens(this._maxTokenCount)}`; + const usageStr = `${this._formatTokens(this._totalTokenCount)} / ${this._formatTokens(this._maxTokenCount)}`; // Quota Indicator (Progress Bar) const quotaIndicator = dom.append(container, $('.quota-indicator')); @@ -299,9 +298,6 @@ export class ChatContextUsageWidget extends Disposable { quotaSubLabel.textContent = this._usagePercent >= 100 ? localize('contextWindowFull', "Context window full") : localize('approachingLimit', "Approaching limit"); - quotaSubLabel.style.fontSize = '12px'; - quotaSubLabel.style.textAlign = 'right'; - quotaSubLabel.style.color = 'var(--vscode-descriptionForeground)'; } // List diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css index 7121acd07d2..7fa3f5198a9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css +++ b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css @@ -111,6 +111,12 @@ background-color: var(--vscode-gauge-errorForeground); } +.chat-context-usage-hover .quota-indicator .quota-sub-label { + font-size: 12px; + text-align: right; + color: var(--vscode-descriptionForeground); +} + .chat-context-usage-hover-list { display: flex; flex-direction: column; From 986be4ec4092d2212bb5f27f2fcc18a90a90c0de Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:52:54 +0000 Subject: [PATCH 188/387] Don't persist extension contributed chat contexts across reload (#288106) * Initial plan * Don't persist extension contributed chat contexts across reload Filter out IChatRequestStringVariableEntry and IChatRequestImplicitVariableEntry with StringChatContextValue values during serialization as these have handles that become invalid after window reload. Fixes microsoft/vscode#280380 Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- .../contrib/chat/common/model/chatModel.ts | 16 ++++- .../chat/test/common/model/chatModel.test.ts | 65 +++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index b752e95e0ae..bae8c889b1d 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -27,7 +27,7 @@ import { EditSuggestionId } from '../../../../../editor/common/textModelEditSour import { localize } from '../../../../../nls.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; -import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js'; +import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry, isImplicitVariableEntry, isStringImplicitContextValue, isStringVariableEntry } from '../attachments/chatVariableEntries.js'; import { migrateLegacyTerminalToolSpecificData } from '../chat.js'; import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatResponseClearToPreviousToolInvocationReason, ElicitationState, IChatAgentMarkdownContentWithVulnerability, IChatClearToPreviousToolInvocation, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatEditingSessionAction, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExtensionsContent, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatMcpServersStarting, IChatMcpServersStartingSerialized, IChatModelReference, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatNotebookEdit, IChatProgress, IChatProgressMessage, IChatPullRequestContent, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatService, IChatSessionContext, IChatSessionTiming, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, ResponseModelState, isIUsedContext } from '../chatService/chatService.js'; import { ChatAgentLocation, ChatModeKind } from '../constants.js'; @@ -1727,9 +1727,21 @@ class InputModel implements IInputModel { return undefined; } + // Filter out extension-contributed context items (kind: 'string' or implicit entries with StringChatContextValue) + // These have handles that become invalid after window reload and cannot be properly restored. + const persistableAttachments = value.attachments.filter(attachment => { + if (isStringVariableEntry(attachment)) { + return false; + } + if (isImplicitVariableEntry(attachment) && isStringImplicitContextValue(attachment.value)) { + return false; + } + return true; + }); + return { contrib: value.contrib, - attachments: value.attachments, + attachments: persistableAttachments, mode: value.mode, selectedModel: value.selectedModel ? { identifier: value.selectedModel.identifier, diff --git a/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts index 23db280a7e9..ee07a7c00d4 100644 --- a/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts @@ -5,6 +5,7 @@ import assert from 'assert'; import * as sinon from 'sinon'; +import { Codicon } from '../../../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { observableValue } from '../../../../../../base/common/observable.js'; import { URI } from '../../../../../../base/common/uri.js'; @@ -22,6 +23,7 @@ import { IStorageService } from '../../../../../../platform/storage/common/stora import { IExtensionService } from '../../../../../services/extensions/common/extensions.js'; import { TestExtensionService, TestStorageService } from '../../../../../test/common/workbenchTestServices.js'; import { CellUri } from '../../../../notebook/common/notebookCommon.js'; +import { IChatRequestImplicitVariableEntry, IChatRequestStringVariableEntry, IChatRequestFileEntry, StringChatContextValue } from '../../../common/attachments/chatVariableEntries.js'; import { ChatAgentService, IChatAgentService } from '../../../common/participants/chatAgents.js'; import { ChatModel, IExportableChatData, ISerializableChatData1, ISerializableChatData2, ISerializableChatData3, isExportableSessionData, isSerializableSessionData, normalizeSerializableChatData, Response } from '../../../common/model/chatModel.js'; import { ChatRequestTextPart } from '../../../common/requestParser/chatParserTypes.js'; @@ -164,6 +166,69 @@ suite('ChatModel', () => { assert.strictEqual(request1.shouldBeRemovedOnSend, undefined); assert.strictEqual(request1.response!.shouldBeRemovedOnSend, undefined); }); + + test('inputModel.toJSON filters extension-contributed contexts', async function () { + const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.Chat, canUseTools: true })); + + const fileAttachment: IChatRequestFileEntry = { + kind: 'file', + value: URI.parse('file:///test.ts'), + id: 'file-id', + name: 'test.ts', + }; + + const stringContextValue: StringChatContextValue = { + value: 'pr-content', + name: 'PR #123', + icon: Codicon.gitPullRequest, + uri: URI.parse('pr://123'), + handle: 1 + }; + + const stringAttachment: IChatRequestStringVariableEntry = { + kind: 'string', + value: 'pr-content', + id: 'string-id', + name: 'PR #123', + icon: Codicon.gitPullRequest, + uri: URI.parse('pr://123'), + handle: 1 + }; + + const implicitWithStringContext: IChatRequestImplicitVariableEntry = { + kind: 'implicit', + isFile: true, + value: stringContextValue, + uri: URI.parse('pr://123'), + isSelection: false, + enabled: true, + id: 'implicit-string-id', + name: 'PR Context', + }; + + const implicitWithUri: IChatRequestImplicitVariableEntry = { + kind: 'implicit', + isFile: true, + value: URI.parse('file:///current.ts'), + uri: URI.parse('file:///current.ts'), + isSelection: false, + enabled: true, + id: 'implicit-uri-id', + name: 'current.ts', + }; + + model.inputModel.setState({ + attachments: [fileAttachment, stringAttachment, implicitWithStringContext, implicitWithUri], + inputText: 'test' + }); + + const serialized = model.inputModel.toJSON(); + assert.ok(serialized); + + // Should filter out string attachments and implicit attachments with StringChatContextValue + // Should keep file attachments and implicit attachments with URI values + assert.deepStrictEqual(serialized.attachments, [fileAttachment, implicitWithUri]); + }); }); suite('Response', () => { From e2b039a8a16ffb29a2be6906bdb30239fcdf69cf Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Mon, 19 Jan 2026 13:00:41 +0000 Subject: [PATCH 189/387] Refactor token counting in ChatContextUsageWidget to include image and selection tokens; update related calculations and display --- .../widget/input/chatContextUsageWidget.ts | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts index 1fd11728799..45bae917399 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts @@ -31,8 +31,10 @@ export class ChatContextUsageWidget extends Disposable { private _totalTokenCount = 0; private _promptsTokenCount = 0; private _filesTokenCount = 0; + private _imagesTokenCount = 0; + private _selectionTokenCount = 0; private _toolsTokenCount = 0; - private _contextTokenCount = 0; + private _otherTokenCount = 0; private _maxTokenCount = 4096; // Default fallback private _usagePercent = 0; @@ -168,8 +170,10 @@ export class ChatContextUsageWidget extends Disposable { const requestCounts = await Promise.all(requests.map(async (request) => { let p = 0; let f = 0; + let i = 0; + let s = 0; let t = 0; - let c = 0; + let o = 0; // Prompts: User message const messageText = typeof request.message === 'string' ? request.message : request.message.text; @@ -182,8 +186,12 @@ export class ChatContextUsageWidget extends Disposable { const defaultEstimate = 500; if (variable.kind === 'file') { f += defaultEstimate; + } else if (variable.kind === 'image') { + i += defaultEstimate; + } else if (variable.kind === 'implicit' && variable.isSelection) { + s += defaultEstimate; } else { - c += defaultEstimate; + o += defaultEstimate; } } } @@ -200,22 +208,26 @@ export class ChatContextUsageWidget extends Disposable { } } - return { p, f, t, c }; + return { p, f, i, s, t, o }; })); this._promptsTokenCount = 0; this._filesTokenCount = 0; + this._imagesTokenCount = 0; + this._selectionTokenCount = 0; this._toolsTokenCount = 0; - this._contextTokenCount = 0; + this._otherTokenCount = 0; for (const count of requestCounts) { this._promptsTokenCount += count.p; this._filesTokenCount += count.f; + this._imagesTokenCount += count.i; + this._selectionTokenCount += count.s; this._toolsTokenCount += count.t; - this._contextTokenCount += count.c; + this._otherTokenCount += count.o; } - this._totalTokenCount = Math.round(this._promptsTokenCount + this._filesTokenCount + this._toolsTokenCount + this._contextTokenCount); + this._totalTokenCount = Math.round(this._promptsTokenCount + this._filesTokenCount + this._imagesTokenCount + this._selectionTokenCount + this._toolsTokenCount + this._otherTokenCount); this._usagePercent = Math.min(100, (this._totalTokenCount / this._maxTokenCount) * 100); this._updateRing(); @@ -266,8 +278,10 @@ export class ChatContextUsageWidget extends Disposable { updateItem('prompts', this._promptsTokenCount); updateItem('files', this._filesTokenCount); + updateItem('images', this._imagesTokenCount); + updateItem('selection', this._selectionTokenCount); updateItem('tools', this._toolsTokenCount); - updateItem('context', this._contextTokenCount); + updateItem('other', this._otherTokenCount); } private _getHoverDomNode(): HTMLElement { @@ -317,8 +331,10 @@ export class ChatContextUsageWidget extends Disposable { addItem('prompts', localize('prompts', "Prompts"), Math.round(this._promptsTokenCount)); addItem('files', localize('files', "Files"), Math.round(this._filesTokenCount)); + addItem('images', localize('images', "Images"), Math.round(this._imagesTokenCount)); + addItem('selection', localize('selection', "Selection"), Math.round(this._selectionTokenCount)); addItem('tools', localize('tools', "Tools"), Math.round(this._toolsTokenCount)); - addItem('context', localize('context', "Context"), Math.round(this._contextTokenCount)); + addItem('other', localize('other', "Other"), Math.round(this._otherTokenCount)); if (this._usagePercent > 75) { const warning = dom.append(container, $('.chat-context-usage-warning')); From 6aa820992bdd22cc0fa5fc435bed4237858acb7c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 19 Jan 2026 14:02:38 +0100 Subject: [PATCH 190/387] chat - end the log spam when reading from disk (#288877) * chat - end the log spam when reading from disk * . * Update src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/vs/base/node/pfs.ts | 8 ++++++-- .../contrib/chat/common/model/chatSessionStore.ts | 10 ++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 55251cf572a..324c60d8452 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -129,7 +129,9 @@ async function safeReaddirWithFileTypes(path: string): Promise { try { return await fs.promises.readdir(path, { withFileTypes: true }); } catch (error) { - console.warn('[node.js fs] readdir with filetypes failed with error: ', error); + if (error.code !== 'ENOENT') { + console.warn('[node.js fs] readdir with filetypes failed with error: ', error); + } } // Fallback to manually reading and resolving each @@ -152,7 +154,9 @@ async function safeReaddirWithFileTypes(path: string): Promise { isDirectory = lstat.isDirectory(); isSymbolicLink = lstat.isSymbolicLink(); } catch (error) { - console.warn('[node.js fs] unexpected error from lstat after readdir: ', error); + if (error.code !== 'ENOENT') { + console.warn('[node.js fs] unexpected error from lstat after readdir: ', error); + } } result.push({ diff --git a/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts b/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts index 531c838fe04..6131633883c 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts @@ -383,9 +383,15 @@ export class ChatSessionStore extends Disposable { } private reportError(reasonForTelemetry: string, message: string, error?: Error): void { - this.logService.error(`ChatSessionStore: ` + message, toErrorMessage(error)); - const fileOperationReason = error && toFileOperationResult(error); + + if (fileOperationReason === FileOperationResult.FILE_NOT_FOUND) { + // Expected case (e.g. reading a non-existent session); keep noise low + this.logService.trace(`ChatSessionStore: ` + message, toErrorMessage(error)); + } else { + // Unexpected or serious error; surface at error level + this.logService.error(`ChatSessionStore: ` + message, toErrorMessage(error)); + } type ChatSessionStoreErrorData = { reason: string; fileOperationReason: number; From 99d227e67c9d2ab3f338095a53ec01470ec4736d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 19 Jan 2026 14:21:19 +0100 Subject: [PATCH 191/387] agents - fix regression with projection not applying when opening session (#288889) * agents - fix regression with projection not applying when opening session * limit to setting only * . --- .../agentSessions/agentSessionsOpener.ts | 6 +- .../agentSessionProjectionActions.ts | 19 ------- .../agentSessionProjectionService.ts | 56 +++++++++++-------- .../agentSessionsExperiments.contribution.ts | 6 +- 4 files changed, 41 insertions(+), 46 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts index 47e10c26ce5..173f5bc852b 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts @@ -12,6 +12,7 @@ import { ACTIVE_GROUP, SIDE_GROUP } from '../../../../services/editor/common/edi import { IEditorOptions } from '../../../../../platform/editor/common/editor.js'; import { IChatSessionsService } from '../../common/chatSessionsService.js'; import { Schemas } from '../../../../../base/common/network.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; //#region Session Opener Registry @@ -48,17 +49,18 @@ export const sessionOpenerRegistry = new SessionOpenerRegistry(); //#endregion export async function openSession(accessor: ServicesAccessor, session: IAgentSession, openOptions?: ISessionOpenOptions): Promise { + const instantiationService = accessor.get(IInstantiationService); // First, give registered participants a chance to handle the session for (const participant of sessionOpenerRegistry.getParticipants()) { - const handled = await participant.handleOpenSession(accessor, session, openOptions); + const handled = await instantiationService.invokeFunction(accessor => participant.handleOpenSession(accessor, session, openOptions)); if (handled) { return undefined; // Participant handled the session, skip default opening } } // Default session opening logic - return openSessionDefault(accessor, session, openOptions); + return instantiationService.invokeFunction(accessor => openSessionDefault(accessor, session, openOptions)); } async function openSessionDefault(accessor: ServicesAccessor, session: IAgentSession, openOptions?: ISessionOpenOptions): Promise { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionActions.ts index 1e017fc3d96..cc74b7234b5 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionActions.ts @@ -108,22 +108,3 @@ export class ToggleAgentStatusAction extends ToggleTitleBarConfigAction { } //#endregion - -//#region Toggle Agent Session Projection - -export class ToggleAgentSessionProjectionAction extends ToggleTitleBarConfigAction { - constructor() { - super( - ChatConfiguration.AgentSessionProjectionEnabled, - localize('toggle.agentSessionProjection', 'Agent Session Projection'), - localize('toggle.agentSessionProjectionDescription', "Toggle Agent Session Projection mode for focused workspace review of agent sessions"), 7, - ContextKeyExpr.and( - ChatContextKeys.enabled, - IsCompactTitleBarContext.negate(), - ChatContextKeys.supported - ) - ); - } -} - -//#endregion diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts index 67e8db416a0..e798dc3ce8a 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts @@ -22,10 +22,11 @@ import { IWorkbenchLayoutService } from '../../../../../services/layout/browser/ import { ACTION_ID_NEW_CHAT } from '../../actions/chatActions.js'; import { IChatEditingService, ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; import { IAgentTitleBarStatusService } from './agentTitleBarStatusService.js'; -import { ISessionOpenerParticipant, ISessionOpenOptions, sessionOpenerRegistry } from '../agentSessionsOpener.js'; -import { ServicesAccessor } from '../../../../../../editor/browser/editorExtensions.js'; +import { IWorkbenchContribution } from '../../../../../common/contributions.js'; import { inAgentSessionProjection } from './agentSessionProjection.js'; import { ChatConfiguration } from '../../../common/constants.js'; +import { ISessionOpenOptions, sessionOpenerRegistry } from '../agentSessionsOpener.js'; +import { ServicesAccessor } from '../../../../../../editor/browser/editorExtensions.js'; //#region Configuration @@ -122,26 +123,6 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS // Listen for editor close events to exit projection mode when all editors are closed this._register(this.editorService.onDidCloseEditor(() => this._checkForEmptyEditors())); - - // Register as a session opener participant to enter projection mode when sessions are opened - this._register(sessionOpenerRegistry.registerParticipant(this._createSessionOpenerParticipant())); - } - - private _createSessionOpenerParticipant(): ISessionOpenerParticipant { - return { - handleOpenSession: async (_accessor: ServicesAccessor, session: IAgentSession, _openOptions?: ISessionOpenOptions): Promise => { - // Only handle if projection mode is enabled - if (!this._isEnabled()) { - return false; - } - - // Enter projection mode for the session - await this.enterProjection(session); - - // Return true to indicate we handled the session (projection mode opens the chat itself) - return true; - } - }; } private _isEnabled(): boolean { @@ -348,3 +329,34 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS } //#endregion + +//#region Agent Session Projection Opener Contribution + +export class AgentSessionProjectionOpenerContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.agentSessionProjectionOpener'; + + constructor( + @IAgentSessionProjectionService private readonly agentSessionProjectionService: IAgentSessionProjectionService, + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + super(); + + this._register(sessionOpenerRegistry.registerParticipant({ + handleOpenSession: async (_accessor: ServicesAccessor, session: IAgentSession, _openOptions?: ISessionOpenOptions): Promise => { + // Only handle if projection mode is enabled + if (this.configurationService.getValue(ChatConfiguration.AgentSessionProjectionEnabled) !== true) { + return false; + } + + // Enter projection mode for the session + await this.agentSessionProjectionService.enterProjection(session); + + // Return true to indicate we handled the session (projection mode opens the chat itself) + return true; + } + })); + } +} + +//#endregion diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts index f510c88aaca..153e27cfde7 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts @@ -5,8 +5,8 @@ import { registerSingleton, InstantiationType } from '../../../../../../platform/instantiation/common/extensions.js'; import { MenuId, MenuRegistry, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; -import { IAgentSessionProjectionService, AgentSessionProjectionService } from './agentSessionProjectionService.js'; -import { EnterAgentSessionProjectionAction, ExitAgentSessionProjectionAction, ToggleAgentStatusAction, ToggleAgentSessionProjectionAction } from './agentSessionProjectionActions.js'; +import { IAgentSessionProjectionService, AgentSessionProjectionService, AgentSessionProjectionOpenerContribution } from './agentSessionProjectionService.js'; +import { EnterAgentSessionProjectionAction, ExitAgentSessionProjectionAction, ToggleAgentStatusAction } from './agentSessionProjectionActions.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../../common/contributions.js'; import { AgentTitleBarStatusRendering } from './agentTitleBarStatusWidget.js'; import { AgentTitleBarStatusService, IAgentTitleBarStatusService } from './agentTitleBarStatusService.js'; @@ -20,11 +20,11 @@ import { ChatConfiguration } from '../../../common/constants.js'; registerAction2(EnterAgentSessionProjectionAction); registerAction2(ExitAgentSessionProjectionAction); registerAction2(ToggleAgentStatusAction); -registerAction2(ToggleAgentSessionProjectionAction); registerSingleton(IAgentSessionProjectionService, AgentSessionProjectionService, InstantiationType.Delayed); registerSingleton(IAgentTitleBarStatusService, AgentTitleBarStatusService, InstantiationType.Delayed); +registerWorkbenchContribution2(AgentSessionProjectionOpenerContribution.ID, AgentSessionProjectionOpenerContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(AgentTitleBarStatusRendering.ID, AgentTitleBarStatusRendering, WorkbenchPhase.AfterRestored); // Register Agent Status as a menu item in the command center (alongside the search box, not replacing it) From afea1bf7cf22469a6e22feb13d89a963ea622814 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:29:44 +0000 Subject: [PATCH 192/387] Log error for invalid chatContext icon format instead of silently ignoring (#288885) * Initial plan * Add error logging for invalid chatContext icon format Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Fix: Only log error when icon exists but has invalid format Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Add comment explaining defensive check Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- .../chat/browser/contextContrib/chatContext.contribution.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/contextContrib/chatContext.contribution.ts b/src/vs/workbench/contrib/chat/browser/contextContrib/chatContext.contribution.ts index 03e4b938c53..33472b08f2f 100644 --- a/src/vs/workbench/contrib/chat/browser/contextContrib/chatContext.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/contextContrib/chatContext.contribution.ts @@ -61,7 +61,12 @@ export class ChatContextContribution extends Disposable implements IWorkbenchCon } for (const contribution of ext.value) { const icon = contribution.icon ? ThemeIcon.fromString(contribution.icon) : undefined; + if (!icon && contribution.icon) { + ext.collector.error(localize('chatContextExtPoint.invalidIcon', "Invalid icon format for chat context contribution '{0}'. Icon must be in the format '$(iconId)' or '$(iconId~spin)', e.g. '$(copilot)'.", contribution.id)); + continue; + } if (!icon) { + // Icon is required by schema, but handle defensively continue; } From b2795bd8bbdf1d711da3b75810126a7091d53b9b Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Mon, 19 Jan 2026 06:43:38 -0800 Subject: [PATCH 193/387] Add `Collapse All` action to SCM view pane (#285407) Added Collapse All action to SCM view pane --- .../contrib/scm/browser/scmViewPane.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 158af1b2388..5acc9274b66 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1369,6 +1369,32 @@ class ExpandAllRepositoriesAction extends ViewAction { registerAction2(CollapseAllRepositoriesAction); registerAction2(ExpandAllRepositoriesAction); +class CollapseAllAction extends ViewAction { + constructor() { + super({ + id: `workbench.scm.action.collapseAll`, + title: localize('scmCollapseAll', "Collapse All"), + viewId: VIEW_PANE_ID, + f1: false, + icon: Codicon.collapseAll, + menu: { + id: MenuId.SCMResourceGroupContext, + group: 'inline', + when: ContextKeys.SCMViewMode.isEqualTo(ViewMode.Tree), + order: 10, + } + }); + } + + async runInView(_accessor: ServicesAccessor, view: SCMViewPane, context?: ISCMResourceGroup): Promise { + if (context) { + view.collapseAllResources(context); + } + } +} + +registerAction2(CollapseAllAction); + const enum SCMInputWidgetCommandId { CancelAction = 'scm.input.cancelAction', SetupAction = 'scm.input.triggerSetup' @@ -2854,6 +2880,14 @@ export class SCMViewPane extends ViewPane { } } + collapseAllResources(group: ISCMResourceGroup): void { + for (const { element } of this.tree.getNode(group).children) { + if (!isSCMViewService(element)) { + this.tree.collapse(element, true); + } + } + } + focusPreviousInput(): void { this.treeOperationSequencer.queue(() => this.focusInput(-1)); } From 0fb4f779292f318523f39fc91e20751633134be9 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 19 Jan 2026 15:44:25 +0100 Subject: [PATCH 194/387] allow agents to define which subagents can be used (#288902) * add ICustomAgent.agents * add agent as built-in toolset * update * update * add tests for completions * use subagents list --- .../tools/languageModelToolsService.ts | 12 + .../contrib/chat/browser/widget/chatWidget.ts | 6 +- .../contrib/chat/common/chatModes.ts | 16 +- .../computeAutomaticInstructions.ts | 43 +-- .../promptHeaderAutocompletion.ts | 57 ++-- .../languageProviders/promptHovers.ts | 2 + .../languageProviders/promptValidator.ts | 34 ++- .../common/promptSyntax/promptFileParser.ts | 22 ++ .../promptSyntax/service/promptsService.ts | 6 + .../service/promptsServiceImpl.ts | 4 +- .../tools/builtinTools/runSubagentTool.ts | 29 +- .../chat/common/tools/builtinTools/tools.ts | 11 +- .../common/tools/languageModelToolsService.ts | 1 + .../promptBodyAutocompletion.test.ts | 4 + .../promptHeaderAutocompletion.test.ts | 288 ++++++++++++++++++ .../languageProviders/promptHovers.test.ts | 12 + .../languageProviders/promptValidator.test.ts | 83 ++++- .../tools/languageModelToolsService.test.ts | 25 +- .../widget/input/chatSelectedTools.test.ts | 4 +- .../service/newPromptsParser.test.ts | 64 ++++ .../service/promptsService.test.ts | 122 +++++++- .../tools/mockLanguageModelToolsService.ts | 1 + 22 files changed, 739 insertions(+), 107 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index a6338486bf7..a37156c6a62 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -79,6 +79,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo readonly vscodeToolSet: ToolSet; readonly executeToolSet: ToolSet; readonly readToolSet: ToolSet; + readonly agentToolSet: ToolSet; private readonly _onDidChangeTools = this._register(new Emitter()); readonly onDidChangeTools = this._onDidChangeTools.event; @@ -172,6 +173,17 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo description: localize('copilot.toolSet.read.description', 'Read files in your workspace'), } )); + + // Create the internal Agent tool set + this.agentToolSet = this._register(this.createToolSet( + ToolDataSource.Internal, + 'agent', + SpecedToolAliases.agent, + { + icon: ThemeIcon.fromId(Codicon.agent.id), + description: localize('copilot.toolSet.agent.description', 'Delegate tasks to other agents'), + } + )); } /** diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 09879aec3a6..395b996bc3b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -2644,9 +2644,9 @@ export class ChatWidget extends Disposable implements IChatWidget { */ private async _autoAttachInstructions({ attachedContext }: IChatRequestInputOptions): Promise { this.logService.debug(`ChatWidget#_autoAttachInstructions: prompt files are always enabled`); - const enabledTools = this.input.currentModeKind === ChatModeKind.Agent ? this.input.selectedToolsModel.entriesMap.get() : undefined; - - const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, enabledTools); + const enabledTools = this.input.currentModeKind === ChatModeKind.Agent ? this.input.selectedToolsModel.userSelectedTools.get() : undefined; + const enabledSubAgents = this.input.currentModeKind === ChatModeKind.Agent ? this.input.currentModeObs.get().agents?.get() : undefined; + const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, enabledTools, enabledSubAgents); await computer.collect(attachedContext, CancellationToken.None); } diff --git a/src/vs/workbench/contrib/chat/common/chatModes.ts b/src/vs/workbench/contrib/chat/common/chatModes.ts index b0998c47482..e41fa96ae97 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -122,6 +122,7 @@ export class ChatModeService extends Disposable implements IChatModeService { handOffs: cachedMode.handOffs, target: cachedMode.target, infer: cachedMode.infer, + agents: cachedMode.agents, source: reviveChatModeSource(cachedMode.source) ?? { storage: PromptsStorage.local } }; const instance = new CustomChatMode(customChatMode); @@ -244,6 +245,7 @@ export interface IChatModeData { readonly source?: IChatModeSourceData; readonly target?: string; readonly infer?: boolean; + readonly agents?: readonly string[]; } export interface IChatMode { @@ -263,6 +265,7 @@ export interface IChatMode { readonly source?: IAgentSource; readonly target?: IObservable; readonly infer?: IObservable; + readonly agents?: IObservable; } export interface IVariableReference { @@ -294,7 +297,8 @@ function isCachedChatModeData(data: unknown): data is IChatModeData { (mode.uri === undefined || (typeof mode.uri === 'object' && mode.uri !== null)) && (mode.source === undefined || isChatModeSourceData(mode.source)) && (mode.target === undefined || typeof mode.target === 'string') && - (mode.infer === undefined || typeof mode.infer === 'boolean'); + (mode.infer === undefined || typeof mode.infer === 'boolean') && + (mode.agents === undefined || Array.isArray(mode.agents)); } export class CustomChatMode implements IChatMode { @@ -308,6 +312,7 @@ export class CustomChatMode implements IChatMode { private readonly _handoffsObservable: ISettableObservable; private readonly _targetObservable: ISettableObservable; private readonly _inferObservable: ISettableObservable; + private readonly _agentsObservable: ISettableObservable; private _source: IAgentSource; public readonly id: string; @@ -368,6 +373,10 @@ export class CustomChatMode implements IChatMode { return this._inferObservable; } + get agents(): IObservable { + return this._agentsObservable; + } + public readonly kind = ChatModeKind.Agent; constructor( @@ -382,6 +391,7 @@ export class CustomChatMode implements IChatMode { this._handoffsObservable = observableValue('handOffs', customChatMode.handOffs); this._targetObservable = observableValue('target', customChatMode.target); this._inferObservable = observableValue('infer', customChatMode.infer); + this._agentsObservable = observableValue('agents', customChatMode.agents); this._modeInstructions = observableValue('_modeInstructions', customChatMode.agentInstructions); this._uriObservable = observableValue('uri', customChatMode.uri); this._source = customChatMode.source; @@ -400,6 +410,7 @@ export class CustomChatMode implements IChatMode { this._handoffsObservable.set(newData.handOffs, tx); this._targetObservable.set(newData.target, tx); this._inferObservable.set(newData.infer, tx); + this._agentsObservable.set(newData.agents, tx); this._modeInstructions.set(newData.agentInstructions, tx); this._uriObservable.set(newData.uri, tx); this._source = newData.source; @@ -420,7 +431,8 @@ export class CustomChatMode implements IChatMode { handOffs: this.handOffs.get(), source: serializeChatModeSource(this._source), target: this.target.get(), - infer: this.infer.get() + infer: this.infer.get(), + agents: this.agents.get() }; } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index 01a40c2b5c6..0e2dd3ca720 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -17,14 +17,15 @@ import { ILogService } from '../../../../../platform/log/common/log.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'; -import { ILanguageModelToolsService, IToolAndToolSetEnablementMap, IToolData, VSCodeToolReference } from '../tools/languageModelToolsService.js'; +import { ILanguageModelToolsService, IToolData, VSCodeToolReference } from '../tools/languageModelToolsService.js'; import { PromptsConfig } from './config/config.js'; import { isPromptOrInstructionsFile } from './config/promptFileLocations.js'; import { PromptsType } from './promptTypes.js'; import { ParsedPromptFile } from './promptFileParser.js'; -import { IPromptPath, IPromptsService } from './service/promptsService.js'; +import { ICustomAgent, IPromptPath, IPromptsService } from './service/promptsService.js'; import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js'; import { ChatConfiguration } from '../constants.js'; +import { UserSelectedTools } from '../participants/chatAgents.js'; export type InstructionsCollectionEvent = { applyingInstructionsCount: number; @@ -52,7 +53,8 @@ export class ComputeAutomaticInstructions { private _parseResults: ResourceMap = new ResourceMap(); constructor( - private readonly _enabledTools: IToolAndToolSetEnablementMap | undefined, + private readonly _enabledTools: UserSelectedTools | undefined, + private readonly _enabledSubagents: (readonly string[]) | undefined, @IPromptsService private readonly _promptsService: IPromptsService, @ILogService public readonly _logService: ILogService, @ILabelService private readonly _labelService: ILabelService, @@ -241,7 +243,7 @@ export class ComputeAutomaticInstructions { return undefined; } const tool = this._languageModelToolsService.getToolByName(referenceName); - if (tool && this._enabledTools.get(tool)) { + if (tool && this._enabledTools[tool.id]) { return { tool, variable: `#tool:${this._languageModelToolsService.getFullReferenceName(tool)}` }; } return undefined; @@ -322,20 +324,23 @@ export class ComputeAutomaticInstructions { entries.push('', '', ''); // add trailing newline } } - if (runSubagentTool) { - const subagentToolCustomAgents = this._configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents); - if (subagentToolCustomAgents) { - const agents = await this._promptsService.getCustomAgents(token); - if (agents.length > 0) { - entries.push(''); - entries.push('Here is a list of agents that can be used when running a subagent.'); - entries.push('Each agent has optionally a description with the agent\'s purpose and expertise. When asked to run a subagent, choose the most appropriate agent from this list.'); - entries.push(`Use the ${runSubagentTool.variable} tool with the agent name to run the subagent.`); - for (const agent of agents) { - if (agent.infer === false) { - // skip agents that are not meant for subagent use - continue; - } + if (runSubagentTool && this._configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents)) { + const canUseAgent = (() => { + if (!this._enabledSubagents || this._enabledSubagents.includes('*')) { + return (agent: ICustomAgent) => (agent.infer !== false); + } else { + const subagents = this._enabledSubagents; + return (agent: ICustomAgent) => subagents.includes(agent.name); + } + })(); + const agents = await this._promptsService.getCustomAgents(token); + if (agents.length > 0) { + entries.push(''); + entries.push('Here is a list of agents that can be used when running a subagent.'); + entries.push('Each agent has optionally a description with the agent\'s purpose and expertise. When asked to run a subagent, choose the most appropriate agent from this list.'); + entries.push(`Use the ${runSubagentTool.variable} tool with the agent name to run the subagent.`); + for (const agent of agents) { + if (canUseAgent(agent)) { entries.push(''); entries.push(`${agent.name}`); if (agent.description) { @@ -346,8 +351,8 @@ export class ComputeAutomaticInstructions { } entries.push(''); } - entries.push('', '', ''); // add trailing newline } + entries.push('', '', ''); // add trailing newline } } if (entries.length === 0) { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts index 682dcd01ea2..5cabc3ec5ed 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -15,7 +15,7 @@ import { IChatModeService } from '../../chatModes.js'; import { getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js'; import { IPromptsService } from '../service/promptsService.js'; import { Iterable } from '../../../../../../base/common/iterator.js'; -import { PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; +import { IHeaderAttribute, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; import { getValidAttributeNames, isGithubTarget, knownGithubCopilotTools } from './promptValidator.js'; import { localize } from '../../../../../../nls.js'; @@ -147,30 +147,35 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { ): Promise { const suggestions: CompletionItem[] = []; - const lineContent = model.getLineContent(position.lineNumber); - const attribute = lineContent.substring(0, colonPosition.column - 1).trim(); + const attribute = header.attributes.find(attr => attr.range.containsPosition(position)); + if (!attribute) { + return undefined; + } const isGitHubTarget = isGithubTarget(promptType, header.target); - if (!getValidAttributeNames(promptType, true, isGitHubTarget).includes(attribute)) { + if (!getValidAttributeNames(promptType, true, isGitHubTarget).includes(attribute.key)) { return undefined; } if (promptType === PromptsType.prompt || promptType === PromptsType.agent) { - // if the position is inside the tools metadata, we provide tool name completions - const result = this.provideToolCompletions(model, position, header, isGitHubTarget); - if (result) { - return result; + if (attribute.key === PromptHeaderAttributes.tools) { + if (attribute.value.type === 'array') { + // if the position is inside the tools metadata, we provide tool name completions + const getValues = async () => isGitHubTarget ? knownGithubCopilotTools : Array.from(this.languageModelToolsService.getFullReferenceNames()); + return this.provideArrayCompletions(model, position, attribute, getValues); + } } } - - const bracketIndex = lineContent.indexOf('['); - if (bracketIndex !== -1 && bracketIndex <= position.column - 1) { - // if the value is already inside a bracket, we don't provide value completions - return undefined; + if (promptType === PromptsType.agent) { + if (attribute.key === PromptHeaderAttributes.agents && !isGitHubTarget) { + if (attribute.value.type === 'array') { + return this.provideArrayCompletions(model, position, attribute, async () => (await this.promptsService.getCustomAgents(CancellationToken.None)).map(agent => agent.name)); + } + } } - + const lineContent = model.getLineContent(attribute.range.startLineNumber); const whilespaceAfterColon = (lineContent.substring(colonPosition.column).match(/^\s*/)?.[0].length) ?? 0; - const values = this.getValueSuggestions(promptType, attribute); + const values = this.getValueSuggestions(promptType, attribute.key); for (const value of values) { const item: CompletionItem = { label: value, @@ -180,7 +185,7 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { }; suggestions.push(item); } - if (attribute === PromptHeaderAttributes.handOffs && (promptType === PromptsType.agent)) { + if (attribute.key === PromptHeaderAttributes.handOffs && (promptType === PromptsType.agent)) { const value = [ '', ' - label: Start Implementation', @@ -238,6 +243,11 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { return ['true', 'false']; } break; + case PromptHeaderAttributes.agents: + if (promptType === PromptsType.agent) { + return ['["*"]']; + } + break; } return []; } @@ -255,14 +265,13 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { return result; } - private provideToolCompletions(model: ITextModel, position: Position, header: PromptHeader, isGitHubTarget: boolean): CompletionList | undefined { - const toolsAttr = header.getAttribute(PromptHeaderAttributes.tools); - if (!toolsAttr || toolsAttr.value.type !== 'array' || !toolsAttr.range.containsPosition(position)) { + private async provideArrayCompletions(model: ITextModel, position: Position, agentsAttr: IHeaderAttribute, getValues: () => Promise): Promise { + if (agentsAttr.value.type !== 'array') { return undefined; } - const getSuggestions = (toolRange: Range) => { + const getSuggestions = async (toolRange: Range) => { const suggestions: CompletionItem[] = []; - const toolNames = isGitHubTarget ? knownGithubCopilotTools : this.languageModelToolsService.getFullReferenceNames(); + const toolNames = await getValues(); for (const toolName of toolNames) { let insertText: string; if (!toolRange.isEmpty()) { @@ -282,16 +291,16 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { return { suggestions }; }; - for (const toolNameNode of toolsAttr.value.items) { + for (const toolNameNode of agentsAttr.value.items) { if (toolNameNode.range.containsPosition(position)) { // if the position is inside a tool range, we provide tool name completions - return getSuggestions(toolNameNode.range); + return await getSuggestions(toolNameNode.range); } } const prefix = model.getValueInRange(new Range(position.lineNumber, 1, position.lineNumber, position.column)); if (prefix.match(/[,[]\s*$/)) { // if the position is after a comma or bracket - return getSuggestions(new Range(position.lineNumber, position.column, position.lineNumber, position.column)); + return await getSuggestions(new Range(position.lineNumber, position.column, position.lineNumber, position.column)); } return undefined; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts index 7cdfe0283fc..2de474fbf09 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts @@ -115,6 +115,8 @@ export class PromptHoverProvider implements HoverProvider { return this.createHover(localize('promptHeader.agent.target', 'The target to which the header attributes like tools apply to. Possible values are `github-copilot` and `vscode`.'), attribute.range); case PromptHeaderAttributes.infer: return this.createHover(localize('promptHeader.agent.infer', 'Whether the agent can be used as a subagent.'), attribute.range); + case PromptHeaderAttributes.agents: + return this.createHover(localize('promptHeader.agent.agents', 'One or more agents that this agent can use as subagents. Use \'*\' to specify all available agents.'), attribute.range); } } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index a98a73988f4..8e630cdaf91 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -16,7 +16,7 @@ import { ChatModeKind } from '../../constants.js'; import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js'; import { ILanguageModelToolsService, SpecedToolAliases } from '../../tools/languageModelToolsService.js'; import { getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js'; -import { GithubPromptHeaderAttributes, IArrayValue, IHeaderAttribute, IStringValue, ParsedPromptFile, PromptHeaderAttributes, Target } from '../promptFileParser.js'; +import { GithubPromptHeaderAttributes, IArrayValue, IHeaderAttribute, IStringValue, ParsedPromptFile, PromptHeader, PromptHeaderAttributes, Target } from '../promptFileParser.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { ResourceMap } from '../../../../../../base/common/map.js'; @@ -186,6 +186,7 @@ export class PromptValidator { if (!isGitHubTarget) { this.validateModel(attributes, ChatModeKind.Agent, report); this.validateHandoffs(attributes, report); + this.validateAgentsAttribute(attributes, header, report); } break; } @@ -532,12 +533,41 @@ export class PromptValidator { report(toMarker(localize('promptValidator.targetInvalidValue', "The 'target' attribute must be one of: {0}.", validTargets.join(', ')), attribute.value.range, MarkerSeverity.Error)); } } + + private validateAgentsAttribute(attributes: IHeaderAttribute[], header: PromptHeader, report: (markers: IMarkerData) => void): undefined { + const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.agents); + if (!attribute) { + return; + } + if (attribute.value.type !== 'array') { + report(toMarker(localize('promptValidator.agentsMustBeArray', "The 'agents' attribute must be an array."), attribute.value.range, MarkerSeverity.Error)); + return; + } + + // Check each item is a string + const agentNames: string[] = []; + for (const item of attribute.value.items) { + if (item.type !== 'string') { + report(toMarker(localize('promptValidator.eachAgentMustBeString', "Each agent name in the 'agents' attribute must be a string."), item.range, MarkerSeverity.Error)); + } else if (item.value) { + agentNames.push(item.value); + } + } + + // If not wildcard and not empty, check that 'agent' tool is available + if (agentNames.length > 0) { + const tools = header.tools; + if (tools && !tools.includes(SpecedToolAliases.agent)) { + report(toMarker(localize('promptValidator.agentsRequiresAgentTool', "When 'agents' and 'tools' are specified, the 'agent' tool must be included in the 'tools' attribute."), attribute.value.range, MarkerSeverity.Warning)); + } + } + } } const allAttributeNames = { [PromptsType.prompt]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.mode, PromptHeaderAttributes.agent, PromptHeaderAttributes.argumentHint], [PromptsType.instructions]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.applyTo, PromptHeaderAttributes.excludeAgent], - [PromptsType.agent]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target, PromptHeaderAttributes.infer], + [PromptsType.agent]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target, PromptHeaderAttributes.infer, PromptHeaderAttributes.agents], [PromptsType.skill]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.license, PromptHeaderAttributes.compatibility, PromptHeaderAttributes.metadata], }; const githubCopilotAgentAttributeNames = [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.tools, PromptHeaderAttributes.target, GithubPromptHeaderAttributes.mcpServers, PromptHeaderAttributes.infer]; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts index 33094047675..a68e3ddbcef 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts @@ -78,6 +78,7 @@ export namespace PromptHeaderAttributes { export const license = 'license'; export const compatibility = 'compatibility'; export const metadata = 'metadata'; + export const agents = 'agents'; } export namespace GithubPromptHeaderAttributes { @@ -275,6 +276,27 @@ export class PromptHeader { } return undefined; } + + private getStringArrayAttribute(key: string): string[] | undefined { + const attribute = this._parsedHeader.attributes.find(attr => attr.key === key); + if (!attribute) { + return undefined; + } + if (attribute.value.type === 'array') { + const result: string[] = []; + for (const item of attribute.value.items) { + if (item.type === 'string' && item.value) { + result.push(item.value); + } + } + return result; + } + return undefined; + } + + public get agents(): string[] | undefined { + return this.getStringArrayAttribute(PromptHeaderAttributes.agents); + } } export interface IHandOff { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index f84cc15db38..69735174644 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -178,6 +178,12 @@ export interface ICustomAgent { */ readonly handOffs?: readonly IHandOff[]; + /** + * List of subagent names that can be used by the agent. + * If empty, no subagents are available. If ['*'] or undefined, all agents can be used. + */ + readonly agents?: readonly string[]; + /** * Where the agent was loaded from. */ diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 83c2c30e2d5..d25cc1fd8b5 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -512,8 +512,8 @@ export class PromptsService extends Disposable implements IPromptsService { if (!ast.header) { return { uri, name, agentInstructions, source }; } - const { description, model, tools, handOffs, argumentHint, target, infer } = ast.header; - return { uri, name, description, model, tools, handOffs, argumentHint, target, infer, agentInstructions, source }; + const { description, model, tools, handOffs, argumentHint, target, infer, agents } = ast.header; + return { uri, name, description, model, tools, handOffs, argumentHint, target, infer, agents, agentInstructions, source }; }) ); diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts index 5ad017201b2..35f327b4cbc 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -14,7 +14,7 @@ import { localize } from '../../../../../../nls.js'; import { IConfigurationChangeEvent, IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; -import { IChatAgentRequest, IChatAgentService, UserSelectedTools } from '../../participants/chatAgents.js'; +import { IChatAgentRequest, IChatAgentService } from '../../participants/chatAgents.js'; import { ChatModel, IChatRequestModeInstructions } from '../../model/chatModel.js'; import { IChatModeService } from '../../chatModes.js'; import { IChatProgress, IChatService } from '../../chatService/chatService.js'; @@ -34,7 +34,6 @@ import { ToolProgress, ToolSet, VSCodeToolReference, - IToolAndToolSetEnablementMap } from '../languageModelToolsService.js'; import { ComputeAutomaticInstructions } from '../../promptSyntax/computeAutomaticInstructions.js'; import { ManageTodoListToolToolId } from './manageTodoListTool.js'; @@ -219,7 +218,9 @@ export class RunSubagentTool extends Disposable implements IToolImpl { modeTools[ManageTodoListToolToolId] = false; } - const variableSet = await this.collectVariables(modeTools, token); + const variableSet = new ChatRequestVariableSet(); + const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, modeTools, undefined); // agents can not call subagents + await computer.collect(variableSet, token); // Build the agent request const agentRequest: IChatAgentRequest = { @@ -278,26 +279,4 @@ export class RunSubagentTool extends Disposable implements IToolImpl { }, }; } - - private async collectVariables(modeTools: UserSelectedTools | undefined, token: CancellationToken): Promise { - let enabledTools: IToolAndToolSetEnablementMap | undefined; - - if (modeTools) { - // Convert tool IDs to full reference names - - const enabledToolIds = Object.entries(modeTools).filter(([, enabled]) => enabled).map(([id]) => id); - const tools = enabledToolIds.map(id => this.languageModelToolsService.getTool(id)).filter(tool => !!tool); - - const fullReferenceNames = tools.map(tool => this.languageModelToolsService.getFullReferenceName(tool)); - if (fullReferenceNames.length > 0) { - enabledTools = this.languageModelToolsService.toToolAndToolSetEnablementMap(fullReferenceNames, undefined); - } - } - - const variableSet = new ChatRequestVariableSet(); - const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, enabledTools); - await computer.collect(variableSet, token); - - return variableSet; - } } diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts index 11b67eb3bec..4e68b258c85 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts @@ -3,13 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Codicon } from '../../../../../../base/common/codicons.js'; import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; -import { ThemeIcon } from '../../../../../../base/common/themables.js'; -import { localize } from '../../../../../../nls.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContribution } from '../../../../../common/contributions.js'; -import { ILanguageModelToolsService, SpecedToolAliases, ToolDataSource } from '../languageModelToolsService.js'; +import { ILanguageModelToolsService } from '../languageModelToolsService.js'; import { ConfirmationTool, ConfirmationToolData } from './confirmationTool.js'; import { EditTool, EditToolData } from './editFileTool.js'; import { createManageTodoListToolData, ManageTodoListTool } from './manageTodoListTool.js'; @@ -37,10 +34,6 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo this._register(toolsService.registerTool(ConfirmationToolData, confirmationTool)); const runSubagentTool = this._register(instantiationService.createInstance(RunSubagentTool)); - const customAgentToolSet = this._register(toolsService.createToolSet(ToolDataSource.Internal, 'custom-agent', SpecedToolAliases.agent, { - icon: ThemeIcon.fromId(Codicon.agent.id), - description: localize('toolset.custom-agent', 'Delegate tasks to other agents'), - })); let runSubagentRegistration: IDisposable | undefined; let toolSetRegistration: IDisposable | undefined; @@ -49,7 +42,7 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo toolSetRegistration?.dispose(); const runSubagentToolData = runSubagentTool.getToolData(); runSubagentRegistration = toolsService.registerTool(runSubagentToolData, runSubagentTool); - toolSetRegistration = customAgentToolSet.addTool(runSubagentToolData); + toolSetRegistration = toolsService.agentToolSet.addTool(runSubagentToolData); }; registerRunSubagentTool(); this._register(runSubagentTool.onDidUpdateToolData(registerRunSubagentTool)); diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts index d8f88d8d802..7d7e0c86fd9 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts @@ -388,6 +388,7 @@ export interface ILanguageModelToolsService { readonly vscodeToolSet: ToolSet; readonly executeToolSet: ToolSet; readonly readToolSet: ToolSet; + readonly agentToolSet: ToolSet; readonly onDidChangeTools: Event; readonly onDidPrepareToolCallBecomeUnresponsive: Event<{ readonly sessionId: string; readonly toolData: IToolData }>; registerToolData(toolData: IToolData): IDisposable; diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptBodyAutocompletion.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptBodyAutocompletion.test.ts index 144c48c01fe..4cc72c0b22b 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptBodyAutocompletion.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptBodyAutocompletion.test.ts @@ -158,6 +158,10 @@ suite('PromptBodyAutocompletion', () => { label: 'read', result: 'Use #tool:read to reference a tool.' }, + { + label: 'agent', + result: 'Use #tool:agent to reference a tool.' + }, { label: 'tool1', result: 'Use #tool:tool1 to reference a tool.' diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts new file mode 100644 index 00000000000..9ed6626107a --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts @@ -0,0 +1,288 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { Position } from '../../../../../../../editor/common/core/position.js'; +import { CompletionContext, CompletionTriggerKind } from '../../../../../../../editor/common/languages.js'; +import { ContextKeyService } from '../../../../../../../platform/contextkey/browser/contextKeyService.js'; +import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { ExtensionIdentifier } from '../../../../../../../platform/extensions/common/extensions.js'; +import { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; +import { LanguageModelToolsService } from '../../../../browser/tools/languageModelToolsService.js'; +import { ChatAgentLocation, ChatConfiguration } from '../../../../common/constants.js'; +import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../../common/tools/languageModelToolsService.js'; +import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../../../common/languageModels.js'; +import { IChatModeService } from '../../../../common/chatModes.js'; +import { PromptHeaderAutocompletion } from '../../../../common/promptSyntax/languageProviders/promptHeaderAutocompletion.js'; +import { ICustomAgent, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; +import { createTextModel } from '../../../../../../../editor/test/common/testTextModel.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { PromptFileParser } from '../../../../common/promptSyntax/promptFileParser.js'; +import { ITextModel } from '../../../../../../../editor/common/model.js'; +import { getLanguageIdForPromptsType, PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; +import { getPromptFileExtension } from '../../../../common/promptSyntax/config/promptFileLocations.js'; +import { Range } from '../../../../../../../editor/common/core/range.js'; + +suite('PromptHeaderAutocompletion', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instaService: TestInstantiationService; + let completionProvider: PromptHeaderAutocompletion; + + setup(async () => { + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); + instaService = workbenchInstantiationService({ + contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, disposables); + + const toolService = disposables.add(instaService.createInstance(LanguageModelToolsService)); + + const testTool1 = { id: 'testTool1', displayName: 'tool1', canBeReferencedInPrompt: true, modelDescription: 'Test Tool 1', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; + disposables.add(toolService.registerToolData(testTool1)); + + const testTool2 = { id: 'testTool2', displayName: 'tool2', canBeReferencedInPrompt: true, toolReferenceName: 'tool2', modelDescription: 'Test Tool 2', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; + disposables.add(toolService.registerToolData(testTool2)); + + instaService.set(ILanguageModelToolsService, toolService); + + const testModels: ILanguageModelChatMetadata[] = [ + { id: 'mae-4', name: 'MAE 4', vendor: 'olama', version: '1.0', family: 'mae', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: true, toolCalling: true }, isDefaultForLocation: { [ChatAgentLocation.Chat]: true } } satisfies ILanguageModelChatMetadata, + { id: 'mae-4.1', name: 'MAE 4.1', vendor: 'copilot', version: '1.0', family: 'mae', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: true, toolCalling: true }, isDefaultForLocation: { [ChatAgentLocation.Chat]: true } } satisfies ILanguageModelChatMetadata, + { id: 'gpt-4', name: 'GPT 4', vendor: 'openai', version: '1.0', family: 'gpt', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: false, toolCalling: true }, isDefaultForLocation: { [ChatAgentLocation.Chat]: true } } satisfies ILanguageModelChatMetadata, + ]; + + instaService.stub(ILanguageModelsService, { + getLanguageModelIds() { return testModels.map(m => m.id); }, + lookupLanguageModel(name: string) { + return testModels.find(m => m.id === name); + } + }); + + const customAgent: ICustomAgent = { + name: 'agent1', + description: 'Agent file 1.', + agentInstructions: { + content: '', + toolReferences: [], + metadata: undefined + }, + uri: URI.parse('myFs://.github/agents/agent1.agent.md'), + source: { storage: PromptsStorage.local } + }; + + const parser = new PromptFileParser(); + instaService.stub(IPromptsService, { + getParsedPromptFile(model: ITextModel) { + return parser.parse(model.uri, model.getValue()); + }, + async getCustomAgents(token: CancellationToken) { + return Promise.resolve([customAgent]); + } + }); + + instaService.stub(IChatModeService, { + getModes() { + return { builtin: [], custom: [] }; + } + }); + + completionProvider = instaService.createInstance(PromptHeaderAutocompletion); + }); + + async function getCompletions(content: string, promptType: PromptsType) { + const languageId = getLanguageIdForPromptsType(promptType); + const uri = URI.parse('test:///test' + getPromptFileExtension(promptType)); + const model = disposables.add(createTextModel(content, languageId, undefined, uri)); + // get the completion location fro the '|' marker + const lineColumnMarkerRange = model.findNextMatch('|', new Position(1, 1), false, false, '', false)?.range; + assert.ok(lineColumnMarkerRange, 'No completion marker found in test content'); + model.applyEdits([{ range: lineColumnMarkerRange, text: '' }]); + + const position = lineColumnMarkerRange.getStartPosition(); + const context: CompletionContext = { triggerKind: CompletionTriggerKind.Invoke }; + const result = await completionProvider.provideCompletionItems(model, position, context, CancellationToken.None); + if (!result || !result.suggestions) { + return []; + } + const lineContent = model.getLineContent(position.lineNumber); + return result.suggestions.map(s => { + assert(s.range instanceof Range); + return { + label: typeof s.label === 'string' ? s.label : s.label.label, + result: lineContent.substring(0, s.range.startColumn - 1) + s.insertText + lineContent.substring(s.range.endColumn - 1) + }; + }); + } + + const sortByLabel = (a: { label: string }, b: { label: string }) => a.label.localeCompare(b.label); + + suite('agent header completions', () => { + test('complete model attribute name', async () => { + const content = [ + '---', + 'description: "Test"', + '|', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'agents', result: 'agents: ${0:["*"]}' }, + { label: 'argument-hint', result: 'argument-hint: $0' }, + { label: 'handoffs', result: 'handoffs: $0' }, + { label: 'infer', result: 'infer: ${0:true}' }, + { label: 'model', result: 'model: ${0:MAE 4 (olama)}' }, + { label: 'name', result: 'name: $0' }, + { label: 'target', result: 'target: ${0:vscode}' }, + { label: 'tools', result: 'tools: ${0:[]}' }, + ].sort(sortByLabel)); + }); + + test('complete model attribute value', async () => { + const content = [ + '---', + 'description: "Test"', + 'model: |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + // GPT 4 is excluded because it has agentMode: false + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'MAE 4 (olama)', result: 'model: MAE 4 (olama)' }, + { label: 'MAE 4.1 (copilot)', result: 'model: MAE 4.1 (copilot)' }, + ].sort(sortByLabel)); + }); + + test('complete model attribute value with partial input', async () => { + const content = [ + '---', + 'description: "Test"', + 'model: MA|', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + // GPT 4 is excluded because it has agentMode: false + assert.deepStrictEqual(actual, [ + { label: 'MAE 4 (olama)', result: 'model: MAE 4 (olama)' }, + { label: 'MAE 4.1 (copilot)', result: 'model: MAE 4.1 (copilot)' }, + ]); + }); + + test('complete tool names inside tools array', async () => { + const content = [ + '---', + 'description: "Test"', + 'tools: [|]', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'agent', result: `tools: ['agent']` }, + { label: 'execute', result: `tools: ['execute']` }, + { label: 'read', result: `tools: ['read']` }, + { label: 'tool1', result: `tools: ['tool1']` }, + { label: 'tool2', result: `tools: ['tool2']` }, + { label: 'vscode', result: `tools: ['vscode']` }, + ].sort(sortByLabel)); + }); + + test('complete tool names inside tools array with existing entries', async () => { + const content = [ + '---', + 'description: "Test"', + `tools: ['read', |]`, + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'agent', result: `tools: ['read', 'agent']` }, + { label: 'execute', result: `tools: ['read', 'execute']` }, + { label: 'read', result: `tools: ['read', 'read']` }, + { label: 'tool1', result: `tools: ['read', 'tool1']` }, + { label: 'tool2', result: `tools: ['read', 'tool2']` }, + { label: 'vscode', result: `tools: ['read', 'vscode']` }, + ].sort(sortByLabel)); + }); + + test('complete tool names inside tools array with existing entries 2', async () => { + const content = [ + '---', + 'description: "Test"', + `tools: ['read', 'exe|cute']`, + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'agent', result: `tools: ['read', 'agent']` }, + { label: 'execute', result: `tools: ['read', 'execute']` }, + { label: 'read', result: `tools: ['read', 'read']` }, + { label: 'tool1', result: `tools: ['read', 'tool1']` }, + { label: 'tool2', result: `tools: ['read', 'tool2']` }, + { label: 'vscode', result: `tools: ['read', 'vscode']` }, + ].sort(sortByLabel)); + }); + + test('complete agents inside agents array', async () => { + const content = [ + '---', + 'description: "Test"', + 'agents: [|]', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'agent1', result: `agents: ['agent1']` }, + ].sort(sortByLabel)); + }); + }); + + suite('prompt header completions', () => { + test('complete model attribute name', async () => { + const content = [ + '---', + 'description: "Test"', + '|', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.prompt); + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'agent', result: 'agent: $0' }, + { label: 'argument-hint', result: 'argument-hint: $0' }, + { label: 'model', result: 'model: ${0:MAE 4 (olama)}' }, + { label: 'name', result: 'name: $0' }, + { label: 'tools', result: 'tools: ${0:[]}' }, + ].sort(sortByLabel)); + }); + + test('complete model attribute value in prompt', async () => { + const content = [ + '---', + 'description: "Test"', + 'model: |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.prompt); + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'MAE 4 (olama)', result: 'model: MAE 4 (olama)' }, + { label: 'MAE 4.1 (copilot)', result: 'model: MAE 4.1 (copilot)' }, + { label: 'GPT 4 (openai)', result: 'model: GPT 4 (openai)' }, + ].sort(sortByLabel)); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts index 926ed459185..e1a5a2d29f8 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts @@ -274,6 +274,18 @@ suite('PromptHoverProvider', () => { const hover = await getHover(content, 4, 1, PromptsType.agent); assert.strictEqual(hover, 'Whether the agent can be used as a subagent.'); }); + + test('hover on agents attribute shows description', async () => { + const content = [ + '---', + 'name: "Test Agent"', + 'description: "Test agent"', + 'agents: ["*"]', + '---', + ].join('\n'); + const hover = await getHover(content, 4, 1, PromptsType.agent); + assert.strictEqual(hover, 'One or more agents that this agent can use as subagents. Use \'*\' to specify all available agents.'); + }); }); suite('prompt hovers', () => { diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts index bc0d4dded99..c2af631b9a8 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts @@ -402,7 +402,7 @@ suite('PromptValidator', () => { assert.deepStrictEqual( markers.map(m => ({ severity: m.severity, message: m.message })), [ - { severity: MarkerSeverity.Warning, message: `Attribute 'applyTo' is not supported in VS Code agent files. Supported: argument-hint, description, handoffs, infer, model, name, target, tools.` }, + { severity: MarkerSeverity.Warning, message: `Attribute 'applyTo' is not supported in VS Code agent files. Supported: agents, argument-hint, description, handoffs, infer, model, name, target, tools.` }, ] ); }); @@ -810,6 +810,87 @@ suite('PromptValidator', () => { assert.deepStrictEqual(markers, [], 'Missing infer attribute should be allowed'); } }); + + test('agents attribute must be an array', async () => { + const content = [ + '---', + 'description: "Test"', + `agents: 'myAgent'`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers.map(m => m.message), [`The 'agents' attribute must be an array.`]); + }); + + test('each agent name in agents attribute must be a string', async () => { + const content = [ + '---', + 'description: "Test"', + `agents: ['valid', 123]`, + `tools: ['agent']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers.map(m => m.message), [`Each agent name in the 'agents' attribute must be a string.`]); + }); + + test('agents attribute with non-empty value requires agent tool 1', async () => { + const content = [ + '---', + 'description: "Test"', + `agents: ['Planning', 'Research']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers.map(m => m.message), [], `No warnings about agents attribute when no tools are specified`); + }); + + test('agents attribute with non-empty value requires agent tool 2', async () => { + const content = [ + '---', + 'description: "Test"', + `agents: ['Planning', 'Research']`, + `tools: ['shell']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers.map(m => m.message), [`When 'agents' and 'tools' are specified, the 'agent' tool must be included in the 'tools' attribute.`]); + }); + + test('agents attribute with non-empty value requires agent tool 3', async () => { + const content = [ + '---', + 'description: "Test"', + `agents: ['Planning', 'Research']`, + `tools: ['agent']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers.map(m => m.message), [], `No warnings about agents attribute when agent tool is in header`); + }); + + test('agents attribute with non-empty value requires agent tool 4', async () => { + const content = [ + '---', + 'description: "Test"', + `agents: ['*']`, + `tools: ['shell']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers.map(m => m.message), [`When 'agents' and 'tools' are specified, the 'agent' tool must be included in the 'tools' attribute.`]); + }); + + test('agents attribute with empty array does not require agent tool', async () => { + const content = [ + '---', + 'description: "Test"', + `agents: []`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, [], 'Empty array should not require agent tool'); + }); }); suite('instructions', () => { diff --git a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts index 8e2751e2fe0..5232ad7deeb 100644 --- a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts @@ -543,7 +543,8 @@ suite('LanguageModelToolsService', () => { 'internalToolSetRefName/internalToolSetTool1RefName', 'vscode', 'execute', - 'read' + 'read', + 'agent' ]; const numOfTools = allFullReferenceNames.length + 1; // +1 for userToolSet which has no full reference name but is a tool set @@ -558,6 +559,7 @@ suite('LanguageModelToolsService', () => { const vscodeToolSet = service.getToolSet('vscode'); const executeToolSet = service.getToolSet('execute'); const readToolSet = service.getToolSet('read'); + const agentToolSet = service.getToolSet('agent'); assert.ok(tool1); assert.ok(tool2); assert.ok(extTool1); @@ -569,6 +571,7 @@ suite('LanguageModelToolsService', () => { assert.ok(vscodeToolSet); assert.ok(executeToolSet); assert.ok(readToolSet); + assert.ok(agentToolSet); // Test with enabled tool { const fullReferenceNames = ['tool1RefName']; @@ -599,10 +602,10 @@ suite('LanguageModelToolsService', () => { { const result1 = service.toToolAndToolSetEnablementMap(allFullReferenceNames, undefined); assert.strictEqual(result1.size, numOfTools, `Expected ${numOfTools} tools and tool sets`); - assert.strictEqual([...result1.entries()].filter(([_, enabled]) => enabled).length, 11, 'Expected 11 tools to be enabled'); // +3 including the vscode, execute, read toolsets + assert.strictEqual([...result1.entries()].filter(([_, enabled]) => enabled).length, 12, 'Expected 12 tools to be enabled'); // +4 including the vscode, execute, read, agent toolsets const fullReferenceNames1 = service.toFullReferenceNames(result1); - const expectedFullReferenceNames = ['tool1RefName', 'Tool2 Display Name', 'my.extension/extTool1RefName', 'mcpToolSetRefName/*', 'internalToolSetRefName', 'vscode', 'execute', 'read']; + const expectedFullReferenceNames = ['tool1RefName', 'Tool2 Display Name', 'my.extension/extTool1RefName', 'mcpToolSetRefName/*', 'internalToolSetRefName', 'vscode', 'execute', 'read', 'agent']; assert.deepStrictEqual(fullReferenceNames1.sort(), expectedFullReferenceNames.sort(), 'toFullReferenceNames should return the original enabled names'); } // Test with no enabled tools @@ -1005,14 +1008,7 @@ suite('LanguageModelToolsService', () => { }; store.add(service.registerToolData(runSubagentToolData)); - - const agentSet = store.add(service.createToolSet( - ToolDataSource.Internal, - SpecedToolAliases.agent, - SpecedToolAliases.agent, - { description: 'Agent' } - )); - store.add(agentSet.addTool(runSubagentToolData)); + store.add(service.agentToolSet.addTool(runSubagentToolData)); const githubMcpDataSource: ToolDataSource = { type: 'mcp', label: 'Github', serverLabel: 'Github MCP Server', instructions: undefined, collectionId: 'githubMCPCollection', definitionId: 'githubMCPDefId' }; const githubMcpTool1: IToolData = { @@ -1067,12 +1063,12 @@ suite('LanguageModelToolsService', () => { const result = service.toToolAndToolSetEnablementMap(toolNames, undefined); assert.strictEqual(result.get(service.executeToolSet), true, 'execute should be enabled'); - assert.strictEqual(result.get(agentSet), true, 'agent should be enabled'); + assert.strictEqual(result.get(service.agentToolSet), true, 'agent should be enabled'); const fullReferenceNames = service.toFullReferenceNames(result).sort(); assert.deepStrictEqual(fullReferenceNames, [SpecedToolAliases.agent, SpecedToolAliases.execute].sort(), 'toFullReferenceNames should return the VS Code tool names'); - assert.deepStrictEqual(toolNames.map(name => service.getToolByFullReferenceName(name)), [agentSet, service.executeToolSet]); + assert.deepStrictEqual(toolNames.map(name => service.getToolByFullReferenceName(name)), [service.agentToolSet, service.executeToolSet]); assert.deepStrictEqual(deprecatesTo('custom-agent'), [SpecedToolAliases.agent], 'customAgent should map to agent'); assert.deepStrictEqual(deprecatesTo('shell'), [SpecedToolAliases.execute], 'shell is now execute'); @@ -2092,7 +2088,8 @@ suite('LanguageModelToolsService', () => { 'internalToolSetRefName/internalToolSetTool1RefName', 'vscode', 'execute', - 'read' + 'read', + 'agent' ].sort(); assert.deepStrictEqual(fullReferenceNames, expectedNames, 'getFullReferenceNames should return correct full reference names'); diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatSelectedTools.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatSelectedTools.test.ts index 3462416b317..60df438df90 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatSelectedTools.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatSelectedTools.test.ts @@ -102,7 +102,7 @@ suite('ChatSelectedTools', () => { await timeout(1000); // UGLY the tools service updates its state sync but emits the event async (750ms) delay. This affects the observable that depends on the event - assert.strictEqual(selectedTools.entriesMap.get().size, 7); // 1 toolset (+3 vscode, execute, read toolsets), 3 tools + assert.strictEqual(selectedTools.entriesMap.get().size, 8); // 1 toolset (+4 vscode, execute, read, agent toolsets), 3 tools const toSet = new Map([[toolData1, true], [toolData2, false], [toolData3, false], [toolset, false]]); selectedTools.set(toSet, false); @@ -166,7 +166,7 @@ suite('ChatSelectedTools', () => { await timeout(1000); // UGLY the tools service updates its state sync but emits the event async (750ms) delay. This affects the observable that depends on the event - assert.strictEqual(selectedTools.entriesMap.get().size, 7); // 1 toolset (+3 vscode, execute, read toolsets), 3 tools + assert.strictEqual(selectedTools.entriesMap.get().size, 8); // 1 toolset (+4 vscode, execute, read, agent toolsets), 3 tools // Toolset is checked, tools 2 and 3 are unchecked const toSet = new Map([[toolData1, true], [toolData2, false], [toolData3, false], [toolset, true]]); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/newPromptsParser.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/newPromptsParser.test.ts index cb5fed7747b..b4326f32c2d 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/newPromptsParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/newPromptsParser.test.ts @@ -308,4 +308,68 @@ suite('NewPromptsParser', () => { assert.ok(result.header.tools); assert.deepEqual(result.header.tools, ['built-in', 'browser-click', 'openPullRequest', 'copilotCodingAgent']); }); + + test('agent with agents', async () => { + const uri = URI.parse('file:///test/test.agent.md'); + const content = [ + '---', + `description: "Agent with restrictions"`, + 'agents: ["subagent1", "subagent2"]', + '---', + 'This is an agent with restricted subagents.', + ].join('\n'); + const result = new PromptFileParser().parse(uri, content); + assert.deepEqual(result.uri, uri); + assert.ok(result.header); + assert.ok(result.body); + assert.deepEqual(result.header.description, 'Agent with restrictions'); + assert.deepEqual(result.header.agents, ['subagent1', 'subagent2']); + }); + + test('agent with empty agents array', async () => { + const uri = URI.parse('file:///test/test.agent.md'); + const content = [ + '---', + `description: "Agent with no access"`, + 'agents: []', + '---', + 'This agent has no access to subagents.', + ].join('\n'); + const result = new PromptFileParser().parse(uri, content); + assert.deepEqual(result.uri, uri); + assert.ok(result.header); + assert.deepEqual(result.header.description, 'Agent with no access'); + assert.deepEqual(result.header.agents, []); + }); + + test('agent with wildcard agents', async () => { + const uri = URI.parse('file:///test/test.agent.md'); + const content = [ + '---', + `description: "Agent with full access"`, + 'agents: ["*"]', + '---', + 'This agent has access to all subagents.', + ].join('\n'); + const result = new PromptFileParser().parse(uri, content); + assert.deepEqual(result.uri, uri); + assert.ok(result.header); + assert.deepEqual(result.header.description, 'Agent with full access'); + assert.deepEqual(result.header.agents, ['*']); + }); + + test('agent without agents (undefined)', async () => { + const uri = URI.parse('file:///test/test.agent.md'); + const content = [ + '---', + `description: "Agent without restrictions"`, + '---', + 'This agent has default access to all.', + ].join('\n'); + const result = new PromptFileParser().parse(uri, content); + assert.deepEqual(result.uri, uri); + assert.ok(result.header); + assert.deepEqual(result.header.description, 'Agent without restrictions'); + assert.deepEqual(result.header.agents, undefined); + }); }); 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 dd32cdedb95..0bb295c18de 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 @@ -460,7 +460,7 @@ suite('PromptsService', () => { ]); const instructionFiles = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); - const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, undefined); + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, undefined, undefined); const context = { files: new ResourceSet([ URI.joinPath(rootFolderUri, 'folder1/main.tsx'), @@ -472,7 +472,7 @@ suite('PromptsService', () => { await contextComputer.addApplyingInstructions(instructionFiles, context, result, newInstructionsCollectionEvent(), CancellationToken.None); assert.deepStrictEqual( - result.asArray().map(i => isPromptFileVariableEntry(i) ? i.value.path : undefined), + result.asArray().map(i => isPromptFileVariableEntry(i) ? i.value.path : undefined, undefined), [ // local instructions URI.joinPath(rootFolderUri, '.github/prompts/file1.instructions.md').path, @@ -631,7 +631,7 @@ suite('PromptsService', () => { ]); const instructionFiles = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); - const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, undefined); + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, undefined, undefined); const context = { files: new ResourceSet([ URI.joinPath(rootFolderUri, 'folder1/main.tsx'), @@ -705,7 +705,7 @@ suite('PromptsService', () => { ]); - const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, undefined); + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, undefined, undefined); const context = new ChatRequestVariableSet(); context.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'README.md'))); @@ -765,6 +765,7 @@ suite('PromptsService', () => { tools: undefined, target: undefined, infer: undefined, + agents: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'), source: { storage: PromptsStorage.local } }, @@ -820,6 +821,7 @@ suite('PromptsService', () => { argumentHint: undefined, target: undefined, infer: undefined, + agents: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'), source: { storage: PromptsStorage.local }, }, @@ -892,6 +894,7 @@ suite('PromptsService', () => { model: undefined, target: undefined, infer: undefined, + agents: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'), source: { storage: PromptsStorage.local } }, @@ -909,6 +912,7 @@ suite('PromptsService', () => { tools: undefined, target: undefined, infer: undefined, + agents: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent2.agent.md'), source: { storage: PromptsStorage.local } }, @@ -978,6 +982,7 @@ suite('PromptsService', () => { model: undefined, argumentHint: undefined, infer: undefined, + agents: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/github-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -995,6 +1000,7 @@ suite('PromptsService', () => { argumentHint: undefined, tools: undefined, infer: undefined, + agents: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/vscode-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1012,6 +1018,7 @@ suite('PromptsService', () => { tools: undefined, target: undefined, infer: undefined, + agents: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/generic-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1066,6 +1073,7 @@ suite('PromptsService', () => { argumentHint: undefined, target: undefined, infer: undefined, + agents: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/demonstrate.md'), source: { storage: PromptsStorage.local }, }, @@ -1088,6 +1096,112 @@ suite('PromptsService', () => { ); }); + test('header with agents', async () => { + const rootFolderName = 'custom-agents-with-restrictions'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/agents/restricted-agent.agent.md`, + contents: [ + '---', + 'description: \'Agent with restricted access.\'', + 'agents: [ subagent1, subagent2 ]', + 'tools: [ tool1 ]', + '---', + 'This agent has restricted access.', + ] + }, + { + path: `${rootFolder}/.github/agents/no-access-agent.agent.md`, + contents: [ + '---', + 'description: \'Agent with no access to subagents, skills, or instructions.\'', + 'agents: []', + '---', + 'This agent has no access.', + ] + }, + { + path: `${rootFolder}/.github/agents/full-access-agent.agent.md`, + contents: [ + '---', + 'description: \'Agent with full access.\'', + 'agents: [ "*" ]', + '---', + 'This agent has full access.', + ] + } + ]); + + const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + const expected: ICustomAgent[] = [ + { + name: 'restricted-agent', + description: 'Agent with restricted access.', + agents: ['subagent1', 'subagent2'], + tools: ['tool1'], + agentInstructions: { + content: 'This agent has restricted access.', + toolReferences: [], + metadata: undefined + }, + handOffs: undefined, + model: undefined, + argumentHint: undefined, + target: undefined, + infer: undefined, + uri: URI.joinPath(rootFolderUri, '.github/agents/restricted-agent.agent.md'), + source: { storage: PromptsStorage.local } + }, + { + name: 'no-access-agent', + description: 'Agent with no access to subagents, skills, or instructions.', + agents: [], + agentInstructions: { + content: 'This agent has no access.', + toolReferences: [], + metadata: undefined + }, + handOffs: undefined, + model: undefined, + argumentHint: undefined, + tools: undefined, + target: undefined, + infer: undefined, + uri: URI.joinPath(rootFolderUri, '.github/agents/no-access-agent.agent.md'), + source: { storage: PromptsStorage.local } + }, + { + name: 'full-access-agent', + description: 'Agent with full access.', + agents: ['*'], + agentInstructions: { + content: 'This agent has full access.', + toolReferences: [], + metadata: undefined + }, + handOffs: undefined, + model: undefined, + argumentHint: undefined, + tools: undefined, + target: undefined, + infer: undefined, + uri: URI.joinPath(rootFolderUri, '.github/agents/full-access-agent.agent.md'), + source: { storage: PromptsStorage.local } + }, + ]; + + assert.deepEqual( + result, + expected, + 'Must get custom agents with agents, skills, and instructions attributes.', + ); + }); + test('agents from user data folder', async () => { const rootFolderName = 'custom-agents-user-data'; const rootFolder = `/${rootFolderName}`; diff --git a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts index 1a185e1bc97..7ee177ea731 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts @@ -20,6 +20,7 @@ export class MockLanguageModelToolsService implements ILanguageModelToolsService vscodeToolSet: ToolSet = new ToolSet('vscode', 'vscode', ThemeIcon.fromId(Codicon.code.id), ToolDataSource.Internal); executeToolSet: ToolSet = new ToolSet('execute', 'execute', ThemeIcon.fromId(Codicon.terminal.id), ToolDataSource.Internal); readToolSet: ToolSet = new ToolSet('read', 'read', ThemeIcon.fromId(Codicon.book.id), ToolDataSource.Internal); + agentToolSet: ToolSet = new ToolSet('agent', 'agent', ThemeIcon.fromId(Codicon.agent.id), ToolDataSource.Internal); constructor() { } From 38e15e93c72c24e6123e530e2daa70c3cb32f3bd Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 19 Jan 2026 15:59:15 +0100 Subject: [PATCH 195/387] chat - indicate running session in view badge (#283051) (#288905) --- .../widgetHosts/viewPane/chatViewPane.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 7fd857ffb22..7f8a33d4f16 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -56,6 +56,7 @@ import { IWorkbenchLayoutService, LayoutSettings, Position } from '../../../../. import { AgentSessionsViewerOrientation, AgentSessionsViewerPosition } from '../../agentSessions/agentSessions.js'; import { IProgressService } from '../../../../../../platform/progress/common/progress.js'; import { ChatViewId } from '../../chat.js'; +import { IActivityService, ProgressBadge } from '../../../../../services/activity/common/activity.js'; import { disposableTimeout } from '../../../../../../base/common/async.js'; import { AgentSessionsFilter } from '../../agentSessions/agentSessionsFilter.js'; import { IAgentSessionsService } from '../../agentSessions/agentSessionsService.js'; @@ -90,6 +91,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private restoringSession: Promise | undefined; private readonly modelRef = this._register(new MutableDisposable()); + private readonly activityBadge = this._register(new MutableDisposable()); + constructor( options: IViewPaneOptions, @IKeybindingService keybindingService: IKeybindingService, @@ -113,6 +116,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, @ICommandService private readonly commandService: ICommandService, + @IActivityService private readonly activityService: IActivityService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -656,6 +660,29 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this.relayout(); } })); + + // Show progress badge when the current session is in progress + const progressBadgeDisposables = this._register(new MutableDisposable()); + const updateProgressBadge = () => { + progressBadgeDisposables.value = new DisposableStore(); + + const model = chatWidget.viewModel?.model; + if (model) { + progressBadgeDisposables.value.add(autorun(reader => { + if (model.requestInProgress.read(reader)) { + this.activityBadge.value = this.activityService.showViewActivity(this.id, { + badge: new ProgressBadge(() => localize('sessionInProgress', "Agent Session in Progress")) + }); + } else { + this.activityBadge.clear(); + } + })); + } else { + this.activityBadge.clear(); + } + }; + this._register(chatWidget.onDidChangeViewModel(() => updateProgressBadge())); + updateProgressBadge(); } private setupContextMenu(parent: HTMLElement): void { From 2882d8f7826aee99e10ed04ccd630c3a5535e723 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 19 Jan 2026 16:22:06 +0100 Subject: [PATCH 196/387] Engineering - release the build that is being triggered at 19:00 (#288903) * Engineering - release the build that is being triggered at 19:00 * Add second schedule * Revert variable --- build/azure-pipelines/product-build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index 516b3b4fffd..d035a13106e 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -6,6 +6,11 @@ schedules: branches: include: - main + - cron: "0 17 * * Mon-Fri" + displayName: Mon-Fri at 19:00 + branches: + include: + - main trigger: batch: true From 05e7063883ec8eb5a82b15cca7c1c16420c6da72 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 19 Jan 2026 16:38:20 +0100 Subject: [PATCH 197/387] fix review comments (#288913) --- .../languageProviders/promptHeaderAutocompletion.test.ts | 2 +- .../test/common/promptSyntax/service/promptsService.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts index 9ed6626107a..1db2b8d62f3 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts @@ -100,7 +100,7 @@ suite('PromptHeaderAutocompletion', () => { const languageId = getLanguageIdForPromptsType(promptType); const uri = URI.parse('test:///test' + getPromptFileExtension(promptType)); const model = disposables.add(createTextModel(content, languageId, undefined, uri)); - // get the completion location fro the '|' marker + // get the completion location from the '|' marker const lineColumnMarkerRange = model.findNextMatch('|', new Position(1, 1), false, false, '', false)?.range; assert.ok(lineColumnMarkerRange, 'No completion marker found in test content'); model.applyEdits([{ range: lineColumnMarkerRange, text: '' }]); 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 0bb295c18de..0217396a826 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 @@ -472,7 +472,7 @@ suite('PromptsService', () => { await contextComputer.addApplyingInstructions(instructionFiles, context, result, newInstructionsCollectionEvent(), CancellationToken.None); assert.deepStrictEqual( - result.asArray().map(i => isPromptFileVariableEntry(i) ? i.value.path : undefined, undefined), + result.asArray().map(i => isPromptFileVariableEntry(i) ? i.value.path : undefined), [ // local instructions URI.joinPath(rootFolderUri, '.github/prompts/file1.instructions.md').path, From b9acc34d1b96104a374447c0cc1e6d52f2e3ffe3 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 19 Jan 2026 16:39:09 +0100 Subject: [PATCH 198/387] Simplify: clipboardData is always empty when `copy` is triggered --- .../browser/copyPasteController.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index be555c84b9a..16a757fd150 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -25,7 +25,7 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; import { ClipboardEventUtils, IClipboardCopyEvent, IClipboardPasteEvent, InMemoryClipboardMetadataManager } from '../../../browser/controller/editContext/clipboardUtils.js'; -import { toExternalVSDataTransfer, toVSDataTransfer } from '../../../browser/dataTransfer.js'; +import { toExternalVSDataTransfer } from '../../../browser/dataTransfer.js'; import { ICodeEditor, PastePayload } from '../../../browser/editorBrowser.js'; import { IBulkEditService } from '../../../browser/services/bulkEditService.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; @@ -171,15 +171,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi private handleCopy(e: IClipboardCopyEvent) { const clipboardData = e.browserEvent?.clipboardData; - let id: string | null = null; - if (clipboardData) { - const [text, metadata] = ClipboardEventUtils.getTextData(clipboardData); - const storedMetadata = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text); - id = storedMetadata?.id || null; - this._logService.trace('CopyPasteController#handleCopy for id : ', id, ' with text.length : ', text.length); - } else { - this._logService.trace('CopyPasteController#handleCopy'); - } + this._logService.trace('CopyPasteController#handleCopy'); if (!this._editor.hasTextFocus()) { return; } @@ -228,11 +220,11 @@ export class CopyPasteController extends Disposable implements IEditorContributi return; } - const dataTransfer = toVSDataTransfer(clipboardData); + const dataTransfer = new VSDataTransfer(); const providerCopyMimeTypes = providers.flatMap(x => x.copyMimeTypes ?? []); // Save off a handle pointing to data that VS Code maintains. - const handle = id ?? generateUuid(); + const handle = generateUuid(); this.setCopyMetadata(clipboardData, { id: handle, providerCopyMimeTypes, From ed0d4c4cbf79a8bc6cdfe489929a176e8adecbd1 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 19 Jan 2026 16:47:05 +0100 Subject: [PATCH 199/387] Avoid exposing the browser event --- .../controller/editContext/clipboardUtils.ts | 7 ------- .../dropOrPasteInto/browser/copyPasteController.ts | 13 ++++++------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/vs/editor/browser/controller/editContext/clipboardUtils.ts b/src/vs/editor/browser/controller/editContext/clipboardUtils.ts index 334e62e1385..313966b6828 100644 --- a/src/vs/editor/browser/controller/editContext/clipboardUtils.ts +++ b/src/vs/editor/browser/controller/editContext/clipboardUtils.ts @@ -230,12 +230,6 @@ export interface IClipboardCopyEvent { */ readonly clipboardData: IWritableClipboardData; - /** - * The underlying DOM event, if available. - * @deprecated Use clipboardData instead. This is provided for backward compatibility. - */ - readonly browserEvent: ClipboardEvent | undefined; - /** * Signal that the event has been handled and default processing should be skipped. */ @@ -318,7 +312,6 @@ export function createClipboardCopyEvent(e: ClipboardEvent, isCut: boolean): ICl getData: () => '', setData: () => { }, }, - browserEvent: e, setHandled: () => { handled = true; e.preventDefault(); diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index 16a757fd150..f5c8fb37aee 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -24,7 +24,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; -import { ClipboardEventUtils, IClipboardCopyEvent, IClipboardPasteEvent, InMemoryClipboardMetadataManager } from '../../../browser/controller/editContext/clipboardUtils.js'; +import { ClipboardEventUtils, IClipboardCopyEvent, IClipboardPasteEvent, InMemoryClipboardMetadataManager, IWritableClipboardData } from '../../../browser/controller/editContext/clipboardUtils.js'; import { toExternalVSDataTransfer } from '../../../browser/dataTransfer.js'; import { ICodeEditor, PastePayload } from '../../../browser/editorBrowser.js'; import { IBulkEditService } from '../../../browser/services/bulkEditService.js'; @@ -170,7 +170,6 @@ export class CopyPasteController extends Disposable implements IEditorContributi } private handleCopy(e: IClipboardCopyEvent) { - const clipboardData = e.browserEvent?.clipboardData; this._logService.trace('CopyPasteController#handleCopy'); if (!this._editor.hasTextFocus()) { return; @@ -181,7 +180,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi // This means the resources clipboard is not properly updated when copying from the editor. this._clipboardService.clearInternalState?.(); - if (!clipboardData || !this.isPasteAsEnabled()) { + if (!this.isPasteAsEnabled()) { return; } @@ -216,7 +215,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi .ordered(model) .filter(x => !!x.prepareDocumentPaste); if (!providers.length) { - this.setCopyMetadata(clipboardData, { defaultPastePayload }); + this.setCopyMetadata(e.clipboardData, { defaultPastePayload }); return; } @@ -225,7 +224,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi // Save off a handle pointing to data that VS Code maintains. const handle = generateUuid(); - this.setCopyMetadata(clipboardData, { + this.setCopyMetadata(e.clipboardData, { id: handle, providerCopyMimeTypes, defaultPastePayload @@ -547,9 +546,9 @@ export class CopyPasteController extends Disposable implements IEditorContributi }, () => p); } - private setCopyMetadata(dataTransfer: DataTransfer, metadata: CopyMetadata) { + private setCopyMetadata(clipboardData: IWritableClipboardData, metadata: CopyMetadata) { this._logService.trace('CopyPasteController#setCopyMetadata new id : ', metadata.id); - dataTransfer.setData(vscodeClipboardMime, JSON.stringify(metadata)); + clipboardData.setData(vscodeClipboardMime, JSON.stringify(metadata)); } private fetchCopyMetadata(clipboardData: DataTransfer): CopyMetadata | undefined { From 28fb9be1ddde1f82b5e1939f4a89d4b3296fddde Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Mon, 19 Jan 2026 15:55:02 +0000 Subject: [PATCH 200/387] Enhance ChatContextUsageWidget with additional token tracking and improved hover display styling --- .../widget/input/chatContextUsageWidget.ts | 61 ++++++++++++++----- .../input/media/chatContextUsageWidget.css | 15 +++++ 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts index 45bae917399..15fe5e4c7a6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts @@ -29,12 +29,13 @@ export class ChatContextUsageWidget extends Disposable { // Stats private _totalTokenCount = 0; + private _systemTokenCount = 0; private _promptsTokenCount = 0; private _filesTokenCount = 0; private _imagesTokenCount = 0; private _selectionTokenCount = 0; private _toolsTokenCount = 0; - private _otherTokenCount = 0; + private _workspaceTokenCount = 0; private _maxTokenCount = 4096; // Default fallback private _usagePercent = 0; @@ -173,7 +174,7 @@ export class ChatContextUsageWidget extends Disposable { let i = 0; let s = 0; let t = 0; - let o = 0; + let w = 0; // Prompts: User message const messageText = typeof request.message === 'string' ? request.message : request.message.text; @@ -191,7 +192,7 @@ export class ChatContextUsageWidget extends Disposable { } else if (variable.kind === 'implicit' && variable.isSelection) { s += defaultEstimate; } else { - o += defaultEstimate; + w += defaultEstimate; } } } @@ -208,15 +209,23 @@ export class ChatContextUsageWidget extends Disposable { } } - return { p, f, i, s, t, o }; + return { p, f, i, s, t, w }; })); + + const lastRequest = requests[requests.length - 1]; + if (lastRequest.modeInfo?.modeInstructions) { + this._systemTokenCount = await countTokens(lastRequest.modeInfo.modeInstructions.content); + } else { + this._systemTokenCount = 0; + } + this._promptsTokenCount = 0; this._filesTokenCount = 0; this._imagesTokenCount = 0; this._selectionTokenCount = 0; this._toolsTokenCount = 0; - this._otherTokenCount = 0; + this._workspaceTokenCount = 0; for (const count of requestCounts) { this._promptsTokenCount += count.p; @@ -224,10 +233,10 @@ export class ChatContextUsageWidget extends Disposable { this._imagesTokenCount += count.i; this._selectionTokenCount += count.s; this._toolsTokenCount += count.t; - this._otherTokenCount += count.o; + this._workspaceTokenCount += count.w; } - this._totalTokenCount = Math.round(this._promptsTokenCount + this._filesTokenCount + this._imagesTokenCount + this._selectionTokenCount + this._toolsTokenCount + this._otherTokenCount); + this._totalTokenCount = Math.round(this._systemTokenCount + this._promptsTokenCount + this._filesTokenCount + this._imagesTokenCount + this._selectionTokenCount + this._toolsTokenCount + this._workspaceTokenCount); this._usagePercent = Math.min(100, (this._totalTokenCount / this._maxTokenCount) * 100); this._updateRing(); @@ -276,12 +285,13 @@ export class ChatContextUsageWidget extends Disposable { } }; - updateItem('prompts', this._promptsTokenCount); - updateItem('files', this._filesTokenCount); + updateItem('system', this._systemTokenCount); + updateItem('messages', this._promptsTokenCount); + updateItem('attachedFiles', this._filesTokenCount); updateItem('images', this._imagesTokenCount); updateItem('selection', this._selectionTokenCount); - updateItem('tools', this._toolsTokenCount); - updateItem('other', this._otherTokenCount); + updateItem('systemTools', this._toolsTokenCount); + updateItem('workspace', this._workspaceTokenCount); } private _getHoverDomNode(): HTMLElement { @@ -329,12 +339,33 @@ export class ChatContextUsageWidget extends Disposable { this._hoverItemValues.set(key, valueSpan); }; - addItem('prompts', localize('prompts', "Prompts"), Math.round(this._promptsTokenCount)); - addItem('files', localize('files', "Files"), Math.round(this._filesTokenCount)); + const addTitle = (label: string) => { + dom.append(list, $('.chat-context-usage-hover-title', undefined, label)); + }; + + const addSeparator = () => { + dom.append(list, $('.chat-context-usage-hover-separator')); + }; + + // Group 1: System + addTitle(localize('systemGroup', "System")); + addItem('system', localize('system', "System prompt"), Math.round(this._systemTokenCount)); + addItem('systemTools', localize('systemTools', "System tools"), Math.round(this._toolsTokenCount)); + + addSeparator(); + + // Group 2: Messages + addTitle(localize('messagesGroup', "Conversation")); + addItem('messages', localize('messages', "Messages"), Math.round(this._promptsTokenCount)); + + addSeparator(); + + // Group 3: Data / Context + addTitle(localize('dataGroup', "Context")); + addItem('attachedFiles', localize('attachedFiles', "Attached files"), Math.round(this._filesTokenCount)); addItem('images', localize('images', "Images"), Math.round(this._imagesTokenCount)); addItem('selection', localize('selection', "Selection"), Math.round(this._selectionTokenCount)); - addItem('tools', localize('tools', "Tools"), Math.round(this._toolsTokenCount)); - addItem('other', localize('other', "Other"), Math.round(this._otherTokenCount)); + addItem('workspace', localize('workspace', "Workspace"), Math.round(this._workspaceTokenCount)); if (this._usagePercent > 75) { const warning = dom.append(container, $('.chat-context-usage-warning')); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css index 7fa3f5198a9..61b00693ca7 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css +++ b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatContextUsageWidget.css @@ -146,3 +146,18 @@ .chat-context-usage-action-link:hover { text-decoration: underline; } + +.chat-context-usage-hover-separator { + height: 1px; + background-color: var(--vscode-menu-separatorBackground); /* Use menu separator color */ + margin: 4px 0; + width: 100%; + opacity: 0.5; +} + +.chat-context-usage-hover-title { + font-weight: 600; + margin-top: 4px; + margin-bottom: 4px; + color: var(--vscode-descriptionForeground); +} From d331d50a2cbdca3ae697e54390426247b6cdd549 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Mon, 19 Jan 2026 16:59:04 +0100 Subject: [PATCH 201/387] See more -> Show pull request (#288916) Part of #288848 --- .../widget/chatContentParts/chatPullRequestContentPart.ts | 2 +- .../widget/chatContentParts/media/chatPullRequestContent.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatPullRequestContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatPullRequestContentPart.ts index 045b9005e30..200c92b9789 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatPullRequestContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatPullRequestContentPart.ts @@ -47,7 +47,7 @@ export class ChatPullRequestContentPart extends Disposable implements IChatConte const seeMoreContainer = dom.append(descriptionElement, dom.$('.see-more')); const seeMore: HTMLAnchorElement = dom.append(seeMoreContainer, dom.$('a')); - seeMore.textContent = localize('chatPullRequest.seeMore', 'See more'); + seeMore.textContent = localize('chatPullRequest.seeMore', 'Show pull request'); this._register(addDisposableListener(seeMore, 'click', (e) => { e.preventDefault(); e.stopPropagation(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatPullRequestContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatPullRequestContent.css index b408ddb7db8..96b734023bd 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatPullRequestContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatPullRequestContent.css @@ -60,7 +60,7 @@ .description-wrapper { /* This mask fades out the end of text so the "see more" message can be displayed over it. */ mask-image: - linear-gradient(to right, rgba(0, 0, 0, 1) calc(100% - 7em), rgba(0, 0, 0, 0) calc(100% - 4.5em)), + linear-gradient(to right, rgba(0, 0, 0, 1) calc(100% - 7em - 50px), rgba(0, 0, 0, 0) calc(100% - 4.5em - 50px)), linear-gradient(to top, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 1.2em, rgba(0, 0, 0, 1) 0.15em, rgba(0, 0, 0, 1) 100%); mask-repeat: no-repeat, no-repeat; pointer-events: none; From 19bd827e45b0ec5ebd4d170e45430ef4c0351af2 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Mon, 19 Jan 2026 16:02:10 +0000 Subject: [PATCH 202/387] fix(chat): adjust position of state indicator in chat CSS --- src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index f4ba328b3b4..54593f6da84 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1629,7 +1629,7 @@ have to be updated for changes to the rules above, or to support more deeply nes .chat-mcp-state-indicator { position: absolute; bottom: 0; - right: 0; + left: 12px; font-size: 12px !important; background: var(--vscode-input-background); width: fit-content; From 28d64353ca972add1eaebb1fdbad8cb273d73f4e Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Mon, 19 Jan 2026 16:21:46 +0000 Subject: [PATCH 203/387] fix padding in chat scroll down button in interactive list --- src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index f4ba328b3b4..005858a76e2 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -30,6 +30,10 @@ display: none !important; } +.interactive-list > .chat-scroll-down { + padding: 4px; +} + .interactive-item-container { padding: 12px 16px; display: flex; From 388d1b9887b39e7fd59f356f8dac7fd39b24664d Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Mon, 19 Jan 2026 17:27:36 +0100 Subject: [PATCH 204/387] add workbench.action.chat.newLocalChat command --- .../chat/browser/actions/chatNewActions.ts | 152 ++++++++++++------ .../browser/chatEditing/chatEditingActions.ts | 4 +- 2 files changed, 103 insertions(+), 53 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts index d662fd086eb..648d7fd1329 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts @@ -24,12 +24,12 @@ import { localChatSessionType } from '../../common/chatSessionsService.js'; import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js'; import { getChatSessionType, LocalChatSessionUri } from '../../common/model/chatUri.js'; import { ChatViewId, IChatWidgetService, isIChatViewViewContext } from '../chat.js'; -import { EditingSessionAction, getEditingSessionContext } from '../chatEditing/chatEditingActions.js'; +import { EditingSessionAction, EditingSessionActionContext, getEditingSessionContext } from '../chatEditing/chatEditingActions.js'; import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js'; import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; import { ACTION_ID_NEW_CHAT, ACTION_ID_NEW_EDIT_SESSION, CHAT_CATEGORY, handleCurrentEditingSession } from './chatActions.js'; import { clearChatEditor } from './chatClear.js'; -import { AgentSessionsViewerOrientation } from '../agentSessions/agentSessions.js'; +import { AgentSessionProviders, AgentSessionsViewerOrientation } from '../agentSessions/agentSessions.js'; export interface INewEditSessionActionContext { @@ -51,6 +51,23 @@ export interface INewEditSessionActionContext { isPartialQuery?: boolean; } +function isNewEditSessionActionContext(arg: unknown): arg is INewEditSessionActionContext { + if (arg && typeof arg === 'object') { + const obj = arg as Record; + if (obj.inputValue !== undefined && typeof obj.inputValue !== 'string') { + return false; + } + if (obj.agentMode !== undefined && typeof obj.agentMode !== 'boolean') { + return false; + } + if (obj.isPartialQuery !== undefined && typeof obj.isPartialQuery !== 'boolean') { + return false; + } + return true; + } + return false; +} + export function registerNewChatActions() { // Add "New Chat" submenu to Chat view menu @@ -119,64 +136,36 @@ export function registerNewChatActions() { } async run(accessor: ServicesAccessor, ...args: unknown[]) { - const accessibilityService = accessor.get(IAccessibilityService); - const viewsService = accessor.get(IViewsService); - - const executeCommandContext = args[0] as INewEditSessionActionContext | undefined; + const executeCommandContext = isNewEditSessionActionContext(args[0]) ? args[0] : undefined; // Context from toolbar or lastFocusedWidget const context = getEditingSessionContext(accessor, args); - const { editingSession, chatWidget: widget } = context ?? {}; - if (!widget) { - return; - } + await runNewChatAction(accessor, context, executeCommandContext); + } + } + ); + CommandsRegistry.registerCommandAlias(ACTION_ID_NEW_EDIT_SESSION, ACTION_ID_NEW_CHAT); - const dialogService = accessor.get(IDialogService); + registerAction2(class NewLocalChatAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.newLocalChat', + title: localize2('chat.newLocalChat.label', "New Local Chat"), + category: CHAT_CATEGORY, + icon: Codicon.plus, + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat)), + f1: false, + }); + } - const model = widget.viewModel?.model; - if (model && !(await handleCurrentEditingSession(model, undefined, dialogService))) { - return; - } + async run(accessor: ServicesAccessor, ...args: unknown[]) { + const executeCommandContext = isNewEditSessionActionContext(args[0]) ? args[0] : undefined; - await editingSession?.stop(); - - // Create a new session with the same type as the current session - const currentResource = widget.viewModel?.model.sessionResource; - const sessionType = currentResource ? getChatSessionType(currentResource) : localChatSessionType; - if (isIChatViewViewContext(widget.viewContext) && sessionType !== localChatSessionType) { - // For the sidebar, we need to explicitly load a session with the same type - const newResource = getResourceForNewChatSession(sessionType); - const view = await viewsService.openView(ChatViewId) as ChatViewPane; - await view.loadSession(newResource); - } else { - // For the editor, widget.clear() already preserves the session type via clearChatEditor - await widget.clear(); - } - - widget.attachmentModel.clear(true); - widget.input.relatedFiles?.clear(); - widget.focusInput(); - - accessibilityService.alert(localize('newChat', "New chat")); - - if (!executeCommandContext) { - return; - } - - if (typeof executeCommandContext.agentMode === 'boolean') { - widget.input.setChatMode(executeCommandContext.agentMode ? ChatModeKind.Agent : ChatModeKind.Edit); - } - - if (executeCommandContext.inputValue) { - if (executeCommandContext.isPartialQuery) { - widget.setInput(executeCommandContext.inputValue); - } else { - widget.acceptInput(executeCommandContext.inputValue); - } - } + // Context from toolbar or lastFocusedWidget + const context = getEditingSessionContext(accessor, args); + await runNewChatAction(accessor, context, executeCommandContext, AgentSessionProviders.Local); } }); - CommandsRegistry.registerCommandAlias(ACTION_ID_NEW_EDIT_SESSION, ACTION_ID_NEW_CHAT); MenuRegistry.appendMenuItem(MenuId.ChatViewSessionTitleNavigationToolbar, { command: { @@ -296,3 +285,62 @@ function getResourceForNewChatSession(sessionType: string): URI { return LocalChatSessionUri.forSession(generateUuid()); } + +async function runNewChatAction( + accessor: ServicesAccessor, + context: EditingSessionActionContext | undefined, + executeCommandContext?: INewEditSessionActionContext, + sessionType?: AgentSessionProviders +) { + const accessibilityService = accessor.get(IAccessibilityService); + const viewsService = accessor.get(IViewsService); + + const { editingSession, chatWidget: widget } = context ?? {}; + if (!widget) { + return; + } + + const dialogService = accessor.get(IDialogService); + + const model = widget.viewModel?.model; + if (model && !(await handleCurrentEditingSession(model, undefined, dialogService))) { + return; + } + + await editingSession?.stop(); + + // Create a new session with the same type as the current session + const currentResource = widget.viewModel?.model.sessionResource; + const newSessionType = sessionType ?? (currentResource ? getChatSessionType(currentResource) : localChatSessionType); + if (isIChatViewViewContext(widget.viewContext) && newSessionType !== localChatSessionType) { + // For the sidebar, we need to explicitly load a session with the same type + const newResource = getResourceForNewChatSession(newSessionType); + const view = await viewsService.openView(ChatViewId) as ChatViewPane; + await view.loadSession(newResource); + } else { + // For the editor, widget.clear() already preserves the session type via clearChatEditor + await widget.clear(); + } + + widget.attachmentModel.clear(true); + widget.input.relatedFiles?.clear(); + widget.focusInput(); + + accessibilityService.alert(localize('newChat', "New chat")); + + if (!executeCommandContext) { + return; + } + + if (typeof executeCommandContext.agentMode === 'boolean') { + widget.input.setChatMode(executeCommandContext.agentMode ? ChatModeKind.Agent : ChatModeKind.Edit); + } + + if (executeCommandContext.inputValue) { + if (executeCommandContext.isPartialQuery) { + widget.setInput(executeCommandContext.inputValue); + } else { + widget.acceptInput(executeCommandContext.inputValue); + } + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index a2cde761222..30404affd13 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -58,11 +58,13 @@ export abstract class EditingSessionAction extends Action2 { abstract runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]): any; } +export type EditingSessionActionContext = { editingSession?: IChatEditingSession; chatWidget: IChatWidget }; + /** * Resolve view title toolbar context. If none, return context from the lastFocusedWidget. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function getEditingSessionContext(accessor: ServicesAccessor, args: any[]): { editingSession?: IChatEditingSession; chatWidget: IChatWidget } | undefined { +export function getEditingSessionContext(accessor: ServicesAccessor, args: any[]): EditingSessionActionContext | undefined { const arg0 = args.at(0); const context = isChatViewTitleActionContext(arg0) ? arg0 : undefined; From 96ea02102394589bd83a2d58784c9315e069be55 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:40:33 +0100 Subject: [PATCH 205/387] Support grouping of selections (#288924) support groupping of selections --- src/vs/base/browser/ui/list/list.ts | 4 + src/vs/base/browser/ui/list/listWidget.ts | 79 +++++++++++++++++-- src/vs/base/browser/ui/tree/abstractTree.ts | 5 +- src/vs/base/browser/ui/tree/asyncDataTree.ts | 5 +- .../agentSessions/agentSessionsViewer.ts | 9 ++- 5 files changed, 94 insertions(+), 8 deletions(-) diff --git a/src/vs/base/browser/ui/list/list.ts b/src/vs/base/browser/ui/list/list.ts index c3085ba6ee2..3e7cb017967 100644 --- a/src/vs/base/browser/ui/list/list.ts +++ b/src/vs/base/browser/ui/list/list.ts @@ -73,8 +73,12 @@ export interface IListContextMenuEvent { readonly anchor: HTMLElement | IMouseEvent; } +export const NotSelectableGroupId = 'notSelectable'; +export type NotSelectableGroupIdType = typeof NotSelectableGroupId; + export interface IIdentityProvider { getId(element: T): { toString(): string }; + getGroupId?(element: T): number | NotSelectableGroupIdType; } export interface IKeyboardNavigationLabelProvider { diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index c637ba43c88..4b191b9dd83 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -27,7 +27,7 @@ import { ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js' import { ISpliceable } from '../../../common/sequence.js'; import { isNumber } from '../../../common/types.js'; import './list.css'; -import { IIdentityProvider, IKeyboardNavigationDelegate, IKeyboardNavigationLabelProvider, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IListEvent, IListGestureEvent, IListMouseEvent, IListElementRenderDetails, IListRenderer, IListTouchEvent, IListVirtualDelegate, ListError } from './list.js'; +import { IIdentityProvider, IKeyboardNavigationDelegate, IKeyboardNavigationLabelProvider, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IListEvent, IListGestureEvent, IListMouseEvent, IListElementRenderDetails, IListRenderer, IListTouchEvent, IListVirtualDelegate, ListError, NotSelectableGroupId, NotSelectableGroupIdType } from './list.js'; import { IListView, IListViewAccessibilityProvider, IListViewDragAndDrop, IListViewOptions, IListViewOptionsUpdate, ListViewTargetSector, ListView } from './listView.js'; import { IMouseWheelEvent, StandardMouseEvent } from '../../mouseEvent.js'; import { autorun, constObservable, IObservable } from '../../../common/observable.js'; @@ -403,7 +403,17 @@ class KeyboardController implements IDisposable { private onCtrlA(e: StandardKeyboardEvent): void { e.preventDefault(); e.stopPropagation(); - this.list.setSelection(range(this.list.length), e.browserEvent); + + let selection = range(this.list.length); + + // Filter by group if identity provider has getGroupId + const focusedElements = this.list.getFocus(); + const referenceGroupId = focusedElements.length > 0 ? this.list.getElementGroupId(focusedElements[0]) : undefined; + if (referenceGroupId !== undefined) { + selection = this.list.filterIndicesByGroup(selection, referenceGroupId); + } + + this.list.setSelection(selection, e.browserEvent); this.list.setAnchor(undefined); this.view.domNode.focus(); } @@ -777,7 +787,11 @@ export class MouseController implements IDisposable { this.list.setAnchor(focus); if (!isMouseRightClick(e.browserEvent)) { - this.list.setSelection([focus], e.browserEvent); + // Check if the element is selectable (getGroupId must not return undefined) + const focusGroupId = this.list.getElementGroupId(focus); + if (focusGroupId !== NotSelectableGroupId) { + this.list.setSelection([focus], e.browserEvent); + } } this._onPointer.fire(e); @@ -814,7 +828,16 @@ export class MouseController implements IDisposable { const min = Math.min(anchor, focus); const max = Math.max(anchor, focus); - const rangeSelection = range(min, max + 1); + let rangeSelection = range(min, max + 1); + + const selectedElement = this.list.getSelection()[0]; + if (selectedElement !== undefined) { + const referenceGroupId = this.list.getElementGroupId(selectedElement); + if (referenceGroupId !== undefined) { + rangeSelection = this.list.filterIndicesByGroup(rangeSelection, referenceGroupId); + } + } + const selection = this.list.getSelection(); const contiguousRange = getContiguousRangeContaining(disjunction(selection, [anchor]), anchor); @@ -833,8 +856,16 @@ export class MouseController implements IDisposable { this.list.setFocus([focus]); this.list.setAnchor(focus); + const focusGroupId = this.list.getElementGroupId(focus); + if (focusGroupId === NotSelectableGroupId) { + return; // Cannot select this element, do nothing + } + if (selection.length === newSelection.length) { - this.list.setSelection([...newSelection, focus], e.browserEvent); + const itemsToBeSelected = focusGroupId !== undefined ? + this.list.filterIndicesByGroup([...newSelection, focus], focusGroupId) + : [...newSelection, focus]; + this.list.setSelection(itemsToBeSelected, e.browserEvent); } else { this.list.setSelection(newSelection, e.browserEvent); } @@ -1698,6 +1729,8 @@ export class List implements ISpliceable, IDisposable { } } + indexes = indexes.filter(i => this.getElementGroupId(i) !== NotSelectableGroupId); + this.selection.set(indexes, browserEvent); } @@ -1731,6 +1764,42 @@ export class List implements ISpliceable, IDisposable { return typeof anchor === 'undefined' ? undefined : this.element(anchor); } + /** + * Gets the group ID for an element at the given index. + * Returns undefined if no identity provider, no getGroupId method, or if the group ID is undefined. + */ + getElementGroupId(index: number): number | NotSelectableGroupIdType | undefined { + const identityProvider = this.options.identityProvider; + if (!identityProvider?.getGroupId) { + return undefined; + } + + const element = this.element(index); + return identityProvider.getGroupId(element); + } + + /** + * Filters the given indices to only include those with a matching group ID. + * If no identity provider or getGroupId method exists, returns the original indices. + * If referenceGroupId is undefined, returns an empty array (elements without group IDs are not selectable). + */ + filterIndicesByGroup(indices: number[], referenceGroupId: number | NotSelectableGroupIdType): number[] { + const identityProvider = this.options.identityProvider; + if (!identityProvider?.getGroupId) { + return indices; + } + + if (referenceGroupId === NotSelectableGroupId) { + return []; + } + + return indices.filter(index => { + const element = this.element(index); + const groupId = identityProvider.getGroupId!(element); + return groupId === referenceGroupId; + }); + } + setFocus(indexes: number[], browserEvent?: UIEvent): void { for (const index of indexes) { if (index < 0 || index >= this.length) { diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 10c3bbfd304..1b37c2218f9 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -162,7 +162,10 @@ function asListOptions(modelProvider: () => ITreeModel { + return options.identityProvider!.getGroupId!(el.element); + } : undefined }, dnd: options.dnd && disposableStore.add(new TreeNodeListDragAndDrop(modelProvider, options.dnd)), multipleSelectionController: options.multipleSelectionController && { diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 6112f2bfba3..75031f72f9f 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -424,7 +424,10 @@ function asObjectTreeOptions(options?: IAsyncDataTreeOpt identityProvider: options.identityProvider && { getId(el) { return options.identityProvider!.getId(el.element as T); - } + }, + getGroupId: options.identityProvider!.getGroupId ? (el) => { + return options.identityProvider!.getGroupId!(el.element as T); + } : undefined }, dnd: options.dnd && new AsyncDataTreeNodeListDragAndDrop(options.dnd), multipleSelectionController: options.multipleSelectionController && { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index c7a9442143b..f508b27f714 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -6,7 +6,7 @@ import './media/agentsessionsviewer.css'; import { h } from '../../../../../base/browser/dom.js'; import { localize } from '../../../../../nls.js'; -import { IIdentityProvider, IListVirtualDelegate } from '../../../../../base/browser/ui/list/list.js'; +import { IIdentityProvider, IListVirtualDelegate, NotSelectableGroupId, NotSelectableGroupIdType } from '../../../../../base/browser/ui/list/list.js'; import { IListAccessibilityProvider } from '../../../../../base/browser/ui/list/listWidget.js'; import { ITreeCompressionDelegate } from '../../../../../base/browser/ui/tree/asyncDataTree.js'; import { ICompressedTreeNode } from '../../../../../base/browser/ui/tree/compressedObjectTreeModel.js'; @@ -764,6 +764,13 @@ export class AgentSessionsIdentityProvider implements IIdentityProvider { From dbc626f1939ed8657f8b93aa4750252d88480a1e Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Mon, 19 Jan 2026 17:28:51 +0100 Subject: [PATCH 206/387] Add power data --- .../electron-main/nativeHostMainService.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index f29c5416306..eb9f6511851 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -122,15 +122,31 @@ export class NativeHostMainService extends Disposable implements INativeHostMain this.onDidResumeOS = Event.fromNodeEventEmitter(powerMonitor, 'resume'); // Telemetry for power events + type PowerEvent = { + readonly idleState: string; + readonly idleTime: number; + readonly thermalState: string; + readonly onBattery: boolean; + }; type PowerEventClassification = { + idleState: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The system idle state (active, idle, locked, unknown).' }; + idleTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The system idle time in seconds.' }; + thermalState: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The system thermal state (unknown, nominal, fair, serious, critical).' }; + onBattery: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether the system is running on battery power.' }; owner: 'chrmarti'; comment: 'Tracks OS power suspend and resume events for reliability insights.'; }; + const getPowerEventData = (): PowerEvent => ({ + idleState: powerMonitor.getSystemIdleState(60), + idleTime: powerMonitor.getSystemIdleTime(), + thermalState: powerMonitor.getCurrentThermalState(), + onBattery: powerMonitor.isOnBatteryPower() + }); this._register(Event.fromNodeEventEmitter(powerMonitor, 'suspend')(() => { - this.telemetryService.publicLog2<{}, PowerEventClassification>('power.suspend', {}); + this.telemetryService.publicLog2('power.suspend', getPowerEventData()); })); this._register(Event.fromNodeEventEmitter(powerMonitor, 'resume')(() => { - this.telemetryService.publicLog2<{}, PowerEventClassification>('power.resume', {}); + this.telemetryService.publicLog2('power.resume', getPowerEventData()); })); this.onDidChangeColorScheme = this.themeMainService.onDidChangeColorScheme; From 72b7fd0e9bcebc65679336cda00e991a8b0c7f98 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Mon, 19 Jan 2026 18:09:48 +0100 Subject: [PATCH 207/387] fixes https://github.com/microsoft/vscode/issues/288339 --- src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 84d3d5c069a..82b4a558555 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1629,6 +1629,7 @@ have to be updated for changes to the rules above, or to support more deeply nes } .action-item.chat-mcp { + min-width: 22px !important; .chat-mcp-state-indicator { position: absolute; @@ -1639,6 +1640,7 @@ have to be updated for changes to the rules above, or to support more deeply nes width: fit-content; height: fit-content; border-radius: 100%; + pointer-events: none; &.chat-mcp-state-new { color: var(--vscode-button-foreground); From 0f750bc55276851400b74d1816d8912f60f1ce5f Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 19 Jan 2026 18:17:15 +0100 Subject: [PATCH 208/387] clarify interactive and notebook instructions (#288849) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * clarify interactive and notebook instructions * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: João Moreno Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/instructions/interactive.instructions.md | 2 +- .github/instructions/notebook.instructions.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/instructions/interactive.instructions.md b/.github/instructions/interactive.instructions.md index d6867257e66..7336e967c3b 100644 --- a/.github/instructions/interactive.instructions.md +++ b/.github/instructions/interactive.instructions.md @@ -1,5 +1,5 @@ --- -description: Architecture documentation for VS Code interactive window component. Use when working in folder +description: Architecture documentation for VS Code interactive window component. Use when working in `src/vs/workbench/contrib/interactive` --- # Interactive Window diff --git a/.github/instructions/notebook.instructions.md b/.github/instructions/notebook.instructions.md index 890b0c20db2..cb351d69b44 100644 --- a/.github/instructions/notebook.instructions.md +++ b/.github/instructions/notebook.instructions.md @@ -1,5 +1,5 @@ --- -description: Architecture documentation for VS Code notebook and interactive window components +description: Architecture documentation for VS Code notebook and interactive window components. Use when working in `src/vs/workbench/contrib/notebook/` --- # Notebook Architecture From 51403976449c0844df2d20163c0e5804eda964ab Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Mon, 19 Jan 2026 09:22:12 -0800 Subject: [PATCH 209/387] Don't auto-focus browser on navigation (#288495) --- .../browserView/electron-main/browserView.ts | 22 +++++++++++-------- .../electron-browser/browserEditor.ts | 10 ++++++--- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/vs/platform/browserView/electron-main/browserView.ts b/src/vs/platform/browserView/electron-main/browserView.ts index b511c59081e..0468d5b82e1 100644 --- a/src/vs/platform/browserView/electron-main/browserView.ts +++ b/src/vs/platform/browserView/electron-main/browserView.ts @@ -77,15 +77,19 @@ export class BrowserView extends Disposable { ) { super(); - this._view = new WebContentsView({ - webPreferences: { - nodeIntegration: false, - contextIsolation: true, - sandbox: true, - webviewTag: false, - session: viewSession - } - }); + const webPreferences: Electron.WebPreferences & { type: ReturnType } = { + nodeIntegration: false, + contextIsolation: true, + sandbox: true, + webviewTag: false, + session: viewSession, + + // TODO@kycutler: Remove this once https://github.com/electron/electron/issues/42578 is fixed + type: 'browserView' + }; + + this._view = new WebContentsView({ webPreferences }); + this._view.setBackgroundColor('#FFFFFF'); this._view.webContents.setWindowOpenHandler((details) => { // For new tab requests, fire event for workbench to handle diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts index 87222cab944..dacaecd4ccd 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts @@ -380,16 +380,20 @@ export class BrowserEditor extends EditorPane { private updateVisibility(): void { const hasUrl = !!this._model?.url; const hasError = !!this._model?.error; + const shouldShowPlaceholder = this._editorVisible && this._overlayVisible && !hasError && hasUrl; // Welcome container: shown when no URL is loaded - this._welcomeContainer.style.display = hasUrl ? 'none' : 'flex'; + this._welcomeContainer.style.display = hasUrl ? 'none' : ''; // Error container: shown when there's a load error - this._errorContainer.style.display = hasError ? 'flex' : 'none'; + this._errorContainer.style.display = hasError ? '' : 'none'; + + // Placeholder screenshot: shown when the view is hidden due to overlays + this._placeholderScreenshot.style.display = shouldShowPlaceholder ? '' : 'none'; + this._placeholderScreenshot.classList.toggle('blur', shouldShowPlaceholder); if (this._model) { // Blur the background placeholder screenshot if the view is hidden due to an overlay. - this._placeholderScreenshot.classList.toggle('blur', this._editorVisible && this._overlayVisible && !hasError); void this._model.setVisible(this.shouldShowView); } } From 3d9625c556379aa800b908f775bc65ce510aae61 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:32:24 +0000 Subject: [PATCH 210/387] Sort custom agents alphabetically in mode picker dropdown (#288476) * Initial plan * Sort custom agents alphabetically in mode picker dropdown Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> * Improve code readability for alphabetical sorting Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> * sort in pickers --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> Co-authored-by: Martin Aeschlimann --- .../browser/promptSyntax/pickers/promptFilePickers.ts | 10 ++++++---- .../chat/browser/widget/input/modePickerActionItem.ts | 7 ++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts index d5de7079a34..60e561cca60 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts @@ -373,10 +373,12 @@ export class PromptFilePickers { getVisibility = p => !disabled.has(p.uri); } + const sortByLabel = (items: IPromptPickerQuickPickItem[]): IPromptPickerQuickPickItem[] => items.sort((a, b) => a.label.localeCompare(b.label)); + const locals = await this._promptsService.listPromptFilesForStorage(options.type, PromptsStorage.local, token); if (locals.length) { result.push({ type: 'separator', label: localize('separator.workspace', "Workspace") }); - result.push(...await Promise.all(locals.map(l => this._createPromptPickItem(l, buttons, getVisibility(l), token)))); + result.push(...sortByLabel(await Promise.all(locals.map(l => this._createPromptPickItem(l, buttons, getVisibility(l), token))))); } // Agent instruction files (copilot-instructions.md and AGENTS.md) are added here and not included in the output of @@ -403,7 +405,7 @@ export class PromptFilePickers { if (agentInstructionFiles.length) { const agentButtons = buttons.filter(b => b !== RENAME_BUTTON); result.push({ type: 'separator', label: localize('separator.workspace-agent-instructions', "Agent Instructions") }); - result.push(...await Promise.all(agentInstructionFiles.map(l => this._createPromptPickItem(l, agentButtons, getVisibility(l), token)))); + result.push(...sortByLabel(await Promise.all(agentInstructionFiles.map(l => this._createPromptPickItem(l, agentButtons, getVisibility(l), token))))); } const exts = await this._promptsService.listPromptFilesForStorage(options.type, PromptsStorage.extension, token); @@ -416,12 +418,12 @@ export class PromptFilePickers { if (options.optionCopy !== false) { extButtons.push(COPY_BUTTON); } - result.push(...await Promise.all(exts.map(e => this._createPromptPickItem(e, extButtons, getVisibility(e), token)))); + result.push(...sortByLabel(await Promise.all(exts.map(e => this._createPromptPickItem(e, extButtons, getVisibility(e), token))))); } const users = await this._promptsService.listPromptFilesForStorage(options.type, PromptsStorage.user, token); if (users.length) { result.push({ type: 'separator', label: localize('separator.user', "User Data") }); - result.push(...await Promise.all(users.map(u => this._createPromptPickItem(u, buttons, getVisibility(u), token)))); + result.push(...sortByLabel(await Promise.all(users.map(u => this._createPromptPickItem(u, buttons, getVisibility(u), token))))); } return result; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index 8824d54fc9f..4faa99244a9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -118,11 +118,16 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { action.category = agentModeDisabledViaPolicy ? policyDisabledCategory : builtInCategory; return action; }) ?? []; + customBuiltinModeActions.sort((a, b) => a.label.localeCompare(b.label)); + + const customModeActions = customModes.custom?.map(mode => makeActionFromCustomMode(mode, currentMode)) ?? []; + customModeActions.sort((a, b) => a.label.localeCompare(b.label)); const orderedModes = coalesce([ agentMode && makeAction(agentMode, currentMode), ...otherBuiltinModes.map(mode => mode && makeAction(mode, currentMode)), - ...customBuiltinModeActions, ...customModes.custom?.map(mode => makeActionFromCustomMode(mode, currentMode)) ?? [] + ...customBuiltinModeActions, + ...customModeActions ]); return orderedModes; } From 0292f074761ffe0f5e7e6742b60cad5cbb819f92 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 19 Jan 2026 18:33:34 +0100 Subject: [PATCH 211/387] Gutter inline edit menu (#287103) * WIP - edit gutter * ignore empty selections * tweaks * wip * wip * wip * proper menu, custom view item with edtor * wip * add setting * Make it inlineChat.showGutterMenu, noZone etc pp * polish * use `IChatEntiteldService.sentiment.hidden` * Add inline chat gutter visibility context and menu actions * Add inline gutter menu proposal for chat editor * fix progress message for the overlay * Add inline chat gutter menu overlay widget and styles * cleanup * Refactor inline chat gutter code: remove unused styles and streamline action handling * Simplify focus tracking logic in inline chat gutter menu widget * Enhance inline chat gutter menu: improve input handling and dynamic height adjustment --- .../browser/ui/actionbar/actionViewItems.ts | 5 +- .../base/browser/ui/actionbar/actionbar.css | 13 +- .../components/gutterIndicatorView.ts | 45 +- src/vs/platform/actions/common/actions.ts | 1 + .../common/extensionsApiProposals.ts | 3 + .../browser/actions/chatContextActions.ts | 14 +- .../browser/actions/chatExecuteActions.ts | 10 +- .../chatEditing/chatEditingEditorActions.ts | 8 +- .../chatEditing/chatEditingEditorOverlay.ts | 19 +- .../debug/browser/debugEditorActions.ts | 13 +- .../browser/inlineChat.contribution.ts | 27 +- .../inlineChat/browser/inlineChatActions.ts | 4 + .../browser/inlineChatController.ts | 26 +- .../inlineChatSelectionGutterIndicator.ts | 456 ++++++++++++++++++ .../inlineChat/browser/media/inlineChat.css | 40 ++ .../contrib/inlineChat/common/inlineChat.ts | 8 + .../actions/common/menusExtensionPoint.ts | 7 + ...sed.contribChatEditorInlineGutterMenu.d.ts | 6 + 18 files changed, 665 insertions(+), 40 deletions(-) create mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatSelectionGutterIndicator.ts create mode 100644 src/vscode-dts/vscode.proposed.contribChatEditorInlineGutterMenu.d.ts diff --git a/src/vs/base/browser/ui/actionbar/actionViewItems.ts b/src/vs/base/browser/ui/actionbar/actionViewItems.ts index ea705bcaa68..5fed79fe771 100644 --- a/src/vs/base/browser/ui/actionbar/actionViewItems.ts +++ b/src/vs/base/browser/ui/actionbar/actionViewItems.ts @@ -385,7 +385,10 @@ export class ActionViewItem extends BaseActionViewItem { if (this.cssClass && this.label) { this.label.classList.remove(...this.cssClass.split(' ')); } - if (this.options.icon) { + if (this.action.id === Separator.ID && this.action.class) { + this.label?.classList.add(this.action.class); + + } else if (this.options.icon) { this.cssClass = this.getClass(); if (this.label) { diff --git a/src/vs/base/browser/ui/actionbar/actionbar.css b/src/vs/base/browser/ui/actionbar/actionbar.css index df1518f2b45..467b1ff6efa 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.css +++ b/src/vs/base/browser/ui/actionbar/actionbar.css @@ -45,7 +45,8 @@ height: 16px; } -.monaco-action-bar .action-label { +.monaco-action-bar .action-label, +.monaco-action-bar .action-item .keybinding { display: flex; font-size: 11px; padding: 3px; @@ -75,12 +76,14 @@ display: block; } -.monaco-action-bar.vertical .action-label.separator { +.monaco-action-bar.vertical .action-item .action-label.separator { display: block; - border-bottom: 1px solid var(--vscode-disabledForeground); + border-bottom: 1px solid var(--vscode-menu-separatorBackground, var(--vscode-disabledForeground)); padding-top: 1px; - margin-left: .8em; - margin-right: .8em; + margin: 4px .8em; + width: 100%; + height: 0; + background-color: transparent; } .monaco-action-bar .action-item .action-label.separator { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts index 9000703a3d6..bc7c8ea299a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts @@ -34,6 +34,15 @@ import { localize } from '../../../../../../../nls.js'; import { InlineCompletionsModel } from '../../../model/inlineCompletionsModel.js'; import { InlineSuggestAlternativeAction } from '../../../model/InlineSuggestAlternativeAction.js'; import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; +import { ThemeIcon } from '../../../../../../../base/common/themables.js'; + +/** + * Customization options for the gutter indicator appearance and behavior. + */ +export interface GutterIndicatorCustomization { + /** Override the default icon */ + readonly icon?: ThemeIcon; +} export class InlineEditsGutterIndicatorData { constructor( @@ -41,6 +50,7 @@ export class InlineEditsGutterIndicatorData { readonly originalRange: LineRange, readonly model: SimpleInlineSuggestModel, readonly altAction: InlineSuggestAlternativeAction | undefined, + readonly customization?: GutterIndicatorCustomization, ) { } } @@ -341,18 +351,23 @@ export class InlineEditsGutterIndicator extends Disposable { const pillIsFullyDocked = gutterViewPortWithoutStickyScrollWithoutPaddingTop.containsRect(pillFullyDockedRect); // The icon which will be rendered in the pill - const iconNoneDocked = this._tabAction.map(action => action === InlineEditTabAction.Accept ? Codicon.keyboardTab : Codicon.arrowRight); - const iconDocked = derived(this, reader => { - if (this._isHoveredOverIconDebounced.read(reader) || this._isHoveredOverInlineEditDebounced.read(reader)) { - return Codicon.check; - } - if (this._tabAction.read(reader) === InlineEditTabAction.Accept) { - return Codicon.keyboardTab; - } - const cursorLineNumber = this._editorObs.cursorLineNumber.read(reader) ?? 0; - const editStartLineNumber = s.range.read(reader).startLineNumber; - return cursorLineNumber <= editStartLineNumber ? Codicon.keyboardTabAbove : Codicon.keyboardTabBelow; - }); + const customIcon = this._data.read(reader)?.customization?.icon; + const iconNoneDocked = customIcon + ? constObservable(customIcon) + : this._tabAction.map(action => action === InlineEditTabAction.Accept ? Codicon.keyboardTab : Codicon.arrowRight); + const iconDocked = customIcon + ? constObservable(customIcon) + : derived(this, reader => { + if (this._isHoveredOverIconDebounced.read(reader) || this._isHoveredOverInlineEditDebounced.read(reader)) { + return Codicon.check; + } + if (this._tabAction.read(reader) === InlineEditTabAction.Accept) { + return Codicon.keyboardTab; + } + const cursorLineNumber = this._editorObs.cursorLineNumber.read(reader) ?? 0; + const editStartLineNumber = s.range.read(reader).startLineNumber; + return cursorLineNumber <= editStartLineNumber ? Codicon.keyboardTabAbove : Codicon.keyboardTabBelow; + }); const idealIconAreaWidth = 22; const iconWidth = (pillRect: Rect) => { @@ -428,18 +443,18 @@ export class InlineEditsGutterIndicator extends Disposable { }); - private readonly _iconRef = n.ref(); + protected readonly _iconRef = n.ref(); public readonly isVisible = this._layout.map(l => !!l); - private readonly _hoverVisible = observableValue(this, false); + protected readonly _hoverVisible = observableValue(this, false); public readonly isHoverVisible: IObservable = this._hoverVisible; private readonly _isHoveredOverIcon = observableValue(this, false); private readonly _isHoveredOverIconDebounced: IObservable = debouncedObservable(this._isHoveredOverIcon, 100); public readonly isHoveredOverIcon: IObservable = this._isHoveredOverIconDebounced; - private _showHover(): void { + protected _showHover(): void { if (this._hoverVisible.get()) { return; } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 5c8cd932f8e..4fc359bba46 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -276,6 +276,7 @@ export class MenuId { static readonly ChatToolOutputResourceContext = new MenuId('ChatToolOutputResourceContext'); static readonly ChatMultiDiffContext = new MenuId('ChatMultiDiffContext'); static readonly ChatConfirmationMenu = new MenuId('ChatConfirmationMenu'); + static readonly ChatEditorInlineGutter = new MenuId('ChatEditorInlineGutter'); static readonly ChatEditorInlineExecute = new MenuId('ChatEditorInputExecute'); static readonly ChatEditorInlineInputSide = new MenuId('ChatEditorInputSide'); static readonly AccessibleView = new MenuId('AccessibleView'); diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 31fab40ccf3..d9bb1d86281 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -104,6 +104,9 @@ const _allApiProposals = { contribAccessibilityHelpContent: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribAccessibilityHelpContent.d.ts', }, + contribChatEditorInlineGutterMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribChatEditorInlineGutterMenu.d.ts', + }, contribCommentEditorActionsMenu: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentEditorActionsMenu.d.ts', }, diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 6d1974325e2..b7bf82a9325 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -171,6 +171,11 @@ class AttachFileToChatAction extends AttachResourceAction { ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeUserData) ) ) + }, { + id: MenuId.ChatEditorInlineGutter, + group: '2_context', + order: 2, + when: ContextKeyExpr.and(ChatContextKeys.enabled, EditorContextKeys.hasNonEmptySelection.negate()) }] }); } @@ -240,7 +245,7 @@ class AttachSelectionToChatAction extends Action2 { category: CHAT_CATEGORY, f1: true, precondition: ChatContextKeys.enabled, - menu: { + menu: [{ id: MenuId.EditorContext, group: '1_chat', order: 1, @@ -254,7 +259,12 @@ class AttachSelectionToChatAction extends Action2 { ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeUserData) ) ) - } + }, { + id: MenuId.ChatEditorInlineGutter, + group: '2_context', + order: 2, + when: ContextKeyExpr.and(ChatContextKeys.enabled, EditorContextKeys.hasNonEmptySelection) + }] }); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index ba1da5abcf3..acc95dc6441 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -31,7 +31,7 @@ import { IChatSessionsService } from '../../common/chatSessionsService.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; import { getAgentSessionProvider, AgentSessionProviders } from '../agentSessions/agentSessions.js'; import { getEditingSessionContext } from '../chatEditing/chatEditingActions.js'; -import { ctxHasEditorModification } from '../chatEditing/chatEditingEditorContextKeys.js'; +import { ctxHasEditorModification, ctxHasRequestInProgress, ctxIsGlobalEditingSession } from '../chatEditing/chatEditingEditorContextKeys.js'; import { ACTION_ID_NEW_CHAT, CHAT_CATEGORY, handleCurrentEditingSession, handleModeSwitch } from './chatActions.js'; import { CreateRemoteAgentJobAction } from './chatContinueInAction.js'; @@ -785,6 +785,14 @@ export class CancelAction extends Action2 { ), order: 4, group: 'navigation', + }, { + id: MenuId.ChatEditingEditorContent, + when: ContextKeyExpr.and( + ctxIsGlobalEditingSession.negate(), + ctxHasRequestInProgress + ), + order: 4, + group: 'navigation', }, ], keybinding: { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts index addad079207..1d86d2d7257 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts @@ -22,7 +22,7 @@ import { NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_EDITOR_FOCUSED } from '../../../no import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, IChatEditingService, IChatEditingSession, IModifiedFileEntry, IModifiedFileEntryChangeHunk, IModifiedFileEntryEditorIntegration, ModifiedFileEntryState, parseChatMultiDiffUri } from '../../common/editing/chatEditingService.js'; import { CHAT_CATEGORY } from '../actions/chatActions.js'; -import { ctxCursorInChangeRange, ctxHasEditorModification, ctxIsCurrentlyBeingModified, ctxIsGlobalEditingSession, ctxReviewModeEnabled } from './chatEditingEditorContextKeys.js'; +import { ctxCursorInChangeRange, ctxHasEditorModification, ctxHasRequestInProgress, ctxIsCurrentlyBeingModified, ctxIsGlobalEditingSession, ctxReviewModeEnabled } from './chatEditingEditorContextKeys.js'; abstract class ChatEditingEditorAction extends Action2 { @@ -170,7 +170,7 @@ abstract class KeepOrUndoAction extends ChatEditingEditorAction { tooltip: _keep ? localize2('accept3', 'Keep Chat Edits in this File') : localize2('discard3', 'Undo Chat Edits in this File'), - precondition: ContextKeyExpr.and(ctxIsGlobalEditingSession, ctxHasEditorModification, ctxIsCurrentlyBeingModified.negate()), + precondition: ContextKeyExpr.and(ctxHasEditorModification, ctxIsCurrentlyBeingModified.negate()), icon: _keep ? Codicon.check : Codicon.discard, @@ -186,7 +186,7 @@ abstract class KeepOrUndoAction extends ChatEditingEditorAction { id: MenuId.ChatEditingEditorContent, group: 'a_resolve', order: _keep ? 0 : 1, - when: !_keep ? ctxReviewModeEnabled : undefined + when: ContextKeyExpr.and(!_keep ? ctxReviewModeEnabled : undefined, ContextKeyExpr.or(ctxIsGlobalEditingSession, ctxHasRequestInProgress.negate())) } }); } @@ -350,7 +350,7 @@ export class ReviewChangesAction extends ChatEditingEditorAction { id: MenuId.ChatEditingEditorContent, group: 'a_resolve', order: 3, - when: ContextKeyExpr.and(ctxReviewModeEnabled.negate(), ctxIsCurrentlyBeingModified.negate()), + when: ContextKeyExpr.and(ctxReviewModeEnabled.negate(), ctxIsCurrentlyBeingModified.negate(), ContextKeyExpr.or(ctxIsGlobalEditingSession, ctxHasRequestInProgress.negate())), }] }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts index ff4e50795c6..d7f0000b99b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts @@ -25,12 +25,13 @@ import { ServiceCollection } from '../../../../../platform/instantiation/common/ import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/editor.js'; import { IInlineChatSessionService } from '../../../inlineChat/browser/inlineChatSessionService.js'; +import { InlineChatConfigKeys } from '../../../inlineChat/common/inlineChat.js'; import { isEqual } from '../../../../../base/common/resources.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ObservableEditorSession } from './chatEditingEditorContextKeys.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; -import * as arrays from '../../../../../base/common/arrays.js'; import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; @@ -71,12 +72,17 @@ class ChatEditorOverlayWidget extends Disposable { return undefined; } - const response = this._entry.read(r)?.lastModifyingResponse.read(r); + // For inline chat (non-global sessions), get progress directly from the chat model's current request/response + // This ensures progress messages appear immediately when streaming starts, before lastModifyingResponse is set + const response = session.isGlobalEditingSession + ? this._entry.read(r)?.lastModifyingResponse.read(r) + : chatModel.lastRequestObs.read(r)?.response; + if (!response) { return { message: localize('working', "Working...") }; } - const lastPart = observableFromEventOpts({ equalsFn: arrays.equals }, response.onDidChange, () => response.response.value) + const lastPart = observableFromEventOpts({ equalsFn: () => false }, response.onDidChange, () => response.response.value) .read(r) .filter(part => part.kind === 'progressMessage' || part.kind === 'toolInvocation') .at(-1); @@ -314,7 +320,8 @@ class ChatEditingOverlayController { @IInstantiationService instaService: IInstantiationService, @IChatService chatService: IChatService, @IChatEditingService chatEditingService: IChatEditingService, - @IInlineChatSessionService inlineChatService: IInlineChatSessionService + @IInlineChatSessionService inlineChatService: IInlineChatSessionService, + @IConfigurationService configurationService: IConfigurationService, ) { this._domNode.classList.add('chat-editing-editor-overlay'); @@ -387,8 +394,8 @@ class ChatEditingOverlayController { const { session, entry } = data; - if (!session.isGlobalEditingSession) { - // inline chat - no chat overlay unless hideOnRequest is on + if (!session.isGlobalEditingSession && !configurationService.getValue(InlineChatConfigKeys.ShowGutterMenu)) { + // inline chat with zone UI - no need for chat overlay hide(); return; } diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index f45cdbb0b9c..0ae688a3bb8 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -24,6 +24,7 @@ import { KeybindingWeight } from '../../../../platform/keybinding/common/keybind import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { PanelFocusContext } from '../../../common/contextkeys.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; +import { CTX_INLINE_CHAT_GUTTER_VISIBLE } from '../../inlineChat/common/inlineChat.js'; import { openBreakpointSource } from './breakpointsView.js'; import { DisassemblyView, IDisassembledInstructionEntry } from './disassemblyView.js'; import { Repl } from './repl.js'; @@ -39,9 +40,10 @@ class ToggleBreakpointAction extends Action2 { super({ id: TOGGLE_BREAKPOINT_ID, title: { - ...nls.localize2('toggleBreakpointAction', "Debug: Toggle Breakpoint"), + ...nls.localize2('toggleBreakpointAction', "Toggle Breakpoint"), mnemonicTitle: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint"), }, + category: nls.localize2('debugCategory', "Debug"), f1: true, precondition: CONTEXT_DEBUGGERS_AVAILABLE, keybinding: { @@ -49,12 +51,17 @@ class ToggleBreakpointAction extends Action2 { primary: KeyCode.F9, weight: KeybindingWeight.EditorContrib }, - menu: { + menu: [{ id: MenuId.MenubarDebugMenu, when: CONTEXT_DEBUGGERS_AVAILABLE, group: '4_new_breakpoint', order: 1 - } + }, { + id: MenuId.ChatEditorInlineGutter, + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CTX_INLINE_CHAT_GUTTER_VISIBLE), + group: '4_debug', + order: 1 + }] }); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index bd91732f53e..ac5eed62a31 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; -import { IMenuItem, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { MenuId, IMenuItem, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { InlineChatController } from './inlineChatController.js'; import * as InlineChatActions from './inlineChatActions.js'; -import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_V1_ENABLED, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_GUTTER_VISIBLE, CTX_INLINE_CHAT_V1_ENABLED, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; @@ -21,6 +21,8 @@ import { localize } from '../../../../nls.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { InlineChatAccessibilityHelp } from './inlineChatAccessibilityHelp.js'; +import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; +import { refactorCommandId, sourceActionCommandId } from '../../../../editor/contrib/codeAction/browser/codeAction.js'; registerEditorContribution(InlineChatController.ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors @@ -93,3 +95,24 @@ workbenchContributionsRegistry.registerWorkbenchContribution(InlineChatNotebookC registerWorkbenchContribution2(InlineChatEnabler.Id, InlineChatEnabler, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(InlineChatEscapeToolContribution.Id, InlineChatEscapeToolContribution, WorkbenchPhase.AfterRestored); AccessibleViewRegistry.register(new InlineChatAccessibilityHelp()); + +// Register Refactor and Source Action to the ChatEditorInlineGutter menu +MenuRegistry.appendMenuItem(MenuId.ChatEditorInlineGutter, { + command: { + id: refactorCommandId, + title: localize('refactor.label', "Refactor..."), + }, + when: ContextKeyExpr.and(EditorContextKeys.writable, CTX_INLINE_CHAT_GUTTER_VISIBLE), + group: '3_codeAction', + order: 1, +}); + +MenuRegistry.appendMenuItem(MenuId.ChatEditorInlineGutter, { + command: { + id: sourceActionCommandId, + title: localize('source.label', "Source Action..."), + }, + when: ContextKeyExpr.and(EditorContextKeys.writable, CTX_INLINE_CHAT_GUTTER_VISIBLE), + group: '3_codeAction', + order: 2, +}); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index ea19a4ee0a1..d2970111d99 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -74,6 +74,10 @@ export class StartSessionAction extends Action2 { id: MenuId.ChatTitleBarMenu, group: 'a_open', order: 3, + }, { + id: MenuId.ChatEditorInlineGutter, + group: '1_chat', + order: 1, }] }); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 700b138b98d..5b164f22167 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -51,6 +51,7 @@ import { INotebookEditorService } from '../../notebook/browser/services/notebook import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommon.js'; import { INotebookService } from '../../notebook/common/notebookService.js'; import { CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; +import { InlineChatSelectionIndicator } from './inlineChatSelectionGutterIndicator.js'; import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js'; import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; @@ -108,7 +109,9 @@ export class InlineChatController implements IEditorContribution { private readonly _store = new DisposableStore(); private readonly _isActiveController = observableValue(this, false); + private readonly _showGutterMenu: IObservable; private readonly _zone: Lazy; + private readonly _gutterIndicator: InlineChatSelectionIndicator; private readonly _currentSession: IObservable; @@ -138,6 +141,9 @@ export class InlineChatController implements IEditorContribution { const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this._configurationService); + this._showGutterMenu = observableConfigValue(InlineChatConfigKeys.ShowGutterMenu, false, this._configurationService); + + this._gutterIndicator = this._store.add(this._instaService.createInstance(InlineChatSelectionIndicator, this._editor)); this._zone = new Lazy(() => { @@ -279,11 +285,17 @@ export class InlineChatController implements IEditorContribution { // HIDE/SHOW const session = visibleSessionObs.read(r); + const showGutterMenu = this._showGutterMenu.read(r); if (!session) { this._zone.rawValue?.hide(); this._zone.rawValue?.widget.chatWidget.setModel(undefined); _editor.focus(); ctxInlineChatVisible.reset(); + } else if (showGutterMenu) { + // showGutterMenu mode: set model but don't show zone, keep focus in editor + this._zone.value.widget.chatWidget.setModel(session.chatModel); + this._zone.rawValue?.hide(); + ctxInlineChatVisible.set(true); } else { ctxInlineChatVisible.set(true); this._zone.value.widget.chatWidget.setModel(session.chatModel); @@ -418,7 +430,6 @@ export class InlineChatController implements IEditorContribution { async run(arg?: InlineChatRunOptions): Promise { assertType(this._editor.hasModel()); - const uri = this._editor.getModel().uri; const existingSession = this._inlineChatSessionService.getSessionByTextModel(uri); @@ -427,6 +438,19 @@ export class InlineChatController implements IEditorContribution { existingSession.dispose(); } + // use gutter menu to ask for input + if (!arg?.message && this._configurationService.getValue(InlineChatConfigKeys.ShowGutterMenu)) { + const position = this._editor.getPosition(); + const editorDomNode = this._editor.getDomNode(); + const scrolledPosition = this._editor.getScrolledVisiblePosition(position); + const editorRect = editorDomNode.getBoundingClientRect(); + const x = editorRect.left + scrolledPosition.left; + const y = editorRect.top + scrolledPosition.top; + // show menu and RETURN because the menu is re-entrant + await this._gutterIndicator.showMenuAt(x, y, scrolledPosition.height); + return true; + } + this._isActiveController.set(true, undefined); const session = this._inlineChatSessionService.createSession(this._editor); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSelectionGutterIndicator.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSelectionGutterIndicator.ts new file mode 100644 index 00000000000..eebb0032be9 --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSelectionGutterIndicator.ts @@ -0,0 +1,456 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { IAction } from '../../../../base/common/actions.js'; +import { ActionBar, ActionsOrientation } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { KeyCode } from '../../../../base/common/keyCodes.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { autorun, constObservable, debouncedObservable, derived, IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; +import { URI } from '../../../../base/common/uri.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { IActiveCodeEditor, ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; +import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; +import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js'; +import { observableCodeEditor, ObservableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { LineRange } from '../../../../editor/common/core/ranges/lineRange.js'; +import { SelectionDirection } from '../../../../editor/common/core/selection.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { InlineEditTabAction } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.js'; +import { InlineEditsGutterIndicator, InlineEditsGutterIndicatorData, InlineSuggestionGutterMenuData, SimpleInlineSuggestModel } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { HoverService } from '../../../../platform/hover/browser/hoverService.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { localize } from '../../../../nls.js'; +import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; +import { ACTION_START, CTX_INLINE_CHAT_GUTTER_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOptions.js'; +import { PlaceholderTextContribution } from '../../../../editor/contrib/placeholderText/browser/placeholderTextContribution.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { InlineChatRunOptions } from './inlineChatController.js'; +import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; +import { Position } from '../../../../editor/common/core/position.js'; + + +export class InlineChatSelectionIndicator extends Disposable { + + private readonly _gutterIndicator: InlineChatGutterIndicator; + + constructor( + private readonly _editor: ICodeEditor, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService, + @IChatEntitlementService chatEntiteldService: IChatEntitlementService, + @IContextKeyService contextKeyService: IContextKeyService + ) { + super(); + + const enabled = observableConfigValue(InlineChatConfigKeys.ShowGutterMenu, false, configurationService); + + const editorObs = observableCodeEditor(this._editor); + const focusIsInMenu = observableValue(this, false); + + // Observable to suppress the gutter when an action is selected + const suppressGutter = observableValue(this, false); + + // Debounce the selection to add a delay before showing the indicator + const debouncedSelection = debouncedObservable(editorObs.cursorSelection, 500); + + // Context key for gutter visibility + const gutterVisibleCtxKey = CTX_INLINE_CHAT_GUTTER_VISIBLE.bindTo(contextKeyService); + this._store.add({ dispose: () => gutterVisibleCtxKey.reset() }); + + // Create data observable based on the primary selection + // Use raw selection for immediate hide, debounced for delayed show + const data = derived(reader => { + // Check if feature is enabled or if AI features are disabled + if (!enabled.read(reader) || chatEntiteldService.sentiment.hidden) { + return undefined; + } + + // Hide when suppressed (e.g., after an action is selected) + if (suppressGutter.read(reader)) { + return undefined; + } + + // Read raw selection - if empty, immediately hide + const rawSelection = editorObs.cursorSelection.read(reader); + if (!rawSelection || rawSelection.isEmpty()) { + return undefined; + } + + // Read debounced selection for showing - this adds delay + const selection = debouncedSelection.read(reader); + if (!selection || selection.isEmpty()) { + return undefined; + } + + // Use the cursor position (active end of selection) to determine the line + const cursorPosition = selection.getPosition(); + const lineRange = new LineRange(cursorPosition.lineNumber, cursorPosition.lineNumber + 1); + + // Create minimal gutter menu data (empty for prototype) + const gutterMenuData = new InlineSuggestionGutterMenuData( + undefined, // action + '', // displayName + [], // extensionCommands + undefined, // alternativeAction + undefined, // modelInfo + undefined, // setModelId + ); + + // Create model with console.log actions for prototyping + const model = new SimpleInlineSuggestModel(() => { }, () => { }); + + return new InlineEditsGutterIndicatorData( + gutterMenuData, + lineRange, + model, + undefined, // altAction + { + icon: Codicon.sparkle, + } + ); + }); + + // Instantiate the gutter indicator + this._gutterIndicator = this._store.add(this._instantiationService.createInstance( + InlineChatGutterIndicator, + editorObs, + data, + constObservable(InlineEditTabAction.Jump), // tabAction - not used with custom styles + constObservable(0), // verticalOffset + constObservable(false), // isHoveringOverInlineEdit + focusIsInMenu, + suppressGutter, + )); + + // Reset suppressGutter when the selection changes + this._store.add(autorun(reader => { + editorObs.cursorSelection.read(reader); + suppressGutter.set(false, undefined); + })); + + // Update context key when gutter visibility changes + this._store.add(autorun(reader => { + const isVisible = data.read(reader) !== undefined; + gutterVisibleCtxKey.set(isVisible); + })); + } + + /** + * Show the gutter menu at the specified coordinates. + * @returns Promise that resolves when menu closes + */ + showMenuAt(x: number, y: number, height: number = 0): Promise { + return this._gutterIndicator.showMenuAt(x, y, height); + } +} + +/** + * Overlay widget that displays a vertical action bar menu. + */ +class InlineChatGutterMenuWidget extends Disposable implements IOverlayWidget { + + private static _idPool = 0; + + private readonly _id = `inline-chat-gutter-menu-${InlineChatGutterMenuWidget._idPool++}`; + private readonly _domNode: HTMLElement; + private readonly _inputContainer: HTMLElement; + private readonly _actionBar: ActionBar; + private readonly _input: IActiveCodeEditor; + private _position: IOverlayWidgetPosition | null = null; + private readonly _onDidHide = this._register(new Emitter()); + readonly onDidHide = this._onDidHide.event; + + readonly allowEditorOverflow = true; + + constructor( + private readonly _editor: ICodeEditor, + top: number, + left: number, + anchorAbove: boolean, + @IKeybindingService keybindingService: IKeybindingService, + @IMenuService menuService: IMenuService, + @IContextKeyService contextKeyService: IContextKeyService, + @IInstantiationService instantiationService: IInstantiationService, + @IModelService modelService: IModelService, + @IConfigurationService configurationService: IConfigurationService, + ) { + super(); + + // Create container + this._domNode = dom.$('.inline-chat-gutter-menu'); + + // Create input editor container + this._inputContainer = dom.append(this._domNode, dom.$('.input')); + this._inputContainer.style.width = '200px'; + this._inputContainer.style.height = '26px'; + this._inputContainer.style.display = 'flex'; + this._inputContainer.style.alignItems = 'center'; + this._inputContainer.style.justifyContent = 'center'; + + // Create editor options + const options = getSimpleEditorOptions(configurationService); + options.wordWrap = 'off'; + options.lineNumbers = 'off'; + options.glyphMargin = false; + options.lineDecorationsWidth = 0; + options.lineNumbersMinChars = 0; + options.folding = false; + options.minimap = { enabled: false }; + options.scrollbar = { vertical: 'auto', horizontal: 'hidden', alwaysConsumeMouseWheel: true, verticalSliderSize: 6 }; + options.renderLineHighlight = 'none'; + options.placeholder = keybindingService.appendKeybinding(localize('placeholderWithSelection', "Edit selection"), ACTION_START); + + const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { + isSimpleWidget: true, + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + PlaceholderTextContribution.ID, + ]) + }; + + this._input = this._store.add(instantiationService.createInstance(CodeEditorWidget, this._inputContainer, options, codeEditorWidgetOptions)) as IActiveCodeEditor; + + const model = this._store.add(modelService.createModel('', null, URI.parse(`gutter-input:${Date.now()}`), true)); + this._input.setModel(model); + this._input.layout({ width: 200, height: 18 }); + + // Listen to content size changes and resize the input editor (max 3 lines) + this._store.add(this._input.onDidContentSizeChange(e => { + if (e.contentHeightChanged) { + this._updateInputHeight(e.contentHeight); + } + })); + + let inlineStartAction: IAction | undefined; + + // Handle Enter key to submit and ArrowDown to focus action bar + this._store.add(this._input.onKeyDown(e => { + if (e.keyCode === KeyCode.Enter && !e.shiftKey) { + const value = this._input.getModel().getValue() ?? ''; + // TODO@jrieken this isn't nice + if (inlineStartAction && value) { + e.preventDefault(); + e.stopPropagation(); + this._actionBar.actionRunner.run( + inlineStartAction, + { message: value, autoSend: true } satisfies InlineChatRunOptions + ); + } + } else if (e.keyCode === KeyCode.DownArrow) { + // Focus first action bar item when at the end of the input + const inputModel = this._input.getModel(); + const position = this._input.getPosition(); + const lastLineNumber = inputModel.getLineCount(); + const lastLineMaxColumn = inputModel.getLineMaxColumn(lastLineNumber); + if (Position.equals(position, new Position(lastLineNumber, lastLineMaxColumn))) { + e.preventDefault(); + e.stopPropagation(); + this._actionBar.focus(); + } + } + })); + + // Get actions from menu + const actions = getFlatActionBarActions(menuService.getMenuActions(MenuId.ChatEditorInlineGutter, contextKeyService, { shouldForwardArgs: true })); + + // Create vertical action bar + this._actionBar = this._store.add(new ActionBar(this._domNode, { + orientation: ActionsOrientation.VERTICAL, + })); + + // Set actions with keybindings (skip ACTION_START since we have the input editor) + for (const action of actions) { + if (action.id === ACTION_START) { + inlineStartAction = action; + continue; + } + const keybinding = keybindingService.lookupKeybinding(action.id)?.getLabel(); + this._actionBar.push(action, { icon: false, label: true, keybinding }); + } + + // Set initial position + this._position = { + preference: { top, left }, + stackOrdinal: 10000, + }; + + // Track focus - hide when focus leaves + const focusTracker = this._store.add(dom.trackFocus(this._domNode)); + this._store.add(focusTracker.onDidBlur(() => this._hide())); + + // Handle action bar cancel (Escape key) + this._store.add(this._actionBar.onDidCancel(() => this._hide())); + this._store.add(this._actionBar.onWillRun(() => this._hide())); + + // Add widget to editor + this._editor.addOverlayWidget(this); + + // If anchoring above, adjust position after render to account for widget height + if (anchorAbove) { + const widgetHeight = this._domNode.offsetHeight; + this._position = { + preference: { top: top - widgetHeight, left }, + stackOrdinal: 10000, + }; + this._editor.layoutOverlayWidget(this); + } + + // Focus the input editor + setTimeout(() => this._input.focus(), 0); + } + + private _hide(): void { + this._onDidHide.fire(); + } + + private _updateInputHeight(contentHeight: number): void { + const lineHeight = this._input.getOption(EditorOption.lineHeight); + const maxHeight = 3 * lineHeight; + const clampedHeight = Math.min(contentHeight, maxHeight); + const containerPadding = 8; + + this._inputContainer.style.height = `${clampedHeight + containerPadding}px`; + this._input.layout({ width: 200, height: clampedHeight }); + this._editor.layoutOverlayWidget(this); + } + + getId(): string { + return this._id; + } + + getDomNode(): HTMLElement { + return this._domNode; + } + + getPosition(): IOverlayWidgetPosition | null { + return this._position; + } + + override dispose(): void { + this._editor.removeOverlayWidget(this); + super.dispose(); + } +} + +/** + * Custom gutter indicator for selection that shows a menu overlay widget. + */ +class InlineChatGutterIndicator extends InlineEditsGutterIndicator { + + private readonly _myInstantiationService: IInstantiationService; + private _currentMenuWidget: InlineChatGutterMenuWidget | undefined; + + constructor( + private readonly _myEditorObs: ObservableCodeEditor, + data: IObservable, + tabAction: IObservable, + verticalOffset: IObservable, + isHoveringOverInlineEdit: IObservable, + focusIsInMenu: ISettableObservable, + private readonly _suppressGutter: ISettableObservable, + @IHoverService hoverService: HoverService, + @IInstantiationService instantiationService: IInstantiationService, + @IAccessibilityService accessibilityService: IAccessibilityService, + @IThemeService themeService: IThemeService, + ) { + super(_myEditorObs, data, tabAction, verticalOffset, isHoveringOverInlineEdit, focusIsInMenu, hoverService, instantiationService, accessibilityService, themeService); + this._myInstantiationService = instantiationService; + } + + protected override _showHover(): void { + + if (this._hoverVisible.get()) { + return; + } + + // Use the icon element from the base class as anchor + const iconElement = this._iconRef.element; + if (!iconElement) { + return; + } + + this._hoverVisible.set(true, undefined); + const rect = iconElement.getBoundingClientRect(); + + this.showMenuAt(rect.left, rect.top, rect.height).finally(() => { + this._hoverVisible.set(false, undefined); + }); + } + + /** + * Show the gutter menu at the specified coordinates. + * @returns Promise that resolves when menu closes + */ + showMenuAt(x: number, y: number, height: number = 0): Promise { + return new Promise(resolve => { + // Clean up existing widget if any + this._currentMenuWidget?.dispose(); + this._currentMenuWidget = undefined; + + // Determine selection direction to position menu above or below + const selection = this._myEditorObs.cursorSelection.get(); + const direction = selection?.getDirection() ?? SelectionDirection.LTR; + + // Convert screen coordinates to editor-relative coordinates + const editor = this._myEditorObs.editor; + const editorDomNode = editor.getDomNode(); + if (!editorDomNode) { + resolve(); + return; + } + + const editorRect = editorDomNode.getBoundingClientRect(); + const padding = 1; + + // Calculate position relative to editor + // For RTL (above), we pass the top of the gutter indicator; widget will adjust after measuring its height + // For LTR (below), we pass the bottom of the gutter indicator + const anchorAbove = direction === SelectionDirection.RTL; + let top: number; + if (anchorAbove) { + // Pass the top of the gutter indicator minus padding + top = y - editorRect.top - padding; + } else { + // Menu appears below - position at bottom of gutter indicator + top = y - editorRect.top + height + padding; + } + const left = x - editorRect.left; + + const store = new DisposableStore(); + + // Create and show overlay widget + this._currentMenuWidget = this._myInstantiationService.createInstance( + InlineChatGutterMenuWidget, + editor, + top, + left, + anchorAbove, + ); + + // Handle widget hide + store.add(this._currentMenuWidget.onDidHide(() => { + this._suppressGutter.set(true, undefined); + store.dispose(); + this._currentMenuWidget?.dispose(); + this._currentMenuWidget = undefined; + + // Focus editor + editor.focus(); + + resolve(); + })); + }); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index e53c6ec761b..e970952c619 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -311,3 +311,43 @@ .monaco-workbench .inline-chat .chat-attached-context { padding: 2px 0px; } + +/* Gutter menu overlay widget */ +.inline-chat-gutter-menu { + background: var(--vscode-menu-background); + border: 1px solid var(--vscode-menu-border, var(--vscode-widget-border)); + border-radius: 4px; + box-shadow: 0 2px 8px var(--vscode-widget-shadow); + padding: 4px 0; + min-width: 160px; + z-index: 10000; +} + +.inline-chat-gutter-menu .input { + padding: 0 18px; +} + +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item { + display: flex; + justify-content: space-between; + padding: 0 .8em; + border-radius: 3px; + margin: 0 4px; +} + +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item .action-label { + font-size: 13px; +} + +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled):hover, +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled).focused { + background-color: var(--vscode-list-activeSelectionBackground); + color: var(--vscode-list-activeSelectionForeground); + outline: 1px solid var(--vscode-menu-selectionBorder, transparent); + outline-offset: -1px; +} + +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled):hover .action-label, +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled).focused .action-label { + color: var(--vscode-list-activeSelectionForeground); +} diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index c1979e024c5..e3a74c6adb6 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -21,6 +21,7 @@ export const enum InlineChatConfigKeys { EnableV2 = 'inlineChat.enableV2', notebookAgent = 'inlineChat.notebookAgent', PersistModelChoice = 'inlineChat.persistModelChoice', + ShowGutterMenu = 'inlineChat.showGutterMenu', } Registry.as(Extensions.Configuration).registerConfiguration({ @@ -61,6 +62,12 @@ Registry.as(Extensions.Configuration).registerConfigurat experiment: { mode: 'auto' } + }, + [InlineChatConfigKeys.ShowGutterMenu]: { + description: localize('showGutterMenu', "Controls whether a gutter indicator is shown when text is selected to quickly access inline chat."), + default: false, + type: 'boolean', + tags: ['experimental'] } } }); @@ -94,6 +101,7 @@ export const CTX_INLINE_CHAT_CHANGE_HAS_DIFF = new RawContextKey('inlin export const CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF = new RawContextKey('inlineChatChangeShowsDiff', false, localize('inlineChatChangeShowsDiff', "Whether the current change showing a diff")); export const CTX_INLINE_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('inlineChatRequestInProgress', false, localize('inlineChatRequestInProgress', "Whether an inline chat request is currently in progress")); export const CTX_INLINE_CHAT_RESPONSE_TYPE = new RawContextKey('inlineChatResponseType', InlineChatResponseType.None, localize('inlineChatResponseTypes', "What type was the responses have been receieved, nothing yet, just messages, or messaged and local edits")); +export const CTX_INLINE_CHAT_GUTTER_VISIBLE = new RawContextKey('inlineChatGutterVisible', false, localize('inlineChatGutterVisible', "Whether the inline chat gutter indicator is visible")); export const CTX_INLINE_CHAT_V1_ENABLED = ContextKeyExpr.or( ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, CTX_INLINE_CHAT_HAS_NOTEBOOK_INLINE) diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index fd4c8385677..304b7bb1147 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -501,6 +501,13 @@ const apiMenus: IAPIMenu[] = [ supportsSubmenus: false, proposed: 'chatSessionsProvider', }, + { + key: 'chat/editor/inlineGutter', + id: MenuId.ChatEditorInlineGutter, + description: localize('menus.chatEditorInlineGutter', "The inline gutter menu in the chat editor."), + supportsSubmenus: false, + proposed: 'contribChatEditorInlineGutterMenu', + }, ]; namespace schema { diff --git a/src/vscode-dts/vscode.proposed.contribChatEditorInlineGutterMenu.d.ts b/src/vscode-dts/vscode.proposed.contribChatEditorInlineGutterMenu.d.ts new file mode 100644 index 00000000000..a06788a2dd0 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.contribChatEditorInlineGutterMenu.d.ts @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// empty placeholder declaration for the `chat/editor/inlineGutter` menu From c302bfa799fe014b875067cd87c7019f56626af6 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Mon, 19 Jan 2026 17:35:02 +0000 Subject: [PATCH 212/387] refactor(debug): replace HTML checkboxes with custom toggle components --- .../contrib/debug/browser/breakpointsView.ts | 74 +++++++++---------- .../debug/browser/media/debugViewlet.css | 21 +++++- 2 files changed, 56 insertions(+), 39 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index fdb06fe25e3..6f82df1624a 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -5,12 +5,12 @@ import * as dom from '../../../../base/browser/dom.js'; import { IKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; -import { Gesture } from '../../../../base/browser/touch.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { AriaRole } from '../../../../base/browser/ui/aria/aria.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IconLabel } from '../../../../base/browser/ui/iconLabel/iconLabel.js'; import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js'; +import { Checkbox, TriStateCheckbox } from '../../../../base/browser/ui/toggle/toggle.js'; import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js'; import { Orientation } from '../../../../base/browser/ui/splitview/splitview.js'; @@ -46,7 +46,7 @@ import { WorkbenchCompressibleObjectTree } from '../../../../platform/list/brows import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; -import { defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import { defaultCheckboxStyles, defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { ViewAction, ViewPane } from '../../../browser/parts/views/viewPane.js'; import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; @@ -64,11 +64,10 @@ import { hasKey } from '../../../../base/common/types.js'; const $ = dom.$; -function createCheckbox(disposables: DisposableStore): HTMLInputElement { - const checkbox = $('input'); - checkbox.type = 'checkbox'; - checkbox.tabIndex = -1; - disposables.add(Gesture.ignoreTarget(checkbox)); +function createCheckbox(disposables: DisposableStore): Checkbox { + const checkbox = new Checkbox('', false, defaultCheckboxStyles); + checkbox.domNode.tabIndex = -1; + disposables.add(checkbox); return checkbox; } @@ -621,7 +620,7 @@ class BreakpointsDelegate implements IListVirtualDelegate interface IBaseBreakpointTemplateData { breakpoint: HTMLElement; name: HTMLElement; - checkbox: HTMLInputElement; + checkbox: Checkbox; context: BreakpointItem; actionBar: ActionBar; templateDisposables: DisposableStore; @@ -656,7 +655,7 @@ interface IInstructionBreakpointTemplateData extends IBaseBreakpointWithIconTemp interface IFunctionBreakpointInputTemplateData { inputBox: InputBox; - checkbox: HTMLInputElement; + checkbox: Checkbox; icon: HTMLElement; breakpoint: IFunctionBreakpoint; templateDisposables: DisposableStore; @@ -667,7 +666,7 @@ interface IFunctionBreakpointInputTemplateData { interface IDataBreakpointInputTemplateData { inputBox: InputBox; - checkbox: HTMLInputElement; + checkbox: Checkbox; icon: HTMLElement; breakpoint: IDataBreakpoint; elementDisposables: DisposableStore; @@ -678,7 +677,7 @@ interface IDataBreakpointInputTemplateData { interface IExceptionBreakpointInputTemplateData { inputBox: InputBox; - checkbox: HTMLInputElement; + checkbox: Checkbox; currentBreakpoint?: IExceptionBreakpoint; templateDisposables: DisposableStore; elementDisposables: DisposableStore; @@ -686,7 +685,7 @@ interface IExceptionBreakpointInputTemplateData { interface IBreakpointsFolderTemplateData { container: HTMLElement; - checkbox: HTMLInputElement; + checkbox: TriStateCheckbox; name: HTMLElement; actionBar: ActionBar; context: BreakpointsFolderItem; @@ -723,15 +722,18 @@ class BreakpointsFolderRenderer implements ICompressibleTreeRenderer { - const enabled = data.checkbox.checked; + data.checkbox = new TriStateCheckbox('', false, defaultCheckboxStyles); + data.checkbox.domNode.tabIndex = -1; + data.templateDisposables.add(data.checkbox); + data.templateDisposables.add(data.checkbox.onChange(() => { + const checked = data.checkbox.checked; + const enabled = checked === 'mixed' ? true : checked; for (const bp of data.context.breakpoints) { this.debugService.enableOrDisableBreakpoints(enabled, bp); } })); - dom.append(data.container, data.checkbox); + dom.append(data.container, data.checkbox.domNode); data.name = dom.append(data.container, $('span.name')); dom.append(data.container, $('span.file-path')); @@ -753,10 +755,8 @@ class BreakpointsFolderRenderer implements ICompressibleTreeRenderer { + data.templateDisposables.add(data.checkbox.onChange(() => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); dom.append(data.breakpoint, data.icon); - dom.append(data.breakpoint, data.checkbox); + dom.append(data.breakpoint, data.checkbox.domNode); data.name = dom.append(data.breakpoint, $('span.name')); @@ -1001,11 +999,11 @@ class ExceptionBreakpointsRenderer implements ICompressibleTreeRenderer { + data.templateDisposables.add(data.checkbox.onChange(() => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); - dom.append(data.breakpoint, data.checkbox); + dom.append(data.breakpoint, data.checkbox.domNode); data.name = dom.append(data.breakpoint, $('span.name')); data.condition = dom.append(data.breakpoint, $('span.condition')); @@ -1096,12 +1094,12 @@ class FunctionBreakpointsRenderer implements ICompressibleTreeRenderer { + data.templateDisposables.add(data.checkbox.onChange(() => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); dom.append(data.breakpoint, data.icon); - dom.append(data.breakpoint, data.checkbox); + dom.append(data.breakpoint, data.checkbox.domNode); data.name = dom.append(data.breakpoint, $('span.name')); data.condition = dom.append(data.breakpoint, $('span.condition')); @@ -1201,12 +1199,12 @@ class DataBreakpointsRenderer implements ICompressibleTreeRenderer { + data.templateDisposables.add(data.checkbox.onChange(() => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); dom.append(data.breakpoint, data.icon); - dom.append(data.breakpoint, data.checkbox); + dom.append(data.breakpoint, data.checkbox.domNode); data.name = dom.append(data.breakpoint, $('span.name')); data.accessType = dom.append(data.breakpoint, $('span.access-type')); @@ -1311,12 +1309,12 @@ class InstructionBreakpointsRenderer implements ICompressibleTreeRenderer { + data.templateDisposables.add(data.checkbox.onChange(() => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); dom.append(data.breakpoint, data.icon); - dom.append(data.breakpoint, data.checkbox); + dom.append(data.breakpoint, data.checkbox.domNode); data.name = dom.append(data.breakpoint, $('span.name')); @@ -1400,7 +1398,7 @@ class FunctionBreakpointInputRenderer implements ICompressibleTreeRenderer { data.inputBox.focus(); diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index 574c770e10a..99f85978b40 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -306,8 +306,27 @@ margin-left: 0; } -.debug-pane .debug-breakpoints .breakpoint input { +.debug-pane .debug-breakpoints .breakpoint .monaco-custom-toggle { flex-shrink: 0; + margin-left: 0; + margin-right: 4px; +} + +.debug-pane .debug-breakpoints .breakpoint .monaco-custom-toggle.monaco-checkbox { + width: 18px; + min-width: 18px; + max-width: 18px; + height: 18px; + padding: 0; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.debug-pane .debug-breakpoints .breakpoint .monaco-custom-toggle.monaco-checkbox::before { + margin: 0; } .debug-pane .debug-breakpoints .breakpoint > .codicon { From ffc926dda148353078f1190b057d5697fd009854 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 19 Jan 2026 18:44:50 +0100 Subject: [PATCH 213/387] cache policy data (#288937) --- .../accounts/common/defaultAccount.ts | 59 +++++++++++++++---- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/services/accounts/common/defaultAccount.ts b/src/vs/workbench/services/accounts/common/defaultAccount.ts index f9192f16c51..e6ab4456307 100644 --- a/src/vs/workbench/services/accounts/common/defaultAccount.ts +++ b/src/vs/workbench/services/accounts/common/defaultAccount.ts @@ -17,7 +17,8 @@ import { Barrier, ThrottledDelayer, timeout } from '../../../../base/common/asyn import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { getErrorMessage } from '../../../../base/common/errors.js'; import { IDefaultAccount, IDefaultAccountAuthenticationProvider, IEntitlementsData, IPolicyData } from '../../../../base/common/defaultAccount.js'; -import { isString } from '../../../../base/common/types.js'; +import { isString, Mutable } from '../../../../base/common/types.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; import { isWeb } from '../../../../base/common/platform.js'; import { IDefaultAccountProvider, IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js'; @@ -58,6 +59,8 @@ const enum DefaultAccountStatus { const CONTEXT_DEFAULT_ACCOUNT_STATE = new RawContextKey('defaultAccountStatus', DefaultAccountStatus.Uninitialized); +const CACHED_POLICY_DATA_KEY = 'defaultAccount.cachedPolicyData'; + interface ITokenEntitlementsResponse { token: string; } @@ -210,6 +213,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid @ILogService private readonly logService: ILogService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IContextKeyService contextKeyService: IContextKeyService, + @IStorageService private readonly storageService: IStorageService, ) { super(); this.accountStatusContext = CONTEXT_DEFAULT_ACCOUNT_STATE.bindTo(contextKeyService); @@ -353,30 +357,39 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid this.logService.debug('[DefaultAccount] Getting Default Account from authenticated sessions for provider:', authenticationProvider.id); const sessions = await this.findMatchingProviderSession(authenticationProvider.id, scopes); - if (!sessions) { + if (!sessions?.length) { this.logService.debug('[DefaultAccount] No matching session found for provider:', authenticationProvider.id); return null; } - const [entitlementsData, policyData] = await Promise.all([ + const accountId = sessions[0].account.id; + const [entitlementsData, tokenEntitlementsData] = await Promise.all([ this.getEntitlements(sessions), this.getTokenEntitlements(sessions), ]); - const mcpRegistryProvider = policyData?.mcp ? await this.getMcpRegistryProvider(sessions) : undefined; + let policyData = this.getCachedPolicyData(accountId); + if (tokenEntitlementsData) { + policyData = policyData ?? {}; + policyData.chat_agent_enabled = tokenEntitlementsData.chat_agent_enabled; + policyData.chat_preview_features_enabled = tokenEntitlementsData.chat_preview_features_enabled; + policyData.mcp = tokenEntitlementsData.mcp; + if (policyData.mcp) { + const mcpRegistryProvider = await this.getMcpRegistryProvider(sessions); + if (mcpRegistryProvider) { + policyData.mcpRegistryUrl = mcpRegistryProvider.url; + policyData.mcpAccess = mcpRegistryProvider.registry_access; + } + } + this.cachePolicyData(accountId, policyData); + } const account: IDefaultAccount = { authenticationProvider, sessionId: sessions[0].id, enterprise: authenticationProvider.enterprise || sessions[0].account.label.includes('_'), entitlementsData, - policyData: policyData ? { - chat_agent_enabled: policyData.chat_agent_enabled, - chat_preview_features_enabled: policyData.chat_preview_features_enabled, - mcp: policyData.mcp, - mcpRegistryUrl: mcpRegistryProvider?.url, - mcpAccess: mcpRegistryProvider?.registry_access, - } : undefined, + policyData, }; this.logService.debug('[DefaultAccount] Successfully created default account for provider:', authenticationProvider.id); return account; @@ -469,7 +482,29 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid this.logService.error('Failed to fetch token entitlements', getErrorMessage(error)); } - return {}; + return undefined; + } + + private cachePolicyData(accountId: string, policyData: IPolicyData): void { + this.logService.debug('[DefaultAccount] Caching policy data for account:', accountId); + this.storageService.store(CACHED_POLICY_DATA_KEY, JSON.stringify({ accountId, policyData }), StorageScope.APPLICATION, StorageTarget.MACHINE); + } + + private getCachedPolicyData(accountId: string): Mutable | undefined { + const cached = this.storageService.get(CACHED_POLICY_DATA_KEY, StorageScope.APPLICATION); + if (cached) { + try { + const { accountId: cachedAccountId, policyData } = JSON.parse(cached); + if (cachedAccountId === accountId) { + this.logService.debug('[DefaultAccount] Using cached policy data for account:', accountId); + return policyData; + } + this.logService.debug('[DefaultAccount] Cached policy data is for different account, ignoring'); + } catch (error) { + this.logService.error('[DefaultAccount] Failed to parse cached policy data', getErrorMessage(error)); + } + } + return undefined; } private async getEntitlements(sessions: AuthenticationSession[]): Promise { From 6f548f3429df60a5f2480ebbd0eee5e8d844a604 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 19 Jan 2026 18:45:04 +0100 Subject: [PATCH 214/387] sending the font token decorations in the custom line heights too (#288941) making the font decorations be also used in the line heights manager initially --- src/vs/editor/common/model/textModel.ts | 4 +++- .../model/tokens/tokenizationFontDecorationsProvider.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 8cb4f567ba3..1953c170203 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1842,7 +1842,9 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } public getCustomLineHeightsDecorations(ownerId: number = 0): model.IModelDecoration[] { - return this._decorationsTree.getAllCustomLineHeights(this, ownerId); + const decs = this._decorationsTree.getAllCustomLineHeights(this, ownerId); + pushMany(decs, this._fontTokenDecorationsProvider.getAllDecorations(ownerId)); + return decs; } private _getInjectedTextInLine(lineNumber: number): LineInjectedText[] { diff --git a/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts b/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts index 9b4f9996262..a5335fa9fca 100644 --- a/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts +++ b/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts @@ -148,7 +148,7 @@ export class TokenizationFontDecorationProvider extends Disposable implements De public getAllDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[] { return this.getDecorationsInRange( - new Range(1, 1, this.textModel.getLineCount(), 1), + new Range(1, 1, this.textModel.getLineCount(), this.textModel.getLineMaxColumn(this.textModel.getLineCount())), ownerId, filterOutValidation ); From 2efcdb92310b5ce12636ad6896080476dc42bf1c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 18:53:11 +0100 Subject: [PATCH 215/387] Show chat agents/prompts in extension features list (#287560) * Initial plan * Add chat prompt files/instructions/agents to extension features registry Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> * update * Merge branch 'main' into copilot/show-chat-agents-prompts * Update src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Merge branch 'main' into copilot/show-chat-agents-prompts * Merge remote-tracking branch 'origin/main' into copilot/show-chat-agents-prompts --- .../platform/extensions/common/extensions.ts | 9 +++ .../chatPromptFilesContribution.ts | 70 ++++++------------- 2 files changed, 29 insertions(+), 50 deletions(-) diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 8961b9011b3..f0039c982d9 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -199,6 +199,12 @@ export interface IMcpCollectionContribution { readonly when?: string; } +export interface IChatFileContribution { + readonly path: string; + readonly name?: string; + readonly description?: string; +} + export interface IExtensionContributions { commands?: ICommand[]; configuration?: any; @@ -226,6 +232,9 @@ export interface IExtensionContributions { readonly notebookRenderer?: INotebookRendererContribution[]; readonly debugVisualizers?: IDebugVisualizationContribution[]; readonly chatParticipants?: ReadonlyArray; + readonly chatPromptFiles?: ReadonlyArray; + readonly chatInstructions?: ReadonlyArray; + readonly chatAgents?: ReadonlyArray; readonly languageModelTools?: ReadonlyArray; readonly languageModelToolSets?: ReadonlyArray; readonly mcpServerDefinitionProviders?: ReadonlyArray; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts index 6179489270e..0eecdcb0d05 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts @@ -3,16 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from '../../../../../base/common/cancellation.js'; + import { DisposableMap } from '../../../../../base/common/lifecycle.js'; import { joinPath, isEqualOrParent } from '../../../../../base/common/resources.js'; -import { UriComponents } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; -import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js'; -import { IPromptsService, PromptsStorage } from './service/promptsService.js'; +import { IPromptsService } from './service/promptsService.js'; import { PromptsType } from './promptTypes.js'; interface IRawChatFileContribution { @@ -21,7 +19,12 @@ interface IRawChatFileContribution { readonly description?: string; } -type ChatContributionPoint = 'chatPromptFiles' | 'chatInstructions' | 'chatAgents' | 'chatSkills'; +enum ChatContributionPoint { + chatInstructions = 'chatInstructions', + chatAgents = 'chatAgents', + chatPromptFiles = 'chatPromptFiles', + chatSkills = 'chatSkills' +} function registerChatFilesExtensionPoint(point: ChatContributionPoint) { return extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ @@ -59,17 +62,17 @@ function registerChatFilesExtensionPoint(point: ChatContributionPoint) { }); } -const epPrompt = registerChatFilesExtensionPoint('chatPromptFiles'); -const epInstructions = registerChatFilesExtensionPoint('chatInstructions'); -const epAgents = registerChatFilesExtensionPoint('chatAgents'); -const epSkills = registerChatFilesExtensionPoint('chatSkills'); +const epPrompt = registerChatFilesExtensionPoint(ChatContributionPoint.chatPromptFiles); +const epInstructions = registerChatFilesExtensionPoint(ChatContributionPoint.chatInstructions); +const epAgents = registerChatFilesExtensionPoint(ChatContributionPoint.chatAgents); +const epSkills = registerChatFilesExtensionPoint(ChatContributionPoint.chatSkills); function pointToType(contributionPoint: ChatContributionPoint): PromptsType { switch (contributionPoint) { - case 'chatPromptFiles': return PromptsType.prompt; - case 'chatInstructions': return PromptsType.instructions; - case 'chatAgents': return PromptsType.agent; - case 'chatSkills': return PromptsType.skill; + case ChatContributionPoint.chatPromptFiles: return PromptsType.prompt; + case ChatContributionPoint.chatInstructions: return PromptsType.instructions; + case ChatContributionPoint.chatAgents: return PromptsType.agent; + case ChatContributionPoint.chatSkills: return PromptsType.skill; } } @@ -85,10 +88,10 @@ export class ChatPromptFilesExtensionPointHandler implements IWorkbenchContribut constructor( @IPromptsService private readonly promptsService: IPromptsService, ) { - this.handle(epPrompt, 'chatPromptFiles'); - this.handle(epInstructions, 'chatInstructions'); - this.handle(epAgents, 'chatAgents'); - this.handle(epSkills, 'chatSkills'); + this.handle(epPrompt, ChatContributionPoint.chatPromptFiles); + this.handle(epInstructions, ChatContributionPoint.chatInstructions); + this.handle(epAgents, ChatContributionPoint.chatAgents); + this.handle(epSkills, ChatContributionPoint.chatSkills); } private handle(extensionPoint: extensionsRegistry.IExtensionPoint, contributionPoint: ChatContributionPoint) { @@ -123,36 +126,3 @@ export class ChatPromptFilesExtensionPointHandler implements IWorkbenchContribut }); } } - -/** - * Result type for the extension prompt file provider command. - */ -export interface IExtensionPromptFileResult { - readonly uri: UriComponents; - readonly type: PromptsType; -} - -/** - * Register the command to list all extension-contributed prompt files. - */ -CommandsRegistry.registerCommand('_listExtensionPromptFiles', async (accessor): Promise => { - const promptsService = accessor.get(IPromptsService); - - // Get extension prompt files for all prompt types in parallel - const [agents, instructions, prompts, skills] = await Promise.all([ - promptsService.listPromptFiles(PromptsType.agent, CancellationToken.None), - promptsService.listPromptFiles(PromptsType.instructions, CancellationToken.None), - promptsService.listPromptFiles(PromptsType.prompt, CancellationToken.None), - promptsService.listPromptFiles(PromptsType.skill, CancellationToken.None), - ]); - - // Combine all files and collect extension-contributed ones - const result: IExtensionPromptFileResult[] = []; - for (const file of [...agents, ...instructions, ...prompts, ...skills]) { - if (file.storage === PromptsStorage.extension) { - result.push({ uri: file.uri.toJSON(), type: file.type }); - } - } - - return result; -}); From 11f7fff799e28db82ab02ad4e6d7212667c05de6 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 19 Jan 2026 20:09:15 +0100 Subject: [PATCH 216/387] refresh default account in regular intervals (#288962) --- src/vs/workbench/browser/web.main.ts | 2 +- .../browser/extensions.contribution.ts | 2 +- .../extensions/browser/extensionsViewlet.ts | 2 +- .../electron-browser/desktop.main.ts | 2 +- .../{common => browser}/defaultAccount.ts | 40 ++++++++++++++++--- .../accountPolicyService.test.ts | 2 +- .../multiplexPolicyService.test.ts | 2 +- src/vs/workbench/workbench.common.main.ts | 2 +- 8 files changed, 42 insertions(+), 12 deletions(-) rename src/vs/workbench/services/accounts/{common => browser}/defaultAccount.ts (94%) rename src/vs/workbench/services/policies/test/{common => browser}/accountPolicyService.test.ts (98%) rename src/vs/workbench/services/policies/test/{common => browser}/multiplexPolicyService.test.ts (99%) diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 1586cb4ca82..ec726447d1c 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -96,7 +96,7 @@ import { TunnelSource } from '../services/remote/common/tunnelModel.js'; import { mainWindow } from '../../base/browser/window.js'; import { INotificationService, Severity } from '../../platform/notification/common/notification.js'; import { IDefaultAccountService } from '../../platform/defaultAccount/common/defaultAccount.js'; -import { DefaultAccountService } from '../services/accounts/common/defaultAccount.js'; +import { DefaultAccountService } from '../services/accounts/browser/defaultAccount.js'; import { AccountPolicyService } from '../services/policies/common/accountPolicyService.js'; export class BrowserMain extends Disposable { diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index f6c294e4912..6956d7918b3 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -51,7 +51,7 @@ import { ResourceContextKey, WorkbenchStateContext } from '../../../common/conte import { IWorkbenchContribution, IWorkbenchContributionsRegistry, registerWorkbenchContribution2, Extensions as WorkbenchExtensions, WorkbenchPhase } from '../../../common/contributions.js'; import { EditorExtensions } from '../../../common/editor.js'; import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation } from '../../../common/views.js'; -import { DEFAULT_ACCOUNT_SIGN_IN_COMMAND } from '../../../services/accounts/common/defaultAccount.js'; +import { DEFAULT_ACCOUNT_SIGN_IN_COMMAND } from '../../../services/accounts/browser/defaultAccount.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { EnablementState, IExtensionManagementServerService, IPublisherInfo, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index a939a12a5a2..942a2f4b91b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -68,7 +68,7 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { IExtensionGalleryManifest, IExtensionGalleryManifestService, ExtensionGalleryManifestStatus } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js'; import { URI } from '../../../../base/common/uri.js'; -import { DEFAULT_ACCOUNT_SIGN_IN_COMMAND } from '../../../services/accounts/common/defaultAccount.js'; +import { DEFAULT_ACCOUNT_SIGN_IN_COMMAND } from '../../../services/accounts/browser/defaultAccount.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; export const ExtensionsSortByContext = new RawContextKey('extensionsSortByValue', ''); diff --git a/src/vs/workbench/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index 03bba5c4b30..30c2d80d574 100644 --- a/src/vs/workbench/electron-browser/desktop.main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -62,7 +62,7 @@ import { IConfigurationService } from '../../platform/configuration/common/confi import { applyZoom } from '../../platform/window/electron-browser/window.js'; import { mainWindow } from '../../base/browser/window.js'; import { IDefaultAccountService } from '../../platform/defaultAccount/common/defaultAccount.js'; -import { DefaultAccountService } from '../services/accounts/common/defaultAccount.js'; +import { DefaultAccountService } from '../services/accounts/browser/defaultAccount.js'; import { AccountPolicyService } from '../services/policies/common/accountPolicyService.js'; import { MultiplexPolicyService } from '../services/policies/common/multiplexPolicyService.js'; diff --git a/src/vs/workbench/services/accounts/common/defaultAccount.ts b/src/vs/workbench/services/accounts/browser/defaultAccount.ts similarity index 94% rename from src/vs/workbench/services/accounts/common/defaultAccount.ts rename to src/vs/workbench/services/accounts/browser/defaultAccount.ts index e6ab4456307..e45b4e10c3c 100644 --- a/src/vs/workbench/services/accounts/common/defaultAccount.ts +++ b/src/vs/workbench/services/accounts/browser/defaultAccount.ts @@ -13,7 +13,8 @@ import { IExtensionService } from '../../extensions/common/extensions.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; -import { Barrier, ThrottledDelayer, timeout } from '../../../../base/common/async.js'; +import { Barrier, RunOnceScheduler, ThrottledDelayer, timeout } from '../../../../base/common/async.js'; +import { IHostService } from '../../host/browser/host.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { getErrorMessage } from '../../../../base/common/errors.js'; import { IDefaultAccount, IDefaultAccountAuthenticationProvider, IEntitlementsData, IPolicyData } from '../../../../base/common/defaultAccount.js'; @@ -58,8 +59,8 @@ const enum DefaultAccountStatus { } const CONTEXT_DEFAULT_ACCOUNT_STATE = new RawContextKey('defaultAccountStatus', DefaultAccountStatus.Uninitialized); - const CACHED_POLICY_DATA_KEY = 'defaultAccount.cachedPolicyData'; +const ACCOUNT_DATA_POLL_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes interface ITokenEntitlementsResponse { token: string; @@ -201,6 +202,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid private initialized = false; private readonly initPromise: Promise; private readonly updateThrottler = this._register(new ThrottledDelayer(100)); + private readonly accountDataPollScheduler = this._register(new RunOnceScheduler(() => this.updateDefaultAccount(), ACCOUNT_DATA_POLL_INTERVAL_MS)); constructor( private readonly defaultAccountConfig: IDefaultAccountConfig, @@ -214,6 +216,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IContextKeyService contextKeyService: IContextKeyService, @IStorageService private readonly storageService: IStorageService, + @IHostService private readonly hostService: IHostService, ) { super(); this.accountStatusContext = CONTEXT_DEFAULT_ACCOUNT_STATE.bindTo(contextKeyService); @@ -284,6 +287,15 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid this.logService.debug('[DefaultAccount] Default account provider unregistered, updating default account'); this.updateDefaultAccount(); })); + + this._register(this.hostService.onDidChangeFocus(focused => { + if (focused && this._defaultAccount) { + // Update default account when window gets focused + this.accountDataPollScheduler.cancel(); + this.logService.debug('[DefaultAccount] Window focused, updating default account'); + this.updateDefaultAccount(); + } + })); } async refresh(): Promise { @@ -305,6 +317,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid try { const defaultAccount = await this.fetchDefaultAccount(); this.setDefaultAccount(defaultAccount); + this.scheduleAccountDataPoll(); } catch (error) { this.logService.error('[DefaultAccount] Error while updating default account', getErrorMessage(error)); } @@ -320,7 +333,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid return null; } - return await this.getDefaultAccountFromAuthenticatedSessions(defaultAccountProvider, this.defaultAccountConfig.authenticationProvider.scopes); + return await this.getDefaultAccountForAuthenticationProvider(defaultAccountProvider); } private setDefaultAccount(account: IDefaultAccount | null): void { @@ -335,11 +348,19 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid this.accountStatusContext.set(DefaultAccountStatus.Available); this.logService.debug('[DefaultAccount] Account status set to Available'); } else { + this.accountDataPollScheduler.cancel(); this.accountStatusContext.set(DefaultAccountStatus.Unavailable); this.logService.debug('[DefaultAccount] Account status set to Unavailable'); } } + private scheduleAccountDataPoll(): void { + if (!this._defaultAccount) { + return; + } + this.accountDataPollScheduler.schedule(ACCOUNT_DATA_POLL_INTERVAL_MS); + } + private extractFromToken(token: string): Map { const result = new Map(); const firstPart = token?.split(':')[0]; @@ -352,16 +373,25 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid return result; } - private async getDefaultAccountFromAuthenticatedSessions(authenticationProvider: IDefaultAccountAuthenticationProvider, scopes: string[][]): Promise { + private async getDefaultAccountForAuthenticationProvider(authenticationProvider: IDefaultAccountAuthenticationProvider): Promise { try { this.logService.debug('[DefaultAccount] Getting Default Account from authenticated sessions for provider:', authenticationProvider.id); - const sessions = await this.findMatchingProviderSession(authenticationProvider.id, scopes); + const sessions = await this.findMatchingProviderSession(authenticationProvider.id, this.defaultAccountConfig.authenticationProvider.scopes); if (!sessions?.length) { this.logService.debug('[DefaultAccount] No matching session found for provider:', authenticationProvider.id); return null; } + return this.getDefaultAccountFromAuthenticatedSessions(authenticationProvider, sessions); + } catch (error) { + this.logService.error('[DefaultAccount] Failed to get default account for provider:', authenticationProvider.id, getErrorMessage(error)); + return null; + } + } + + private async getDefaultAccountFromAuthenticatedSessions(authenticationProvider: IDefaultAccountAuthenticationProvider, sessions: AuthenticationSession[]): Promise { + try { const accountId = sessions[0].account.id; const [entitlementsData, tokenEntitlementsData] = await Promise.all([ this.getEntitlements(sessions), diff --git a/src/vs/workbench/services/policies/test/common/accountPolicyService.test.ts b/src/vs/workbench/services/policies/test/browser/accountPolicyService.test.ts similarity index 98% rename from src/vs/workbench/services/policies/test/common/accountPolicyService.test.ts rename to src/vs/workbench/services/policies/test/browser/accountPolicyService.test.ts index 73b8f9eff67..555a62494c0 100644 --- a/src/vs/workbench/services/policies/test/common/accountPolicyService.test.ts +++ b/src/vs/workbench/services/policies/test/browser/accountPolicyService.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { Event } from '../../../../../base/common/event.js'; import { NullLogService } from '../../../../../platform/log/common/log.js'; import { IDefaultAccountProvider, IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; -import { DefaultAccountService } from '../../../accounts/common/defaultAccount.js'; +import { DefaultAccountService } from '../../../accounts/browser/defaultAccount.js'; import { AccountPolicyService } from '../../common/accountPolicyService.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; diff --git a/src/vs/workbench/services/policies/test/common/multiplexPolicyService.test.ts b/src/vs/workbench/services/policies/test/browser/multiplexPolicyService.test.ts similarity index 99% rename from src/vs/workbench/services/policies/test/common/multiplexPolicyService.test.ts rename to src/vs/workbench/services/policies/test/browser/multiplexPolicyService.test.ts index 481e3a4e795..49c631ca49b 100644 --- a/src/vs/workbench/services/policies/test/common/multiplexPolicyService.test.ts +++ b/src/vs/workbench/services/policies/test/browser/multiplexPolicyService.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { Event } from '../../../../../base/common/event.js'; import { NullLogService } from '../../../../../platform/log/common/log.js'; import { IDefaultAccountProvider, IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; -import { DefaultAccountService } from '../../../accounts/common/defaultAccount.js'; +import { DefaultAccountService } from '../../../accounts/browser/defaultAccount.js'; import { AccountPolicyService } from '../../common/accountPolicyService.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 3159df0f6ce..f802122ef4b 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -186,7 +186,7 @@ registerSingleton(IAllowedMcpServersService, AllowedMcpServersService, Instantia //#region --- workbench contributions // Default Account -import './services/accounts/common/defaultAccount.js'; +import './services/accounts/browser/defaultAccount.js'; // Telemetry import './contrib/telemetry/browser/telemetry.contribution.js'; From a320e1230dde778cc68962948d15df11cbf6bbbd Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 19 Jan 2026 20:23:53 +0100 Subject: [PATCH 217/387] enable lm management editor for business and enterprise (#288966) * enable lm management editor for business and enterprise * show for internal --- .../chatManagement/chatManagement.contribution.ts | 2 ++ .../chat/browser/chatManagement/chatModelsWidget.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagement.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagement.contribution.ts index 9258a8f280c..df2a0d58e1e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagement.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagement.contribution.ts @@ -110,6 +110,8 @@ registerAction2(class extends Action2 { ChatContextKeys.Entitlement.planFree, ChatContextKeys.Entitlement.planPro, ChatContextKeys.Entitlement.planProPlus, + ChatContextKeys.Entitlement.planBusiness, + ChatContextKeys.Entitlement.planEnterprise, ChatContextKeys.Entitlement.internal )), f1: true, diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts index c7a62628b10..bd8ffdb7f7b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts @@ -1170,8 +1170,13 @@ export class ChatModelsWidget extends Disposable { const configurableVendors = this.languageModelsService.getVendors().filter(vendor => vendor.managementCommand || vendor.configuration); - const hasPlan = this.chatEntitlementService.entitlement !== ChatEntitlement.Unknown && this.chatEntitlementService.entitlement !== ChatEntitlement.Available; - this.addButton.enabled = hasPlan && configurableVendors.length > 0; + const entitlement = this.chatEntitlementService.entitlement; + const supportsAddingModels = this.chatEntitlementService.isInternal + || (entitlement !== ChatEntitlement.Unknown + && entitlement !== ChatEntitlement.Available + && entitlement !== ChatEntitlement.Business + && entitlement !== ChatEntitlement.Enterprise); + this.addButton.enabled = supportsAddingModels && configurableVendors.length > 0; this.dropdownActions = configurableVendors.map(vendor => toAction({ id: `enable-${vendor.vendor}`, From a6c1a0c19bbf568bf6c364fb28ef7b5db5ab231e Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 19 Jan 2026 20:25:30 +0100 Subject: [PATCH 218/387] Simplify code --- .../controller/editContext/clipboardUtils.ts | 113 +++++++----------- .../editContext/native/nativeEditContext.ts | 18 ++- .../textArea/textAreaEditContextInput.ts | 18 +-- .../browser/copyPasteController.ts | 45 ++++--- 4 files changed, 78 insertions(+), 116 deletions(-) diff --git a/src/vs/editor/browser/controller/editContext/clipboardUtils.ts b/src/vs/editor/browser/controller/editContext/clipboardUtils.ts index 313966b6828..96afd175c85 100644 --- a/src/vs/editor/browser/controller/editContext/clipboardUtils.ts +++ b/src/vs/editor/browser/controller/editContext/clipboardUtils.ts @@ -10,6 +10,8 @@ import { ViewContext } from '../../../common/viewModel/viewContext.js'; import { ILogService, LogLevel } from '../../../../platform/log/common/log.js'; import { EditorOption, IComputedEditorOptions } from '../../../common/config/editorOptions.js'; import { generateUuid } from '../../../../base/common/uuid.js'; +import { VSDataTransfer } from '../../../../base/common/dataTransfer.js'; +import { toExternalVSDataTransfer } from '../../dataTransfer.js'; export function ensureClipboardGetsEditorSelection(e: ClipboardEvent, context: ViewContext, logService: ILogService, isFirefox: boolean): void { const viewModel = context.viewModel; @@ -139,7 +141,7 @@ interface InMemoryClipboardMetadata { export const ClipboardEventUtils = { - getTextData(clipboardData: DataTransfer): [string, ClipboardStoredMetadata | null] { + getTextData(clipboardData: IReadableClipboardData | DataTransfer): [string, ClipboardStoredMetadata | null] { const text = clipboardData.getData(Mimes.text); let metadata: ClipboardStoredMetadata | null = null; const rawmetadata = clipboardData.getData('vscode-editor-data'); @@ -161,7 +163,7 @@ export const ClipboardEventUtils = { return [text, metadata]; }, - setTextData(clipboardData: DataTransfer, text: string, html: string | null | undefined, metadata: ClipboardStoredMetadata): void { + setTextData(clipboardData: IWritableClipboardData, text: string, html: string | null | undefined, metadata: ClipboardStoredMetadata): void { clipboardData.setData(Mimes.text, text); if (typeof html === 'string') { clipboardData.setData('text/html', html); @@ -171,29 +173,13 @@ export const ClipboardEventUtils = { }; /** - * Abstracted clipboard data that does not directly expose DOM ClipboardEvent/DataTransfer. - * This allows editor contributions to work with clipboard data without DOM dependencies. + * Readable clipboard data for paste operations. */ -export interface IClipboardData { - /** - * The text content from the clipboard. - */ - readonly text: string; - - /** - * The HTML content from the clipboard, if available. - */ - readonly html: string | undefined; - - /** - * VS Code editor metadata associated with this clipboard data. - */ - readonly metadata: ClipboardStoredMetadata | null; - +export interface IReadableClipboardData { /** * All MIME types present in the clipboard. */ - readonly types: readonly string[]; + types: string[]; /** * Files from the clipboard (for paste operations). @@ -209,7 +195,7 @@ export interface IClipboardData { /** * Writable clipboard data for copy/cut operations. */ -export interface IWritableClipboardData extends IClipboardData { +export interface IWritableClipboardData { /** * Set data for a specific MIME type. */ @@ -248,7 +234,17 @@ export interface IClipboardPasteEvent { /** * The clipboard data being pasted. */ - readonly clipboardData: IClipboardData; + readonly clipboardData: IReadableClipboardData; + + /** + * The metadata stored alongside the clipboard data, if any. + */ + readonly metadata: ClipboardStoredMetadata | null; + + /** + * The text content being pasted. + */ + readonly text: string; /** * The underlying DOM event, if available. @@ -256,6 +252,8 @@ export interface IClipboardPasteEvent { */ readonly browserEvent: ClipboardEvent | undefined; + toExternalVSDataTransfer(): VSDataTransfer | undefined; + /** * Signal that the event has been handled and default processing should be skipped. */ @@ -267,35 +265,6 @@ export interface IClipboardPasteEvent { readonly isHandled: boolean; } -/** - * Creates an IClipboardData from a DOM DataTransfer. - */ -export function createClipboardData(dataTransfer: DataTransfer): IClipboardData { - const [text, metadata] = ClipboardEventUtils.getTextData(dataTransfer); - const html = dataTransfer.getData('text/html') || undefined; - const files: File[] = Array.prototype.slice.call(dataTransfer.files, 0); - - return { - text, - html, - metadata, - types: Array.from(dataTransfer.types), - files, - getData: (type: string) => dataTransfer.getData(type), - }; -} - -/** - * Creates an IWritableClipboardData from a DOM DataTransfer. - */ -export function createWritableClipboardData(dataTransfer: DataTransfer): IWritableClipboardData { - const base = createClipboardData(dataTransfer); - return { - ...base, - setData: (type: string, value: string) => dataTransfer.setData(type, value), - }; -} - /** * Creates an IClipboardCopyEvent from a DOM ClipboardEvent. */ @@ -303,14 +272,10 @@ export function createClipboardCopyEvent(e: ClipboardEvent, isCut: boolean): ICl let handled = false; return { isCut, - clipboardData: e.clipboardData ? createWritableClipboardData(e.clipboardData) : { - text: '', - html: undefined, - metadata: null, - types: [], - files: [], - getData: () => '', - setData: () => { }, + clipboardData: { + setData: (type: string, value: string) => { + e.clipboardData?.setData(type, value); + }, }, setHandled: () => { handled = true; @@ -326,15 +291,13 @@ export function createClipboardCopyEvent(e: ClipboardEvent, isCut: boolean): ICl */ export function createClipboardPasteEvent(e: ClipboardEvent): IClipboardPasteEvent { let handled = false; + let [text, metadata] = e.clipboardData ? ClipboardEventUtils.getTextData(e.clipboardData) : ['', null]; + metadata = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text); return { - clipboardData: e.clipboardData ? createClipboardData(e.clipboardData) : { - text: '', - html: undefined, - metadata: null, - types: [], - files: [], - getData: () => '', - }, + clipboardData: createReadableClipboardData(e.clipboardData), + metadata, + text, + toExternalVSDataTransfer: () => e.clipboardData ? toExternalVSDataTransfer(e.clipboardData) : undefined, browserEvent: e, setHandled: () => { handled = true; @@ -344,3 +307,17 @@ export function createClipboardPasteEvent(e: ClipboardEvent): IClipboardPasteEve get isHandled() { return handled; }, }; } + +export function createReadableClipboardData(dataTransfer: DataTransfer | undefined | null): IReadableClipboardData { + return { + types: Array.from(dataTransfer?.types ?? []), + files: Array.prototype.slice.call(dataTransfer?.files ?? [], 0), + getData: (type: string) => dataTransfer?.getData(type) ?? '', + }; +} + +export function createWritableClipboardData(dataTransfer: DataTransfer | undefined | null): IWritableClipboardData { + return { + setData: (type: string, value: string) => dataTransfer?.setData(type, value), + }; +} diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index 0ccadb7d8bd..f1c27d90805 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -16,7 +16,7 @@ import { ViewConfigurationChangedEvent, ViewCursorStateChangedEvent, ViewDecorat import { ViewContext } from '../../../../common/viewModel/viewContext.js'; import { RestrictedRenderingContext, RenderingContext, HorizontalPosition } from '../../../view/renderingContext.js'; import { ViewController } from '../../../view/viewController.js'; -import { ClipboardEventUtils, createClipboardCopyEvent, createClipboardPasteEvent, ensureClipboardGetsEditorSelection, InMemoryClipboardMetadataManager } from '../clipboardUtils.js'; +import { createClipboardCopyEvent, createClipboardPasteEvent, ensureClipboardGetsEditorSelection } from '../clipboardUtils.js'; import { AbstractEditContext } from '../editContext.js'; import { editContextAddDisposableListener, FocusTracker, ITypeData } from './nativeEditContextUtils.js'; import { ScreenReaderSupport } from './screenReaderSupport.js'; @@ -161,24 +161,22 @@ export class NativeEditContext extends AbstractEditContext { if (!e.clipboardData) { return; } - let [text, metadata] = ClipboardEventUtils.getTextData(e.clipboardData); - this.logService.trace('NativeEditContext#paste with id : ', metadata?.id, ' with text.length: ', text.length); - if (!text) { + this.logService.trace('NativeEditContext#paste with id : ', pasteEvent.metadata?.id, ' with text.length: ', pasteEvent.text.length); + if (!pasteEvent.text) { return; } - metadata = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text); let pasteOnNewLine = false; let multicursorText: string[] | null = null; let mode: string | null = null; - if (metadata) { + if (pasteEvent.metadata) { const options = this._context.configuration.options; const emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard); - pasteOnNewLine = emptySelectionClipboard && !!metadata.isFromEmptySelection; - multicursorText = typeof metadata.multicursorText !== 'undefined' ? metadata.multicursorText : null; - mode = metadata.mode; + pasteOnNewLine = emptySelectionClipboard && !!pasteEvent.metadata.isFromEmptySelection; + multicursorText = typeof pasteEvent.metadata.multicursorText !== 'undefined' ? pasteEvent.metadata.multicursorText : null; + mode = pasteEvent.metadata.mode; } this.logService.trace('NativeEditContext#paste (before viewController.paste)'); - this._viewController.paste(text, pasteOnNewLine, multicursorText, mode); + this._viewController.paste(pasteEvent.text, pasteOnNewLine, multicursorText, mode); })); // Edit context events diff --git a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts index a3e1d8a5b3c..04bd4419162 100644 --- a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts +++ b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts @@ -18,7 +18,7 @@ import { Position } from '../../../../common/core/position.js'; import { Selection } from '../../../../common/core/selection.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; -import { ClipboardEventUtils, ClipboardStoredMetadata, createClipboardCopyEvent, createClipboardPasteEvent, ensureClipboardGetsEditorSelection, IClipboardCopyEvent, IClipboardPasteEvent, InMemoryClipboardMetadataManager } from '../clipboardUtils.js'; +import { ClipboardStoredMetadata, createClipboardCopyEvent, createClipboardPasteEvent, ensureClipboardGetsEditorSelection, IClipboardCopyEvent, IClipboardPasteEvent, InMemoryClipboardMetadataManager } from '../clipboardUtils.js'; import { _debugComposition, ITextAreaWrapper, ITypeData, TextAreaState } from './textAreaEditContextState.js'; import { ViewContext } from '../../../../common/viewModel/viewContext.js'; @@ -420,23 +420,15 @@ export class TextAreaInput extends Disposable { e.preventDefault(); - if (!e.clipboardData) { + this._logService.trace(`TextAreaInput#onPaste with id : `, pasteEvent.metadata?.id, ' with text.length: ', pasteEvent.text.length); + if (!pasteEvent.text) { return; } - let [text, metadata] = ClipboardEventUtils.getTextData(e.clipboardData); - this._logService.trace(`TextAreaInput#onPaste with id : `, metadata?.id, ' with text.length: ', text.length); - if (!text) { - return; - } - - // try the in-memory store - metadata = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text); - this._logService.trace(`TextAreaInput#onPaste (before onPaste)`); this._onPaste.fire({ - text: text, - metadata: metadata + text: pasteEvent.text, + metadata: pasteEvent.metadata }); })); diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index f5c8fb37aee..d2ed77fb5a4 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -24,8 +24,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; -import { ClipboardEventUtils, IClipboardCopyEvent, IClipboardPasteEvent, InMemoryClipboardMetadataManager, IWritableClipboardData } from '../../../browser/controller/editContext/clipboardUtils.js'; -import { toExternalVSDataTransfer } from '../../../browser/dataTransfer.js'; +import { IClipboardCopyEvent, IClipboardPasteEvent, IWritableClipboardData } from '../../../browser/controller/editContext/clipboardUtils.js'; import { ICodeEditor, PastePayload } from '../../../browser/editorBrowser.js'; import { IBulkEditService } from '../../../browser/services/bulkEditService.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; @@ -247,18 +246,18 @@ export class CopyPasteController extends Disposable implements IEditorContributi } private async handlePaste(e: IClipboardPasteEvent) { - const clipboardData = e.browserEvent?.clipboardData; - if (clipboardData) { - const [text, metadata] = ClipboardEventUtils.getTextData(clipboardData); - const metadataComputed = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text); - this._logService.trace('CopyPasteController#handlePaste for id : ', metadataComputed?.id); - } else { - this._logService.trace('CopyPasteController#handlePaste'); - } - if (!clipboardData || !this._editor.hasTextFocus()) { + this._logService.trace('CopyPasteController#handlePaste for id : ', e.metadata?.id); + + if (!this._editor.hasTextFocus()) { return; } + const dataTransfer = e.toExternalVSDataTransfer(); + if (!dataTransfer) { + return; + } + dataTransfer.delete(vscodeClipboardMime); + MessageController.get(this._editor)?.closeMessage(); this._currentPasteOperation?.cancel(); this._currentPasteOperation = undefined; @@ -276,15 +275,13 @@ export class CopyPasteController extends Disposable implements IEditorContributi return; } - const metadata = this.fetchCopyMetadata(clipboardData); - this._logService.trace('CopyPasteController#handlePaste with metadata : ', metadata?.id, ' and text.length : ', clipboardData.getData('text/plain').length); - const dataTransfer = toExternalVSDataTransfer(clipboardData); - dataTransfer.delete(vscodeClipboardMime); + const metadata = this.fetchCopyMetadata(e); + this._logService.trace('CopyPasteController#handlePaste with metadata : ', metadata?.id, ' and text.length : ', e.clipboardData.getData('text/plain').length); - const fileTypes = Array.from(clipboardData.files).map(file => file.type); + const fileTypes = Array.from(e.clipboardData.files).map(file => file.type); const allPotentialMimeTypes = [ - ...clipboardData.types, + ...e.clipboardData.types, ...fileTypes, ...metadata?.providerCopyMimeTypes ?? [], // TODO: always adds `uri-list` because this get set if there are resources in the system clipboard. @@ -551,11 +548,11 @@ export class CopyPasteController extends Disposable implements IEditorContributi clipboardData.setData(vscodeClipboardMime, JSON.stringify(metadata)); } - private fetchCopyMetadata(clipboardData: DataTransfer): CopyMetadata | undefined { + private fetchCopyMetadata(e: IClipboardPasteEvent): CopyMetadata | undefined { this._logService.trace('CopyPasteController#fetchCopyMetadata'); // Prefer using the clipboard data we saved off - const rawMetadata = clipboardData.getData(vscodeClipboardMime); + const rawMetadata = e.clipboardData.getData(vscodeClipboardMime); if (rawMetadata) { try { return JSON.parse(rawMetadata); @@ -564,14 +561,12 @@ export class CopyPasteController extends Disposable implements IEditorContributi } } - // Otherwise try to extract the generic text editor metadata - const [_, metadata] = ClipboardEventUtils.getTextData(clipboardData); - if (metadata) { + if (e.metadata) { return { defaultPastePayload: { - mode: metadata.mode, - multicursorText: metadata.multicursorText ?? null, - pasteOnNewLine: !!metadata.isFromEmptySelection, + mode: e.metadata.mode, + multicursorText: e.metadata.multicursorText ?? null, + pasteOnNewLine: !!e.metadata.isFromEmptySelection, }, }; } From 7f460a4ad8472d53c28725e486954b0d1172efbf Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 19 Jan 2026 20:43:48 +0100 Subject: [PATCH 219/387] Explicitly archiving all sessions from Sessions view context menu should not show dialog (fix #288910) (#288922) --- .../agentSessions/agentSessionsActions.ts | 59 +++++++++++++------ 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts index 5154b6dcd23..8543fc206bd 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts @@ -34,6 +34,7 @@ import { IQuickInputService } from '../../../../../platform/quickinput/common/qu import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { coalesce } from '../../../../../base/common/arrays.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; //#region Chat View @@ -255,6 +256,8 @@ export class ArchiveAllAgentSessionsAction extends Action2 { } } +const ConfirmArchiveStorageKey = 'chat.sessions.confirmArchive'; + export class ArchiveAgentSessionSectionAction extends Action2 { constructor() { @@ -282,17 +285,28 @@ export class ArchiveAgentSessionSectionAction extends Action2 { } const dialogService = accessor.get(IDialogService); + const storageService = accessor.get(IStorageService); - const confirmed = await dialogService.confirm({ - message: context.sessions.length === 1 - ? localize('archiveSectionSessions.confirmSingle', "Are you sure you want to archive 1 agent session from '{0}'?", context.label) - : localize('archiveSectionSessions.confirm', "Are you sure you want to archive {0} agent sessions from '{1}'?", context.sessions.length, context.label), - detail: localize('archiveSectionSessions.detail', "You can unarchive sessions later if needed from the sessions view."), - primaryButton: localize('archiveSectionSessions.archive', "Archive All") - }); + const skipConfirmation = storageService.getBoolean(ConfirmArchiveStorageKey, StorageScope.PROFILE, false); + if (!skipConfirmation) { + const confirmed = await dialogService.confirm({ + message: context.sessions.length === 1 + ? localize('archiveSectionSessions.confirmSingle', "Are you sure you want to archive 1 agent session from '{0}'?", context.label) + : localize('archiveSectionSessions.confirm', "Are you sure you want to archive {0} agent sessions from '{1}'?", context.sessions.length, context.label), + detail: localize('archiveSectionSessions.detail', "You can unarchive sessions later if needed from the sessions view."), + primaryButton: localize('archiveSectionSessions.archive', "Archive All"), + checkbox: { + label: localize('doNotAskAgain', "Do not ask me again") + } + }); - if (!confirmed.confirmed) { - return; + if (!confirmed.confirmed) { + return; + } + + if (confirmed.checkboxChecked) { + storageService.store(ConfirmArchiveStorageKey, true, StorageScope.PROFILE, StorageTarget.USER); + } } for (const session of context.sessions) { @@ -328,16 +342,27 @@ export class UnarchiveAgentSessionSectionAction extends Action2 { } const dialogService = accessor.get(IDialogService); + const storageService = accessor.get(IStorageService); - const confirmed = await dialogService.confirm({ - message: context.sessions.length === 1 - ? localize('unarchiveSectionSessions.confirmSingle', "Are you sure you want to unarchive 1 agent session?") - : localize('unarchiveSectionSessions.confirm', "Are you sure you want to unarchive {0} agent sessions?", context.sessions.length), - primaryButton: localize('unarchiveSectionSessions.unarchive', "Unarchive All") - }); + const skipConfirmation = storageService.getBoolean(ConfirmArchiveStorageKey, StorageScope.PROFILE, false); + if (!skipConfirmation) { + const confirmed = await dialogService.confirm({ + message: context.sessions.length === 1 + ? localize('unarchiveSectionSessions.confirmSingle', "Are you sure you want to unarchive 1 agent session?") + : localize('unarchiveSectionSessions.confirm', "Are you sure you want to unarchive {0} agent sessions?", context.sessions.length), + primaryButton: localize('unarchiveSectionSessions.unarchive', "Unarchive All"), + checkbox: { + label: localize('doNotAskAgain', "Do not ask me again") + } + }); - if (!confirmed.confirmed) { - return; + if (!confirmed.confirmed) { + return; + } + + if (confirmed.checkboxChecked) { + storageService.store(ConfirmArchiveStorageKey, true, StorageScope.PROFILE, StorageTarget.USER); + } } for (const session of context.sessions) { From 583dc8b7c0205b7d548eddc8624a205744ed7369 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 19 Jan 2026 20:57:18 +0100 Subject: [PATCH 220/387] agent sessions - expand archived when searching (#288918) * agent sessions - expand archived when searching * Update src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../browser/agentSessions/agentSessionsControl.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index d42dc7c778c..da55fc44340 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -64,6 +64,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo get element(): HTMLElement | undefined { return this.sessionsContainer; } private sessionsList: WorkbenchCompressibleAsyncDataTree | undefined; + private sessionsListFindIsOpen = false; private visible: boolean = true; @@ -200,6 +201,12 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo const selection = list.getSelection().filter(isAgentSession); this.hasMultipleAgentSessionsSelectedContextKey.set(selection.length > 1); })); + + this._register(list.onDidChangeFindOpenState(open => { + this.sessionsListFindIsOpen = open; + + this.updateArchivedSectionCollapseState(); + })); } private async openAgentSession(e: IOpenEvent): Promise { @@ -289,7 +296,10 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo continue; } - const shouldCollapseArchived = this.options.filter.getExcludes().archived; + const shouldCollapseArchived = + !this.sessionsListFindIsOpen && // always expand when find is open + this.options.filter.getExcludes().archived; // only collapse when archived are excluded from filter + if (shouldCollapseArchived && !child.collapsed) { this.sessionsList.collapse(child.element); } else if (!shouldCollapseArchived && child.collapsed) { From ce96577c8eb33025b8208b98c3e2c7c97135d974 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 19 Jan 2026 22:09:15 +0100 Subject: [PATCH 221/387] Further code simplification --- .../controller/editContext/clipboardUtils.ts | 21 ++++++----- src/vs/editor/common/viewModel.ts | 5 ++- .../editor/common/viewModel/viewModelImpl.ts | 37 ++++++++++++------- .../contrib/clipboard/browser/clipboard.ts | 2 +- .../browser/copyPasteController.ts | 29 ++++----------- .../browser/viewModel/viewModelImpl.test.ts | 4 +- 6 files changed, 49 insertions(+), 49 deletions(-) diff --git a/src/vs/editor/browser/controller/editContext/clipboardUtils.ts b/src/vs/editor/browser/controller/editContext/clipboardUtils.ts index 96afd175c85..4b8069c2df3 100644 --- a/src/vs/editor/browser/controller/editContext/clipboardUtils.ts +++ b/src/vs/editor/browser/controller/editContext/clipboardUtils.ts @@ -8,20 +8,19 @@ import { isWindows } from '../../../../base/common/platform.js'; import { Mimes } from '../../../../base/common/mime.js'; import { ViewContext } from '../../../common/viewModel/viewContext.js'; import { ILogService, LogLevel } from '../../../../platform/log/common/log.js'; -import { EditorOption, IComputedEditorOptions } from '../../../common/config/editorOptions.js'; +import { EditorOption } from '../../../common/config/editorOptions.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { VSDataTransfer } from '../../../../base/common/dataTransfer.js'; import { toExternalVSDataTransfer } from '../../dataTransfer.js'; export function ensureClipboardGetsEditorSelection(e: ClipboardEvent, context: ViewContext, logService: ILogService, isFirefox: boolean): void { const viewModel = context.viewModel; - const options = context.configuration.options; let id: string | undefined = undefined; if (logService.getLevel() === LogLevel.Trace) { id = generateUuid(); } - const { dataToCopy, storedMetadata } = generateDataToCopyAndStoreInMemory(viewModel, options, id, isFirefox); + const { dataToCopy, storedMetadata } = generateDataToCopyAndStoreInMemory(viewModel, id, isFirefox); // !!!!! // This is a workaround for what we think is an Electron bug where @@ -37,9 +36,9 @@ export function ensureClipboardGetsEditorSelection(e: ClipboardEvent, context: V logService.trace('ensureClipboardGetsEditorSelection with id : ', id, ' with text.length: ', dataToCopy.text.length); } -export function generateDataToCopyAndStoreInMemory(viewModel: IViewModel, options: IComputedEditorOptions, id: string | undefined, isFirefox: boolean) { - const emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard); - const copyWithSyntaxHighlighting = options.get(EditorOption.copyWithSyntaxHighlighting); +export function generateDataToCopyAndStoreInMemory(viewModel: IViewModel, id: string | undefined, isFirefox: boolean) { + const emptySelectionClipboard = viewModel.getEditorOption(EditorOption.emptySelectionClipboard); + const copyWithSyntaxHighlighting = viewModel.getEditorOption(EditorOption.copyWithSyntaxHighlighting); const selections = viewModel.getCursorStates().map(cursorState => cursorState.modelState.selection); const dataToCopy = getDataToCopy(viewModel, selections, emptySelectionClipboard, copyWithSyntaxHighlighting); const storedMetadata: ClipboardStoredMetadata = { @@ -59,16 +58,16 @@ export function generateDataToCopyAndStoreInMemory(viewModel: IViewModel, option } function getDataToCopy(viewModel: IViewModel, modelSelections: Range[], emptySelectionClipboard: boolean, copyWithSyntaxHighlighting: boolean): ClipboardDataToCopy { - const rawTextToCopy = viewModel.getPlainTextToCopy(modelSelections, emptySelectionClipboard, isWindows); + const { sourceRanges, sourceText } = viewModel.getPlainTextToCopy(modelSelections, emptySelectionClipboard, isWindows); const newLineCharacter = viewModel.model.getEOL(); const isFromEmptySelection = (emptySelectionClipboard && modelSelections.length === 1 && modelSelections[0].isEmpty()); - const multicursorText = (Array.isArray(rawTextToCopy) ? rawTextToCopy : null); - const text = (Array.isArray(rawTextToCopy) ? rawTextToCopy.join(newLineCharacter) : rawTextToCopy); + const multicursorText = (Array.isArray(sourceText) ? sourceText : null); + const text = (Array.isArray(sourceText) ? sourceText.join(newLineCharacter) : sourceText); let html: string | null | undefined = undefined; let mode: string | null = null; - if (CopyOptions.forceCopyWithSyntaxHighlighting || (copyWithSyntaxHighlighting && text.length < 65536)) { + if (CopyOptions.forceCopyWithSyntaxHighlighting || (copyWithSyntaxHighlighting && sourceText.length < 65536)) { const richText = viewModel.getRichTextToCopy(modelSelections, emptySelectionClipboard); if (richText) { html = richText.html; @@ -77,6 +76,7 @@ function getDataToCopy(viewModel: IViewModel, modelSelections: Range[], emptySel } const dataToCopy: ClipboardDataToCopy = { isFromEmptySelection, + sourceRanges, multicursorText, text, html, @@ -115,6 +115,7 @@ export class InMemoryClipboardMetadataManager { export interface ClipboardDataToCopy { isFromEmptySelection: boolean; + sourceRanges: Range[]; multicursorText: string[] | null | undefined; text: string; html: string | null | undefined; diff --git a/src/vs/editor/common/viewModel.ts b/src/vs/editor/common/viewModel.ts index 4c1aeff7482..6b25f3fbe61 100644 --- a/src/vs/editor/common/viewModel.ts +++ b/src/vs/editor/common/viewModel.ts @@ -21,6 +21,7 @@ import { IViewLineTokens } from './tokens/lineTokens.js'; import { ViewEventHandler } from './viewEventHandler.js'; import { VerticalRevealType } from './viewEvents.js'; import { InlineDecoration, SingleLineInlineDecoration } from './viewModel/inlineDecorations.js'; +import { EditorOption, FindComputedEditorOptionValueById } from './config/editorOptions.js'; export interface IViewModel extends ICursorSimpleModel, ISimpleModel { @@ -37,6 +38,8 @@ export interface IViewModel extends ICursorSimpleModel, ISimpleModel { addViewEventHandler(eventHandler: ViewEventHandler): void; removeViewEventHandler(eventHandler: ViewEventHandler): void; + getEditorOption(id: T): FindComputedEditorOptionValueById; + /** * Gives a hint that a lot of requests are about to come in for these line numbers. */ @@ -79,7 +82,7 @@ export interface IViewModel extends ICursorSimpleModel, ISimpleModel { getInjectedTextAt(viewPosition: Position): InjectedText | null; deduceModelPositionRelativeToViewPosition(viewAnchorPosition: Position, deltaOffset: number, lineFeedCnt: number): Position; - getPlainTextToCopy(modelRanges: Range[], emptySelectionClipboard: boolean, forceCRLF: boolean): string | string[]; + getPlainTextToCopy(modelRanges: Range[], emptySelectionClipboard: boolean, forceCRLF: boolean): { sourceRanges: Range[]; sourceText: string | string[] }; getRichTextToCopy(modelRanges: Range[], emptySelectionClipboard: boolean): { html: string; mode: string } | null; createLineBreaksComputer(): ILineBreaksComputer; diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 8e5564d466e..a6a89aee06c 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -10,7 +10,7 @@ import { Event } from '../../../base/common/event.js'; import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; import * as platform from '../../../base/common/platform.js'; import * as strings from '../../../base/common/strings.js'; -import { ConfigurationChangedEvent, EditorOption, filterValidationDecorations, filterFontDecorations } from '../config/editorOptions.js'; +import { ConfigurationChangedEvent, EditorOption, filterValidationDecorations, filterFontDecorations, FindComputedEditorOptionValueById } from '../config/editorOptions.js'; import { EDITOR_FONT_DEFAULTS } from '../config/fontInfo.js'; import { CursorsController } from '../cursor/cursor.js'; import { CursorConfiguration, CursorState, EditOperationType, IColumnSelectData, PartialCursorState } from '../cursorCommon.js'; @@ -178,6 +178,10 @@ export class ViewModel extends Disposable implements IViewModel { this._eventDispatcher.dispose(); } + public getEditorOption(id: T): FindComputedEditorOptionValueById { + return this._configuration.options.get(id); + } + public createLineBreaksComputer(): ILineBreaksComputer { return this._lines.createLineBreaksComputer(); } @@ -973,7 +977,7 @@ export class ViewModel extends Disposable implements IViewModel { return this.model.getPositionAt(resultOffset); } - public getPlainTextToCopy(modelRanges: Range[], emptySelectionClipboard: boolean, forceCRLF: boolean): string | string[] { + public getPlainTextToCopy(modelRanges: Range[], emptySelectionClipboard: boolean, forceCRLF: boolean): { sourceRanges: Range[]; sourceText: string | string[] } { const newLineCharacter = forceCRLF ? '\r\n' : this.model.getEOL(); modelRanges = modelRanges.slice(0); @@ -991,34 +995,39 @@ export class ViewModel extends Disposable implements IViewModel { if (!hasNonEmptyRange && !emptySelectionClipboard) { // all ranges are empty - return ''; + return { sourceRanges: [], sourceText: '' }; } + const ranges: Range[] = []; + const result: string[] = []; + const pushRange = (modelRange: Range, append: string = '') => { + ranges.push(modelRange); + result.push(this.model.getValueInRange(modelRange, forceCRLF ? EndOfLinePreference.CRLF : EndOfLinePreference.TextDefined) + append); + }; + if (hasEmptyRange && emptySelectionClipboard) { // some (maybe all) empty selections - const result: string[] = []; let prevModelLineNumber = 0; for (const modelRange of modelRanges) { const modelLineNumber = modelRange.startLineNumber; if (modelRange.isEmpty()) { if (modelLineNumber !== prevModelLineNumber) { - result.push(this.model.getLineContent(modelLineNumber) + newLineCharacter); + pushRange(new Range(modelLineNumber, this.model.getLineMinColumn(modelLineNumber), modelLineNumber, this.model.getLineMaxColumn(modelLineNumber)), newLineCharacter); } } else { - result.push(this.model.getValueInRange(modelRange, forceCRLF ? EndOfLinePreference.CRLF : EndOfLinePreference.TextDefined)); + pushRange(modelRange); } prevModelLineNumber = modelLineNumber; } - return result.length === 1 ? result[0] : result; - } - - const result: string[] = []; - for (const modelRange of modelRanges) { - if (!modelRange.isEmpty()) { - result.push(this.model.getValueInRange(modelRange, forceCRLF ? EndOfLinePreference.CRLF : EndOfLinePreference.TextDefined)); + } else { + for (const modelRange of modelRanges) { + if (!modelRange.isEmpty()) { + pushRange(modelRange); + } } } - return result.length === 1 ? result[0] : result; + + return { sourceRanges: [], sourceText: result.length === 1 ? result[0] : result }; } public getRichTextToCopy(modelRanges: Range[], emptySelectionClipboard: boolean): { html: string; mode: string } | null { diff --git a/src/vs/editor/contrib/clipboard/browser/clipboard.ts b/src/vs/editor/contrib/clipboard/browser/clipboard.ts index c492206c2f2..376399368a6 100644 --- a/src/vs/editor/contrib/clipboard/browser/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/browser/clipboard.ts @@ -203,7 +203,7 @@ function executeClipboardCopyWithWorkaround(editor: IActiveCodeEditor, clipboard // We have encountered the Electron bug! // As a workaround, we will write (only the plaintext data) to the clipboard in a different way // We will use the clipboard service (which in the native case will go to electron's clipboard API) - const { dataToCopy } = generateDataToCopyAndStoreInMemory(editor._getViewModel(), editor.getOptions(), undefined, browser.isFirefox); + const { dataToCopy } = generateDataToCopyAndStoreInMemory(editor._getViewModel(), undefined, browser.isFirefox); clipboardService.writeText(dataToCopy.text); } } diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index d2ed77fb5a4..6cc8e853ab3 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as browser from '../../../../base/browser/browser.js'; import { IAction } from '../../../../base/common/actions.js'; import { coalesce } from '../../../../base/common/arrays.js'; import { CancelablePromise, createCancelablePromise, DeferredPromise, raceCancellation } from '../../../../base/common/async.js'; @@ -12,7 +13,6 @@ import { isCancellationError } from '../../../../base/common/errors.js'; import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { Mimes } from '../../../../base/common/mime.js'; -import * as platform from '../../../../base/common/platform.js'; import { upcast } from '../../../../base/common/types.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { localize } from '../../../../nls.js'; @@ -24,11 +24,10 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; -import { IClipboardCopyEvent, IClipboardPasteEvent, IWritableClipboardData } from '../../../browser/controller/editContext/clipboardUtils.js'; +import { generateDataToCopyAndStoreInMemory, IClipboardCopyEvent, IClipboardPasteEvent, IWritableClipboardData } from '../../../browser/controller/editContext/clipboardUtils.js'; import { ICodeEditor, PastePayload } from '../../../browser/editorBrowser.js'; import { IBulkEditService } from '../../../browser/services/bulkEditService.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; -import { IRange, Range } from '../../../common/core/range.js'; import { Selection } from '../../../common/core/selection.js'; import { Handler, IEditorContribution } from '../../../common/editorCommon.js'; import { DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteTriggerKind } from '../../../common/languages.js'; @@ -184,29 +183,17 @@ export class CopyPasteController extends Disposable implements IEditorContributi } const model = this._editor.getModel(); + const viewModel = this._editor._getViewModel(); const selections = this._editor.getSelections(); - if (!model || !selections?.length) { + if (!model || !viewModel || !selections?.length) { return; } - const enableEmptySelectionClipboard = this._editor.getOption(EditorOption.emptySelectionClipboard); - - let ranges: readonly IRange[] = selections; - const wasFromEmptySelection = selections.length === 1 && selections[0].isEmpty(); - if (wasFromEmptySelection) { - if (!enableEmptySelectionClipboard) { - return; - } - - ranges = [new Range(ranges[0].startLineNumber, 1, ranges[0].startLineNumber, 1 + model.getLineLength(ranges[0].startLineNumber))]; - } - - const toCopy = this._editor._getViewModel()?.getPlainTextToCopy(selections, enableEmptySelectionClipboard, platform.isWindows); - const multicursorText = Array.isArray(toCopy) ? toCopy : null; + const { dataToCopy } = generateDataToCopyAndStoreInMemory(viewModel, undefined, browser.isFirefox); const defaultPastePayload = { - multicursorText, - pasteOnNewLine: wasFromEmptySelection, + multicursorText: dataToCopy.multicursorText ?? null, + pasteOnNewLine: dataToCopy.isFromEmptySelection, mode: null }; @@ -233,7 +220,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi return { providerMimeTypes: provider.copyMimeTypes, operation: createCancelablePromise(token => - provider.prepareDocumentPaste!(model, ranges, dataTransfer, token) + provider.prepareDocumentPaste!(model, dataToCopy.sourceRanges, dataTransfer, token) .catch(err => { console.error(err); return undefined; diff --git a/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts b/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts index c5ae0e5ac9c..4767de7021d 100644 --- a/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts +++ b/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts @@ -122,7 +122,7 @@ suite('ViewModel', () => { function assertGetPlainTextToCopy(text: string[], ranges: Range[], emptySelectionClipboard: boolean, expected: string | string[]): void { testViewModel(text, {}, (viewModel, model) => { const actual = viewModel.getPlainTextToCopy(ranges, emptySelectionClipboard, false); - assert.deepStrictEqual(actual, expected); + assert.deepStrictEqual(actual.sourceText, expected); }); } @@ -290,7 +290,7 @@ suite('ViewModel', () => { testViewModel(USUAL_TEXT, {}, (viewModel, model) => { model.setEOL(EndOfLineSequence.LF); const actual = viewModel.getPlainTextToCopy([new Range(2, 1, 5, 1)], true, true); - assert.deepStrictEqual(actual, 'line2\r\nline3\r\nline4\r\n'); + assert.deepStrictEqual(actual.sourceText, 'line2\r\nline3\r\nline4\r\n'); }); }); From 9022d6c61f38e840af7702cb5782ecc46d174ed6 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:25:13 +0000 Subject: [PATCH 222/387] Add command to attach all pinned editors to chat (#288818) * Initial plan * Add command to attach all pinned editors to chat Co-authored-by: benibenj <44439583+benibenj@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: benibenj <44439583+benibenj@users.noreply.github.com> --- .../browser/actions/chatContextActions.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index b7bf82a9325..4215c4158ee 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -58,6 +58,7 @@ export function registerChatContextActions() { registerAction2(AttachFolderToChatAction); registerAction2(AttachSelectionToChatAction); registerAction2(AttachSearchResultAction); + registerAction2(AttachPinnedEditorsToChatAction); registerPromptActions(); } @@ -234,6 +235,52 @@ class AttachFolderToChatAction extends AttachResourceAction { } } +class AttachPinnedEditorsToChatAction extends Action2 { + + static readonly ID = 'workbench.action.chat.attachPinnedEditors'; + + constructor() { + super({ + id: AttachPinnedEditorsToChatAction.ID, + title: localize2('workbench.action.chat.attachPinnedEditors.label', "Add Pinned Editors to Chat"), + category: CHAT_CATEGORY, + precondition: ChatContextKeys.enabled, + f1: true, + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const editorGroupsService = accessor.get(IEditorGroupsService); + const instaService = accessor.get(IInstantiationService); + + const widget = await instaService.invokeFunction(withChatView); + if (!widget) { + return; + } + + const files: URI[] = []; + for (const group of editorGroupsService.groups) { + for (const editor of group.editors) { + if (group.isPinned(editor)) { + const uri = EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY }); + if (uri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(uri.scheme)) { + files.push(uri); + } + } + } + } + + if (!files.length) { + return; + } + + widget.focusInput(); + for (const file of files) { + widget.attachmentModel.addFile(file); + } + } +} + class AttachSelectionToChatAction extends Action2 { static readonly ID = 'workbench.action.chat.attachSelection'; From 19b8cf5f91d75019dcc0f2fe2a9b8cdd746f310e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:49:31 +0000 Subject: [PATCH 223/387] Fix hang in findAgentMDsInWorkspace when FileSearchProvider unavailable (#288842) * Initial plan * Add fallback to file service when FileSearchProvider is unavailable - Check schemeHasFileSearchProvider before using searchService.fileSearch - Implement recursive file service traversal as fallback - Add comprehensive tests for both code paths Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> * Add FileSearchProvider check to searchFilesInLocation Prevent hanging when searching for files with glob patterns in file systems without FileSearchProvider. Log a warning and return empty array instead. Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> * update * update * fix tests --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> Co-authored-by: Martin Aeschlimann --- .../promptSyntax/utils/promptFilesLocator.ts | 91 +++++-- .../service/promptsService.test.ts | 1 + .../utils/promptFilesLocator.test.ts | 223 ++++++++++++++++-- 3 files changed, 272 insertions(+), 43 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts index 73848fc2fd9..1b03705d4a7 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts @@ -387,9 +387,16 @@ export class PromptFilesLocator { } /** - * Uses the search service to find all files at the provided location + * Uses the search service to find all files at the provided location. + * Requires a FileSearchProvider to be available for the folder's scheme. */ private async searchFilesInLocation(folder: URI, filePattern: string | undefined, token: CancellationToken): Promise { + // Check if a FileSearchProvider is available for this scheme + if (!this.searchService.schemeHasFileSearchProvider(folder.scheme)) { + this.logService.warn(`[PromptFilesLocator] No FileSearchProvider available for scheme '${folder.scheme}'. Cannot search for pattern '${filePattern}' in ${folder.toString()}`); + return []; + } + const disregardIgnoreFiles = this.configService.getValue('explorer.excludeGitIgnore'); const workspaceRoot = this.workspaceService.getWorkspaceFolder(folder); @@ -445,29 +452,69 @@ export class PromptFilesLocator { } private async findAgentMDsInFolder(folder: URI, token: CancellationToken): Promise { - const disregardIgnoreFiles = this.configService.getValue('explorer.excludeGitIgnore'); - const getExcludePattern = (folder: URI) => getExcludes(this.configService.getValue({ resource: folder })) || {}; - const searchOptions: IFileQuery = { - folderQueries: [{ folder, disregardIgnoreFiles }], - type: QueryType.File, - shouldGlobMatchFilePattern: true, - excludePattern: getExcludePattern(folder), - filePattern: '**/AGENTS.md', + // Check if a FileSearchProvider is available for this scheme + if (this.searchService.schemeHasFileSearchProvider(folder.scheme)) { + // Use the search service if a FileSearchProvider is available + const disregardIgnoreFiles = this.configService.getValue('explorer.excludeGitIgnore'); + const getExcludePattern = (folder: URI) => getExcludes(this.configService.getValue({ resource: folder })) || {}; + const searchOptions: IFileQuery = { + folderQueries: [{ folder, disregardIgnoreFiles }], + type: QueryType.File, + shouldGlobMatchFilePattern: true, + excludePattern: getExcludePattern(folder), + filePattern: '**/AGENTS.md', + ignoreGlobCase: true, + }; + + try { + const searchResult = await this.searchService.fileSearch(searchOptions, token); + if (token.isCancellationRequested) { + return []; + } + return searchResult.results.map(r => r.resource); + } catch (e) { + if (!isCancellationError(e)) { + throw e; + } + } + return []; + } else { + // Fallback to recursive traversal using file service + return this.findAgentMDsUsingFileService(folder, token); + } + } + + /** + * Recursively traverses a folder using the file service to find AGENTS.md files. + * This is used as a fallback when no FileSearchProvider is available for the scheme. + */ + private async findAgentMDsUsingFileService(folder: URI, token: CancellationToken): Promise { + const result: URI[] = []; + const agentsMdFileName = 'agents.md'; + + const traverse = async (uri: URI): Promise => { + if (token.isCancellationRequested) { + return; + } + + try { + const stat = await this.fileService.resolve(uri); + if (stat.isFile && stat.name.toLowerCase() === agentsMdFileName) { + result.push(stat.resource); + } else if (stat.isDirectory && stat.children) { + // Recursively traverse subdirectories + for (const child of stat.children) { + await traverse(child.resource); + } + } + } catch (error) { + // Ignore errors for individual files/folders (e.g., permission denied) + this.logService.trace(`[PromptFilesLocator] Error traversing ${uri.toString()}: ${error}`); + } }; - try { - const searchResult = await this.searchService.fileSearch(searchOptions, token); - if (token.isCancellationRequested) { - return []; - } - return searchResult.results.map(r => r.resource); - } catch (e) { - if (!isCancellationError(e)) { - throw e; - } - } - return []; - + await traverse(folder); + return result; } /** 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 0217396a826..af040c6e439 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 @@ -115,6 +115,7 @@ suite('PromptsService', () => { instaService.stub(IPathService, pathService); instaService.stub(ISearchService, { + schemeHasFileSearchProvider: () => true, async fileSearch(query: IFileQuery) { // mock the search service - recursively find files matching pattern const findFilesInLocation = async (location: URI, results: URI[] = []): Promise => { diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts index c983f777c3d..80eb7085f14 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../../../../base/common/cancellation.js'; import { match } from '../../../../../../../base/common/glob.js'; import { Schemas } from '../../../../../../../base/common/network.js'; import { basename, relativePath } from '../../../../../../../base/common/resources.js'; @@ -25,7 +25,7 @@ import { IPathService } from '../../../../../../services/path/common/pathService import { PromptsConfig } from '../../../../common/promptSyntax/config/config.js'; import { PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; import { isValidGlob, isValidSkillPath, PromptFilesLocator } from '../../../../common/promptSyntax/utils/promptFilesLocator.js'; -import { IMockFolder, MockFilesystem } from '../testUtils/mockFilesystem.js'; +import { IMockFileEntry, IMockFolder, MockFilesystem } from '../testUtils/mockFilesystem.js'; import { mockService } from './mock.js'; import { TestUserDataProfileService } from '../../../../../../test/common/workbenchTestServices.js'; import { PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; @@ -34,23 +34,20 @@ import { runWithFakedTimers } from '../../../../../../../base/test/common/timeTr /** * Mocked instance of {@link IConfigurationService}. */ -function mockConfigService(value: T): IConfigurationService { +function mockConfigService(configValues: Record): IConfigurationService { return mockService({ getValue(key?: string | IConfigurationOverrides) { - assert( - typeof key === 'string', - `Expected string configuration key, got '${typeof key}'.`, - ); - if ('explorer.excludeGitIgnore' === key) { - return false; + // Handle object configuration overrides (e.g., for file exclude patterns) + if (typeof key === 'object') { + return {}; } - - assert( - [PromptsConfig.PROMPT_LOCATIONS_KEY, PromptsConfig.INSTRUCTIONS_LOCATION_KEY, PromptsConfig.MODE_LOCATION_KEY, PromptsConfig.SKILLS_LOCATION_KEY].includes(key), - `Unsupported configuration key '${key}'.`, - ); - - return value; + if (typeof key !== 'string') { + assert.fail(`Unsupported configuration key '${key}'.`); + } + if (configValues.hasOwnProperty(key)) { + return configValues[key]; + } + assert.fail(`Unsupported configuration key '${key}'.`); }, }); } @@ -79,10 +76,6 @@ function testT(name: string, fn: () => Promise): Mocha.Test { suite('PromptFilesLocator', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); - // if (isWindows) { - // return; - // } - let instantiationService: TestInstantiationService; setup(async () => { instantiationService = disposables.add(new TestInstantiationService()); @@ -104,7 +97,15 @@ suite('PromptFilesLocator', () => { const mockFs = instantiationService.createInstance(MockFilesystem, filesystem); await mockFs.mock(); - instantiationService.stub(IConfigurationService, mockConfigService(configValue)); + instantiationService.stub(IConfigurationService, mockConfigService({ + 'explorer.excludeGitIgnore': false, + 'files.exclude': {}, + 'search.exclude': {}, + [PromptsConfig.PROMPT_LOCATIONS_KEY]: configValue, + [PromptsConfig.INSTRUCTIONS_LOCATION_KEY]: configValue, + [PromptsConfig.MODE_LOCATION_KEY]: configValue, + [PromptsConfig.SKILLS_LOCATION_KEY]: configValue, + })); const workspaceFolders = workspaceFolderPaths.map((path, index) => { const uri = URI.file(path); @@ -119,6 +120,9 @@ suite('PromptFilesLocator', () => { instantiationService.stub(IWorkbenchEnvironmentService, {} as IWorkbenchEnvironmentService); instantiationService.stub(IUserDataProfileService, new TestUserDataProfileService()); instantiationService.stub(ISearchService, { + schemeHasFileSearchProvider(scheme: string): boolean { + return true; + }, async fileSearch(query: IFileQuery) { // mock the search service const fs = instantiationService.get(IFileService); @@ -3091,6 +3095,183 @@ suite('PromptFilesLocator', () => { await locator.disposeAsync(); }); }); + + suite('findAgentMDsInWorkspace', () => { + testT('finds AGENTS.md files using FileSearchProvider', async () => { + const locator = await createPromptsLocatorForAgentMD( + {}, + ['/Users/legomushroom/repos/workspace'], + [ + { + path: '/Users/legomushroom/repos/workspace/AGENTS.md', + contents: ['# Root agents'] + }, + { + path: '/Users/legomushroom/repos/workspace/src/AGENTS.md', + contents: ['# Src agents'] + } + ], + true // has FileSearchProvider + ); + + const result = await locator.findAgentMDsInWorkspace(CancellationToken.None); + assertOutcome( + result, + [ + '/Users/legomushroom/repos/workspace/AGENTS.md', + '/Users/legomushroom/repos/workspace/src/AGENTS.md' + ], + 'Must find all AGENTS.md files using search service.' + ); + await locator.disposeAsync(); + }); + + testT('finds AGENTS.md files using file service fallback', async () => { + const locator = await createPromptsLocatorForAgentMD( + {}, + ['/Users/legomushroom/repos/workspace'], + [ + { + path: '/Users/legomushroom/repos/workspace/AGENTS.md', + contents: ['# Root agents'] + }, + { + path: '/Users/legomushroom/repos/workspace/src/AGENTS.md', + contents: ['# Src agents'] + }, + { + path: '/Users/legomushroom/repos/workspace/src/nested/AGENTS.md', + contents: ['# Nested agents'] + } + ], + false // no FileSearchProvider - should use file service fallback + ); + + const result = await locator.findAgentMDsInWorkspace(CancellationToken.None); + assertOutcome( + result, + [ + '/Users/legomushroom/repos/workspace/AGENTS.md', + '/Users/legomushroom/repos/workspace/src/AGENTS.md', + '/Users/legomushroom/repos/workspace/src/nested/AGENTS.md' + ], + 'Must find all AGENTS.md files using file service fallback.' + ); + await locator.disposeAsync(); + }); + + testT('handles cancellation token in file service fallback', async () => { + const locator = await createPromptsLocatorForAgentMD( + {}, + ['/Users/legomushroom/repos/workspace'], + [ + { + path: '/Users/legomushroom/repos/workspace/AGENTS.md', + contents: ['# Root agents'] + } + ], + false // no FileSearchProvider + ); + + const source = new CancellationTokenSource(); + // Cancel immediately + source.cancel(); + const result = await locator.findAgentMDsInWorkspace(source.token); + assertOutcome( + result, + [], + 'Must return empty array when cancelled.' + ); + await locator.disposeAsync(); + }); + + const createPromptsLocatorForAgentMD = async ( + configValue: unknown, + workspaceFolderPaths: string[], + filesystem: IMockFileEntry[], + hasFileSearchProvider: boolean + ) => { + const mockFs = instantiationService.createInstance(MockFilesystem, filesystem); + await mockFs.mock(); + + instantiationService.stub(IConfigurationService, mockConfigService({ + 'explorer.excludeGitIgnore': false, + 'files.exclude': {}, + 'search.exclude': {} + })); + + const workspaceFolders = workspaceFolderPaths.map((path, index) => { + const uri = URI.file(path); + + return new class extends mock() { + override uri = uri; + override name = basename(uri); + override index = index; + }; + }); + instantiationService.stub(IWorkspaceContextService, mockWorkspaceService(workspaceFolders)); + instantiationService.stub(IWorkbenchEnvironmentService, {} as IWorkbenchEnvironmentService); + instantiationService.stub(IUserDataProfileService, new TestUserDataProfileService()); + instantiationService.stub(ISearchService, { + schemeHasFileSearchProvider(scheme: string): boolean { + return hasFileSearchProvider; + }, + async fileSearch(query: IFileQuery) { + if (!hasFileSearchProvider) { + throw new Error('FileSearchProvider not available'); + } + // mock the search service + const fs = instantiationService.get(IFileService); + const findFilesInLocation = async (location: URI, results: URI[] = []) => { + try { + const resolve = await fs.resolve(location); + if (resolve.isFile) { + results.push(resolve.resource); + } else if (resolve.isDirectory && resolve.children) { + for (const child of resolve.children) { + await findFilesInLocation(child.resource, results); + } + } + } catch (error) { + } + return results; + }; + const results: IFileMatch[] = []; + for (const folderQuery of query.folderQueries) { + const allFiles = await findFilesInLocation(folderQuery.folder); + for (const resource of allFiles) { + const pathInFolder = relativePath(folderQuery.folder, resource) ?? ''; + if (query.filePattern === undefined || match(query.filePattern, pathInFolder)) { + results.push({ resource }); + } + } + + } + return { results, messages: [] }; + } + }); + instantiationService.stub(IPathService, { + userHome(options?: { preferLocal: boolean }): URI | Promise { + const uri = URI.file('/Users/legomushroom'); + if (options?.preferLocal) { + return uri; + } + return Promise.resolve(uri); + } + } as IPathService); + + const locator = instantiationService.createInstance(PromptFilesLocator); + + return { + async findAgentMDsInWorkspace(token: CancellationToken): Promise { + return locator.findAgentMDsInWorkspace(token); + }, + async disposeAsync(): Promise { + await mockFs.delete(); + } + }; + }; + }); }); function assertOutcome(actual: readonly URI[], expected: string[], message: string) { From c1b3340d4a7f4ae9e5748c2cc008f453f383a34f Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 19 Jan 2026 22:49:48 +0100 Subject: [PATCH 224/387] allow to configure SubagentToolCustomAgents by experiment (#288967) --- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index f466ceac1a7..7dfeb8d0044 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -879,6 +879,9 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.subagentTool.customAgents', "Whether the runSubagent tool is able to use custom agents. When enabled, the tool can take the name of a custom agent, but it must be given the exact name of the agent."), default: false, tags: ['experimental'], + experiment: { + mode: 'auto' + } } } }); From c3664a64985022d6bef48bae8ee1a34ba8445951 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 19 Jan 2026 22:50:10 +0100 Subject: [PATCH 225/387] package.json in trusted workspace showing untrusted schema warning (#288976) --- extensions/json-language-features/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index 429e051159e..9529d657286 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -133,6 +133,7 @@ "https://schemastore.azurewebsites.net/": true, "https://raw.githubusercontent.com/": true, "https://www.schemastore.org/": true, + "https://json.schemastore.org/": true, "https://json-schema.org/": true }, "additionalProperties": { From d60835524a26f08159bb9ceb69c8ebf1a23e11b1 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 19 Jan 2026 14:11:30 -0800 Subject: [PATCH 226/387] Fix chat input shifting (#288991) offsetHeight returns 1 more than expected, because it includes the session container's border. So compensate for this. --- .../contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 7f8a33d4f16..7ad78b3c131 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -987,7 +987,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { sessionsHeight = availableSessionsHeight; } - sessionsHeight = Math.min(availableSessionsHeight, sessionsHeight); + const borderBottom = 1; + sessionsHeight = Math.min(availableSessionsHeight, sessionsHeight) - borderBottom; this.sessionsControlContainer.style.height = `${sessionsHeight}px`; this.sessionsControlContainer.style.width = ``; From 4aa4a2b293b295624ab646c7373c4bc51624745c Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Mon, 19 Jan 2026 23:19:25 +0100 Subject: [PATCH 227/387] Improved continue chat in --- .../delegationSessionPickerActionItem.ts | 60 +++++++++++++++++- .../input/sessionTargetPickerActionItem.ts | 62 ++++++++++++------- 2 files changed, 98 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts index 86d444ae395..8953e9c250a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts @@ -3,6 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IAction } from '../../../../../../base/common/actions.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { localize } from '../../../../../../nls.js'; +import { IActionWidgetDropdownAction } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; +import { ACTION_ID_NEW_CHAT } from '../../actions/chatActions.js'; import { AgentSessionProviders, getAgentSessionProvider } from '../../agentSessions/agentSessions.js'; import { ISessionTypeItem, SessionTypePickerActionItem } from './sessionTargetPickerActionItem.js'; @@ -31,9 +37,57 @@ export class DelegationSessionPickerActionItem extends SessionTypePickerActionIt protected override _isSessionTypeEnabled(type: AgentSessionProviders): boolean { const allContributions = this.chatSessionsService.getAllChatSessionContributions(); const contribution = allContributions.find(contribution => getAgentSessionProvider(contribution.type) === type); - if (contribution !== undefined && !!contribution.canDelegate) { - return true; // Session type supports delegation + + if (this.delegate.getActiveSessionProvider() !== AgentSessionProviders.Local) { + return false; // Can only delegate when active session is local } - return this.delegate.getActiveSessionProvider() === type; // Always allow switching back to active session + + if (contribution && !contribution.canDelegate && this.delegate.getActiveSessionProvider() !== type /* Allow switching back to active type */) { + return false; + } + + return this._getSelectedSessionType() !== type; // Always allow switching back to active session + } + + protected override _getSessionCategory(sessionTypeItem: ISessionTypeItem) { + return { label: localize('continueIn', "Continue In"), order: 1, showHeader: true }; + } + + protected override _getSessionDescription(sessionTypeItem: ISessionTypeItem): string | undefined { + const allContributions = this.chatSessionsService.getAllChatSessionContributions(); + const contribution = allContributions.find(contribution => getAgentSessionProvider(contribution.type) === sessionTypeItem.type); + + return contribution?.name ? `@${contribution.name}` : undefined; + } + + protected override _getLearnMore(): IAction { + const learnMoreUrl = 'https://aka.ms/vscode-continue-chat-in'; + return { + id: 'workbench.action.chat.agentOverview.learnMoreHandOff', + label: localize('chat.learnMoreAgentHandOff', "Learn about agent handoff..."), + tooltip: learnMoreUrl, + class: undefined, + enabled: true, + run: async () => { + await this.openerService.open(URI.parse(learnMoreUrl)); + } + }; + } + + protected override _getAdditionalActions(): IActionWidgetDropdownAction[] { + return [{ + id: 'newChatSession', + class: undefined, + label: localize('chat.newChatSession', "New Chat Session"), + tooltip: localize('chat.newChatSession.tooltip', "Create a new chat session"), + checked: false, + icon: Codicon.plus, + enabled: true, + category: { label: localize('newChatSession', "newChatSession"), order: 0, showHeader: false }, + description: this.keybindingService.lookupKeybinding(ACTION_ID_NEW_CHAT)?.getLabel() || undefined, + run: async () => { + this.commandService.executeCommand(ACTION_ID_NEW_CHAT, this.chatSessionPosition); + }, + }]; } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts index 5e99e2d96fe..5f5761e0e30 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts @@ -20,6 +20,7 @@ import { IChatSessionsService } from '../../../common/chatSessionsService.js'; import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName, isFirstPartyAgentSessionProvider } from '../../agentSessions/agentSessions.js'; import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; import { ISessionTypePickerDelegate } from '../../chat.js'; +import { IActionProvider } from '../../../../../../base/browser/ui/dropdown/dropdown.js'; export interface ISessionTypeItem { type: AgentSessionProviders; @@ -28,6 +29,9 @@ export interface ISessionTypeItem { commandId: string; } +const firstPartyCategory = { label: localize('chat.sessionTarget.category.agent', "Agent Types"), order: 1 }; +const otherCategory = { label: localize('chat.sessionTarget.category.other', "Other"), order: 2 }; + /** * Action view item for selecting a session target in the chat interface. * This picker allows switching between different chat session types for new/empty sessions. @@ -41,20 +45,18 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { protected readonly delegate: ISessionTypePickerDelegate, pickerOptions: IChatInputPickerOptions, @IActionWidgetService actionWidgetService: IActionWidgetService, - @IKeybindingService keybindingService: IKeybindingService, + @IKeybindingService protected readonly keybindingService: IKeybindingService, @IContextKeyService contextKeyService: IContextKeyService, @IChatSessionsService protected readonly chatSessionsService: IChatSessionsService, - @ICommandService private readonly commandService: ICommandService, - @IOpenerService openerService: IOpenerService, + @ICommandService protected readonly commandService: ICommandService, + @IOpenerService protected readonly openerService: IOpenerService, ) { - const firstPartyCategory = { label: localize('chat.sessionTarget.category.agent', "Agent Types"), order: 1 }; - const otherCategory = { label: localize('chat.sessionTarget.category.other', "Other"), order: 2 }; const actionProvider: IActionWidgetDropdownActionProvider = { getActions: () => { const currentType = this._getSelectedSessionType(); - const actions: IActionWidgetDropdownAction[] = []; + const actions: IActionWidgetDropdownAction[] = [...this._getAdditionalActions()]; for (const sessionTypeItem of this._sessionTypeItems) { actions.push({ ...action, @@ -64,7 +66,8 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { checked: currentType === sessionTypeItem.type, icon: getAgentSessionProviderIcon(sessionTypeItem.type), enabled: this._isSessionTypeEnabled(sessionTypeItem.type), - category: isFirstPartyAgentSessionProvider(sessionTypeItem.type) ? firstPartyCategory : otherCategory, + category: this._getSessionCategory(sessionTypeItem), + description: this._getSessionDescription(sessionTypeItem), run: async () => { this._run(sessionTypeItem); }, @@ -75,24 +78,15 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { } }; - const actionBarActions: IAction[] = []; - - const learnMoreUrl = 'https://code.visualstudio.com/docs/copilot/agents/overview'; - actionBarActions.push({ - id: 'workbench.action.chat.agentOverview.learnMore', - label: localize('chat.learnMoreAgentTypes', "Learn about agent types..."), - tooltip: learnMoreUrl, - class: undefined, - enabled: true, - run: async () => { - await openerService.open(URI.parse(learnMoreUrl)); + const actionBarActionProvider: IActionProvider = { + getActions: () => { + return [this._getLearnMore()]; } - }); + }; const sessionTargetPickerOptions: Omit = { actionProvider, - actionBarActions, - actionBarActionProvider: undefined, + actionBarActionProvider, showItemKeybindings: true, }; @@ -121,6 +115,24 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { return this.delegate.getActiveSessionProvider(); } + protected _getAdditionalActions(): IActionWidgetDropdownAction[] { + return []; + } + + protected _getLearnMore(): IAction { + const learnMoreUrl = 'https://code.visualstudio.com/docs/copilot/agents/overview'; + return { + id: 'workbench.action.chat.agentOverview.learnMore', + label: localize('chat.learnMoreAgentTypes', "Learn about agent types..."), + tooltip: learnMoreUrl, + class: undefined, + enabled: true, + run: async () => { + await this.openerService.open(URI.parse(learnMoreUrl)); + } + }; + } + private _updateAgentSessionItems(): void { const localSessionItem = { type: AgentSessionProviders.Local, @@ -152,6 +164,14 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { return true; } + protected _getSessionCategory(sessionTypeItem: ISessionTypeItem) { + return isFirstPartyAgentSessionProvider(sessionTypeItem.type) ? firstPartyCategory : otherCategory; + } + + protected _getSessionDescription(sessionTypeItem: ISessionTypeItem): string | undefined { + return undefined; + } + protected override renderLabel(element: HTMLElement): IDisposable | null { this.setAriaLabelAttributes(element); const currentType = this._getSelectedSessionType(); From 0876c04e26121ffa038e9f51e26fed94b551e670 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 19 Jan 2026 23:36:14 +0100 Subject: [PATCH 228/387] Update src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../browser/widget/input/delegationSessionPickerActionItem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts index 8953e9c250a..c5c0fc6ad2e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts @@ -83,7 +83,7 @@ export class DelegationSessionPickerActionItem extends SessionTypePickerActionIt checked: false, icon: Codicon.plus, enabled: true, - category: { label: localize('newChatSession', "newChatSession"), order: 0, showHeader: false }, + category: { label: localize('chat.newChatSession.category', "New Chat Session"), order: 0, showHeader: false }, description: this.keybindingService.lookupKeybinding(ACTION_ID_NEW_CHAT)?.getLabel() || undefined, run: async () => { this.commandService.executeCommand(ACTION_ID_NEW_CHAT, this.chatSessionPosition); From 1cde95547e0bcd7b2c846ba1aad8ac8d3532e832 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 19 Jan 2026 15:21:06 -0800 Subject: [PATCH 229/387] Subagent UX fixes (#288769) * Lazy render tool invocations inside subagents, same as thinking groups * Styling for subagent prompt/result and icons * More padding and layout fixes, add AnimationFrameScheduler * Fix warning * Avoid faked timers --- src/vs/base/browser/dom.ts | 53 +++- src/vs/base/test/browser/dom.test.ts | 99 +++++++- .../chatSubagentContentPart.ts | 229 +++++++++++++++--- .../media/chatSubagentContent.css | 36 +-- .../chat/browser/widget/chatListRenderer.ts | 38 +-- 5 files changed, 386 insertions(+), 69 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 11862c1b299..1a62307464b 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -11,7 +11,7 @@ import { AbstractIdleValue, IntervalTimer, TimeoutTimer, _runWhenIdle, IdleDeadl import { BugIndicatingError, onUnexpectedError } from '../common/errors.js'; import * as event from '../common/event.js'; import { KeyCode } from '../common/keyCodes.js'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from '../common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../common/lifecycle.js'; import { RemoteAuthorities } from '../common/network.js'; import * as platform from '../common/platform.js'; import { URI } from '../common/uri.js'; @@ -413,6 +413,57 @@ export function modify(targetWindow: Window, callback: () => void): IDisposable return scheduleAtNextAnimationFrame(targetWindow, callback, -10000 /* must be late */); } +/** + * A scheduler that coalesces multiple `schedule()` calls into a single callback + * at the next animation frame. Similar to `RunOnceScheduler` but uses animation frames + * instead of timeouts. + */ +export class AnimationFrameScheduler implements IDisposable { + + private readonly runner: () => void; + private readonly node: Node; + private readonly pendingRunner = new MutableDisposable(); + + constructor(node: Node, runner: () => void) { + this.node = node; + this.runner = runner; + } + + dispose(): void { + this.pendingRunner.dispose(); + } + + /** + * Cancel the currently scheduled runner (if any). + */ + cancel(): void { + this.pendingRunner.clear(); + } + + /** + * Schedule the runner to execute at the next animation frame. + * If already scheduled, this is a no-op (the existing schedule is kept). + * If currently in an animation frame, the runner will execute immediately. + */ + schedule(): void { + if (this.pendingRunner.value) { + return; // Already scheduled + } + + this.pendingRunner.value = runAtThisOrScheduleAtNextAnimationFrame(getWindow(this.node), () => { + this.pendingRunner.clear(); + this.runner(); + }); + } + + /** + * Returns true if a runner is scheduled. + */ + isScheduled(): boolean { + return this.pendingRunner.value !== undefined; + } +} + /** * Add a throttled listener. `handler` is fired at most every 8.33333ms or with the next animation frame (if browser supports it). */ diff --git a/src/vs/base/test/browser/dom.test.ts b/src/vs/base/test/browser/dom.test.ts index a7975897761..bf78a2afb13 100644 --- a/src/vs/base/test/browser/dom.test.ts +++ b/src/vs/base/test/browser/dom.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { $, h, trackAttributes, copyAttributes, disposableWindowInterval, getWindows, getWindowsCount, getWindowId, getWindowById, hasWindow, getWindow, getDocument, isHTMLElement, SafeTriangle } from '../../browser/dom.js'; +import { $, h, trackAttributes, copyAttributes, disposableWindowInterval, getWindows, getWindowsCount, getWindowId, getWindowById, hasWindow, getWindow, getDocument, isHTMLElement, SafeTriangle, AnimationFrameScheduler } from '../../browser/dom.js'; import { asCssValueWithDefault } from '../../../base/browser/cssValue.js'; import { ensureCodeWindow, isAuxiliaryWindow, mainWindow } from '../../browser/window.js'; import { DeferredPromise, timeout } from '../../common/async.js'; @@ -435,5 +435,102 @@ suite('dom', () => { }); }); + suite('AnimationFrameScheduler', () => { + // Helper to wait for an animation frame + const waitForAnimationFrame = () => new Promise(resolve => mainWindow.requestAnimationFrame(() => resolve())); + + test('schedules and runs the callback', async () => { + const node = document.createElement('div'); + let callCount = 0; + const scheduler = new AnimationFrameScheduler(node, () => { + callCount++; + }); + + assert.strictEqual(scheduler.isScheduled(), false); + scheduler.schedule(); + assert.strictEqual(scheduler.isScheduled(), true); + + // Wait for the animation frame + await waitForAnimationFrame(); + + assert.strictEqual(callCount, 1); + assert.strictEqual(scheduler.isScheduled(), false); + scheduler.dispose(); + }); + + test('coalesces multiple schedule calls', async () => { + const node = document.createElement('div'); + let callCount = 0; + const scheduler = new AnimationFrameScheduler(node, () => { + callCount++; + }); + + scheduler.schedule(); + scheduler.schedule(); + scheduler.schedule(); + + assert.strictEqual(scheduler.isScheduled(), true); + + // Wait for the animation frame + await waitForAnimationFrame(); + + assert.strictEqual(callCount, 1); + scheduler.dispose(); + }); + + test('cancel prevents execution', async () => { + const node = document.createElement('div'); + let callCount = 0; + const scheduler = new AnimationFrameScheduler(node, () => { + callCount++; + }); + + scheduler.schedule(); + assert.strictEqual(scheduler.isScheduled(), true); + scheduler.cancel(); + assert.strictEqual(scheduler.isScheduled(), false); + + // Wait for the animation frame + await waitForAnimationFrame(); + + assert.strictEqual(callCount, 0); + scheduler.dispose(); + }); + + test('dispose prevents execution', async () => { + const node = document.createElement('div'); + let callCount = 0; + const scheduler = new AnimationFrameScheduler(node, () => { + callCount++; + }); + + scheduler.schedule(); + scheduler.dispose(); + + // Wait for the animation frame + await waitForAnimationFrame(); + + assert.strictEqual(callCount, 0); + }); + + test('can schedule again after execution', async () => { + const node = document.createElement('div'); + let callCount = 0; + const scheduler = new AnimationFrameScheduler(node, () => { + callCount++; + }); + + scheduler.schedule(); + await waitForAnimationFrame(); + assert.strictEqual(callCount, 1); + + scheduler.schedule(); + await waitForAnimationFrame(); + assert.strictEqual(callCount, 2); + + scheduler.dispose(); + }); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts index e2f4dfcadbb..a2ffe19a43a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../../../base/browser/dom.js'; -import { $ } from '../../../../../../base/browser/dom.js'; +import { $, AnimationFrameScheduler } from '../../../../../../base/browser/dom.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { rcut } from '../../../../../../base/common/strings.js'; @@ -20,12 +20,25 @@ import { ChatCollapsibleMarkdownContentPart } from './chatCollapsibleMarkdownCon import { IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService/chatService.js'; import { IRunSubagentToolInputParams, RunSubagentTool } from '../../../common/tools/builtinTools/runSubagentTool.js'; import { autorun } from '../../../../../../base/common/observable.js'; -import { RunOnceScheduler } from '../../../../../../base/common/async.js'; +import { Lazy } from '../../../../../../base/common/lazy.js'; import { createThinkingIcon, getToolInvocationIcon } from './chatThinkingContentPart.js'; +import { CollapsibleListPool } from './chatReferencesContentPart.js'; +import { EditorPool } from './chatContentCodePools.js'; +import { CodeBlockModelCollection } from '../../../common/widget/codeBlockModelCollection.js'; +import { ChatToolInvocationPart } from './toolInvocationParts/chatToolInvocationPart.js'; import './media/chatSubagentContent.css'; const MAX_TITLE_LENGTH = 100; +/** + * Represents a lazy tool item that will be created when the subagent section is expanded. + */ +interface ILazyToolItem { + lazy: Lazy; + toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized; + codeBlockStartIndex: number; +} + /** * This is generally copied from ChatThinkingContentPart. We are still experimenting with both UIs so I'm not * trying to refactor to share code. Both could probably be simplified when stable. @@ -38,11 +51,17 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen private promptContainer: HTMLElement | undefined; private resultContainer: HTMLElement | undefined; private lastItemWrapper: HTMLElement | undefined; - private readonly layoutScheduler: RunOnceScheduler; + private readonly layoutScheduler: AnimationFrameScheduler; private description: string; private agentName: string | undefined; private prompt: string | undefined; + // Lazy rendering support + private readonly lazyItems: ILazyToolItem[] = []; + private hasExpandedOnce: boolean = false; + private pendingPromptRender: boolean = false; + private pendingResultText: string | undefined; + /** * Extracts subagent info (description, agentName, prompt) from a tool invocation. */ @@ -83,6 +102,11 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, private readonly context: IChatContentPartRenderContext, private readonly chatContentMarkdownRenderer: IMarkdownRenderer, + private readonly listPool: CollapsibleListPool, + private readonly editorPool: EditorPool, + private readonly currentWidthDelegate: () => number, + private readonly codeBlockModelCollection: CodeBlockModelCollection, + private readonly announcedToolProgressKeys: Set, @IInstantiationService private readonly instantiationService: IInstantiationService, @IHoverService hoverService: IHoverService, ) { @@ -121,11 +145,19 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen } })); + // Materialize lazy items when first expanded + this._register(autorun(r => { + if (this._isExpanded.read(r) && !this.hasExpandedOnce) { + this.hasExpandedOnce = true; + this.materializePendingContent(); + } + })); + // Start collapsed - fixed scrolling mode shows limited height when collapsed this.setExpanded(false); // Scheduler for coalescing layout operations - this.layoutScheduler = this._register(new RunOnceScheduler(() => this.performLayout(), 0)); + this.layoutScheduler = this._register(new AnimationFrameScheduler(this.domNode, () => this.performLayout())); // Render the prompt section at the start if available (must be after wrapper is initialized) this.renderPromptSection(); @@ -145,12 +177,28 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen /** * Renders the prompt as a collapsible section at the start of the content. + * If the subagent is initially complete (old/restored), this is deferred until expanded. */ private renderPromptSection(): void { if (!this.prompt || this.promptContainer) { return; } + // Defer rendering for old completed subagents until expanded + if (this.isInitiallyComplete && !this.isExpanded() && !this.hasExpandedOnce) { + this.pendingPromptRender = true; + return; + } + + this.pendingPromptRender = false; + this.doRenderPromptSection(); + } + + private doRenderPromptSection(): void { + if (!this.prompt || this.promptContainer) { + return; + } + // Split into first line and rest const lines = this.prompt.split('\n'); const rawFirstLine = lines[0] || localize('chat.subagent.prompt', 'Prompt'); @@ -165,7 +213,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen ? (titleRemainder + (restOfLines ? '\n' + restOfLines : '')) : (restOfLines || this.prompt); - // Create collapsible prompt part with comment icon + // Create collapsible prompt part const collapsiblePart = this._register(this.instantiationService.createInstance( ChatCollapsibleMarkdownContentPart, title, @@ -173,9 +221,14 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen this.context, this.chatContentMarkdownRenderer )); - collapsiblePart.icon = Codicon.comment; this._register(collapsiblePart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); - this.promptContainer = collapsiblePart.domNode; + + // Wrap in a container for chain of thought line styling + this.promptContainer = $('.chat-thinking-tool-wrapper.chat-subagent-section'); + const promptIcon = createThinkingIcon(Codicon.comment); + this.promptContainer.appendChild(promptIcon); + this.promptContainer.appendChild(collapsiblePart.domNode); + // Insert at the beginning of the wrapper if (this.wrapper.firstChild) { this.wrapper.insertBefore(this.promptContainer, this.wrapper.firstChild); @@ -261,11 +314,30 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen } } + /** + * Renders the result text as a collapsible section. + * If the subagent is initially complete (old/restored), this is deferred until expanded. + */ public renderResultText(resultText: string): void { if (this.resultContainer || !resultText) { return; // Already rendered or no content } + // Defer rendering for old completed subagents until expanded + if (this.isInitiallyComplete && !this.isExpanded() && !this.hasExpandedOnce) { + this.pendingResultText = resultText; + return; + } + + this.pendingResultText = undefined; + this.doRenderResultText(resultText); + } + + private doRenderResultText(resultText: string): void { + if (this.resultContainer || !resultText) { + return; + } + // Split into first line and rest const lines = resultText.split('\n'); const rawFirstLine = lines[0] || ''; @@ -289,7 +361,12 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen this.chatContentMarkdownRenderer )); this._register(collapsiblePart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); - this.resultContainer = collapsiblePart.domNode; + + // Wrap in a container for chain of thought line styling + this.resultContainer = $('.chat-thinking-tool-wrapper.chat-subagent-section'); + const resultIcon = createThinkingIcon(Codicon.check); + this.resultContainer.appendChild(resultIcon); + this.resultContainer.appendChild(collapsiblePart.domNode); dom.append(this.wrapper, this.resultContainer); // Show the container if it was hidden @@ -300,39 +377,58 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen this._onDidChangeHeight.fire(); } - public appendItem(content: HTMLElement, toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): void { - if (!content.hasChildNodes() || content.textContent?.trim() === '') { - return; - } - + /** + * Appends a tool invocation to the subagent group. + * The tool part is created lazily - only when the subagent section is expanded, + * unless it's actively streaming (not initially complete), in which case render immediately. + */ + public appendToolInvocation(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, codeBlockStartIndex: number): void { // Show the container when first tool item is added if (!this.hasToolItems) { this.hasToolItems = true; this.wrapper.style.display = ''; } - // Wrap with icon like thinking parts do, but skip icon for tools needing confirmation - const itemWrapper = $('.chat-thinking-tool-wrapper'); - let needsConfirmation = false; - if (toolInvocation.kind === 'toolInvocation' && toolInvocation.state) { - const state = toolInvocation.state.get(); - needsConfirmation = state.type === IChatToolInvocation.StateKind.WaitingForConfirmation || state.type === IChatToolInvocation.StateKind.WaitingForPostApproval; - } - - if (!needsConfirmation) { - const icon = getToolInvocationIcon(toolInvocation.toolId); - const iconElement = createThinkingIcon(icon); - itemWrapper.appendChild(iconElement); - } - itemWrapper.appendChild(content); - - // Insert before result container if it exists, otherwise append - if (this.resultContainer) { - this.wrapper.insertBefore(itemWrapper, this.resultContainer); + // Render immediately if: + // - The section is expanded + // - It has been expanded once before + // - It's actively streaming (not an old completed subagent being restored) + if (this.isExpanded() || this.hasExpandedOnce || !this.isInitiallyComplete) { + const part = this.createToolPart(toolInvocation, codeBlockStartIndex); + this.appendToolPartToDOM(part, toolInvocation); } else { - this.wrapper.appendChild(itemWrapper); + // Defer rendering until expanded (for old completed subagents) + const item: ILazyToolItem = { + lazy: new Lazy(() => this.createToolPart(toolInvocation, codeBlockStartIndex)), + toolInvocation, + codeBlockStartIndex, + }; + this.lazyItems.push(item); } - this.lastItemWrapper = itemWrapper; + } + + /** + * Creates a ChatToolInvocationPart for the given tool invocation. + */ + private createToolPart(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, codeBlockStartIndex: number): ChatToolInvocationPart { + const part = this.instantiationService.createInstance( + ChatToolInvocationPart, + toolInvocation, + this.context, + this.chatContentMarkdownRenderer, + this.listPool, + this.editorPool, + this.currentWidthDelegate, + this.codeBlockModelCollection, + this.announcedToolProgressKeys, + codeBlockStartIndex + ); + + this._register(part); + this._register(part.onDidChangeHeight(() => { + this.layoutScheduler.schedule(); + this._onDidChangeHeight.fire(); + })); // Watch for tool completion to update height when label changes if (toolInvocation.kind === 'toolInvocation') { @@ -344,15 +440,78 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen })); } + return part; + } + + /** + * Appends a tool part's DOM node to the wrapper with appropriate icon wrapper. + */ + private appendToolPartToDOM(part: ChatToolInvocationPart, toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): void { + const content = part.domNode; + if (!content.hasChildNodes() || content.textContent?.trim() === '') { + return; + } + + // Wrap with icon like thinking parts do, but skip icon for tools needing confirmation + const itemWrapper = $('.chat-thinking-tool-wrapper'); + const icon = getToolInvocationIcon(toolInvocation.toolId); + const iconElement = createThinkingIcon(icon); + itemWrapper.appendChild(iconElement); + itemWrapper.appendChild(content); + + // Insert before result container if it exists, otherwise append + if (this.resultContainer) { + this.wrapper.insertBefore(itemWrapper, this.resultContainer); + } else { + this.wrapper.appendChild(itemWrapper); + } + this.lastItemWrapper = itemWrapper; + // Schedule layout to measure last item and scroll this.layoutScheduler.schedule(); } + /** + * Materializes a lazy tool item by creating the tool part and adding it to the DOM. + */ + private materializeLazyItem(item: ILazyToolItem): void { + if (item.lazy.hasValue) { + return; // Already materialized + } + + const part = item.lazy.value; + this.appendToolPartToDOM(part, item.toolInvocation); + } + + /** + * Materializes all pending lazy content (prompt, tool items, result) when the section is expanded. + */ + private materializePendingContent(): void { + // Render pending prompt section + if (this.pendingPromptRender) { + this.pendingPromptRender = false; + this.doRenderPromptSection(); + } + + // Materialize lazy tool items + for (const item of this.lazyItems) { + this.materializeLazyItem(item); + } + + // Render pending result text + if (this.pendingResultText) { + const resultText = this.pendingResultText; + this.pendingResultText = undefined; + this.doRenderResultText(resultText); + } + + this._onDidChangeHeight.fire(); + } + private performLayout(): void { // Measure last item height once after layout, set CSS variable for collapsed max-height if (this.lastItemWrapper) { - const itemHeight = this.lastItemWrapper.offsetHeight; - const height = itemHeight + 4; + const height = this.lastItemWrapper.offsetHeight; if (height > 0) { this.wrapper.style.setProperty('--chat-subagent-last-item-height', `${height}px`); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatSubagentContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatSubagentContent.css index 417be791784..3d606a77814 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatSubagentContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatSubagentContent.css @@ -5,6 +5,7 @@ /* Subagent-specific styles */ .interactive-session .interactive-response .value .chat-thinking-fixed-mode.chat-subagent-part { + /* Collapsed + streaming: show only the last item with max-height */ &.chat-used-context-collapsed .chat-used-context-list.chat-thinking-collapsible.chat-thinking-streaming { max-height: var(--chat-subagent-last-item-height, 200px); @@ -17,29 +18,30 @@ max-height: none; overflow: visible; } -} -/* Subagent result collapsible section */ -.chat-subagent-result { - margin-top: 4px; - padding: 4px 8px; + /* Prompt and result section styling */ + .chat-subagent-section { + padding: 4px 12px 4px 18px; - .chat-used-context-label { - cursor: pointer; - - .monaco-button { - color: var(--vscode-descriptionForeground); - font-size: var(--vscode-chat-font-size-body-s); + .chat-used-context { + margin-bottom: 0px; + margin-left: 2px; + padding-left: 2px; } } - .chat-subagent-result-content { - padding: 4px 8px 4px 20px; - font-size: var(--vscode-chat-font-size-body-s); - color: var(--vscode-descriptionForeground); + .chat-thinking-tool-wrapper { - p { - margin: 0; + /* Hide the collapsible button's icon since we use the thinking icon */ + .codicon:not(.chat-thinking-icon) { + display: none; + } + + .chat-collapsible-markdown-content { + .rendered-markdown { + font-size: var(--vscode-chat-font-size-body-s); + line-height: 1.5em; + } } } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 29a3750664c..ea5647b20a4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -1398,7 +1398,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this._currentLayoutWidth.get(), + this._toolInvocationCodeBlockCollection, + this._announcedToolProgressKeys, + ); // Don't append the runSubagent tool itself - its description is already shown in the title // Only append child tools (those with subAgentInvocationId) if (toolInvocation.toolId !== RunSubagentTool.Id) { - subagentPart.appendItem(part.domNode!, toolInvocation); + subagentPart.appendToolInvocation(toolInvocation, codeBlockStartIndex); } - subagentPart.addDisposable(part); subagentPart.addDisposable(subagentPart.onDidChangeHeight(() => { this.updateItemHeight(templateData); })); @@ -1678,7 +1687,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + const createToolPart = (): { domNode: HTMLElement; part: ChatToolInvocationPart } => { lazilyCreatedPart = this.instantiationService.createInstance(ChatToolInvocationPart, toolInvocation, context, this.chatContentMarkdownRenderer, this._contentReferencesListPool, this._toolEditorPool, () => this._currentLayoutWidth.get(), this._toolInvocationCodeBlockCollection, this._announcedToolProgressKeys, codeBlockStartIndex); lazilyCreatedPart.addDisposable(lazilyCreatedPart.onDidChangeHeight(() => { this.updateItemHeight(templateData); @@ -1706,7 +1715,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer Date: Mon, 19 Jan 2026 20:04:59 -0800 Subject: [PATCH 230/387] Correcting the srt cli path in the published package. (#289008) correcting the srt path --- .../chatAgentTools/common/terminalSandboxService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts index 1f93088394f..5eef10bff66 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts @@ -47,7 +47,8 @@ export class TerminalSandboxService implements ITerminalSandboxService { @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, ) { const appRoot = dirname(FileAccess.asFileUri('').fsPath); - this._srtPath = join(appRoot, 'node_modules', '.bin', 'srt'); + // srt path is dist/cli.js inside the sandbox-runtime package. + this._srtPath = join(appRoot, 'node_modules', '@anthropic-ai', 'sandbox-runtime', 'dist', 'cli.js'); this._sandboxSettingsId = generateUuid(); this._initTempDir(); this._remoteAgentService.getEnvironment().then(remoteEnv => this._os = remoteEnv?.os ?? OS); From 09ed845e6944afa063a7c53421dbe6b3b07d543d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 20 Jan 2026 06:42:35 +0100 Subject: [PATCH 231/387] chat - better align input margins (#288983) --- src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 82b4a558555..caa434d031b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1568,7 +1568,7 @@ have to be updated for changes to the rules above, or to support more deeply nes } .interactive-session .interactive-input-part { - margin: 0px 16px; + margin: 0px 12px; padding: 4px 0 8px 0px; display: flex; flex-direction: column; From a0ac005701775a6d3ea68f0767534daa0b94df9d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 20 Jan 2026 07:32:00 +0100 Subject: [PATCH 232/387] layout - polish icons and state for maximisation/restore (#288980) --- .../parts/auxiliarybar/auxiliaryBarActions.ts | 7 ++-- .../browser/parts/panel/panelActions.ts | 41 +++++++++++++------ 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts index 15adc24dd63..5abfb88dd9c 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts @@ -18,10 +18,10 @@ import { ServicesAccessor } from '../../../../platform/instantiation/common/inst import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { SwitchCompositeViewAction } from '../compositeBarActions.js'; -import { closeIcon as panelCloseIcon } from '../panel/panelActions.js'; const maximizeIcon = registerIcon('auxiliarybar-maximize', Codicon.screenFull, localize('maximizeIcon', 'Icon to maximize the secondary side bar.')); -const closeIcon = registerIcon('auxiliarybar-close', panelCloseIcon, localize('closeIcon', 'Icon to close the secondary side bar.')); +const restoreIcon = registerIcon('auxiliarybar-restore', Codicon.screenNormal, localize('restoreIcon', 'Icon to restore the secondary side bar.')); +const closeIcon = registerIcon('auxiliarybar-close', Codicon.close, localize('closeIcon', 'Icon to close the secondary side bar.')); const auxiliaryBarRightIcon = registerIcon('auxiliarybar-right-layout-icon', Codicon.layoutSidebarRight, localize('toggleAuxiliaryIconRight', 'Icon to toggle the secondary side bar off in its right position.')); const auxiliaryBarRightOffIcon = registerIcon('auxiliarybar-right-off-layout-icon', Codicon.layoutSidebarRightOff, localize('toggleAuxiliaryIconRightOn', 'Icon to toggle the secondary side bar on in its right position.')); @@ -258,8 +258,7 @@ class RestoreAuxiliaryBar extends Action2 { category: Categories.View, f1: true, precondition: AuxiliaryBarMaximizedContext, - toggled: AuxiliaryBarMaximizedContext, - icon: maximizeIcon, + icon: restoreIcon, menu: { id: MenuId.AuxiliaryBarTitle, group: 'navigation', diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index 1391a22f7a6..3432144229c 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -23,7 +23,8 @@ import { KeybindingWeight } from '../../../../platform/keybinding/common/keybind import { SwitchCompositeViewAction } from '../compositeBarActions.js'; const maximizeIcon = registerIcon('panel-maximize', Codicon.screenFull, localize('maximizeIcon', 'Icon to maximize a panel.')); -export const closeIcon = registerIcon('panel-close', Codicon.close, localize('closeIcon', 'Icon to close a panel.')); +const restoreIcon = registerIcon('panel-restore', Codicon.screenNormal, localize('restoreIcon', 'Icon to restore a panel.')); +const closeIcon = registerIcon('panel-close', Codicon.close, localize('closeIcon', 'Icon to close a panel.')); const panelIcon = registerIcon('panel-layout-icon', Codicon.layoutPanel, localize('togglePanelOffIcon', 'Icon to toggle the panel off when it is on.')); const panelOffIcon = registerIcon('panel-layout-icon-off', Codicon.layoutPanelOff, localize('togglePanelOnIcon', 'Icon to toggle the panel on when it is off.')); @@ -272,25 +273,19 @@ registerAction2(class extends SwitchCompositeViewAction { } }); +const panelMaximizationSupportedWhen = ContextKeyExpr.or(PanelAlignmentContext.isEqualTo('center'), ContextKeyExpr.and(PanelPositionContext.notEqualsTo('bottom'), PanelPositionContext.notEqualsTo('top'))); +const ToggleMaximizedPanelActionId = 'workbench.action.toggleMaximizedPanel'; + registerAction2(class extends Action2 { constructor() { super({ - id: 'workbench.action.toggleMaximizedPanel', + id: ToggleMaximizedPanelActionId, title: localize2('toggleMaximizedPanel', 'Toggle Maximized Panel'), tooltip: localize('maximizePanel', "Maximize Panel Size"), category: Categories.View, f1: true, icon: maximizeIcon, - // the workbench grid currently prevents us from supporting panel maximization with non-center panel alignment - precondition: ContextKeyExpr.or(PanelAlignmentContext.isEqualTo('center'), ContextKeyExpr.and(PanelPositionContext.notEqualsTo('bottom'), PanelPositionContext.notEqualsTo('top'))), - toggled: { condition: PanelMaximizedContext, icon: maximizeIcon, tooltip: localize('minimizePanel', "Restore Panel Size") }, - menu: [{ - id: MenuId.PanelTitle, - group: 'navigation', - order: 1, - // the workbench grid currently prevents us from supporting panel maximization with non-center panel alignment - when: ContextKeyExpr.or(PanelAlignmentContext.isEqualTo('center'), ContextKeyExpr.and(PanelPositionContext.notEqualsTo('bottom'), PanelPositionContext.notEqualsTo('top'))) - }] + precondition: panelMaximizationSupportedWhen, // the workbench grid currently prevents us from supporting panel maximization with non-center panel alignment }); } run(accessor: ServicesAccessor) { @@ -314,6 +309,28 @@ registerAction2(class extends Action2 { } }); +MenuRegistry.appendMenuItem(MenuId.PanelTitle, { + command: { + id: ToggleMaximizedPanelActionId, + title: localize('maximizePanel', "Maximize Panel Size"), + icon: maximizeIcon + }, + group: 'navigation', + order: 1, + when: ContextKeyExpr.and(panelMaximizationSupportedWhen, PanelMaximizedContext.negate()) +}); + +MenuRegistry.appendMenuItem(MenuId.PanelTitle, { + command: { + id: ToggleMaximizedPanelActionId, + title: localize('minimizePanel', "Restore Panel Size"), + icon: restoreIcon + }, + group: 'navigation', + order: 1, + when: ContextKeyExpr.and(panelMaximizationSupportedWhen, PanelMaximizedContext) +}); + MenuRegistry.appendMenuItems([ { id: MenuId.LayoutControlMenu, From b6d6a3cc874fe3c445baa3b621b8a919fd220e98 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 20 Jan 2026 07:32:33 +0100 Subject: [PATCH 233/387] Move power monitor logging (fix #288875) (#289021) --- src/vs/code/electron-main/app.ts | 41 +++++++++++++++++-- .../electron-main/nativeHostMainService.ts | 32 +-------------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 05ebd48ea6b..a3efce99744 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, protocol, session, Session, systemPreferences, WebFrameMain } from 'electron'; +import { app, powerMonitor, protocol, session, Session, systemPreferences, WebFrameMain } from 'electron'; import { addUNCHostToAllowlist, disableUNCAccessRestrictions } from '../../base/node/unc.js'; import { validatedIpcMain } from '../../base/parts/ipc/electron-main/ipcMain.js'; import { hostname, release } from 'os'; @@ -611,7 +611,7 @@ export class CodeApplication extends Disposable { this.lifecycleMainService.phase = LifecycleMainPhase.AfterWindowOpen; // Post Open Windows Tasks - this.afterWindowOpen(); + this.afterWindowOpen(appInstantiationService); // Set lifecycle phase to `Eventually` after a short delay and when idle (min 2.5sec, max 5sec) const eventuallyPhaseScheduler = this._register(new RunOnceScheduler(() => { @@ -1374,7 +1374,7 @@ export class CodeApplication extends Disposable { }); } - private afterWindowOpen(): void { + private afterWindowOpen(instantiationService: IInstantiationService): void { // Windows: mutex this.installMutex(); @@ -1400,6 +1400,41 @@ export class CodeApplication extends Disposable { if (isMacintosh && app.runningUnderARM64Translation) { this.windowsMainService?.sendToFocused('vscode:showTranslatedBuildWarning'); } + + // Power telemetry + instantiationService.invokeFunction(accessor => { + const telemetryService = accessor.get(ITelemetryService); + + type PowerEvent = { + readonly idleState: string; + readonly idleTime: number; + readonly thermalState: string; + readonly onBattery: boolean; + }; + type PowerEventClassification = { + idleState: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The system idle state (active, idle, locked, unknown).' }; + idleTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The system idle time in seconds.' }; + thermalState: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The system thermal state (unknown, nominal, fair, serious, critical).' }; + onBattery: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether the system is running on battery power.' }; + owner: 'chrmarti'; + comment: 'Tracks OS power suspend and resume events for reliability insights.'; + }; + + const getPowerEventData = (): PowerEvent => ({ + idleState: powerMonitor.getSystemIdleState(60), + idleTime: powerMonitor.getSystemIdleTime(), + thermalState: powerMonitor.getCurrentThermalState(), + onBattery: powerMonitor.isOnBatteryPower() + }); + + this._register(Event.fromNodeEventEmitter(powerMonitor, 'suspend')(() => { + telemetryService.publicLog2('power.suspend', getPowerEventData()); + })); + + this._register(Event.fromNodeEventEmitter(powerMonitor, 'resume')(() => { + telemetryService.publicLog2('power.resume', getPowerEventData()); + })); + }); } private async installMutex(): Promise { diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index eb9f6511851..ee61af05310 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -47,7 +47,6 @@ import { zip } from '../../../base/node/zip.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { IProxyAuthService } from './auth.js'; import { AuthInfo, Credentials, IRequestService } from '../../request/common/request.js'; -import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { randomPath } from '../../../base/common/extpath.js'; export interface INativeHostMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } @@ -71,8 +70,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain @IConfigurationService private readonly configurationService: IConfigurationService, @IRequestService private readonly requestService: IRequestService, @IProxyAuthService private readonly proxyAuthService: IProxyAuthService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); @@ -121,34 +119,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain this.onDidResumeOS = Event.fromNodeEventEmitter(powerMonitor, 'resume'); - // Telemetry for power events - type PowerEvent = { - readonly idleState: string; - readonly idleTime: number; - readonly thermalState: string; - readonly onBattery: boolean; - }; - type PowerEventClassification = { - idleState: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The system idle state (active, idle, locked, unknown).' }; - idleTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The system idle time in seconds.' }; - thermalState: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The system thermal state (unknown, nominal, fair, serious, critical).' }; - onBattery: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether the system is running on battery power.' }; - owner: 'chrmarti'; - comment: 'Tracks OS power suspend and resume events for reliability insights.'; - }; - const getPowerEventData = (): PowerEvent => ({ - idleState: powerMonitor.getSystemIdleState(60), - idleTime: powerMonitor.getSystemIdleTime(), - thermalState: powerMonitor.getCurrentThermalState(), - onBattery: powerMonitor.isOnBatteryPower() - }); - this._register(Event.fromNodeEventEmitter(powerMonitor, 'suspend')(() => { - this.telemetryService.publicLog2('power.suspend', getPowerEventData()); - })); - this._register(Event.fromNodeEventEmitter(powerMonitor, 'resume')(() => { - this.telemetryService.publicLog2('power.resume', getPowerEventData()); - })); - this.onDidChangeColorScheme = this.themeMainService.onDidChangeColorScheme; this.onDidChangeDisplay = Event.debounce(Event.any( From 31ee1e48774715d959875014937998cdcb740072 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 19 Jan 2026 22:37:16 -0800 Subject: [PATCH 234/387] Add subagent name to chat request for logging (#289007) * Add subagent name to chat request for logging * Remove unneeded additions --- src/vs/workbench/api/browser/mainThreadChatAgents2.ts | 2 +- src/vs/workbench/api/common/extHostChatAgents2.ts | 2 +- src/vs/workbench/api/common/extHostTypeConverters.ts | 4 ++++ src/vs/workbench/api/common/extHostTypes.ts | 1 + .../workbench/contrib/chat/common/participants/chatAgents.ts | 4 ++++ .../chat/common/tools/builtinTools/runSubagentTool.ts | 1 + src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts | 5 +++++ 7 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 2a26c187d73..f0fd0935c21 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -291,7 +291,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA toolId: progress.toolName, chatRequestId: requestId, sessionResource: chatSession?.sessionResource, - subagentInvocationId: progress.subagentInvocationId + subagentInvocationId: progress.subagentInvocationId, }); continue; } diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index a9285d5534d..6c32e537504 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -313,7 +313,7 @@ export class ChatAgentResponseStream { streamData: streamData ? { partialInput: streamData.partialInput } : undefined, - subagentInvocationId: streamData?.subagentInvocationId + subagentInvocationId: streamData?.subagentInvocationId, }; _report(dto); return this; diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index ea078db10b6..c22c4e08f77 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2883,6 +2883,7 @@ export namespace ChatToolInvocationPart { toolInvocation.toolSpecificData = convertFromInternalToolSpecificData(part.toolSpecificData); } toolInvocation.subAgentInvocationId = part.subAgentInvocationId; + toolInvocation.subAgentName = part.subAgentName; return toolInvocation; } @@ -3162,6 +3163,7 @@ export namespace ChatAgentRequest { modeInstructions: request.modeInstructions?.content, modeInstructions2: ChatRequestModeInstructions.to(request.modeInstructions), subAgentInvocationId: request.subAgentInvocationId, + subAgentName: request.subAgentName, }; if (!isProposedApiEnabled(extension, 'chatParticipantPrivate')) { @@ -3183,6 +3185,8 @@ export namespace ChatAgentRequest { delete (requestWithAllProps as any).sessionId; // eslint-disable-next-line local/code-no-any-casts delete (requestWithAllProps as any).subAgentInvocationId; + // eslint-disable-next-line local/code-no-any-casts + delete (requestWithAllProps as any).subAgentName; } if (!isProposedApiEnabled(extension, 'chatParticipantAdditions')) { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index b24085ac813..9e19f40e259 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3360,6 +3360,7 @@ export class ChatToolInvocationPart { isComplete?: boolean; toolSpecificData?: ChatTerminalToolInvocationData2; subAgentInvocationId?: string; + subAgentName?: string; presentation?: 'hidden' | 'hiddenAfterComplete' | undefined; constructor(toolName: string, diff --git a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts index 36f2fb547bc..d8f79a27aad 100644 --- a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts @@ -153,6 +153,10 @@ export interface IChatAgentRequest { * Unique ID for the subagent invocation, used to group tool calls from the same subagent run together. */ subAgentInvocationId?: string; + /** + * Display name of the subagent that is invoking this request. + */ + subAgentName?: string; } diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts index 35f327b4cbc..d7d3a3ed1e7 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -231,6 +231,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { variables: { variables: variableSet.asArray() }, location: ChatAgentLocation.Chat, subAgentInvocationId: invocation.callId, + subAgentName: args.agentName, userSelectedModelId: modeModelId, userSelectedTools: modeTools, modeInstructions, diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index 39861c8e498..4196e31d903 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -98,6 +98,11 @@ declare module 'vscode' { * Pass this to tool invocations when calling tools from within a subagent context. */ readonly subAgentInvocationId?: string; + + /** + * Display name of the subagent that is invoking this request. + */ + readonly subAgentName?: string; } export enum ChatRequestEditedFileEventKind { From 32fc704566e8a1d111ccfa6153a7de8c00b8d8c9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 06:40:41 +0000 Subject: [PATCH 235/387] Draw active indication atop the activity bar icons when shown at the bottom (#286694) * Initial plan * Fix activity bar active indicator position when shown at bottom When the activity bar is positioned at the bottom of the sidebar, the active indicator (underline) now renders above the icons instead of below them. This provides better visual feedback for the active item. Changes: - paneCompositePart.css: Separate styling for header (top) and footer (bottom) positions, with indicator at bottom for header and top for footer - sidebarpart.css: Updated border color rules for footer position - auxiliaryBarPart.css: Updated border color rules for footer position Co-authored-by: benibenj <44439583+benibenj@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: benibenj <44439583+benibenj@users.noreply.github.com> --- .../auxiliarybar/media/auxiliaryBarPart.css | 9 ++++-- .../browser/parts/media/paneCompositePart.css | 29 ++++++++++++++++--- .../parts/sidebar/media/sidebarpart.css | 9 ++++-- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css b/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css index 7b30f627239..aec3de2d96d 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css +++ b/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css @@ -66,11 +66,16 @@ border-top-color: var(--vscode-panelTitle-activeBorder) !important; } -.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, -.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked.clicked:focus .active-item-indicator:before { +.monaco-workbench .part.auxiliarybar > .header-or-footer.header > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, +.monaco-workbench .part.auxiliarybar > .header-or-footer.header > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked.clicked:focus .active-item-indicator:before { border-top-color: var(--vscode-activityBarTop-activeBorder) !important; } +.monaco-workbench .part.auxiliarybar > .header-or-footer.footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, +.monaco-workbench .part.auxiliarybar > .header-or-footer.footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked.clicked:focus .active-item-indicator:before { + border-bottom-color: var(--vscode-activityBarTop-activeBorder) !important; +} + .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .action-label { color: var(--vscode-sideBarTitle-foreground) !important; diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index d063e8c4f22..8ec76362dd0 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -284,8 +284,8 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .active-item-indicator:before, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before, -.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .active-item-indicator:before, -.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { +.monaco-workbench .pane-composite-part > .header-or-footer.header > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .active-item-indicator:before, +.monaco-workbench .pane-composite-part > .header-or-footer.header > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { content: ""; position: absolute; z-index: 1; @@ -296,17 +296,38 @@ border-top-style: solid; } +.monaco-workbench .pane-composite-part > .header-or-footer.footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .active-item-indicator:before, +.monaco-workbench .pane-composite-part > .header-or-footer.footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { + content: ""; + position: absolute; + z-index: 1; + top: 2px; + width: 100%; + height: 0; + border-bottom-width: 1px; + border-bottom-style: solid; +} + .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.clicked:not(.checked):focus .active-item-indicator:before, -.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.clicked:not(.checked):focus .active-item-indicator:before { +.monaco-workbench .pane-composite-part > .header-or-footer.header > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.clicked:not(.checked):focus .active-item-indicator:before { border-top-color: transparent !important; /* hides border on clicked state */ } +.monaco-workbench .pane-composite-part > .header-or-footer.footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.clicked:not(.checked):focus .active-item-indicator:before { + border-bottom-color: transparent !important; /* hides border on clicked state */ +} + .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before, -.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { +.monaco-workbench .pane-composite-part > .header-or-footer.header > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { border-top-color: var(--vscode-focusBorder) !important; border-top-width: 2px; } +.monaco-workbench .pane-composite-part > .header-or-footer.footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { + border-bottom-color: var(--vscode-focusBorder) !important; + border-bottom-width: 2px; +} + .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, .monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, diff --git a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css index fff0b2cfeb2..decb51aba55 100644 --- a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css +++ b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css @@ -87,13 +87,18 @@ left: 5px; /* place icon in center */ } -.monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, -.monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked.clicked:focus .active-item-indicator:before, +.monaco-workbench .part.sidebar > .header-or-footer.header > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, +.monaco-workbench .part.sidebar > .header-or-footer.header > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked.clicked:focus .active-item-indicator:before, .monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, .monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked.clicked:focus .active-item-indicator:before { border-top-color: var(--vscode-activityBarTop-activeBorder) !important; } +.monaco-workbench .part.sidebar > .header-or-footer.footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, +.monaco-workbench .part.sidebar > .header-or-footer.footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked.clicked:focus .active-item-indicator:before { + border-bottom-color: var(--vscode-activityBarTop-activeBorder) !important; +} + .monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, .monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .action-label, .monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, From ad980f0b210caa89ec07d7bd8b55144bdbafc6e9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 20 Jan 2026 07:43:13 +0100 Subject: [PATCH 236/387] Better Shebang Language Detection (Deno, Bun, etc) (fix #287819) (#289026) --- extensions/typescript-basics/package.json | 1 + .../services/languagesAssociations.test.ts | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/extensions/typescript-basics/package.json b/extensions/typescript-basics/package.json index d64e6df2147..830b32762e7 100644 --- a/extensions/typescript-basics/package.json +++ b/extensions/typescript-basics/package.json @@ -27,6 +27,7 @@ ".cts", ".mts" ], + "firstLine": "^#!.*\\b(deno|bun|ts-node)\\b", "configuration": "./language-configuration.json" }, { diff --git a/src/vs/editor/test/common/services/languagesAssociations.test.ts b/src/vs/editor/test/common/services/languagesAssociations.test.ts index 891260d0fb5..f32a33d4a89 100644 --- a/src/vs/editor/test/common/services/languagesAssociations.test.ts +++ b/src/vs/editor/test/common/services/languagesAssociations.test.ts @@ -129,4 +129,26 @@ suite('LanguagesAssociations', () => { assert.deepStrictEqual(getMimeTypes(URI.parse(`data:;label:something.data;description:data,`)), ['text/data', 'text/plain']); }); + + test('Shebang detection for TypeScript runtimes', () => { + registerPlatformLanguageAssociation({ id: 'typescript', mime: 'text/typescript', firstline: /^#!.*\b(deno|bun|ts-node)\b/ }); + + // Deno shebangs + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/usr/bin/env deno'), ['text/typescript', 'text/plain']); + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/usr/bin/env -S deno -A'), ['text/typescript', 'text/plain']); + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/usr/bin/deno'), ['text/typescript', 'text/plain']); + + // Bun shebangs + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/usr/bin/env bun'), ['text/typescript', 'text/plain']); + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/usr/bin/env -S bun run'), ['text/typescript', 'text/plain']); + + // ts-node shebangs + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/usr/bin/env ts-node'), ['text/typescript', 'text/plain']); + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/usr/bin/env -S ts-node --esm'), ['text/typescript', 'text/plain']); + + // Should NOT match other shebangs + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/usr/bin/env node'), ['application/unknown']); + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/usr/bin/env python'), ['application/unknown']); + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/bin/bash'), ['application/unknown']); + }); }); From d52522be26ca5048ac05c0a9760f56fe4bcda414 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 20 Jan 2026 07:54:44 +0100 Subject: [PATCH 237/387] feat - add host service and update chat retry command (#289024) --- .../browser/chatSetup/chatSetupProviders.ts | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts index e417344be53..129876fb0de 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts @@ -52,6 +52,7 @@ import { IOutputService } from '../../../../services/output/common/output.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { IWorkbenchIssueService } from '../../../issue/common/issue.js'; import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; +import { IHostService } from '../../../../services/host/browser/host.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -239,18 +240,13 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { // Retry chat command this._register(CommandsRegistry.registerCommand(SetupAgent.CHAT_RETRY_COMMAND_ID, async (accessor, sessionResource: URI) => { - const chatService = accessor.get(IChatService); + const hostService = accessor.get(IHostService); const chatWidgetService = accessor.get(IChatWidgetService); const widget = chatWidgetService.getWidgetBySessionResource(sessionResource); - const lastRequest = widget?.viewModel?.model.getRequests().at(-1); - if (lastRequest) { - await chatService.resendRequest(lastRequest, { - ...widget?.getModeRequestOptions(), - modeInfo: widget?.input.currentModeInfo, - userSelectedModelId: widget?.input.currentLanguageModel - }); - } + await widget?.clear(); + + hostService.reload(); })); } @@ -369,9 +365,9 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { if (ready === 'timedout') { let warningMessage: string; if (this.chatEntitlementService.anonymous) { - warningMessage = localize('chatTookLongWarningAnonymous', "Chat took too long to get ready. Please ensure that the extension `{0}` is installed and enabled.", defaultChat.chatExtensionId); + warningMessage = localize('chatTookLongWarningAnonymous', "Chat took too long to get ready. Please ensure that the extension `{0}` is installed and enabled. Click restart to try again if this issue persists.", defaultChat.chatExtensionId); } else { - warningMessage = localize('chatTookLongWarning', "Chat took too long to get ready. Please ensure you are signed in to {0} and that the extension `{1}` is installed and enabled.", defaultChat.provider.default.name, defaultChat.chatExtensionId); + warningMessage = localize('chatTookLongWarning', "Chat took too long to get ready. Please ensure you are signed in to {0} and that the extension `{1}` is installed and enabled. Click restart to try again if this issue persists.", defaultChat.provider.default.name, defaultChat.chatExtensionId); } this.logService.warn(warningMessage, { @@ -417,7 +413,7 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { kind: 'command', command: { id: SetupAgent.CHAT_RETRY_COMMAND_ID, - title: localize('retryChat', "Retry"), + title: localize('retryChat', "Restart"), arguments: [requestModel.session.sessionResource] }, additionalCommands: [{ From 4ec8c09e46c06a026cf78bacd77e044ccdc9fa5d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 20 Jan 2026 09:00:03 +0100 Subject: [PATCH 238/387] Do not mark sessions which completed while visible in the UI as unread (fix #288369) (#289039) --- src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 4e94376ed44..642cc3bea38 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -1873,6 +1873,11 @@ export class ChatWidget extends Disposable implements IChatWidget { } // Only show if response wasn't canceled this.renderChatSuggestNextWidget(); + + // Mark the session as read when the request completes and the widget is visible + if (this.visible && this.viewModel?.sessionResource) { + this.agentSessionsService.getSession(this.viewModel.sessionResource)?.setRead(true); + } } })); From 5a2a2c61adbc2b9f7f43633c2cbe44cc1940f63d Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 20 Jan 2026 09:08:31 +0100 Subject: [PATCH 239/387] More simplifications --- .../controller/editContext/clipboardUtils.ts | 74 ++++++++++--------- .../editContext/native/nativeEditContext.ts | 18 +++-- .../textArea/textAreaEditContextInput.ts | 23 +++--- .../editor/common/viewModel/viewModelImpl.ts | 2 +- .../browser/copyPasteController.ts | 11 +-- .../browser/controller/textAreaInput.test.ts | 2 +- 6 files changed, 72 insertions(+), 58 deletions(-) diff --git a/src/vs/editor/browser/controller/editContext/clipboardUtils.ts b/src/vs/editor/browser/controller/editContext/clipboardUtils.ts index 4b8069c2df3..47c64ef1c5b 100644 --- a/src/vs/editor/browser/controller/editContext/clipboardUtils.ts +++ b/src/vs/editor/browser/controller/editContext/clipboardUtils.ts @@ -7,54 +7,40 @@ import { Range } from '../../../common/core/range.js'; import { isWindows } from '../../../../base/common/platform.js'; import { Mimes } from '../../../../base/common/mime.js'; import { ViewContext } from '../../../common/viewModel/viewContext.js'; -import { ILogService, LogLevel } from '../../../../platform/log/common/log.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { VSDataTransfer } from '../../../../base/common/dataTransfer.js'; import { toExternalVSDataTransfer } from '../../dataTransfer.js'; -export function ensureClipboardGetsEditorSelection(e: ClipboardEvent, context: ViewContext, logService: ILogService, isFirefox: boolean): void { - const viewModel = context.viewModel; - let id: string | undefined = undefined; - if (logService.getLevel() === LogLevel.Trace) { - id = generateUuid(); - } - - const { dataToCopy, storedMetadata } = generateDataToCopyAndStoreInMemory(viewModel, id, isFirefox); - - // !!!!! - // This is a workaround for what we think is an Electron bug where - // execCommand('copy') does not always work (it does not fire a clipboard event) - // !!!!! - // We signal that we have executed a copy command - CopyOptions.electronBugWorkaroundCopyEventHasFired = true; - - e.preventDefault(); - if (e.clipboardData) { - ClipboardEventUtils.setTextData(e.clipboardData, dataToCopy.text, dataToCopy.html, storedMetadata); - } - logService.trace('ensureClipboardGetsEditorSelection with id : ', id, ' with text.length: ', dataToCopy.text.length); +export function generateDataToCopyAndStoreInMemory(viewModel: IViewModel, id: string | undefined, isFirefox: boolean): { dataToCopy: ClipboardDataToCopy; metadata: ClipboardStoredMetadata } { + const { dataToCopy, metadata } = generateDataToCopy(viewModel); + storeMetadataInMemory(dataToCopy.text, metadata, isFirefox); + return { dataToCopy, metadata }; } -export function generateDataToCopyAndStoreInMemory(viewModel: IViewModel, id: string | undefined, isFirefox: boolean) { +function storeMetadataInMemory(textToCopy: string, metadata: ClipboardStoredMetadata, isFirefox: boolean): void { + InMemoryClipboardMetadataManager.INSTANCE.set( + // When writing "LINE\r\n" to the clipboard and then pasting, + // Firefox pastes "LINE\n", so let's work around this quirk + (isFirefox ? textToCopy.replace(/\r\n/g, '\n') : textToCopy), + metadata + ); +} + +function generateDataToCopy(viewModel: IViewModel): { dataToCopy: ClipboardDataToCopy; metadata: ClipboardStoredMetadata } { const emptySelectionClipboard = viewModel.getEditorOption(EditorOption.emptySelectionClipboard); const copyWithSyntaxHighlighting = viewModel.getEditorOption(EditorOption.copyWithSyntaxHighlighting); const selections = viewModel.getCursorStates().map(cursorState => cursorState.modelState.selection); const dataToCopy = getDataToCopy(viewModel, selections, emptySelectionClipboard, copyWithSyntaxHighlighting); - const storedMetadata: ClipboardStoredMetadata = { + const metadata: ClipboardStoredMetadata = { version: 1, - id, + id: generateUuid(), isFromEmptySelection: dataToCopy.isFromEmptySelection, multicursorText: dataToCopy.multicursorText, mode: dataToCopy.mode }; - InMemoryClipboardMetadataManager.INSTANCE.set( - // When writing "LINE\r\n" to the clipboard and then pasting, - // Firefox pastes "LINE\n", so let's work around this quirk - (isFirefox ? dataToCopy.text.replace(/\r\n/g, '\n') : dataToCopy.text), - storedMetadata - ); - return { dataToCopy, storedMetadata }; + return { dataToCopy, metadata }; } function getDataToCopy(viewModel: IViewModel, modelSelections: Range[], emptySelectionClipboard: boolean, copyWithSyntaxHighlighting: boolean): ClipboardDataToCopy { @@ -140,7 +126,7 @@ interface InMemoryClipboardMetadata { data: ClipboardStoredMetadata; } -export const ClipboardEventUtils = { +const ClipboardEventUtils = { getTextData(clipboardData: IReadableClipboardData | DataTransfer): [string, ClipboardStoredMetadata | null] { const text = clipboardData.getData(Mimes.text); @@ -217,6 +203,16 @@ export interface IClipboardCopyEvent { */ readonly clipboardData: IWritableClipboardData; + /** + * The data to be copied to the clipboard. + */ + readonly dataToCopy: ClipboardDataToCopy; + + /** + * Ensure that the clipboard gets the editor data. + */ + ensureClipboardGetsEditorData(): void; + /** * Signal that the event has been handled and default processing should be skipped. */ @@ -269,7 +265,8 @@ export interface IClipboardPasteEvent { /** * Creates an IClipboardCopyEvent from a DOM ClipboardEvent. */ -export function createClipboardCopyEvent(e: ClipboardEvent, isCut: boolean): IClipboardCopyEvent { +export function createClipboardCopyEvent(e: ClipboardEvent, isCut: boolean, context: ViewContext, logService: ILogService, isFirefox: boolean): IClipboardCopyEvent { + const { dataToCopy, metadata } = generateDataToCopy(context.viewModel); let handled = false; return { isCut, @@ -278,6 +275,15 @@ export function createClipboardCopyEvent(e: ClipboardEvent, isCut: boolean): ICl e.clipboardData?.setData(type, value); }, }, + dataToCopy, + ensureClipboardGetsEditorData: (): void => { + e.preventDefault(); + if (e.clipboardData) { + ClipboardEventUtils.setTextData(e.clipboardData, dataToCopy.text, dataToCopy.html, metadata); + } + storeMetadataInMemory(dataToCopy.text, metadata, isFirefox); + logService.trace('ensureClipboardGetsEditorSelection with id : ', metadata.id, ' with text.length: ', dataToCopy.text.length); + }, setHandled: () => { handled = true; e.preventDefault(); diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index f1c27d90805..017a7bc6270 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -16,7 +16,7 @@ import { ViewConfigurationChangedEvent, ViewCursorStateChangedEvent, ViewDecorat import { ViewContext } from '../../../../common/viewModel/viewContext.js'; import { RestrictedRenderingContext, RenderingContext, HorizontalPosition } from '../../../view/renderingContext.js'; import { ViewController } from '../../../view/viewController.js'; -import { createClipboardCopyEvent, createClipboardPasteEvent, ensureClipboardGetsEditorSelection } from '../clipboardUtils.js'; +import { CopyOptions, createClipboardCopyEvent, createClipboardPasteEvent } from '../clipboardUtils.js'; import { AbstractEditContext } from '../editContext.js'; import { editContextAddDisposableListener, FocusTracker, ITypeData } from './nativeEditContextUtils.js'; import { ScreenReaderSupport } from './screenReaderSupport.js'; @@ -114,16 +114,24 @@ export class NativeEditContext extends AbstractEditContext { this._register(addDisposableListener(this.domNode.domNode, 'copy', (e) => { this.logService.trace('NativeEditContext#copy'); - const copyEvent = createClipboardCopyEvent(e, /* isCut */ false); + + // !!!!! + // This is a workaround for what we think is an Electron bug where + // execCommand('copy') does not always work (it does not fire a clipboard event) + // !!!!! + // We signal that we have executed a copy command + CopyOptions.electronBugWorkaroundCopyEventHasFired = true; + + const copyEvent = createClipboardCopyEvent(e, /* isCut */ false, this._context, this.logService, isFirefox); this._onWillCopy.fire(copyEvent); if (copyEvent.isHandled) { return; } - ensureClipboardGetsEditorSelection(e, this._context, this.logService, isFirefox); + copyEvent.ensureClipboardGetsEditorData(); })); this._register(addDisposableListener(this.domNode.domNode, 'cut', (e) => { this.logService.trace('NativeEditContext#cut'); - const cutEvent = createClipboardCopyEvent(e, /* isCut */ true); + const cutEvent = createClipboardCopyEvent(e, /* isCut */ true, this._context, this.logService, isFirefox); this._onWillCut.fire(cutEvent); if (cutEvent.isHandled) { return; @@ -131,7 +139,7 @@ export class NativeEditContext extends AbstractEditContext { // Pretend here we touched the text area, as the `cut` event will most likely // result in a `selectionchange` event which we want to ignore this._screenReaderSupport.onWillCut(); - ensureClipboardGetsEditorSelection(e, this._context, this.logService, isFirefox); + cutEvent.ensureClipboardGetsEditorData(); this.logService.trace('NativeEditContext#cut (before viewController.cut)'); this._viewController.cut(); })); diff --git a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts index 04bd4419162..774fc8ab10e 100644 --- a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts +++ b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts @@ -18,7 +18,7 @@ import { Position } from '../../../../common/core/position.js'; import { Selection } from '../../../../common/core/selection.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; -import { ClipboardStoredMetadata, createClipboardCopyEvent, createClipboardPasteEvent, ensureClipboardGetsEditorSelection, IClipboardCopyEvent, IClipboardPasteEvent, InMemoryClipboardMetadataManager } from '../clipboardUtils.js'; +import { ClipboardStoredMetadata, CopyOptions, createClipboardCopyEvent, createClipboardPasteEvent, IClipboardCopyEvent, IClipboardPasteEvent, InMemoryClipboardMetadataManager } from '../clipboardUtils.js'; import { _debugComposition, ITextAreaWrapper, ITypeData, TextAreaState } from './textAreaEditContextState.js'; import { ViewContext } from '../../../../common/viewModel/viewContext.js'; @@ -37,7 +37,7 @@ export interface IPasteData { } export interface ITextAreaInputHost { - readonly context: ViewContext | null; + readonly context: ViewContext; getScreenReaderContent(): TextAreaState; deduceModelPosition(viewAnchorPosition: Position, deltaOffset: number, lineFeedCnt: number): Position; } @@ -370,7 +370,7 @@ export class TextAreaInput extends Disposable { this._logService.trace(`TextAreaInput#onCut`, e); // Fire onWillCut event to allow interception - const cutEvent = createClipboardCopyEvent(e, /* isCut */ true); + const cutEvent = createClipboardCopyEvent(e, /* isCut */ true, this._host.context, this._logService, this._browser.isFirefox); this._onWillCut.fire(cutEvent); if (cutEvent.isHandled) { // Event was handled externally, skip default processing @@ -381,26 +381,29 @@ export class TextAreaInput extends Disposable { // result in a `selectionchange` event which we want to ignore this._textArea.setIgnoreSelectionChangeTime('received cut event'); - if (this._host.context) { - ensureClipboardGetsEditorSelection(e, this._host.context, this._logService, this._browser.isFirefox); - } + cutEvent.ensureClipboardGetsEditorData(); this._asyncTriggerCut.schedule(); })); this._register(this._textArea.onCopy((e) => { this._logService.trace(`TextAreaInput#onCopy`, e); + // !!!!! + // This is a workaround for what we think is an Electron bug where + // execCommand('copy') does not always work (it does not fire a clipboard event) + // !!!!! + // We signal that we have executed a copy command + CopyOptions.electronBugWorkaroundCopyEventHasFired = true; + // Fire onWillCopy event to allow interception - const copyEvent = createClipboardCopyEvent(e, /* isCut */ false); + const copyEvent = createClipboardCopyEvent(e, /* isCut */ false, this._host.context, this._logService, this._browser.isFirefox); this._onWillCopy.fire(copyEvent); if (copyEvent.isHandled) { // Event was handled externally, skip default processing return; } - if (this._host.context) { - ensureClipboardGetsEditorSelection(e, this._host.context, this._logService, this._browser.isFirefox); - } + copyEvent.ensureClipboardGetsEditorData(); })); this._register(this._textArea.onPaste((e) => { diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index a6a89aee06c..8b5b053b4df 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -1027,7 +1027,7 @@ export class ViewModel extends Disposable implements IViewModel { } } - return { sourceRanges: [], sourceText: result.length === 1 ? result[0] : result }; + return { sourceRanges: ranges, sourceText: result.length === 1 ? result[0] : result }; } public getRichTextToCopy(modelRanges: Range[], emptySelectionClipboard: boolean): { html: string; mode: string } | null { diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index 6cc8e853ab3..79e5132ede9 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as browser from '../../../../base/browser/browser.js'; import { IAction } from '../../../../base/common/actions.js'; import { coalesce } from '../../../../base/common/arrays.js'; import { CancelablePromise, createCancelablePromise, DeferredPromise, raceCancellation } from '../../../../base/common/async.js'; @@ -24,7 +23,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; -import { generateDataToCopyAndStoreInMemory, IClipboardCopyEvent, IClipboardPasteEvent, IWritableClipboardData } from '../../../browser/controller/editContext/clipboardUtils.js'; +import { IClipboardCopyEvent, IClipboardPasteEvent, IWritableClipboardData } from '../../../browser/controller/editContext/clipboardUtils.js'; import { ICodeEditor, PastePayload } from '../../../browser/editorBrowser.js'; import { IBulkEditService } from '../../../browser/services/bulkEditService.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; @@ -189,11 +188,9 @@ export class CopyPasteController extends Disposable implements IEditorContributi return; } - const { dataToCopy } = generateDataToCopyAndStoreInMemory(viewModel, undefined, browser.isFirefox); - const defaultPastePayload = { - multicursorText: dataToCopy.multicursorText ?? null, - pasteOnNewLine: dataToCopy.isFromEmptySelection, + multicursorText: e.dataToCopy.multicursorText ?? null, + pasteOnNewLine: e.dataToCopy.isFromEmptySelection, mode: null }; @@ -220,7 +217,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi return { providerMimeTypes: provider.copyMimeTypes, operation: createCancelablePromise(token => - provider.prepareDocumentPaste!(model, dataToCopy.sourceRanges, dataTransfer, token) + provider.prepareDocumentPaste!(model, e.dataToCopy.sourceRanges, dataTransfer, token) .catch(err => { console.error(err); return undefined; diff --git a/src/vs/editor/test/browser/controller/textAreaInput.test.ts b/src/vs/editor/test/browser/controller/textAreaInput.test.ts index 7e8d404a2b3..ab25ca262c4 100644 --- a/src/vs/editor/test/browser/controller/textAreaInput.test.ts +++ b/src/vs/editor/test/browser/controller/textAreaInput.test.ts @@ -48,7 +48,7 @@ suite('TextAreaInput', () => { async function simulateInteraction(recorded: IRecorded): Promise { const disposables = new DisposableStore(); const host: ITextAreaInputHost = { - context: null, + context: null!, getScreenReaderContent: function (): TextAreaState { return new TextAreaState('', 0, 0, null, undefined); }, From a9daa80ebf207b24d7b39182fdb7b648af6d452c Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 20 Jan 2026 09:30:04 +0100 Subject: [PATCH 240/387] SCM - fix regression with missing text document diff information (#289042) --- .../api/browser/mainThreadEditors.ts | 57 +++++++++++++------ .../contrib/scm/browser/quickDiffModel.ts | 2 + .../workbench/contrib/scm/common/quickDiff.ts | 8 ++- 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index 5af157c2ac0..fb3ef0087d9 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { illegalArgument } from '../../../base/common/errors.js'; -import { IDisposable, dispose, DisposableStore, IReference } from '../../../base/common/lifecycle.js'; +import { IDisposable, dispose, DisposableStore } from '../../../base/common/lifecycle.js'; import { equals as objectEquals } from '../../../base/common/objects.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { ICodeEditorService } from '../../../editor/browser/services/codeEditorService.js'; @@ -28,12 +28,13 @@ import { IExtHostContext } from '../../services/extensions/common/extHostCustome import { IEditorControl } from '../../common/editor.js'; import { getCodeEditor, ICodeEditor } from '../../../editor/browser/editorBrowser.js'; import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; -import { IQuickDiffModelService, QuickDiffModel } from '../../contrib/scm/browser/quickDiffModel.js'; +import { IQuickDiffModelService } from '../../contrib/scm/browser/quickDiffModel.js'; import { autorun, constObservable, derived, derivedOpts, IObservable, observableFromEvent } from '../../../base/common/observable.js'; import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js'; import { isITextModel } from '../../../editor/common/model.js'; import { LineRangeMapping } from '../../../editor/common/diff/rangeMapping.js'; import { equals } from '../../../base/common/arrays.js'; +import { Event } from '../../../base/common/event.js'; import { DiffAlgorithmName } from '../../../editor/common/services/editorWorker.js'; export interface IMainThreadEditorLocator { @@ -148,42 +149,64 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { const editorChangesObs = derived>(reader => { const editorModel = editorModelObs.read(reader); - const editorModelUri = codeEditor.getModel()?.uri; - - if (!editorModel || !editorModelUri) { + if (!editorModel) { return constObservable(undefined); } - let quickDiffModelRef: IReference | undefined; + // TextEditor if (isITextModel(editorModel)) { - // TextEditor - quickDiffModelRef = this._quickDiffModelService.createQuickDiffModelReference(editorModelUri); - } else { - // DiffEditor - we create a quick diff model (using the diff algorithm used by the diff editor) - // even for diff editor so that we can provide multiple "original resources" to diff with the original - // and modified resources. - const diffAlgorithm = this._configurationService.getValue('diffEditor.diffAlgorithm'); - quickDiffModelRef = this._quickDiffModelService.createQuickDiffModelReference(editorModelUri, { algorithm: diffAlgorithm }); + const quickDiffModelRef = this._quickDiffModelService.createQuickDiffModelReference(editorModel.uri); + if (!quickDiffModelRef) { + return constObservable(undefined); + } + + toDispose.push(quickDiffModelRef); + return observableFromEvent(this, quickDiffModelRef.object.onDidChange, () => { + return quickDiffModelRef.object.getQuickDiffResults() + .map(result => ({ + original: result.original, + modified: result.modified, + changes: result.changes2 + })); + }); } + // DiffEditor - we create a quick diff model (using the diff algorithm used by the diff editor) + // even for diff editor so that we can provide multiple "original resources" to diff with the original + // and modified resources. + const diffAlgorithm = this._configurationService.getValue('diffEditor.diffAlgorithm'); + const quickDiffModelRef = this._quickDiffModelService.createQuickDiffModelReference(editorModel.modified.uri, { algorithm: diffAlgorithm }); if (!quickDiffModelRef) { return constObservable(undefined); } toDispose.push(quickDiffModelRef); - return observableFromEvent(this, quickDiffModelRef.object.onDidChange, () => { - return quickDiffModelRef.object.getQuickDiffResults() + return observableFromEvent(Event.any(quickDiffModelRef.object.onDidChange, diffEditor.onDidUpdateDiff), () => { + const diffChanges = diffEditor.getDiffComputationResult()?.changes2 ?? []; + const diffInformation = [{ + original: editorModel.original.uri, + modified: editorModel.modified.uri, + changes: diffChanges.map(change => change as LineRangeMapping) + }]; + + // Add quick diff information from secondary/contributed providers + const quickDiffInformation = quickDiffModelRef.object.getQuickDiffResults() + .filter(result => result.providerKind !== 'primary') .map(result => ({ original: result.original, modified: result.modified, changes: result.changes2 })); + + // Combine diff and quick diff information + return diffInformation.concat(quickDiffInformation); }); }); return derivedOpts({ owner: this, - equalsFn: (diff1, diff2) => equals(diff1, diff2, (a, b) => isTextEditorDiffInformationEqual(this._uriIdentityService, a, b)) + equalsFn: (diff1, diff2) => equals(diff1, diff2, (a, b) => + isTextEditorDiffInformationEqual(this._uriIdentityService, a, b)) }, reader => { const editorModel = editorModelObs.read(reader); const editorChanges = editorChangesObs.read(reader).read(reader); diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts index 68c2c261db0..b1eef6b6908 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts @@ -183,6 +183,8 @@ export class QuickDiffModel extends Disposable { .filter(change => change.providerId === quickDiff.id); return { + providerId: quickDiff.id, + providerKind: quickDiff.kind, original: quickDiff.originalResource, modified: this._model.resource, changes: changes.map(change => change.change), diff --git a/src/vs/workbench/contrib/scm/common/quickDiff.ts b/src/vs/workbench/contrib/scm/common/quickDiff.ts index bc994f648cb..543a3b69204 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiff.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiff.ts @@ -66,12 +66,14 @@ export const editorGutterItemGlyphForeground = registerColor('editorGutter.itemG ); export const editorGutterItemBackground = registerColor('editorGutter.itemBackground', { dark: opaque(listInactiveSelectionBackground, editorBackground), light: darken(opaque(listInactiveSelectionBackground, editorBackground), .05), hcDark: Color.white, hcLight: Color.black }, nls.localize('editorGutterItemBackground', 'Editor gutter decoration color for gutter item background. This color should be opaque.')); +type QuickDiffProviderKind = 'primary' | 'secondary' | 'contributed'; + export interface QuickDiffProvider { readonly id: string; readonly label: string; readonly rootUri: URI | undefined; readonly selector?: LanguageSelector; - readonly kind: 'primary' | 'secondary' | 'contributed'; + readonly kind: QuickDiffProviderKind; getOriginalResource(uri: URI): Promise; } @@ -79,7 +81,7 @@ export interface QuickDiff { readonly id: string; readonly label: string; readonly originalResource: URI; - readonly kind: 'primary' | 'secondary' | 'contributed'; + readonly kind: QuickDiffProviderKind; } export interface QuickDiffChange { @@ -91,6 +93,8 @@ export interface QuickDiffChange { } export interface QuickDiffResult { + readonly providerId: string; + readonly providerKind: QuickDiffProviderKind; readonly original: URI; readonly modified: URI; readonly changes: IChange[]; From fc884f0e50e8e5c8aa84043f656e8a4a8d3be916 Mon Sep 17 00:00:00 2001 From: Jimmy Leung <43258070+hkleungai@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:50:59 +0800 Subject: [PATCH 241/387] vscode-dts: Fix typedoc for WebviewPanel.dispose() --- src/vscode-dts/vscode.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index d858a7745fe..30ba7df267a 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -10138,7 +10138,7 @@ declare module 'vscode' { * * This closes the panel if it showing and disposes of the resources owned by the webview. * Webview panels are also disposed when the user closes the webview panel. Both cases - * fire the `onDispose` event. + * fire the `onDidDispose` event. */ dispose(): any; } From b08502b62c2624ced0c9be6d609b6df25e2b8d12 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Tue, 20 Jan 2026 09:52:50 +0100 Subject: [PATCH 242/387] Ensure flags are set correctly --- .../contrib/chat/browser/chatSetup/chatSetupProviders.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts index 129876fb0de..7169f91b9e2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts @@ -340,8 +340,17 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { const whenAgentActivated = this.whenAgentActivated(chatService).then(() => agentActivated = true); const whenAgentReady = this.whenAgentReady(chatAgentService, modeInfo?.kind)?.then(() => agentReady = true); + if (!whenAgentReady) { + agentReady = true; + } const whenLanguageModelReady = this.whenLanguageModelReady(languageModelsService, requestModel.modelId)?.then(() => languageModelReady = true); + if (!whenLanguageModelReady) { + languageModelReady = true; + } const whenToolsModelReady = this.whenToolsModelReady(languageModelToolsService, requestModel)?.then(() => toolsModelReady = true); + if (!whenToolsModelReady) { + toolsModelReady = true; + } if (whenLanguageModelReady instanceof Promise || whenAgentReady instanceof Promise || whenToolsModelReady instanceof Promise) { const timeoutHandle = setTimeout(() => { From 64dc92cec2dc09a356967b7da2389f6689cc0b07 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 20 Jan 2026 10:26:26 +0100 Subject: [PATCH 243/387] fix #287549 (#289047) * fix #287549 --- .../mcp/common/mcpManagementService.ts | 18 +++- .../test/common/mcpManagementService.test.ts | 98 ++++++++++++++++++- 2 files changed, 109 insertions(+), 7 deletions(-) diff --git a/src/vs/platform/mcp/common/mcpManagementService.ts b/src/vs/platform/mcp/common/mcpManagementService.ts index 570bcfa89e5..22768fd4448 100644 --- a/src/vs/platform/mcp/common/mcpManagementService.ts +++ b/src/vs/platform/mcp/common/mcpManagementService.ts @@ -126,17 +126,31 @@ export abstract class AbstractCommonMcpManagementService extends Disposable impl switch (serverPackage.registryType) { case RegistryType.NODE: + if (serverPackage.registryBaseUrl) { + args.push('--registry', serverPackage.registryBaseUrl); + } args.push(serverPackage.version ? `${serverPackage.identifier}@${serverPackage.version}` : serverPackage.identifier); break; case RegistryType.PYTHON: + if (serverPackage.registryBaseUrl) { + args.push('--index-url', serverPackage.registryBaseUrl); + } args.push(serverPackage.version ? `${serverPackage.identifier}==${serverPackage.version}` : serverPackage.identifier); break; case RegistryType.DOCKER: - args.push(serverPackage.version ? `${serverPackage.identifier}:${serverPackage.version}` : serverPackage.identifier); - break; + { + const dockerIdentifier = serverPackage.registryBaseUrl + ? `${serverPackage.registryBaseUrl}/${serverPackage.identifier}` + : serverPackage.identifier; + args.push(serverPackage.version ? `${dockerIdentifier}:${serverPackage.version}` : dockerIdentifier); + break; + } case RegistryType.NUGET: args.push(serverPackage.version ? `${serverPackage.identifier}@${serverPackage.version}` : serverPackage.identifier); args.push('--yes'); // installation is confirmed by the UI, so --yes is appropriate here + if (serverPackage.registryBaseUrl) { + args.push('--add-source', serverPackage.registryBaseUrl); + } if (serverPackage.packageArguments?.length) { args.push('--'); } diff --git a/src/vs/platform/mcp/test/common/mcpManagementService.test.ts b/src/vs/platform/mcp/test/common/mcpManagementService.test.ts index 1756c100a36..37cd7386215 100644 --- a/src/vs/platform/mcp/test/common/mcpManagementService.test.ts +++ b/src/vs/platform/mcp/test/common/mcpManagementService.test.ts @@ -60,7 +60,6 @@ suite('McpManagementService - getMcpServerConfigurationFromManifest', () => { const manifest: IGalleryMcpServerConfiguration = { packages: [{ registryType: RegistryType.NODE, - registryBaseUrl: 'https://registry.npmjs.org', identifier: '@modelcontextprotocol/server-brave-search', transport: { type: TransportType.STDIO }, version: '1.0.2', @@ -82,11 +81,33 @@ suite('McpManagementService - getMcpServerConfigurationFromManifest', () => { assert.strictEqual(result.mcpServerConfiguration.inputs, undefined); }); + test('NPM package with custom registry URL', () => { + const manifest: IGalleryMcpServerConfiguration = { + packages: [{ + registryType: RegistryType.NODE, + registryBaseUrl: 'https://custom-registry.example.com', + identifier: '@company/internal-package', + transport: { type: TransportType.STDIO }, + version: '2.1.0' + }] + }; + + const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE); + + assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL); + if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) { + assert.strictEqual(result.mcpServerConfiguration.config.command, 'npx'); + assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [ + '--registry', 'https://custom-registry.example.com', + '@company/internal-package@2.1.0' + ]); + } + }); + test('NPM package without version', () => { const manifest: IGalleryMcpServerConfiguration = { packages: [{ registryType: RegistryType.NODE, - registryBaseUrl: 'https://registry.npmjs.org', identifier: '@modelcontextprotocol/everything', version: '', transport: { type: TransportType.STDIO } @@ -237,7 +258,6 @@ suite('McpManagementService - getMcpServerConfigurationFromManifest', () => { packages: [{ registryType: RegistryType.PYTHON, transport: { type: TransportType.STDIO }, - registryBaseUrl: 'https://pypi.org', identifier: 'weather-mcp-server', version: '0.5.0', environmentVariables: [{ @@ -263,6 +283,29 @@ suite('McpManagementService - getMcpServerConfigurationFromManifest', () => { } }); + test('Python package with custom registry URL', () => { + const manifest: IGalleryMcpServerConfiguration = { + packages: [{ + registryType: RegistryType.PYTHON, + registryBaseUrl: 'https://custom-pypi.example.com/simple', + transport: { type: TransportType.STDIO }, + identifier: 'internal-python-server', + version: '1.2.3' + }] + }; + + const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.PYTHON); + + assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL); + if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) { + assert.strictEqual(result.mcpServerConfiguration.config.command, 'uvx'); + assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [ + '--index-url', 'https://custom-pypi.example.com/simple', + 'internal-python-server==1.2.3' + ]); + } + }); + test('Python package without version', () => { const manifest: IGalleryMcpServerConfiguration = { packages: [{ @@ -287,7 +330,6 @@ suite('McpManagementService - getMcpServerConfigurationFromManifest', () => { packages: [{ registryType: RegistryType.DOCKER, transport: { type: TransportType.STDIO }, - registryBaseUrl: 'https://docker.io', identifier: 'mcp/filesystem', version: '1.0.2', runtimeArguments: [{ @@ -325,6 +367,29 @@ suite('McpManagementService - getMcpServerConfigurationFromManifest', () => { } }); + test('Docker package with custom registry URL', () => { + const manifest: IGalleryMcpServerConfiguration = { + packages: [{ + registryType: RegistryType.DOCKER, + registryBaseUrl: 'registry.company.com', + transport: { type: TransportType.STDIO }, + identifier: 'internal/mcp-server', + version: '3.2.1' + }] + }; + + const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.DOCKER); + + assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL); + if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) { + assert.strictEqual(result.mcpServerConfiguration.config.command, 'docker'); + assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [ + 'run', '-i', '--rm', + 'registry.company.com/internal/mcp-server:3.2.1' + ]); + } + }); + test('Docker package with variables in runtime arguments', () => { const manifest: IGalleryMcpServerConfiguration = { packages: [{ @@ -445,7 +510,6 @@ suite('McpManagementService - getMcpServerConfigurationFromManifest', () => { packages: [{ registryType: RegistryType.NUGET, transport: { type: TransportType.STDIO }, - registryBaseUrl: 'https://api.nuget.org', identifier: 'Knapcode.SampleMcpServer', version: '0.5.0', environmentVariables: [{ @@ -465,6 +529,30 @@ suite('McpManagementService - getMcpServerConfigurationFromManifest', () => { } }); + test('NuGet package with custom registry URL', () => { + const manifest: IGalleryMcpServerConfiguration = { + packages: [{ + registryType: RegistryType.NUGET, + registryBaseUrl: 'https://nuget.company.com/v3/index.json', + transport: { type: TransportType.STDIO }, + identifier: 'Company.Internal.McpServer', + version: '4.5.6' + }] + }; + + const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NUGET); + + assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL); + if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) { + assert.strictEqual(result.mcpServerConfiguration.config.command, 'dnx'); + assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [ + 'Company.Internal.McpServer@4.5.6', + '--yes', + '--add-source', 'https://nuget.company.com/v3/index.json' + ]); + } + }); + test('NuGet package with package arguments', () => { const manifest: IGalleryMcpServerConfiguration = { packages: [{ From f067226bffe94f776f2172ca57f7dac0fb13e1d8 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 20 Jan 2026 10:49:21 +0100 Subject: [PATCH 244/387] fix #283572 (#289056) --- src/vs/platform/mcp/common/mcpManagementService.ts | 2 +- .../platform/mcp/test/common/mcpManagementService.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/mcp/common/mcpManagementService.ts b/src/vs/platform/mcp/common/mcpManagementService.ts index 22768fd4448..6a50e743653 100644 --- a/src/vs/platform/mcp/common/mcpManagementService.ts +++ b/src/vs/platform/mcp/common/mcpManagementService.ts @@ -135,7 +135,7 @@ export abstract class AbstractCommonMcpManagementService extends Disposable impl if (serverPackage.registryBaseUrl) { args.push('--index-url', serverPackage.registryBaseUrl); } - args.push(serverPackage.version ? `${serverPackage.identifier}==${serverPackage.version}` : serverPackage.identifier); + args.push(serverPackage.version ? `${serverPackage.identifier}@${serverPackage.version}` : serverPackage.identifier); break; case RegistryType.DOCKER: { diff --git a/src/vs/platform/mcp/test/common/mcpManagementService.test.ts b/src/vs/platform/mcp/test/common/mcpManagementService.test.ts index 37cd7386215..e3d63c7d275 100644 --- a/src/vs/platform/mcp/test/common/mcpManagementService.test.ts +++ b/src/vs/platform/mcp/test/common/mcpManagementService.test.ts @@ -275,7 +275,7 @@ suite('McpManagementService - getMcpServerConfigurationFromManifest', () => { assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL); if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) { assert.strictEqual(result.mcpServerConfiguration.config.command, 'uvx'); - assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['weather-mcp-server==0.5.0']); + assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['weather-mcp-server@0.5.0']); assert.deepStrictEqual(result.mcpServerConfiguration.config.env, { 'WEATHER_API_KEY': 'test-key', 'WEATHER_UNITS': 'celsius' @@ -301,7 +301,7 @@ suite('McpManagementService - getMcpServerConfigurationFromManifest', () => { assert.strictEqual(result.mcpServerConfiguration.config.command, 'uvx'); assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [ '--index-url', 'https://custom-pypi.example.com/simple', - 'internal-python-server==1.2.3' + 'internal-python-server@1.2.3' ]); } }); @@ -894,7 +894,7 @@ suite('McpManagementService - getMcpServerConfigurationFromManifest', () => { assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL); if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) { assert.strictEqual(result.mcpServerConfiguration.config.command, 'uvx'); // Python command since that's the package type - assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['python-server==1.0.0']); + assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['python-server@1.0.0']); } }); From 5c0c3899291d743dc1ff5ea4c7f219d012cc8597 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 20 Jan 2026 10:54:49 +0100 Subject: [PATCH 245/387] layout - add new settings to always restore 2nd sidebar maximised and to not restore editors (#289055) * layout - add new settings to always restore 2nd sidebar maximised and to not restore editors * Update src/vs/workbench/browser/workbench.contribution.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/vs/workbench/browser/layout.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * feedback --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/vs/workbench/browser/layout.ts | 13 +++++++++++-- src/vs/workbench/browser/workbench.contribution.ts | 10 ++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index f069e02eca2..4b732fec850 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -773,6 +773,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Restore editors based on a set of rules: // - never when running on temporary workspace + // - never when `workbench.editor.restoreEditors` is disabled // - not when we have files to open, unless: // - always when `window.restoreWindows: preserve` @@ -780,6 +781,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return false; } + if (this.configurationService.getValue(WorkbenchLayoutSettings.EDITOR_RESTORE_EDITORS) === false) { + return false; + } + const forceRestoreEditors = this.configurationService.getValue('window.restoreWindows') === 'preserve'; return !!forceRestoreEditors || initialEditorsState === undefined; } @@ -2766,11 +2771,13 @@ interface ILayoutStateChangeEvent { enum WorkbenchLayoutSettings { AUXILIARYBAR_DEFAULT_VISIBILITY = 'workbench.secondarySideBar.defaultVisibility', + AUXILIARYBAR_RESTORE_MAXIMIZED = 'workbench.secondarySideBar.restoreMaximized', ACTIVITY_BAR_VISIBLE = 'workbench.activityBar.visible', PANEL_POSITION = 'workbench.panel.defaultLocation', PANEL_OPENS_MAXIMIZED = 'workbench.panel.opensMaximized', ZEN_MODE_CONFIG = 'zenMode', EDITOR_CENTERED_LAYOUT_AUTO_RESIZE = 'workbench.editor.centeredLayoutAutoResize', + EDITOR_RESTORE_EDITORS = 'workbench.editor.restoreEditors', } enum LegacyWorkbenchLayoutSettings { @@ -2937,10 +2944,12 @@ class LayoutStateModel extends Disposable { private applyOverrides(configuration: ILayoutStateLoadConfiguration): void { - // Auxiliary bar: Maximized setting (new workspaces) - if (this.isNew[StorageScope.WORKSPACE]) { + // Auxiliary bar: Maximized settings + const restoreAuxiliaryBarMaximized = this.configurationService.getValue(WorkbenchLayoutSettings.AUXILIARYBAR_RESTORE_MAXIMIZED); + if (this.isNew[StorageScope.WORKSPACE] || restoreAuxiliaryBarMaximized) { const defaultAuxiliaryBarVisibility = this.configurationService.getValue(WorkbenchLayoutSettings.AUXILIARYBAR_DEFAULT_VISIBILITY); if ( + restoreAuxiliaryBarMaximized || defaultAuxiliaryBarVisibility === 'maximized' || (defaultAuxiliaryBarVisibility === 'maximizedInWorkspace' && this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY) ) { diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index dd9f46edda5..4a2a5883a55 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -382,6 +382,11 @@ const registry = Registry.as(ConfigurationExtensions.Con 'description': localize('sharedViewState', "Preserves the most recent editor view state (such as scroll position) across all editor groups and restores that if no specific editor view state is found for the editor group."), 'default': false }, + 'workbench.editor.restoreEditors': { + 'type': 'boolean', + 'description': localize('restoreOnStartup', "Controls whether editors are restored on startup. When disabled, only dirty editors will be restored from the previous session."), + 'default': true + }, 'workbench.editor.splitInGroupLayout': { 'type': 'string', 'enum': ['vertical', 'horizontal'], @@ -580,6 +585,11 @@ const registry = Registry.as(ConfigurationExtensions.Con localize('workbench.secondarySideBar.defaultVisibility.maximized', "The secondary side bar is visible and maximized by default.") ] }, + 'workbench.secondarySideBar.restoreMaximized': { + 'type': 'boolean', + 'default': false, + 'description': localize('secondarySideBarRestoreMaximized', "Controls whether the secondary side bar restores to a maximized state after startup, irrespective of its previous state."), + }, 'workbench.secondarySideBar.showLabels': { 'type': 'boolean', 'default': true, From 756bc9cddfe4d44d2cf0efa40c99dc553a5df971 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Tue, 20 Jan 2026 10:56:27 +0100 Subject: [PATCH 246/387] Codex agent in agent type picker --- .../browser/agentSessions/agentSessions.ts | 30 ++++++++++++---- .../chatSessions/chatSessions.contribution.ts | 35 ++++++++++++++++--- .../delegationSessionPickerActionItem.ts | 15 ++++++-- .../input/sessionTargetPickerActionItem.ts | 16 +++++++-- 4 files changed, 80 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts index 16ed35cb16b..4cc9edf8ac9 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts @@ -15,7 +15,8 @@ export enum AgentSessionProviders { Local = localChatSessionType, Background = 'copilotcli', Cloud = 'copilot-cloud-agent', - ClaudeCode = 'claude-code', + Claude = 'claude-code', + Codex = 'openai-codex', } export function getAgentSessionProvider(sessionResource: URI | string): AgentSessionProviders | undefined { @@ -24,7 +25,8 @@ export function getAgentSessionProvider(sessionResource: URI | string): AgentSes case AgentSessionProviders.Local: case AgentSessionProviders.Background: case AgentSessionProviders.Cloud: - case AgentSessionProviders.ClaudeCode: + case AgentSessionProviders.Claude: + case AgentSessionProviders.Codex: return type; default: return undefined; @@ -39,8 +41,10 @@ export function getAgentSessionProviderName(provider: AgentSessionProviders): st return localize('chat.session.providerLabel.background', "Background"); case AgentSessionProviders.Cloud: return localize('chat.session.providerLabel.cloud', "Cloud"); - case AgentSessionProviders.ClaudeCode: - return localize('chat.session.providerLabel.claude', "Claude"); + case AgentSessionProviders.Claude: + return 'Claude'; + case AgentSessionProviders.Codex: + return 'Codex'; } } @@ -52,7 +56,8 @@ export function getAgentSessionProviderIcon(provider: AgentSessionProviders): Th return Codicon.worktree; case AgentSessionProviders.Cloud: return Codicon.cloud; - case AgentSessionProviders.ClaudeCode: + case AgentSessionProviders.Codex: + case AgentSessionProviders.Claude: return Codicon.code; } } @@ -63,7 +68,20 @@ export function isFirstPartyAgentSessionProvider(provider: AgentSessionProviders case AgentSessionProviders.Background: case AgentSessionProviders.Cloud: return true; - case AgentSessionProviders.ClaudeCode: + case AgentSessionProviders.Claude: + case AgentSessionProviders.Codex: + return false; + } +} + +export function getAgentCanContinueIn(provider: AgentSessionProviders): boolean { + switch (provider) { + case AgentSessionProviders.Local: + case AgentSessionProviders.Background: + case AgentSessionProviders.Cloud: + return true; + case AgentSessionProviders.Claude: + case AgentSessionProviders.Codex: return false; } } diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index 7b973bc922d..9212545a22f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -49,6 +49,7 @@ import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; import { LocalChatSessionUri } from '../../common/model/chatUri.js'; import { assertNever } from '../../../../../base/common/assert.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; const extensionPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'chatSessions', @@ -516,12 +517,18 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ const disposables = new DisposableStore(); // Mirror all create submenu actions into the global Chat New menu - for (const action of menuActions) { + for (let i = 0; i < menuActions.length; i++) { + const action = menuActions[i]; if (action instanceof MenuItemAction) { - disposables.add(MenuRegistry.appendMenuItem(MenuId.ChatNewMenu, { - command: action.item, - group: '4_externally_contributed', - })); + // TODO: This is an odd way to do this, but the best we can do currently + if (i === 0 && !contribution.canDelegate) { + disposables.add(registerNewSessionExternalAction(contribution.type, contribution.displayName, action.item.id)); + } else { + disposables.add(MenuRegistry.appendMenuItem(MenuId.ChatNewMenu, { + command: action.item, + group: '4_externally_contributed', + })); + } } } return { @@ -1125,6 +1132,24 @@ function registerNewSessionInPlaceAction(type: string, displayName: string): IDi }); } +function registerNewSessionExternalAction(type: string, displayName: string, commandId: string): IDisposable { + return registerAction2(class NewChatSessionExternalAction extends Action2 { + constructor() { + super({ + id: `workbench.action.chat.openNewChatSessionExternal.${type}`, + title: localize2('interactiveSession.openNewChatSessionExternal', "New {0}", displayName), + category: CHAT_CATEGORY, + f1: false, + precondition: ChatContextKeys.enabled, + }); + } + async run(accessor: ServicesAccessor): Promise { + const commandService = accessor.get(ICommandService); + await commandService.executeCommand(commandId); + } + }); +} + enum ChatSessionPosition { Editor = 'editor', Sidebar = 'sidebar' diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts index c5c0fc6ad2e..9e562c53e3d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts @@ -9,7 +9,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { localize } from '../../../../../../nls.js'; import { IActionWidgetDropdownAction } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; import { ACTION_ID_NEW_CHAT } from '../../actions/chatActions.js'; -import { AgentSessionProviders, getAgentSessionProvider } from '../../agentSessions/agentSessions.js'; +import { AgentSessionProviders, getAgentCanContinueIn, getAgentSessionProvider, isFirstPartyAgentSessionProvider } from '../../agentSessions/agentSessions.js'; import { ISessionTypeItem, SessionTypePickerActionItem } from './sessionTargetPickerActionItem.js'; /** @@ -49,8 +49,19 @@ export class DelegationSessionPickerActionItem extends SessionTypePickerActionIt return this._getSelectedSessionType() !== type; // Always allow switching back to active session } + protected override _isVisible(type: AgentSessionProviders): boolean { + if (this.delegate.getActiveSessionProvider() === type) { + return true; // Always show active session type + } + + return getAgentCanContinueIn(type); + } + protected override _getSessionCategory(sessionTypeItem: ISessionTypeItem) { - return { label: localize('continueIn', "Continue In"), order: 1, showHeader: true }; + if (isFirstPartyAgentSessionProvider(sessionTypeItem.type)) { + return { label: localize('continueIn', "Continue In"), order: 1, showHeader: true }; + } + return { label: localize('continueInThirdParty', "Continue In (Third Party)"), order: 2, showHeader: false }; } protected override _getSessionDescription(sessionTypeItem: ISessionTypeItem): string | undefined { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts index 5f5761e0e30..bc378fc83fe 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts @@ -58,6 +58,10 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { const actions: IActionWidgetDropdownAction[] = [...this._getAdditionalActions()]; for (const sessionTypeItem of this._sessionTypeItems) { + if (!this._isVisible(sessionTypeItem.type)) { + continue; + } + actions.push({ ...action, id: sessionTypeItem.commandId, @@ -134,14 +138,14 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { } private _updateAgentSessionItems(): void { - const localSessionItem = { + const localSessionItem: ISessionTypeItem = { type: AgentSessionProviders.Local, label: getAgentSessionProviderName(AgentSessionProviders.Local), description: localize('chat.sessionTarget.local.description', "Local chat session"), commandId: `workbench.action.chat.openNewChatSessionInPlace.${AgentSessionProviders.Local}`, }; - const agentSessionItems = [localSessionItem]; + const agentSessionItems: ISessionTypeItem[] = [localSessionItem]; const contributions = this.chatSessionsService.getAllChatSessionContributions(); for (const contribution of contributions) { @@ -154,12 +158,18 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { type: agentSessionType, label: getAgentSessionProviderName(agentSessionType), description: contribution.description, - commandId: `workbench.action.chat.openNewChatSessionInPlace.${contribution.type}`, + commandId: contribution.canDelegate ? + `workbench.action.chat.openNewChatSessionInPlace.${contribution.type}` : + `workbench.action.chat.openNewChatSessionExternal.${contribution.type}`, }); } this._sessionTypeItems = agentSessionItems; } + protected _isVisible(type: AgentSessionProviders): boolean { + return true; + } + protected _isSessionTypeEnabled(type: AgentSessionProviders): boolean { return true; } From 043ceac535313b6b6523b7e5d373ef372b940cbe Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 20 Jan 2026 11:13:31 +0100 Subject: [PATCH 247/387] implement action to open language models configuration file (#289059) --- .../chatManagement.contribution.ts | 165 +++++++++++++----- .../languageModelsConfigurationService.ts | 1 + .../common/languageModelsConfiguration.ts | 3 + 3 files changed, 127 insertions(+), 42 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagement.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagement.contribution.ts index df2a0d58e1e..f3c901e717c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagement.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagement.contribution.ts @@ -6,7 +6,7 @@ import { KeyCode } from '../../../../../base/common/keyCodes.js'; import { isObject, isString } from '../../../../../base/common/types.js'; import { localize, localize2 } from '../../../../../nls.js'; -import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; @@ -17,11 +17,28 @@ import { EditorExtensions, IEditorFactoryRegistry, IEditorSerializer } from '../ import { EditorInput } from '../../../../common/editor/editorInput.js'; import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { ResourceContextKey } from '../../../../common/contextkeys.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { CONTEXT_MODELS_EDITOR, CONTEXT_MODELS_SEARCH_FOCUS, MANAGE_CHAT_COMMAND_ID } from '../../common/constants.js'; import { CHAT_CATEGORY } from '../actions/chatActions.js'; import { ChatManagementEditor, ModelsManagementEditor } from './chatManagementEditor.js'; import { ChatManagementEditorInput, ModelsManagementEditorInput } from './chatManagementEditorInput.js'; +import { ILanguageModelsConfigurationService } from '../../common/languageModelsConfiguration.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { registerIcon } from '../../../../../platform/theme/common/iconRegistry.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js'; + +const languageModelsOpenSettingsIcon = registerIcon('language-models-open-settings', Codicon.goToFile, localize('languageModelsOpenSettings', 'Icon for open language models settings commands.')); + +const LANGUAGE_MODELS_ENTITLEMENT_PRECONDITION = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or( + ChatContextKeys.Entitlement.planFree, + ChatContextKeys.Entitlement.planPro, + ChatContextKeys.Entitlement.planProPlus, + ChatContextKeys.Entitlement.planBusiness, + ChatContextKeys.Entitlement.planEnterprise, + ChatContextKeys.Entitlement.internal +)); Registry.as(EditorExtensions.EditorPane).registerEditorPane( EditorPaneDescriptor.create( @@ -100,49 +117,113 @@ function sanitizeOpenManageCopilotEditorArgs(input: unknown): IOpenManageCopilot }; } -registerAction2(class extends Action2 { - constructor() { - super({ - id: MANAGE_CHAT_COMMAND_ID, - title: localize2('openAiManagement', "Manage Language Models"), - category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or( - ChatContextKeys.Entitlement.planFree, - ChatContextKeys.Entitlement.planPro, - ChatContextKeys.Entitlement.planProPlus, - ChatContextKeys.Entitlement.planBusiness, - ChatContextKeys.Entitlement.planEnterprise, - ChatContextKeys.Entitlement.internal - )), - f1: true, - }); - } - async run(accessor: ServicesAccessor, args: string | IOpenManageCopilotEditorActionOptions) { - const editorGroupsService = accessor.get(IEditorGroupsService); - args = sanitizeOpenManageCopilotEditorArgs(args); - return editorGroupsService.activeGroup.openEditor(new ModelsManagementEditorInput(), { pinned: true }); - } -}); +class ChatManagementActionsContribution extends Disposable implements IWorkbenchContribution { -registerAction2(class extends Action2 { - constructor() { - super({ - id: 'chat.models.action.clearSearchResults', - precondition: CONTEXT_MODELS_EDITOR, - keybinding: { - primary: KeyCode.Escape, - weight: KeybindingWeight.EditorContrib, - when: CONTEXT_MODELS_SEARCH_FOCUS + static readonly ID = 'workbench.contrib.chatManagementActions'; + + constructor( + @ILanguageModelsConfigurationService private readonly languageModelsConfigurationService: ILanguageModelsConfigurationService, + ) { + super(); + this.registerChatManagementActions(); + this.registerLanguageModelsEditorTitleActions(); + } + + private registerChatManagementActions() { + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: MANAGE_CHAT_COMMAND_ID, + title: localize2('openAiManagement', "Manage Language Models"), + category: CHAT_CATEGORY, + precondition: LANGUAGE_MODELS_ENTITLEMENT_PRECONDITION, + f1: true, + }); + } + async run(accessor: ServicesAccessor, args: string | IOpenManageCopilotEditorActionOptions) { + const editorGroupsService = accessor.get(IEditorGroupsService); + args = sanitizeOpenManageCopilotEditorArgs(args); + return editorGroupsService.activeGroup.openEditor(new ModelsManagementEditorInput(), { pinned: true }); + } + })); + + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: 'chat.models.action.clearSearchResults', + precondition: CONTEXT_MODELS_EDITOR, + keybinding: { + primary: KeyCode.Escape, + weight: KeybindingWeight.EditorContrib, + when: CONTEXT_MODELS_SEARCH_FOCUS + }, + title: localize2('models.clearResults', "Clear Models Search Results") + }); + } + + run(accessor: ServicesAccessor) { + const activeEditorPane = accessor.get(IEditorService).activeEditorPane; + if (activeEditorPane instanceof ModelsManagementEditor) { + activeEditorPane.clearSearch(); + } + return null; + } + })); + + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.openLanguageModelsJson', + title: localize2('openLanguageModelsJson', "Open Language Models (JSON)"), + category: CHAT_CATEGORY, + precondition: LANGUAGE_MODELS_ENTITLEMENT_PRECONDITION, + f1: true, + }); + } + + async run(accessor: ServicesAccessor) { + const languageModelsConfigurationService = accessor.get(ILanguageModelsConfigurationService); + await languageModelsConfigurationService.configureLanguageModels(); + } + })); + } + + private registerLanguageModelsEditorTitleActions() { + const modelsConfigurationFile = this.languageModelsConfigurationService.configurationFile; + const openModelsManagementEditorWhen = ContextKeyExpr.and( + CONTEXT_MODELS_EDITOR.toNegated(), + ResourceContextKey.Resource.isEqualTo(modelsConfigurationFile.toString()), + ContextKeyExpr.not('isInDiffEditor'), + LANGUAGE_MODELS_ENTITLEMENT_PRECONDITION + ); + + MenuRegistry.appendMenuItem(MenuId.EditorTitle, { + command: { + id: MANAGE_CHAT_COMMAND_ID, + title: localize2('openAiManagement', "Manage Language Models"), + icon: languageModelsOpenSettingsIcon }, - title: localize2('models.clearResults', "Clear Models Search Results") + when: openModelsManagementEditorWhen, + group: 'navigation', + order: 1 + }); + + const openLanguageModelsJsonWhen = ContextKeyExpr.and( + CONTEXT_MODELS_EDITOR, + LANGUAGE_MODELS_ENTITLEMENT_PRECONDITION + ); + + MenuRegistry.appendMenuItem(MenuId.EditorTitle, { + command: { + id: 'workbench.action.openLanguageModelsJson', + title: localize2('openLanguageModelsJson', "Open Language Models (JSON)"), + icon: languageModelsOpenSettingsIcon + }, + when: openLanguageModelsJsonWhen, + group: 'navigation', + order: 1 }); } +} - run(accessor: ServicesAccessor) { - const activeEditorPane = accessor.get(IEditorService).activeEditorPane; - if (activeEditorPane instanceof ModelsManagementEditor) { - activeEditorPane.clearSearch(); - } - return null; - } -}); +registerWorkbenchContribution2(ChatManagementActionsContribution.ID, ChatManagementActionsContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts b/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts index 860fa330c89..9468d4f413c 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts @@ -34,6 +34,7 @@ export class LanguageModelsConfigurationService extends Disposable implements IL declare _serviceBrand: undefined; private readonly modelsConfigurationFile: URI; + get configurationFile(): URI { return this.modelsConfigurationFile; } private readonly _onDidChangeLanguageModelGroups = new Emitter(); readonly onDidChangeLanguageModelGroups: Event = this._onDidChangeLanguageModelGroups.event; diff --git a/src/vs/workbench/contrib/chat/common/languageModelsConfiguration.ts b/src/vs/workbench/contrib/chat/common/languageModelsConfiguration.ts index 57baba9e9c7..9ed7cd0e378 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelsConfiguration.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelsConfiguration.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from '../../../../base/common/event.js'; +import { URI } from '../../../../base/common/uri.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; @@ -13,6 +14,8 @@ export const ILanguageModelsConfigurationService = createDecorator; getLanguageModelsProviderGroups(): readonly ILanguageModelsProviderGroup[]; From 9aaa385099d2f69cb95b1d5356d09199a4b31af1 Mon Sep 17 00:00:00 2001 From: Ved BHadani Date: Tue, 20 Jan 2026 16:13:45 +0530 Subject: [PATCH 248/387] Automatic activation event for chat context provider (#280677) Fix onChatContextProvider activation event not working Fixes #280643 The issue was that extensions contributing chat context providers were not being activated when their context providers were needed. This was because the chatContext extension point was missing an activationEventsGenerator. When an extension contributes a chatContext item in package.json, it also defines an activation event like 'onChatContextProvider:my-chat-context-provider'. However, without the activationEventsGenerator, VSCode didn't know to activate the extension when that context provider was requested. This commit adds an activationEventsGenerator to the chatContext extension point registration, following the same pattern used in other extension points like chatParticipants. The generator yields 'onChatContextProvider:${contrib.id}' for each chatContext contribution, ensuring extensions are properly activated when their context providers are needed. Co-authored-by: Alex Ross <38270282+alexr00@users.noreply.github.com> --- .../browser/contextContrib/chatContext.contribution.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/contextContrib/chatContext.contribution.ts b/src/vs/workbench/contrib/chat/browser/contextContrib/chatContext.contribution.ts index 33472b08f2f..8109c362c3b 100644 --- a/src/vs/workbench/contrib/chat/browser/contextContrib/chatContext.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/contextContrib/chatContext.contribution.ts @@ -41,7 +41,12 @@ const extensionPoint = ExtensionsRegistry.registerExtensionPoint Date: Tue, 20 Jan 2026 10:47:52 +0000 Subject: [PATCH 249/387] style: add right margin to interactive item container for improved layout --- src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index caa434d031b..055cd427deb 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -943,6 +943,7 @@ have to be updated for changes to the rules above, or to support more deeply nes flex-direction: row; gap: 4px; margin-top: 6px; + margin-right: 20px; flex-wrap: wrap; cursor: default; } From 05279bc0e6f49bdedf7818d8adbd2f912097b17a Mon Sep 17 00:00:00 2001 From: Alexandru Dima Date: Tue, 20 Jan 2026 11:49:59 +0100 Subject: [PATCH 250/387] Merge pull request #289068 from microsoft/revert-285906-avoid_editcontext_reflow Revert "Optimize rendering performance by scheduling DOM updates at the next animation frame in NativeEditContext and TextAreaEditContext" --- .../editContext/native/nativeEditContext.ts | 20 ++----------------- .../textArea/textAreaEditContext.ts | 20 +------------------ 2 files changed, 3 insertions(+), 37 deletions(-) diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index 67b7424bdeb..017a7bc6270 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -5,11 +5,10 @@ import './nativeEditContext.css'; import { isFirefox } from '../../../../../base/browser/browser.js'; -import { addDisposableListener, getActiveElement, getWindow, getWindowId, scheduleAtNextAnimationFrame } from '../../../../../base/browser/dom.js'; +import { addDisposableListener, getActiveElement, getWindow, getWindowId } from '../../../../../base/browser/dom.js'; import { FastDomNode } from '../../../../../base/browser/fastDomNode.js'; import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js'; import { KeyCode } from '../../../../../base/common/keyCodes.js'; -import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { EditorOption } from '../../../../common/config/editorOptions.js'; import { EndOfLinePreference, IModelDeltaDecoration } from '../../../../common/model.js'; @@ -69,7 +68,6 @@ export class NativeEditContext extends AbstractEditContext { private _targetWindowId: number = -1; private _scrollTop: number = 0; private _scrollLeft: number = 0; - private _selectionAndControlBoundsUpdateDisposable: IDisposable | undefined; private readonly _focusTracker: FocusTracker; @@ -257,8 +255,6 @@ export class NativeEditContext extends AbstractEditContext { this.domNode.domNode.blur(); this.domNode.domNode.remove(); this._imeTextArea.domNode.remove(); - this._selectionAndControlBoundsUpdateDisposable?.dispose(); - this._selectionAndControlBoundsUpdateDisposable = undefined; super.dispose(); } @@ -531,19 +527,7 @@ export class NativeEditContext extends AbstractEditContext { } } - private _updateSelectionAndControlBoundsAfterRender(): void { - if (this._selectionAndControlBoundsUpdateDisposable) { - return; - } - // Schedule this work after render so we avoid triggering a layout while still painting. - const targetWindow = getWindow(this.domNode.domNode); - this._selectionAndControlBoundsUpdateDisposable = scheduleAtNextAnimationFrame(targetWindow, () => { - this._selectionAndControlBoundsUpdateDisposable = undefined; - this._applySelectionAndControlBounds(); - }); - } - - private _applySelectionAndControlBounds(): void { + private _updateSelectionAndControlBoundsAfterRender() { const options = this._context.configuration.options; const contentLeft = options.get(EditorOption.layoutInfo).contentLeft; diff --git a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts index d3d56b8f690..f19cc424373 100644 --- a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts @@ -6,7 +6,6 @@ import './textAreaEditContext.css'; import * as nls from '../../../../../nls.js'; import * as browser from '../../../../../base/browser/browser.js'; -import { scheduleAtNextAnimationFrame, getWindow } from '../../../../../base/browser/dom.js'; import { FastDomNode, createFastDomNode } from '../../../../../base/browser/fastDomNode.js'; import { IKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js'; import * as platform from '../../../../../base/common/platform.js'; @@ -32,7 +31,6 @@ import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from '../../../../../base/browser/ui import { TokenizationRegistry } from '../../../../common/languages.js'; import { ColorId, ITokenPresentation } from '../../../../common/encodedTokenAttributes.js'; import { Color } from '../../../../../base/common/color.js'; -import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { IME } from '../../../../../base/common/ime.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; @@ -141,7 +139,6 @@ export class TextAreaEditContext extends AbstractEditContext { * This is useful for hit-testing and determining the mouse position. */ private _lastRenderPosition: Position | null; - private _scheduledRender: IDisposable | null = null; public readonly textArea: FastDomNode; public readonly textAreaCover: FastDomNode; @@ -467,8 +464,6 @@ export class TextAreaEditContext extends AbstractEditContext { } public override dispose(): void { - this._scheduledRender?.dispose(); - this._scheduledRender = null; super.dispose(); this.textArea.domNode.remove(); this.textAreaCover.domNode.remove(); @@ -692,20 +687,7 @@ export class TextAreaEditContext extends AbstractEditContext { public render(ctx: RestrictedRenderingContext): void { this._textAreaInput.writeNativeTextAreaContent('render'); - this._scheduleRender(); - } - - // Delay expensive DOM updates until the next animation frame to reduce reflow pressure. - private _scheduleRender(): void { - if (this._scheduledRender) { - return; - } - - const targetWindow = getWindow(this.textArea.domNode); - this._scheduledRender = scheduleAtNextAnimationFrame(targetWindow, () => { - this._scheduledRender = null; - this._render(); - }); + this._render(); } private _render(): void { From b7b1562421bce413bc0cea05d5421bcd636b7899 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 20 Jan 2026 11:55:27 +0100 Subject: [PATCH 251/387] Better activation info for chat context provider (#289069) Fixes #280643 --- .../services/extensions/common/extensionsRegistry.ts | 5 +++++ src/vscode-dts/vscode.proposed.chatContextProvider.d.ts | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index f6c18c64321..7c947abe9b0 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -394,6 +394,11 @@ export const schema: IJSONSchema = { body: 'onChatParticipant:${1:participantId}', description: nls.localize('vscode.extension.activationEvents.onChatParticipant', 'An activation event emitted when the specified chat participant is invoked.'), }, + { + label: 'onChatContextProvider', + body: 'onChatContextProvider:${1:contextProviderId}', + description: nls.localize('vscode.extension.activationEvents.onChatContextProvider', 'An activation event emitted when the specified chat context provider is invoked.'), + }, { label: 'onLanguageModelChatProvider', body: 'onLanguageModelChatProvider:${1:vendor}', diff --git a/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts index fb810775142..043e067be16 100644 --- a/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts @@ -16,7 +16,10 @@ declare module 'vscode' { * Providers registered without a selector will not be called for resource-based context. * - Explicitly. These context items are shown as options when the user explicitly attaches context. * - * To ensure your extension is activated when chat context is requested, make sure to include the `onChatContextProvider:` activation event in your `package.json`. + * To ensure your extension is activated when chat context is requested, make sure to include the following activations events: + * - If your extension implements `provideWorkspaceChatContext` or `provideChatContextForResource`, find an activation event which is a good signal to activate. + * Ex: `onLanguage:`, `onWebviewPanel:`, etc.` + * - If your extension implements `provideChatContextExplicit`, your extension will be automatically activated when the user requests explicit context. * * @param selector Optional document selector to filter which resources the provider is called for. If omitted, the provider will only be called for explicit context requests. * @param id Unique identifier for the provider. From fd3c340afc05f2f981305eb764bd8e03fd934f12 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 20 Jan 2026 12:00:03 +0100 Subject: [PATCH 252/387] Speed up code block rendering - Add renderAsync method to CodeEditorWidget for deferred rendering - Enable postponeRendering in CodeBlockPart.layout() to batch editor layouts - Implement onDidRemount callback in CodeBlockPart and ChatMarkdownContentPart to re-render editors when reconnected to the DOM after virtualization --- src/vs/editor/browser/editorBrowser.ts | 5 +++++ .../widget/codeEditor/codeEditorWidget.ts | 9 +++++++++ src/vs/monaco.d.ts | 4 ++++ .../chatContentParts/chatMarkdownContentPart.ts | 8 ++++++++ .../widget/chatContentParts/codeBlockPart.ts | 16 +++++++++++++++- 5 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index cf774c63bba..a510c12506a 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -1229,6 +1229,11 @@ export interface ICodeEditor extends editorCommon.IEditor { */ render(forceRedraw?: boolean): void; + /** + * Render the editor at the next animation frame. + */ + renderAsync(forceRedraw?: boolean): void; + /** * Get the hit test target at coordinates `clientX` and `clientY`. * The coordinates are relative to the top-left of the viewport. diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index a53d266f5f4..9a4ae242fce 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -1703,6 +1703,15 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE }); } + public renderAsync(forceRedraw: boolean = false): void { + if (!this._modelData || !this._modelData.hasRealView) { + return; + } + this._modelData.viewModel.batchEvents(() => { + this._modelData!.view.render(false, forceRedraw); + }); + } + public setAriaOptions(options: editorBrowser.IEditorAriaOptions): void { if (!this._modelData || !this._modelData.hasRealView) { return; diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index a85f63bf973..368f0e5ca89 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -6389,6 +6389,10 @@ declare namespace monaco.editor { * Force an editor render now. */ render(forceRedraw?: boolean): void; + /** + * Render the editor at the next animation frame. + */ + renderAsync(forceRedraw?: boolean): void; /** * Get the hit test target at coordinates `clientX` and `clientY`. * The coordinates are relative to the top-left of the viewport. 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 8e571e80b46..6485bd8f9dc 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts @@ -447,6 +447,14 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP this.mathLayoutParticipants.forEach(layout => layout()); } + onDidRemount(): void { + for (const ref of this.allRefs) { + if (ref.object instanceof CodeBlockPart) { + ref.object.onDidRemount(); + } + } + } + addDisposable(disposable: IDisposable): void { this._register(disposable); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/codeBlockPart.ts index d19dd33ecf4..35b7d57c59e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/codeBlockPart.ts @@ -389,7 +389,12 @@ export class CodeBlockPart extends Disposable { const editorBorder = 2; width = width - editorBorder - (this.currentCodeBlockData?.renderOptions?.reserveWidth ?? 0); - this.editor.layout({ width: isRequestVM(this.currentCodeBlockData?.element) ? width * 0.9 : width, height }); + // !!!! + // Important: Using here postponeRendering = true to avoid doing a sync layout on the editor + // which can be very expensive if there are many code blocks being laid out at once. + // This allows multiple editors to coordinate and render together at the next animation frame. + // !!!! + this.editor.layout({ width: isRequestVM(this.currentCodeBlockData?.element) ? width * 0.9 : width, height }, /* postponeRendering */ true); this.updatePaddingForLayout(); } @@ -454,6 +459,15 @@ export class CodeBlockPart extends Disposable { this.currentCodeBlockData = undefined; } + onDidRemount(): void { + if (this.currentCodeBlockData) { + // !!!! + // Important: if the editor was off-dom and is now connected, we need to re-render it + // !!!! + this.editor.renderAsync(true); + } + } + private clearWidgets() { ContentHoverController.get(this.editor)?.hideContentHover(); GlyphHoverController.get(this.editor)?.hideGlyphHover(); From a5c7fde869e90402397b2205a2bfb9313d62ee82 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:12:22 +0100 Subject: [PATCH 253/387] Fix comment collapse keybinding (#289073) --- .../contrib/comments/browser/commentsEditorContribution.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index dd96b65fef4..688fd773561 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -435,6 +435,8 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler: async (accessor, args) => { const activeCodeEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor(); const keybindingService = accessor.get(IKeybindingService); + const notificationService = accessor.get(INotificationService); + const commentService = accessor.get(ICommentService); // Unfortunate, but collapsing the comment thread might cause a dialog to show // If we don't wait for the key up here, then the dialog will consume it and immediately close await keybindingService.enableKeybindingHoldMode(CommentCommandId.Hide); @@ -445,8 +447,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ if (!controller) { return; } - const notificationService = accessor.get(INotificationService); - const commentService = accessor.get(ICommentService); + let error = false; try { const activeComment = commentService.lastActiveCommentcontroller?.activeComment; From 90901bdbf11945aab3ba128971a3dc28617287b7 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 20 Jan 2026 11:27:03 +0000 Subject: [PATCH 254/387] update color theme settings for improved UI consistency in light and dark modes --- extensions/theme-2026/themes/2026-dark.json | 2 +- extensions/theme-2026/themes/2026-light.json | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 5031dc271c7..4cfdf873343 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -171,7 +171,7 @@ "statusBar.debuggingForeground": "#FFFFFF", "statusBar.noFolderBackground": "#191B1D", "statusBar.noFolderForeground": "#bfbfbf", - "statusBarItem.activeBackground": "#498FAE", + "statusBarItem.activeBackground": "#4A4D4F", "statusBarItem.hoverBackground": "#252829", "statusBarItem.focusBorder": "#498FADB3", "statusBarItem.prominentBackground": "#498FAE", diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index 2ef9666b656..1248cd2fd9c 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -13,16 +13,16 @@ "textBlockQuote.border": "#EEEEEE00", "textCodeBlock.background": "#E9E9E9", "textLink.foreground": "#3457C0", - "textLink.activeForeground": "#395DC9", + "textLink.activeForeground": "#3355BA", "textPreformat.foreground": "#666666", "textSeparator.foreground": "#EEEEEE00", "button.background": "#4466CC", "button.foreground": "#FFFFFF", - "button.hoverBackground": "#4F6FCF", + "button.hoverBackground": "#3E61CA", "button.border": "#EEEEEE00", "button.secondaryBackground": "#E9E9E9", "button.secondaryForeground": "#202020", - "button.secondaryHoverBackground": "#F5F5F5", + "button.secondaryHoverBackground": "#E2E2E2", "checkbox.background": "#E9E9E9", "checkbox.border": "#EEEEEE00", "checkbox.foreground": "#202020", @@ -57,7 +57,7 @@ "list.activeSelectionForeground": "#202020", "list.inactiveSelectionBackground": "#E9E9E9", "list.inactiveSelectionForeground": "#202020", - "list.hoverBackground": "#FFFFFF", + "list.hoverBackground": "#F2F2F2", "list.hoverForeground": "#202020", "list.dropBackground": "#4466CC1A", "list.focusBackground": "#4466CC26", @@ -98,7 +98,7 @@ "commandCenter.foreground": "#202020", "commandCenter.activeForeground": "#202020", "commandCenter.background": "#F9F9F9", - "commandCenter.activeBackground": "#FFFFFF", + "commandCenter.activeBackground": "#F2F2F2", "commandCenter.border": "#D6D7D880", "editor.background": "#FDFDFD", "editor.foreground": "#202123", @@ -171,8 +171,8 @@ "statusBar.debuggingForeground": "#FFFFFF", "statusBar.noFolderBackground": "#F9F9F9", "statusBar.noFolderForeground": "#202020", - "statusBarItem.activeBackground": "#4466CC", - "statusBarItem.hoverBackground": "#FFFFFF", + "statusBarItem.activeBackground": "#E0E0E0", + "statusBarItem.hoverBackground": "#F2F2F2", "statusBarItem.focusBorder": "#4466CCFF", "statusBarItem.prominentBackground": "#4466CC", "statusBarItem.prominentForeground": "#FFFFFF", @@ -184,7 +184,7 @@ "tab.border": "#EEEEEE00", "tab.lastPinnedBorder": "#EEEEEE00", "tab.activeBorder": "#FBFBFD", - "tab.hoverBackground": "#FFFFFF", + "tab.hoverBackground": "#F2F2F2", "tab.hoverForeground": "#202020", "tab.unfocusedActiveBackground": "#FDFDFD", "tab.unfocusedActiveForeground": "#666666", @@ -207,7 +207,7 @@ "notificationLink.foreground": "#4466CC", "extensionButton.prominentBackground": "#4466CC", "extensionButton.prominentForeground": "#FFFFFF", - "extensionButton.prominentHoverBackground": "#4F6FCF", + "extensionButton.prominentHoverBackground": "#3E61CA", "pickerGroup.border": "#EEEEEE00", "pickerGroup.foreground": "#202020", "quickInput.background": "#FCFCFC", @@ -215,7 +215,7 @@ "quickInputList.focusBackground": "#4466CC26", "quickInputList.focusForeground": "#202020", "quickInputList.focusIconForeground": "#202020", - "quickInputList.hoverBackground": "#FAFAFA", + "quickInputList.hoverBackground": "#E7E7E7", "terminal.selectionBackground": "#4466CC33", "terminalCursor.foreground": "#202020", "terminalCursor.background": "#F9F9F9", From d346fdcdfeecf76e56433dedfaf8df32126ad9ad Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Tue, 20 Jan 2026 12:29:04 +0100 Subject: [PATCH 255/387] make small ghosttext suggestions more visible --- .../browser/view/ghostText/ghostTextView.css | 7 ++++++ .../browser/view/ghostText/ghostTextView.ts | 25 ++++++++++++++++--- .../browser/view/inlineSuggestionsView.ts | 1 + 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.css b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.css index 16148bd87b0..e46dca6d726 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.css @@ -64,6 +64,13 @@ border-bottom: 4px double var(--vscode-editorWarning-border); } +.monaco-editor .ghost-text-decoration.short-text, +.monaco-editor .ghost-text-decoration-preview.short-text, +.monaco-editor .suggest-preview-text .ghost-text.short-text { + text-decoration: underline dotted var(--vscode-editorGhostText-foreground); + text-underline-position: under; +} + .ghost-text-view-warning-widget-icon { .codicon { color: var(--vscode-editorWarning-foreground) !important; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts index e244d37aaf2..96053b4e19a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts @@ -78,6 +78,7 @@ export class GhostTextView extends Disposable { private readonly _shouldKeepCursorStable: boolean; private readonly _minReservedLineCount: IObservable; private readonly _useSyntaxHighlighting: IObservable; + private readonly _highlightShortText: boolean; constructor( private readonly _editor: ICodeEditor, @@ -88,6 +89,7 @@ export class GhostTextView extends Disposable { shouldKeepCursorStable?: boolean; minReservedLineCount?: IObservable; useSyntaxHighlighting?: IObservable; + highlightShortSuggestions?: boolean; }, @ILanguageService private readonly _languageService: ILanguageService ) { @@ -98,6 +100,7 @@ export class GhostTextView extends Disposable { this._shouldKeepCursorStable = options.shouldKeepCursorStable ?? false; this._minReservedLineCount = options.minReservedLineCount ?? constObservable(0); this._useSyntaxHighlighting = options.useSyntaxHighlighting ?? constObservable(true); + this._highlightShortText = options.highlightShortSuggestions ?? false; this._editorObs = observableCodeEditor(this._editor); this._additionalLinesWidget = this._register( @@ -203,14 +206,25 @@ export class GhostTextView extends Disposable { return undefined; } + private readonly _nonWhitespaceCount = derived(this, reader => { + const data = this._data.read(reader); + if (!data) { return undefined; } + const ghostText = data.ghostText; + const allText = ghostText.parts.map(p => p.lines.map(l => l.line).join('')).join(''); + return allText.replace(/\s/g, '').length; + }); + private readonly _extraClassNames = derived(this, reader => { const extraClasses = this._extraClasses.slice(); - if (this._useSyntaxHighlighting.read(reader)) { - extraClasses.push('syntax-highlighted'); - } if (USE_SQUIGGLES_FOR_WARNING && this._warningState.read(reader)) { extraClasses.push('warning'); } + const nonWhitespaceCount = this._nonWhitespaceCount.read(reader); + if (this._highlightShortText && nonWhitespaceCount && nonWhitespaceCount < 3) { + extraClasses.push('short-text'); + } else if (this._useSyntaxHighlighting.read(reader)) { + extraClasses.push('syntax-highlighted'); + } const extraClassNames = extraClasses.map(c => ` ${c}`).join(''); return extraClassNames; }); @@ -301,6 +315,10 @@ export class GhostTextView extends Disposable { } for (const p of uiState.inlineTexts) { + let inlineExtraClassNames = ''; + if (this._highlightShortText && p.text.length < 5) { + inlineExtraClassNames += ' short-text'; + } decorations.push({ range: Range.fromPositions(new Position(uiState.lineNumber, p.column)), options: { @@ -311,6 +329,7 @@ export class GhostTextView extends Disposable { inlineClassName: (p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration') + (this._isClickable ? ' clickable' : '') + extraClassNames + + inlineExtraClassNames + p.lineDecorations.map(d => ' ' + d.className).join(' '), // TODO: take the ranges into account for line decorations cursorStops: InjectedTextCursorStops.Left, attachedData: new GhostTextAttachedData(this), diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineSuggestionsView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineSuggestionsView.ts index 2b18dfa6d15..21a2c598aa0 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineSuggestionsView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineSuggestionsView.ts @@ -146,6 +146,7 @@ export class InlineSuggestionsView extends Disposable { }), { useSyntaxHighlighting: this._editorObs.getOption(EditorOption.inlineSuggest).map(v => v.syntaxHighlightingEnabled), + highlightShortSuggestions: true, }, ); } From bb885aff9424c71e8c53f2eeb4b091dcdb25c955 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Tue, 20 Jan 2026 13:01:11 +0100 Subject: [PATCH 256/387] Add telemetry for tools picker --- .../chat/browser/actions/chatToolActions.ts | 2 +- .../chat/browser/actions/chatToolPicker.ts | 37 +++++++++++++++++++ .../promptToolsCodeLensProvider.ts | 2 +- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts index a8ad2887cac..f37633f0f94 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts @@ -188,7 +188,7 @@ class ConfigureToolsAction extends Action2 { }); try { - const result = await instaService.invokeFunction(showToolsPicker, placeholder, description, () => entriesMap.get(), cts.token); + const result = await instaService.invokeFunction(showToolsPicker, placeholder, 'chatInput', description, () => entriesMap.get(), cts.token); if (result) { widget.input.selectedToolsModel.set(result, false); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts b/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts index 75a41db3f6e..2c17502c21b 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts @@ -17,6 +17,7 @@ import { IContextKeyService } from '../../../../../platform/contextkey/common/co import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickTreeItem } from '../../../../../platform/quickinput/common/quickInput.js'; +import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ExtensionEditorTab, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js'; import { McpCommandIds } from '../../../mcp/common/mcpCommandIds.js'; @@ -190,6 +191,7 @@ function createToolSetTreeItem(toolset: ToolSet, checked: boolean, editorService export async function showToolsPicker( accessor: ServicesAccessor, placeHolder: string, + source: string, description?: string, getToolsEntries?: () => ReadonlyMap, token?: CancellationToken @@ -203,6 +205,7 @@ export async function showToolsPicker( const editorService = accessor.get(IEditorService); const mcpWorkbenchService = accessor.get(IMcpWorkbenchService); const toolsService = accessor.get(ILanguageModelToolsService); + const telemetryService = accessor.get(ITelemetryService); const toolLimit = accessor.get(IContextKeyService).getContextKeyValue(ChatContextKeys.chatToolGroupingThreshold.key); const mcpServerByTool = new Map(); @@ -593,11 +596,45 @@ export async function showToolsPicker( })); } + // Capture initial state for telemetry comparison + const initialStateString = serializeToolsState(collectResults()); + treePicker.show(); await Promise.race([Event.toPromise(Event.any(treePicker.onDidHide, didAcceptFinalItem.event), store)]); + // Send telemetry whether the tool selection changed + sendDidChangeEvent(source, telemetryService, initialStateString !== serializeToolsState(collectResults())); + store.dispose(); return didAccept ? collectResults() : undefined; } + +function serializeToolsState(state: ReadonlyMap): string { + const entries: [string, boolean][] = []; + state.forEach((value, key) => { + entries.push([key.id, value]); + }); + entries.sort((a, b) => a[0].localeCompare(b[0])); + return JSON.stringify(entries); +} + +function sendDidChangeEvent(source: string, telemetryService: ITelemetryService, changed: boolean): void { + type ToolPickerClosedEvent = { + changed: boolean; + source: string; + }; + + type ToolPickerClosedClassification = { + changed: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user changed the tool selection from the initial state.' }; + source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the tool picker event.' }; + owner: 'benibenj'; + comment: 'Tracks whether users modify tool selection in the tool picker.'; + }; + + telemetryService.publicLog2('chatToolPickerClosed', { + source, + changed, + }); +} diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts index 4c9a0c63203..37a96a89d10 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts @@ -85,7 +85,7 @@ class PromptToolsCodeLensProvider extends Disposable implements CodeLensProvider private async updateTools(model: ITextModel, range: Range, selectedTools: readonly string[], target: string | undefined): Promise { const selectedToolsNow = () => this.languageModelToolsService.toToolAndToolSetEnablementMap(selectedTools, target); - const newSelectedAfter = await this.instantiationService.invokeFunction(showToolsPicker, localize('placeholder', "Select tools"), undefined, selectedToolsNow); + const newSelectedAfter = await this.instantiationService.invokeFunction(showToolsPicker, localize('placeholder', "Select tools"), 'codeLens', undefined, selectedToolsNow); if (!newSelectedAfter) { return; } From d2049e87310e71fa63498f1f9425a564a7722cbd Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:11:28 +0100 Subject: [PATCH 257/387] Git - tweak how files are being copied to the worktree (#289065) --- extensions/git/src/repository.ts | 44 ++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 24d2dae8040..edb16b5f4d0 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1888,7 +1888,7 @@ export class Repository implements Disposable { }); } - private async _getWorktreeIncludeFiles(): Promise> { + private async _getWorktreeIncludePaths(): Promise> { const config = workspace.getConfiguration('git', Uri.file(this.root)); const worktreeIncludeFiles = config.get('worktreeIncludeFiles', ['**/node_modules/**']); @@ -1917,29 +1917,51 @@ export class Repository implements Disposable { gitIgnoredFiles.delete(uri.fsPath); } - return gitIgnoredFiles; + // Add the folder paths for git ignored files + const gitIgnoredPaths = new Set(gitIgnoredFiles); + + for (const filePath of gitIgnoredFiles) { + let dir = path.dirname(filePath); + while (dir !== this.root && !gitIgnoredFiles.has(dir)) { + gitIgnoredPaths.add(dir); + dir = path.dirname(dir); + } + } + + return gitIgnoredPaths; } private async _copyWorktreeIncludeFiles(worktreePath: string): Promise { - const ignoredFiles = await this._getWorktreeIncludeFiles(); - if (ignoredFiles.size === 0) { + const gitIgnoredPaths = await this._getWorktreeIncludePaths(); + if (gitIgnoredPaths.size === 0) { return; } try { - // Copy files + // Find minimal set of paths (folders and files) to copy. + // The goal is to reduce the number of copy operations + // needed. + const pathsToCopy = new Set(); + for (const filePath of gitIgnoredPaths) { + const relativePath = path.relative(this.root, filePath); + const firstSegment = relativePath.split(path.sep)[0]; + pathsToCopy.add(path.join(this.root, firstSegment)); + } + const startTime = Date.now(); const limiter = new Limiter(15); - const files = Array.from(ignoredFiles); + const files = Array.from(pathsToCopy); + // Copy files const results = await Promise.allSettled(files.map(sourceFile => limiter.queue(async () => { const targetFile = path.join(worktreePath, relativePath(this.root, sourceFile)); await fsPromises.mkdir(path.dirname(targetFile), { recursive: true }); await fsPromises.cp(sourceFile, targetFile, { + filter: src => gitIgnoredPaths.has(src), force: true, mode: fs.constants.COPYFILE_FICLONE, - recursive: false, + recursive: true, verbatimSymlinks: true }); }) @@ -1947,18 +1969,18 @@ export class Repository implements Disposable { // Log any failed operations const failedOperations = results.filter(r => r.status === 'rejected'); - this.logger.info(`[Repository][_copyWorktreeIncludeFiles] Copied ${files.length - failedOperations.length} files to worktree. Failed to copy ${failedOperations.length} files. [${Date.now() - startTime}ms]`); + this.logger.info(`[Repository][_copyWorktreeIncludeFiles] Copied ${files.length - failedOperations.length}/${files.length} folder(s)/file(s) to worktree. [${Date.now() - startTime}ms]`); if (failedOperations.length > 0) { - window.showWarningMessage(l10n.t('Failed to copy {0} files to the worktree.', failedOperations.length)); + window.showWarningMessage(l10n.t('Failed to copy {0} folder(s)/file(s) to the worktree.', failedOperations.length)); - this.logger.warn(`[Repository][_copyWorktreeIncludeFiles] Failed to copy ${failedOperations.length} files to worktree.`); + this.logger.warn(`[Repository][_copyWorktreeIncludeFiles] Failed to copy ${failedOperations.length} folder(s)/file(s) to worktree.`); for (const error of failedOperations) { this.logger.warn(` - ${(error as PromiseRejectedResult).reason}`); } } } catch (err) { - this.logger.warn(`[Repository][_copyWorktreeIncludeFiles] Failed to copy files to worktree: ${err}`); + this.logger.warn(`[Repository][_copyWorktreeIncludeFiles] Failed to copy folder(s)/file(s) to worktree: ${err}`); } } From 66b43c339afe0c5a00229ec3526c8f6a701e1bab Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Tue, 20 Jan 2026 13:11:56 +0100 Subject: [PATCH 258/387] making the getViewportViewLineRenderingData return the correct hasFontInfo (#289083) --- .../common/viewModel/viewModelDecorations.ts | 17 ++++++++++++----- src/vs/editor/common/viewModel/viewModelImpl.ts | 8 +++++--- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/common/viewModel/viewModelDecorations.ts b/src/vs/editor/common/viewModel/viewModelDecorations.ts index 50cf40decf4..8b4f52f96bf 100644 --- a/src/vs/editor/common/viewModel/viewModelDecorations.ts +++ b/src/vs/editor/common/viewModel/viewModelDecorations.ts @@ -29,7 +29,7 @@ export interface IViewDecorationsCollection { /** * Whether the decorations affect the fonts. */ - readonly hasVariableFonts: boolean; + readonly hasVariableFonts: boolean[]; } export class ViewModelDecorations implements IDisposable { @@ -131,11 +131,12 @@ export class ViewModelDecorations implements IDisposable { const decorationsInViewport: ViewModelDecoration[] = []; let decorationsInViewportLen = 0; const inlineDecorations: InlineDecoration[][] = []; + const hasVariableFonts: boolean[] = []; for (let j = startLineNumber; j <= endLineNumber; j++) { inlineDecorations[j - startLineNumber] = []; + hasVariableFonts[j - startLineNumber] = false; } - let hasVariableFonts = false; for (let i = 0, len = modelDecorations.length; i < len; i++) { const modelDecoration = modelDecorations[i]; const decorationOptions = modelDecoration.options; @@ -155,6 +156,9 @@ export class ViewModelDecorations implements IDisposable { const intersectedEndLineNumber = Math.min(endLineNumber, viewRange.endLineNumber); for (let j = intersectedStartLineNumber; j <= intersectedEndLineNumber; j++) { inlineDecorations[j - startLineNumber].push(inlineDecoration); + if (decorationOptions.affectsFont) { + hasVariableFonts[j - startLineNumber] = true; + } } } if (decorationOptions.beforeContentClassName) { @@ -165,6 +169,9 @@ export class ViewModelDecorations implements IDisposable { InlineDecorationType.Before ); inlineDecorations[viewRange.startLineNumber - startLineNumber].push(inlineDecoration); + if (decorationOptions.affectsFont) { + hasVariableFonts[viewRange.startLineNumber - startLineNumber] = true; + } } } if (decorationOptions.afterContentClassName) { @@ -175,11 +182,11 @@ export class ViewModelDecorations implements IDisposable { InlineDecorationType.After ); inlineDecorations[viewRange.endLineNumber - startLineNumber].push(inlineDecoration); + if (decorationOptions.affectsFont) { + hasVariableFonts[viewRange.endLineNumber - startLineNumber] = true; + } } } - if (decorationOptions.affectsFont) { - hasVariableFonts = true; - } } return { diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 8b5b053b4df..299ae5618a2 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -861,13 +861,15 @@ export class ViewModel extends Disposable implements IViewModel { public getViewportViewLineRenderingData(visibleRange: Range, lineNumber: number): ViewLineRenderingData { const viewportDecorationsCollection = this._decorations.getDecorationsViewportData(visibleRange); - const inlineDecorations = viewportDecorationsCollection.inlineDecorations[lineNumber - visibleRange.startLineNumber]; - return this._getViewLineRenderingData(lineNumber, inlineDecorations, viewportDecorationsCollection.hasVariableFonts, viewportDecorationsCollection.decorations); + const relativeLineNumber = lineNumber - visibleRange.startLineNumber; + const inlineDecorations = viewportDecorationsCollection.inlineDecorations[relativeLineNumber]; + const hasVariableFonts = viewportDecorationsCollection.hasVariableFonts[relativeLineNumber]; + return this._getViewLineRenderingData(lineNumber, inlineDecorations, hasVariableFonts, viewportDecorationsCollection.decorations); } public getViewLineRenderingData(lineNumber: number): ViewLineRenderingData { const decorationsCollection = this._decorations.getDecorationsOnLine(lineNumber); - return this._getViewLineRenderingData(lineNumber, decorationsCollection.inlineDecorations[0], decorationsCollection.hasVariableFonts, decorationsCollection.decorations); + return this._getViewLineRenderingData(lineNumber, decorationsCollection.inlineDecorations[0], decorationsCollection.hasVariableFonts[0], decorationsCollection.decorations); } private _getViewLineRenderingData(lineNumber: number, inlineDecorations: InlineDecoration[], hasVariableFonts: boolean, decorations: ViewModelDecoration[]): ViewLineRenderingData { From e28cce4a942ef325348a3319986808cd5406fe0f Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Tue, 20 Jan 2026 14:09:40 +0100 Subject: [PATCH 259/387] adding the line height into the model decorations returned by font token decorations provider (#289099) --- src/vs/editor/common/model.ts | 2 +- .../common/model/tokens/tokenizationFontDecorationsProvider.ts | 1 + src/vs/editor/common/viewModel/viewModelImpl.ts | 3 ++- src/vs/monaco.d.ts | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 28862df0b5d..c70fa95a387 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -222,7 +222,7 @@ export interface IModelDecorationOptions { */ glyphMargin?: IModelDecorationGlyphMarginOptions | null; /** - * If set, the decoration will override the line height of the lines it spans. Maximum value is 300px. + * If set, the decoration will override the line height of the lines it spans. This value is a multiplier to the default line height. */ lineHeight?: number | null; /** diff --git a/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts b/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts index a5335fa9fca..0481e1507fc 100644 --- a/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts +++ b/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts @@ -137,6 +137,7 @@ export class TokenizationFontDecorationProvider extends Disposable implements De options: { description: 'FontOptionDecoration', inlineClassName: className, + lineHeight: anno.fontToken.lineHeightMultiplier, affectsFont }, ownerId: 0, diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 299ae5618a2..0657caff859 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -199,6 +199,7 @@ export class ViewModel extends Disposable implements IViewModel { if (!allowVariableLineHeights) { return []; } + const defaultLineHeight = this._configuration.options.get(EditorOption.lineHeight); const decorations = this.model.getCustomLineHeightsDecorations(this._editorId); return decorations.map((d) => { const lineNumber = d.range.startLineNumber; @@ -207,7 +208,7 @@ export class ViewModel extends Disposable implements IViewModel { decorationId: d.id, startLineNumber: viewRange.startLineNumber, endLineNumber: viewRange.endLineNumber, - lineHeight: d.options.lineHeight || 0 + lineHeight: d.options.lineHeight ? d.options.lineHeight * defaultLineHeight : 0 }; }); } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 368f0e5ca89..22641b4e01a 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1747,7 +1747,7 @@ declare namespace monaco.editor { */ glyphMargin?: IModelDecorationGlyphMarginOptions | null; /** - * If set, the decoration will override the line height of the lines it spans. Maximum value is 300px. + * If set, the decoration will override the line height of the lines it spans. This value is a multiplier to the default line height. */ lineHeight?: number | null; /** From 225d729f967627a60ceb11af7a1a8d3c5047a020 Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Tue, 20 Jan 2026 06:32:29 -0800 Subject: [PATCH 260/387] Integrated browser focus fixes (#287907) * Integrated browser focus fixes * PR feedback * comment --- src/vs/base/browser/dom.ts | 60 +++++++++++++++++++ .../browserView/common/browserView.ts | 1 + .../browserView/electron-main/browserView.ts | 1 + .../windows/electron-main/windowImpl.ts | 6 ++ src/vs/workbench/browser/window.ts | 7 ++- .../contrib/browserView/common/browserView.ts | 8 +++ .../electron-browser/browserEditor.ts | 20 +++---- 7 files changed, 90 insertions(+), 13 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 1a62307464b..36848442d36 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -123,6 +123,66 @@ export const { //#endregion +//#region External Focus Tracking + +/** + * A registry for functions that check if a component outside the normal DOM tree has focus. + * This is used to extend the concept of "window has focus" to include things like + * Electron WebContentsViews (browser views) that exist outside the workbench DOM. + */ +const externalFocusCheckers = new Set<() => boolean>(); + +/** + * Register a function that checks if a component outside the DOM has focus. + * This allows `hasExternalFocus` to detect when focus is in components like browser views. + * + * @param checker A function that returns true if the component has focus + * @returns A disposable to unregister the checker + */ +export function registerExternalFocusChecker(checker: () => boolean): IDisposable { + externalFocusCheckers.add(checker); + + return toDisposable(() => { + externalFocusCheckers.delete(checker); + }); +} + +/** + * Check if any registered external component has focus. + * This is used to extend focus detection beyond the normal DOM to include + * components like Electron WebContentsViews. + * + * @returns true if any registered external component has focus + */ +export function hasExternalFocus(): boolean { + for (const checker of externalFocusCheckers) { + if (checker()) { + return true; + } + } + return false; +} + +/** + * Check if the application has focus in any window, either via the normal DOM or via an + * external component like a browser view (which exists outside the document tree). + * + * @returns true if the application owns the current focus + */ +export function hasAppFocus(): boolean { + for (const { window } of getWindows()) { + if (window.document.hasFocus()) { + return true; + } + } + if (hasExternalFocus()) { + return true; + } + return false; +} + +//#endregion + export function clearNode(node: HTMLElement): void { while (node.firstChild) { node.firstChild.remove(); diff --git a/src/vs/platform/browserView/common/browserView.ts b/src/vs/platform/browserView/common/browserView.ts index 335147600e6..c666c29087b 100644 --- a/src/vs/platform/browserView/common/browserView.ts +++ b/src/vs/platform/browserView/common/browserView.ts @@ -26,6 +26,7 @@ export interface IBrowserViewState { canGoBack: boolean; canGoForward: boolean; loading: boolean; + focused: boolean; isDevToolsOpen: boolean; lastScreenshot: VSBuffer | undefined; lastFavicon: string | undefined; diff --git a/src/vs/platform/browserView/electron-main/browserView.ts b/src/vs/platform/browserView/electron-main/browserView.ts index 0468d5b82e1..1830d245e34 100644 --- a/src/vs/platform/browserView/electron-main/browserView.ts +++ b/src/vs/platform/browserView/electron-main/browserView.ts @@ -261,6 +261,7 @@ export class BrowserView extends Disposable { canGoBack: webContents.navigationHistory.canGoBack(), canGoForward: webContents.navigationHistory.canGoForward(), loading: webContents.isLoading(), + focused: webContents.isFocused(), isDevToolsOpen: webContents.isDevToolsOpened(), lastScreenshot: this._lastScreenshot, lastFavicon: this._lastFavicon, diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 63652a5639e..3841cfa34a7 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -392,6 +392,12 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { } win.focus(); + + // When focusing the window, the workbench should always be the view that receives focus. + // However, in scenarios where the window has multiple child views (e.g. browser WebContentsViews), + // the last focused view in the window may not be the workbench. + // So we explicitly focus the workbench web contents here to ensure it gets focus. + win.webContents.focus(); } //#region Window Control Overlays diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index 63dfbb43d35..bff3f2cf1b0 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isSafari, setFullscreen } from '../../base/browser/browser.js'; -import { addDisposableListener, EventHelper, EventType, getActiveWindow, getWindow, getWindowById, getWindows, getWindowsCount, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from '../../base/browser/dom.js'; +import { addDisposableListener, EventHelper, EventType, getWindow, getWindowById, getWindows, getWindowsCount, hasAppFocus, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from '../../base/browser/dom.js'; import { DomEmitter } from '../../base/browser/event.js'; import { HidDeviceData, requestHidDevice, requestSerialPort, requestUsbDevice, SerialPortData, UsbDeviceData } from '../../base/browser/deviceAccess.js'; import { timeout } from '../../base/common/async.js'; @@ -74,8 +74,9 @@ export abstract class BaseWindow extends Disposable { } private onElementFocus(targetWindow: CodeWindow): void { - const activeWindow = getActiveWindow(); - if (activeWindow !== targetWindow && activeWindow.document.hasFocus()) { + + // Check if focus should transfer: the application currently has focus somewhere, but not in the target window. + if (!targetWindow.document.hasFocus() && hasAppFocus()) { // Call original focus() targetWindow.focus(); diff --git a/src/vs/workbench/contrib/browserView/common/browserView.ts b/src/vs/workbench/contrib/browserView/common/browserView.ts index 8af47a79bb1..3d644c83229 100644 --- a/src/vs/workbench/contrib/browserView/common/browserView.ts +++ b/src/vs/workbench/contrib/browserView/common/browserView.ts @@ -79,6 +79,7 @@ export interface IBrowserViewModel extends IDisposable { readonly favicon: string | undefined; readonly screenshot: VSBuffer | undefined; readonly loading: boolean; + readonly focused: boolean; readonly canGoBack: boolean; readonly isDevToolsOpen: boolean; readonly canGoForward: boolean; @@ -117,6 +118,7 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { private _favicon: string | undefined = undefined; private _screenshot: VSBuffer | undefined = undefined; private _loading: boolean = false; + private _focused: boolean = false; private _isDevToolsOpen: boolean = false; private _canGoBack: boolean = false; private _canGoForward: boolean = false; @@ -141,6 +143,7 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { get title(): string { return this._title; } get favicon(): string | undefined { return this._favicon; } get loading(): boolean { return this._loading; } + get focused(): boolean { return this._focused; } get isDevToolsOpen(): boolean { return this._isDevToolsOpen; } get canGoBack(): boolean { return this._canGoBack; } get canGoForward(): boolean { return this._canGoForward; } @@ -207,6 +210,7 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { this._url = state.url; this._title = state.title; this._loading = state.loading; + this._focused = state.focused; this._isDevToolsOpen = state.isDevToolsOpen; this._canGoBack = state.canGoBack; this._canGoForward = state.canGoForward; @@ -244,6 +248,10 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { this._register(this.onDidChangeFavicon(e => { this._favicon = e.favicon; })); + + this._register(this.onDidChangeFocus(({ focused }) => { + this._focused = focused; + })); } async layout(bounds: IBrowserViewBounds): Promise { diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts index dacaecd4ccd..ccececc4b40 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts @@ -5,7 +5,7 @@ import './media/browser.css'; import { localize } from '../../../../nls.js'; -import { $, addDisposableListener, disposableWindowInterval, EventType, scheduleAtNextAnimationFrame } from '../../../../base/browser/dom.js'; +import { $, addDisposableListener, disposableWindowInterval, EventType, isHTMLElement, registerExternalFocusChecker, scheduleAtNextAnimationFrame } from '../../../../base/browser/dom.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { RawContextKey, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -247,13 +247,9 @@ export class BrowserEditor extends EditorPane { } })); - this._register(addDisposableListener(this._browserContainer, EventType.BLUR, () => { - // When focus goes to another part of the workbench, make sure the workbench view becomes focused. - const focused = this.window.document.activeElement; - if (focused && focused !== this._browserContainer) { - this.window.focus(); - } - })); + // Register external focus checker so that cross-window focus logic knows when + // this browser view has focus (since it's outside the normal DOM tree). + this._register(registerExternalFocusChecker(() => this._model?.focused ?? false)); } override async setInput(input: BrowserEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { @@ -308,9 +304,13 @@ export class BrowserEditor extends EditorPane { })); this._inputDisposables.add(this._model.onDidChangeFocus(({ focused }) => { - // When the view gets focused, make sure the container also has focus. + // When the view gets focused, make sure the editor reports that it has focus, + // but focus is removed from the workbench. if (focused) { - this._browserContainer.focus(); + this._onDidFocus?.fire(); + if (isHTMLElement(this.window.document.activeElement)) { + this.window.document.activeElement.blur(); + } } })); From 6611b47c7d500ed62a0b7bb5c54a452cac6b92a5 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 20 Jan 2026 15:39:01 +0100 Subject: [PATCH 261/387] fix #288864 (#289087) * fix #288864 * fix tests --- .../chatManagement/chatModelsViewModel.ts | 112 ++-- .../chatManagement/chatModelsWidget.ts | 40 +- .../languageModelsConfigurationService.ts | 28 +- .../contrib/chat/common/languageModels.ts | 80 ++- .../common/languageModelsConfiguration.ts | 2 +- .../chatModelsViewModel.test.ts | 101 +-- .../chat/test/common/languageModels.test.ts | 601 +++++++++++++++++- .../chat/test/common/languageModels.ts | 2 +- 8 files changed, 791 insertions(+), 175 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsViewModel.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsViewModel.ts index e2205448dbe..614e2b2f281 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsViewModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsViewModel.ts @@ -7,11 +7,9 @@ import { distinct } from '../../../../../base/common/arrays.js'; import { IMatch, IFilter, or, matchesCamelCase, matchesWords, matchesBaseContiguousSubString } from '../../../../../base/common/filters.js'; import { Emitter } from '../../../../../base/common/event.js'; import { ILanguageModelsService, IUserFriendlyLanguageModel, ILanguageModelChatMetadataAndIdentifier } from '../../../chat/common/languageModels.js'; -import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; import { localize } from '../../../../../nls.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { ILanguageModelsProviderGroup, ILanguageModelsConfigurationService } from '../../common/languageModelsConfiguration.js'; -import { Throttler } from '../../../../../base/common/async.js'; +import { ILanguageModelsProviderGroup } from '../../common/languageModelsConfiguration.js'; import Severity from '../../../../../base/common/severity.js'; export const MODEL_ENTRY_TEMPLATE_ID = 'model.entry.template'; @@ -143,17 +141,12 @@ export class ChatModelsViewModel extends Disposable { } } - private readonly refreshThrottler = this._register(new Throttler()); - constructor( @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, - @ILanguageModelsConfigurationService private readonly languageModelsConfigurationService: ILanguageModelsConfigurationService, - @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService ) { super(); this.languageModels = []; - this._register(this.chatEntitlementService.onDidChangeEntitlement(() => this.refresh())); - this._register(this.languageModelsConfigurationService.onDidChangeLanguageModelGroups(() => this.refresh())); + this._register(this.languageModelsService.onDidChangeLanguageModels(vendor => this.refreshVendor(vendor))); } private readonly _viewModelEntries: IViewModelEntry[] = []; @@ -470,52 +463,73 @@ export class ChatModelsViewModel extends Disposable { }); } - refresh(): Promise { - return this.refreshThrottler.queue(() => this.doRefresh()); + async refresh(): Promise { + await this.languageModelsService.selectLanguageModels({}); + await this.refreshAllVendors(); } - private async doRefresh(): Promise { + private async refreshAllVendors(): Promise { this.languageModels = []; this.languageModelGroupStatuses = []; for (const vendor of this.getVendors()) { - const models: ILanguageModel[] = []; - const languageModelsGroups = await this.languageModelsService.fetchLanguageModelGroups(vendor.vendor); - for (const group of languageModelsGroups) { - const provider: ILanguageModelProvider = { - group: group.group ?? { - vendor: vendor.vendor, - name: vendor.displayName - }, - vendor - }; - if (group.status) { - this.languageModelGroupStatuses.push({ - provider, - status: { - message: group.status.message, - severity: group.status.severity - } - }); - } - for (const identifier of group.modelIdentifiers) { - const metadata = this.languageModelsService.lookupLanguageModel(identifier); - if (!metadata) { - continue; - } - if (vendor.vendor === 'copilot' && metadata.id === 'auto') { - continue; - } - models.push({ - identifier, - metadata, - provider, - }); - } - } - this.languageModels.push(...models.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name))); - this.languageModelGroups = this.groupModels(this.languageModels); - this.doFilter(); + this.addVendorModels(vendor); } + this.languageModelGroups = this.groupModels(this.languageModels); + this.doFilter(); + } + + private refreshVendor(vendorId: string): void { + const vendor = this.getVendors().find(v => v.vendor === vendorId); + if (!vendor) { + return; + } + + // Remove existing models for this vendor + this.languageModels = this.languageModels.filter(m => m.provider.vendor.vendor !== vendorId); + this.languageModelGroupStatuses = this.languageModelGroupStatuses.filter(s => s.provider.vendor.vendor !== vendorId); + + // Add updated models for this vendor + this.addVendorModels(vendor); + this.languageModelGroups = this.groupModels(this.languageModels); + this.doFilter(); + } + + private addVendorModels(vendor: IUserFriendlyLanguageModel): void { + const models: ILanguageModel[] = []; + const languageModelsGroups = this.languageModelsService.getLanguageModelGroups(vendor.vendor); + for (const group of languageModelsGroups) { + const provider: ILanguageModelProvider = { + group: group.group ?? { + vendor: vendor.vendor, + name: vendor.displayName + }, + vendor + }; + if (group.status) { + this.languageModelGroupStatuses.push({ + provider, + status: { + message: group.status.message, + severity: group.status.severity + } + }); + } + for (const identifier of group.modelIdentifiers) { + const metadata = this.languageModelsService.lookupLanguageModel(identifier); + if (!metadata) { + continue; + } + if (vendor.vendor === 'copilot' && metadata.id === 'auto') { + continue; + } + models.push({ + identifier, + metadata, + provider, + }); + } + } + this.languageModels.push(...models.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name))); } toggleVisibility(model: ILanguageModelEntry): void { diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts index bd8ffdb7f7b..1dc5e58c8eb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts @@ -992,6 +992,7 @@ export class ChatModelsWidget extends Disposable { // Create table this.createTable(); this._register(this.viewModel.onDidChangeGrouping(() => this.createTable())); + this._register(this.chatEntitlementService.onDidChangeEntitlement(() => this.updateAddModelsButton())); } private createTable(): void { @@ -1167,24 +1168,7 @@ export class ChatModelsWidget extends Disposable { this.table.setFocus([selectedEntryIndex]); this.table.setSelection([selectedEntryIndex]); } - - const configurableVendors = this.languageModelsService.getVendors().filter(vendor => vendor.managementCommand || vendor.configuration); - - const entitlement = this.chatEntitlementService.entitlement; - const supportsAddingModels = this.chatEntitlementService.isInternal - || (entitlement !== ChatEntitlement.Unknown - && entitlement !== ChatEntitlement.Available - && entitlement !== ChatEntitlement.Business - && entitlement !== ChatEntitlement.Enterprise); - this.addButton.enabled = supportsAddingModels && configurableVendors.length > 0; - - this.dropdownActions = configurableVendors.map(vendor => toAction({ - id: `enable-${vendor.vendor}`, - label: vendor.displayName, - run: async () => { - await this.addModelsForVendor(vendor); - } - })); + this.updateAddModelsButton(); })); this.tableDisposables.add(this.table.onDidOpen(async ({ element, browserEvent }) => { @@ -1212,6 +1196,26 @@ export class ChatModelsWidget extends Disposable { this.layout(this.element.clientHeight, this.element.clientWidth); } + private updateAddModelsButton(): void { + const configurableVendors = this.languageModelsService.getVendors().filter(vendor => vendor.managementCommand || vendor.configuration); + + const entitlement = this.chatEntitlementService.entitlement; + const supportsAddingModels = this.chatEntitlementService.isInternal + || (entitlement !== ChatEntitlement.Unknown + && entitlement !== ChatEntitlement.Available + && entitlement !== ChatEntitlement.Business + && entitlement !== ChatEntitlement.Enterprise); + this.addButton.enabled = supportsAddingModels && configurableVendors.length > 0; + + this.dropdownActions = configurableVendors.map(vendor => toAction({ + id: `enable-${vendor.vendor}`, + label: vendor.displayName, + run: async () => { + await this.addModelsForVendor(vendor); + } + })); + } + private filterModels(): void { this.delayedFiltering.trigger(() => { this.viewModel.filter(this.searchWidget.getValue()); diff --git a/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts b/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts index 9468d4f413c..cde93d3d618 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts @@ -36,8 +36,8 @@ export class LanguageModelsConfigurationService extends Disposable implements IL private readonly modelsConfigurationFile: URI; get configurationFile(): URI { return this.modelsConfigurationFile; } - private readonly _onDidChangeLanguageModelGroups = new Emitter(); - readonly onDidChangeLanguageModelGroups: Event = this._onDidChangeLanguageModelGroups.event; + private readonly _onDidChangeLanguageModelGroups = new Emitter(); + readonly onDidChangeLanguageModelGroups: Event = this._onDidChangeLanguageModelGroups.event; private languageModelsProviderGroups: LanguageModelsProviderGroups = []; @@ -62,11 +62,29 @@ export class LanguageModelsConfigurationService extends Disposable implements IL } private setLanguageModelsConfiguration(languageModelsConfiguration: LanguageModelsProviderGroups): void { - if (equals(this.languageModelsProviderGroups, languageModelsConfiguration)) { - return; + const changedGroups: ILanguageModelsProviderGroup[] = []; + const oldGroupMap = new Map(this.languageModelsProviderGroups.map(g => [`${g.vendor}:${g.name}`, g])); + const newGroupMap = new Map(languageModelsConfiguration.map(g => [`${g.vendor}:${g.name}`, g])); + + // Find added or modified groups + for (const [key, newGroup] of newGroupMap) { + const oldGroup = oldGroupMap.get(key); + if (!oldGroup || !equals(oldGroup, newGroup)) { + changedGroups.push(newGroup); + } } + + // Find removed groups + for (const [key, oldGroup] of oldGroupMap) { + if (!newGroupMap.has(key)) { + changedGroups.push(oldGroup); + } + } + this.languageModelsProviderGroups = languageModelsConfiguration; - this._onDidChangeLanguageModelGroups.fire(); + if (changedGroups.length > 0) { + this._onDidChangeLanguageModelGroups.fire(changedGroups); + } } private async updateLanguageModelsConfiguration(): Promise { diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index a612296c3d6..f73289281e2 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -13,6 +13,7 @@ import { hash } from '../../../../base/common/hash.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { IJSONSchema, TypeFromJsonSchema } from '../../../../base/common/jsonSchema.js'; import { DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { equals } from '../../../../base/common/objects.js'; import Severity from '../../../../base/common/severity.js'; import { format, isFalsyOrWhitespace } from '../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; @@ -296,7 +297,7 @@ export interface ILanguageModelsService { lookupLanguageModel(modelId: string): ILanguageModelChatMetadata | undefined; - fetchLanguageModelGroups(vendor: string): Promise; + getLanguageModelGroups(vendor: string): ILanguageModelsGroup[]; /** * Given a selector, returns a list of model identifiers @@ -401,6 +402,8 @@ export const languageModelChatProviderExtensionPoint = ExtensionsRegistry.regist } }); +const CHAT_MODEL_PICKER_PREFERENCES_STORAGE_KEY = 'chatModelPickerPreferences'; + export class LanguageModelsService implements ILanguageModelsService { private static SECRET_KEY_PREFIX = 'chat.lm.secret.'; @@ -433,9 +436,11 @@ export class LanguageModelsService implements ILanguageModelsService { @ISecretStorageService private readonly _secretStorageService: ISecretStorageService, ) { this._hasUserSelectableModels = ChatContextKeys.languageModelsAreUserSelectable.bindTo(_contextKeyService); - this._modelPickerUserPreferences = this._storageService.getObject>('chatModelPickerPreferences', StorageScope.PROFILE, this._modelPickerUserPreferences); + this._modelPickerUserPreferences = this._readModelPickerPreferences(); + this._store.add(this._storageService.onDidChangeValue(StorageScope.PROFILE, CHAT_MODEL_PICKER_PREFERENCES_STORAGE_KEY, this._store)(() => this._onDidChangeModelPickerPreferences())); this._store.add(this.onDidChangeLanguageModels(() => this._hasUserSelectableModels.set(this._modelCache.size > 0 && Array.from(this._modelCache.values()).some(model => model.isUserSelectable)))); + this._store.add(this._languageModelsConfigurationService.onDidChangeLanguageModelGroups(changedGroups => this._onDidChangeLanguageModelGroups(changedGroups))); this._store.add(languageModelChatProviderExtensionPoint.setHandler((extensions) => { @@ -470,6 +475,54 @@ export class LanguageModelsService implements ILanguageModelsService { })); } + private async _onDidChangeLanguageModelGroups(changedGroups: readonly ILanguageModelsProviderGroup[]): Promise { + const changedVendors = new Set(changedGroups.map(g => g.vendor)); + await Promise.all(Array.from(changedVendors).map(vendor => this._resolveAllLanguageModels(vendor, true))); + } + + private _readModelPickerPreferences(): IStringDictionary { + return this._storageService.getObject>(CHAT_MODEL_PICKER_PREFERENCES_STORAGE_KEY, StorageScope.PROFILE, {}); + } + + private _onDidChangeModelPickerPreferences(): void { + const newPreferences = this._readModelPickerPreferences(); + const oldPreferences = this._modelPickerUserPreferences; + + // Check if there are any changes by computing diff + const affectedVendors = new Set(); + let hasChanges = false; + + // Check for added or updated keys + for (const modelId in newPreferences) { + if (oldPreferences[modelId] !== newPreferences[modelId]) { + hasChanges = true; + const model = this._modelCache.get(modelId); + if (model) { + affectedVendors.add(model.vendor); + } + } + } + + // Check for removed keys + for (const modelId in oldPreferences) { + if (!newPreferences.hasOwnProperty(modelId)) { + hasChanges = true; + const model = this._modelCache.get(modelId); + if (model) { + affectedVendors.add(model.vendor); + } + } + } + + if (hasChanges) { + this._logService.trace('[LM] Updated model picker preferences from storage'); + this._modelPickerUserPreferences = newPreferences; + for (const vendor of affectedVendors) { + this._onLanguageModelChange.fire(vendor); + } + } + } + private _hasStoredModelForVendor(vendor: string): boolean { return Object.keys(this._modelPickerUserPreferences).some(modelId => { return modelId.startsWith(vendor); @@ -477,7 +530,7 @@ export class LanguageModelsService implements ILanguageModelsService { } private _saveModelPickerPreferences(): void { - this._storageService.store('chatModelPickerPreferences', this._modelPickerUserPreferences, StorageScope.PROFILE, StorageTarget.USER); + this._storageService.store(CHAT_MODEL_PICKER_PREFERENCES_STORAGE_KEY, this._modelPickerUserPreferences, StorageScope.PROFILE, StorageTarget.USER); } updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void { @@ -601,21 +654,29 @@ export class LanguageModelsService implements ILanguageModelsService { } this._modelsGroups.set(vendorId, languageModelsGroups); - this._clearModelCache(vendorId); + const oldModels = this._clearModelCache(vendorId); + let hasChanges = false; for (const model of allModels) { if (this._modelCache.has(model.identifier)) { this._logService.warn(`[LM] Model ${model.identifier} is already registered. Skipping.`); continue; } this._modelCache.set(model.identifier, model.metadata); + hasChanges = hasChanges || !equals(oldModels.get(model.identifier), model.metadata); + oldModels.delete(model.identifier); } this._logService.trace(`[LM] Resolved language models for vendor ${vendorId}`, allModels); - this._onLanguageModelChange.fire(vendorId); + hasChanges = hasChanges || oldModels.size > 0; + + if (hasChanges) { + this._onLanguageModelChange.fire(vendorId); + } else { + this._logService.trace(`[LM] No changes in language models for vendor ${vendorId}`); + } }); } - async fetchLanguageModelGroups(vendor: string): Promise { - await this._resolveAllLanguageModels(vendor, true); + getLanguageModelGroups(vendor: string): ILanguageModelsGroup[] { return this._modelsGroups.get(vendor) ?? []; } @@ -990,12 +1051,15 @@ export class LanguageModelsService implements ILanguageModelsService { return secretInput.substring(secretInput.indexOf(':') + 1, secretInput.length - 1); } - private _clearModelCache(vendor: string): void { + private _clearModelCache(vendor: string): Map { + const removed = new Map(); for (const [id, model] of this._modelCache.entries()) { if (model.vendor === vendor) { + removed.set(id, model); this._modelCache.delete(id); } } + return removed; } private async _resolveConfiguration(group: ILanguageModelsProviderGroup, schema: IJSONSchema | undefined): Promise> { diff --git a/src/vs/workbench/contrib/chat/common/languageModelsConfiguration.ts b/src/vs/workbench/contrib/chat/common/languageModelsConfiguration.ts index 9ed7cd0e378..9573426343e 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelsConfiguration.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelsConfiguration.ts @@ -16,7 +16,7 @@ export interface ILanguageModelsConfigurationService { readonly configurationFile: URI; - readonly onDidChangeLanguageModelGroups: Event; + readonly onDidChangeLanguageModelGroups: Event; getLanguageModelsProviderGroups(): readonly ILanguageModelsProviderGroup[]; diff --git a/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts index d008e7e89a8..0e3ebc1a642 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts @@ -4,17 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { Emitter, Event } from '../../../../../../base/common/event.js'; +import { Emitter } from '../../../../../../base/common/event.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatProvider, ILanguageModelChatSelector, ILanguageModelsGroup, ILanguageModelsService, IUserFriendlyLanguageModel } from '../../../common/languageModels.js'; import { ChatModelGroup, ChatModelsViewModel, ILanguageModelEntry, ILanguageModelProviderEntry, isLanguageModelProviderEntry, isLanguageModelGroupEntry, ILanguageModelGroupEntry } from '../../../browser/chatManagement/chatModelsViewModel.js'; -import { IChatEntitlementService, ChatEntitlement } from '../../../../../services/chat/common/chatEntitlementService.js'; -import { IObservable, observableValue } from '../../../../../../base/common/observable.js'; import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js'; import { IStringDictionary } from '../../../../../../base/common/collections.js'; -import { ILanguageModelsConfigurationService, ILanguageModelsProviderGroup } from '../../../common/languageModelsConfiguration.js'; -import { mock } from '../../../../../../base/test/common/mock.js'; +import { ILanguageModelsProviderGroup } from '../../../common/languageModelsConfiguration.js'; import { ChatAgentLocation } from '../../../common/constants.js'; class MockLanguageModelsService implements ILanguageModelsService { @@ -113,7 +110,7 @@ class MockLanguageModelsService implements ILanguageModelsService { async addLanguageModelsProviderGroup(name: string, vendorId: string, configuration: IStringDictionary | undefined): Promise { } - async fetchLanguageModelGroups(vendor: string): Promise { + getLanguageModelGroups(vendor: string): ILanguageModelsGroup[] { return this.modelGroups.get(vendor) || []; } @@ -123,67 +120,13 @@ class MockLanguageModelsService implements ILanguageModelsService { async migrateLanguageModelsProviderGroup(languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise { } } -class MockChatEntitlementService implements IChatEntitlementService { - _serviceBrand: undefined; - - private readonly _onDidChangeEntitlement = new Emitter(); - readonly onDidChangeEntitlement = this._onDidChangeEntitlement.event; - - readonly entitlement = ChatEntitlement.Unknown; - readonly entitlementObs: IObservable = observableValue('entitlement', ChatEntitlement.Unknown); - - readonly organisations: string[] | undefined = undefined; - readonly isInternal = false; - readonly sku: string | undefined = undefined; - - readonly onDidChangeQuotaExceeded = Event.None; - readonly onDidChangeQuotaRemaining = Event.None; - - readonly quotas = { - chat: { - total: 100, - remaining: 100, - percentRemaining: 100, - overageEnabled: false, - overageCount: 0, - unlimited: false - }, - completions: { - total: 100, - remaining: 100, - percentRemaining: 100, - overageEnabled: false, - overageCount: 0, - unlimited: false - } - }; - - readonly onDidChangeSentiment = Event.None; - readonly sentiment: any = { installed: true, hidden: false, disabled: false }; - readonly sentimentObs: IObservable = observableValue('sentiment', { installed: true, hidden: false, disabled: false }); - - readonly onDidChangeAnonymous = Event.None; - readonly anonymous = false; - readonly anonymousObs: IObservable = observableValue('anonymous', false); - - fireEntitlementChange(): void { - this._onDidChangeEntitlement.fire(); - } - - async update(): Promise { - // Not needed for tests - } -} - suite('ChatModelsViewModel', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); let languageModelsService: MockLanguageModelsService; - let chatEntitlementService: MockChatEntitlementService; let viewModel: ChatModelsViewModel; setup(async () => { languageModelsService = new MockLanguageModelsService(); - chatEntitlementService = new MockChatEntitlementService(); // Setup test data languageModelsService.addVendor({ @@ -286,15 +229,7 @@ suite('ChatModelsViewModel', () => { } }); - viewModel = store.add(new ChatModelsViewModel( - languageModelsService, - new class extends mock() { - override get onDidChangeLanguageModelGroups() { - return Event.None; - } - }, - chatEntitlementService, - )); + viewModel = store.add(new ChatModelsViewModel(languageModelsService)); await viewModel.refresh(); }); @@ -513,20 +448,6 @@ suite('ChatModelsViewModel', () => { assert.strictEqual(copilotModelsAfterExpand.length, 2); }); - test('should fire onDidChangeModelEntries when entitlement changes', async () => { - let fired = false; - store.add(viewModel.onDidChange(() => { - fired = true; - })); - - chatEntitlementService.fireEntitlementChange(); - - // Wait a bit for async resolve - await new Promise(resolve => setTimeout(resolve, 10)); - - assert.strictEqual(fired, true); - }); - test('should handle quoted search strings', () => { // When a search string is fully quoted (starts and ends with quotes), // the completeMatch flag is set to true, which currently skips all matching @@ -594,7 +515,7 @@ suite('ChatModelsViewModel', () => { } }); - function createSingleVendorViewModel(chatEntitlementService: IChatEntitlementService, includeSecondModel: boolean = true): { service: MockLanguageModelsService; viewModel: ChatModelsViewModel } { + function createSingleVendorViewModel(includeSecondModel: boolean = true): { service: MockLanguageModelsService; viewModel: ChatModelsViewModel } { const service = new MockLanguageModelsService(); service.addVendor({ vendor: 'copilot', @@ -648,16 +569,12 @@ suite('ChatModelsViewModel', () => { }); } - const viewModel = store.add(new ChatModelsViewModel(service, new class extends mock() { - override get onDidChangeLanguageModelGroups() { - return Event.None; - } - }, chatEntitlementService)); + const viewModel = store.add(new ChatModelsViewModel(service)); return { service, viewModel }; } test('should not show vendor header when only one vendor exists', async () => { - const { viewModel: singleVendorViewModel } = createSingleVendorViewModel(chatEntitlementService); + const { viewModel: singleVendorViewModel } = createSingleVendorViewModel(); await singleVendorViewModel.refresh(); const results = singleVendorViewModel.filter(''); @@ -684,7 +601,7 @@ suite('ChatModelsViewModel', () => { }); test('should filter single vendor models by capability', async () => { - const { viewModel: singleVendorViewModel } = createSingleVendorViewModel(chatEntitlementService); + const { viewModel: singleVendorViewModel } = createSingleVendorViewModel(); await singleVendorViewModel.refresh(); const results = singleVendorViewModel.filter('@capability:agent'); @@ -798,7 +715,7 @@ suite('ChatModelsViewModel', () => { }); test('should not show vendor headers when filtered if only one vendor exists', async () => { - const { viewModel: singleVendorViewModel } = createSingleVendorViewModel(chatEntitlementService); + const { viewModel: singleVendorViewModel } = createSingleVendorViewModel(); await singleVendorViewModel.refresh(); const results = singleVendorViewModel.filter('GPT'); diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts index 3bc0df9fce8..d664d303b8b 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts @@ -16,6 +16,7 @@ import { ExtensionsRegistry } from '../../../../services/extensions/common/exten import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../common/widget/input/modelPickerWidget.js'; import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { TestStorageService } from '../../../../test/common/workbenchTestServices.js'; +import { StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { Event } from '../../../../../base/common/event.js'; import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; @@ -45,6 +46,7 @@ suite('LanguageModels', function () { new MockContextKeyService(), new TestConfigurationService(), new class extends mock() { + override onDidChangeLanguageModelGroups = Event.None; override getLanguageModelsProviderGroups() { return []; } @@ -258,7 +260,9 @@ suite('LanguageModels - When Clause', function () { new TestStorageService(), contextKeyService, new TestConfigurationService(), - new class extends mock() { }, + new class extends mock() { + override onDidChangeLanguageModelGroups = Event.None; + }, new class extends mock() { }, new TestSecretStorageService(), ); @@ -304,3 +308,598 @@ suite('LanguageModels - When Clause', function () { assert.ok(!vendors.some(v => v.vendor === 'hidden-vendor'), 'hidden-vendor should be hidden when falseKey is false'); }); }); + +suite('LanguageModels - Model Picker Preferences Storage', function () { + + let languageModelsService: LanguageModelsService; + let storageService: TestStorageService; + const disposables = new DisposableStore(); + + setup(async function () { + storageService = new TestStorageService(); + + languageModelsService = new LanguageModelsService( + new class extends mock() { + override activateByEvent(name: string) { + return Promise.resolve(); + } + }, + new NullLogService(), + storageService, + new MockContextKeyService(), + new TestConfigurationService(), + new class extends mock() { + override onDidChangeLanguageModelGroups = Event.None; + override getLanguageModelsProviderGroups() { + return []; + } + }, + new class extends mock() { }, + new TestSecretStorageService(), + ); + + // Register vendor1 used in most tests + const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!; + ext.acceptUsers([{ + description: { ...nullExtensionDescription }, + value: { vendor: 'vendor1' }, + collector: null! + }]); + + disposables.add(languageModelsService.registerLanguageModelProvider('vendor1', { + onDidChange: Event.None, + provideLanguageModelChatInfo: async () => { + return [{ + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 1', + vendor: 'vendor1', + family: 'family1', + version: '1.0', + id: 'vendor1/model1', + maxInputTokens: 100, + maxOutputTokens: 100, + modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'vendor1/model1' + }]; + }, + sendChatRequest: async () => { throw new Error(); }, + provideTokenCount: async () => { throw new Error(); } + })); + + // Populate the model cache + await languageModelsService.selectLanguageModels({}); + }); + + teardown(function () { + languageModelsService.dispose(); + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('fires onChange event when new model preferences are added', async function () { + // Listen for change event + let firedVendorId: string | undefined; + disposables.add(languageModelsService.onDidChangeLanguageModels(vendorId => firedVendorId = vendorId)); + + // Add new preferences to storage - store() automatically triggers change event synchronously + const preferences = { + 'vendor1/model1': true + }; + storageService.store('chatModelPickerPreferences', JSON.stringify(preferences), StorageScope.PROFILE, StorageTarget.USER); + + // Verify change event was fired + assert.strictEqual(firedVendorId, 'vendor1', 'Should fire change event for vendor1'); + + // Verify preference was updated + const model = languageModelsService.lookupLanguageModel('vendor1/model1'); + assert.ok(model); + assert.strictEqual(model.isUserSelectable, true); + }); + + test('fires onChange event when model preferences are removed', async function () { + // Set initial preference using the API + languageModelsService.updateModelPickerPreference('vendor1/model1', true); + + // Listen for change event + let firedVendorId: string | undefined; + disposables.add(languageModelsService.onDidChangeLanguageModels(vendorId => { + firedVendorId = vendorId; + })); + + // Remove preferences via storage API + const updatedPreferences = {}; + storageService.store('chatModelPickerPreferences', JSON.stringify(updatedPreferences), StorageScope.PROFILE, StorageTarget.USER); + + // Verify change event was fired + assert.strictEqual(firedVendorId, 'vendor1', 'Should fire change event for vendor1 when preference removed'); + + // Verify preference was removed + const model = languageModelsService.lookupLanguageModel('vendor1/model1'); + assert.ok(model); + assert.strictEqual(model.isUserSelectable, undefined); + }); + + test('fires onChange event when model preferences are updated', async function () { + // Set initial preference using the API + languageModelsService.updateModelPickerPreference('vendor1/model1', true); + + // Listen for change event + let firedVendorId: string | undefined; + disposables.add(languageModelsService.onDidChangeLanguageModels(vendorId => { + firedVendorId = vendorId; + })); + + // Update the preference value + const updatedPreferences = { + 'vendor1/model1': false + }; + storageService.store('chatModelPickerPreferences', JSON.stringify(updatedPreferences), StorageScope.PROFILE, StorageTarget.USER); + + // Verify change event was fired + assert.strictEqual(firedVendorId, 'vendor1', 'Should fire change event for vendor1 when preference updated'); + + // Verify preference was updated + const model = languageModelsService.lookupLanguageModel('vendor1/model1'); + assert.ok(model); + assert.strictEqual(model.isUserSelectable, false); + }); + + test('only fires onChange event for affected vendors', async function () { + // Register vendor2 + const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!; + ext.acceptUsers([{ + description: { ...nullExtensionDescription }, + value: { vendor: 'vendor2' }, + collector: null! + }]); + + disposables.add(languageModelsService.registerLanguageModelProvider('vendor2', { + onDidChange: Event.None, + provideLanguageModelChatInfo: async () => { + return [{ + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 2', + vendor: 'vendor2', + family: 'family2', + version: '1.0', + id: 'vendor2/model2', + maxInputTokens: 100, + maxOutputTokens: 100, + modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'vendor2/model2' + }]; + }, + sendChatRequest: async () => { throw new Error(); }, + provideTokenCount: async () => { throw new Error(); } + })); + + await languageModelsService.selectLanguageModels({}); + + // Set initial preferences using the API + languageModelsService.updateModelPickerPreference('vendor1/model1', true); + languageModelsService.updateModelPickerPreference('vendor2/model2', false); + + // Listen for change event + let firedVendorId: string | undefined; + disposables.add(languageModelsService.onDidChangeLanguageModels(vendorId => { + firedVendorId = vendorId; + })); + + // Update only vendor1 preference + const updatedPreferences = { + 'vendor1/model1': false, + 'vendor2/model2': false // unchanged + }; + storageService.store('chatModelPickerPreferences', JSON.stringify(updatedPreferences), StorageScope.PROFILE, StorageTarget.USER); + + // Verify only vendor1 was affected + assert.strictEqual(firedVendorId, 'vendor1', 'Should only affect vendor1'); + + // Verify preferences were updated correctly + const model1 = languageModelsService.lookupLanguageModel('vendor1/model1'); + assert.ok(model1); + assert.strictEqual(model1.isUserSelectable, false, 'vendor1/model1 should be updated to false'); + + const model2 = languageModelsService.lookupLanguageModel('vendor2/model2'); + assert.ok(model2); + assert.strictEqual(model2.isUserSelectable, false, 'vendor2/model2 should remain false'); + }); + + test('does not fire onChange event when preferences are unchanged', async function () { + // Set initial preference using the API + languageModelsService.updateModelPickerPreference('vendor1/model1', true); + + // Listen for change event + let eventFired = false; + disposables.add(languageModelsService.onDidChangeLanguageModels(() => { + eventFired = true; + })); + + // Store the same preferences again + const initialPreferences = { + 'vendor1/model1': true + }; + storageService.store('chatModelPickerPreferences', JSON.stringify(initialPreferences), StorageScope.PROFILE, StorageTarget.USER); + + // Verify no event was fired + assert.strictEqual(eventFired, false, 'Should not fire event when preferences are unchanged'); + + // Verify preference remains the same + const model = languageModelsService.lookupLanguageModel('vendor1/model1'); + assert.ok(model); + assert.strictEqual(model.isUserSelectable, true); + }); + + test('handles malformed JSON in storage gracefully', function () { + // Listen for change event + let eventFired = false; + disposables.add(languageModelsService.onDidChangeLanguageModels(() => { + eventFired = true; + })); + + // Store empty preferences - store() automatically triggers change event + storageService.store('chatModelPickerPreferences', '{}', StorageScope.PROFILE, StorageTarget.USER); + + // Verify no event was fired - empty preferences is valid and causes no changes + assert.strictEqual(eventFired, false, 'Should not fire event for empty preferences'); + }); +}); + +suite('LanguageModels - Model Change Events', function () { + + let languageModelsService: LanguageModelsService; + let storageService: TestStorageService; + const disposables = new DisposableStore(); + + setup(async function () { + storageService = new TestStorageService(); + const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!; + ext.acceptUsers([{ + description: { ...nullExtensionDescription }, + value: { vendor: 'test-vendor' }, + collector: null! + }]); + + languageModelsService = new LanguageModelsService( + new class extends mock() { + override activateByEvent(name: string) { + return Promise.resolve(); + } + }, + new NullLogService(), + storageService, + new MockContextKeyService(), + new TestConfigurationService(), + new class extends mock() { + override onDidChangeLanguageModelGroups = Event.None; + override getLanguageModelsProviderGroups() { + return []; + } + }, + new class extends mock() { }, + new TestSecretStorageService(), + ); + }); + + teardown(function () { + languageModelsService.dispose(); + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('fires onChange event when new models are added', async function () { + // Create a promise that resolves when the event fires + const eventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModels((vendorId) => { + resolve(vendorId); + })); + }); + + // Store a preference to trigger auto-resolution when provider is registered + storageService.store('chatModelPickerPreferences', JSON.stringify({ 'test-vendor/model1': true }), StorageScope.PROFILE, StorageTarget.USER); + + disposables.add(languageModelsService.registerLanguageModelProvider('test-vendor', { + onDidChange: Event.None, + provideLanguageModelChatInfo: async () => { + return [{ + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 1', + vendor: 'test-vendor', + family: 'family1', + version: '1.0', + id: 'model1', + maxInputTokens: 100, + maxOutputTokens: 100, + modelPickerCategory: undefined, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'test-vendor/model1' + }]; + }, + sendChatRequest: async () => { throw new Error(); }, + provideTokenCount: async () => { throw new Error(); } + })); + + const firedVendorId = await eventPromise; + assert.strictEqual(firedVendorId, 'test-vendor', 'Should fire event when new models are added'); + }); + + test('does not fire onChange event when models are unchanged', async function () { + const models = [{ + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 1', + vendor: 'test-vendor', + family: 'family1', + version: '1.0', + id: 'model1', + maxInputTokens: 100, + maxOutputTokens: 100, + modelPickerCategory: undefined, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'test-vendor/model1' + }]; + + let onDidChangeEmitter: any; + disposables.add(languageModelsService.registerLanguageModelProvider('test-vendor', { + onDidChange: (listener) => { + onDidChangeEmitter = { fire: () => listener() }; + return { dispose: () => { } }; + }, + provideLanguageModelChatInfo: async () => models, + sendChatRequest: async () => { throw new Error(); }, + provideTokenCount: async () => { throw new Error(); } + })); + + // Initial resolution + await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' }); + + // Listen for change event + let eventFired = false; + disposables.add(languageModelsService.onDidChangeLanguageModels(() => { + eventFired = true; + })); + // Trigger provider change with same models + onDidChangeEmitter.fire(); + + // Call selectLanguageModels again - provider will return different models + await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' }); + assert.strictEqual(eventFired, false, 'Should not fire event when models are unchanged'); + }); + + test('fires onChange event when model metadata changes', async function () { + const initialModels = [{ + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 1', + vendor: 'test-vendor', + family: 'family1', + version: '1.0', + id: 'model1', + maxInputTokens: 100, + maxOutputTokens: 100, + modelPickerCategory: undefined, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'test-vendor/model1' + }]; + + let currentModels = initialModels; + let onDidChangeEmitter: any; + disposables.add(languageModelsService.registerLanguageModelProvider('test-vendor', { + onDidChange: (listener) => { + onDidChangeEmitter = { fire: () => listener() }; + return { dispose: () => { } }; + }, + provideLanguageModelChatInfo: async () => currentModels, + sendChatRequest: async () => { throw new Error(); }, + provideTokenCount: async () => { throw new Error(); } + })); + + // Initial resolution + await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' }); + + // Create a promise that resolves when the event fires + const eventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModels(() => { + resolve(); + })); + }); + + // Change model metadata (e.g., maxInputTokens) + currentModels = [{ + metadata: { + ...initialModels[0].metadata, + maxInputTokens: 200 // Changed from 100 + }, + identifier: 'test-vendor/model1' + }]; + + onDidChangeEmitter.fire(); + + await eventPromise; + assert.ok(true, 'Event fired when model metadata changed'); + }); + + test('fires onChange event when models are removed', async function () { + let currentModels = [{ + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 1', + vendor: 'test-vendor', + family: 'family1', + version: '1.0', + id: 'model1', + maxInputTokens: 100, + maxOutputTokens: 100, + modelPickerCategory: undefined, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'test-vendor/model1' + }]; + + let onDidChangeEmitter: any; + disposables.add(languageModelsService.registerLanguageModelProvider('test-vendor', { + onDidChange: (listener) => { + onDidChangeEmitter = { fire: () => listener() }; + return { dispose: () => { } }; + }, + provideLanguageModelChatInfo: async () => currentModels, + sendChatRequest: async () => { throw new Error(); }, + provideTokenCount: async () => { throw new Error(); } + })); + + // Initial resolution + await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' }); + + // Create a promise that resolves when the event fires + const eventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModels(() => { + resolve(); + })); + }); + + // Remove all models + currentModels = []; + + onDidChangeEmitter.fire(); + + await eventPromise; + assert.ok(true, 'Event fired when models were removed'); + }); + + test('fires onChange event when new model is added to existing set', async function () { + let currentModels = [{ + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 1', + vendor: 'test-vendor', + family: 'family1', + version: '1.0', + id: 'model1', + maxInputTokens: 100, + maxOutputTokens: 100, + modelPickerCategory: undefined, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'test-vendor/model1' + }]; + + let onDidChangeEmitter: any; + disposables.add(languageModelsService.registerLanguageModelProvider('test-vendor', { + onDidChange: (listener) => { + onDidChangeEmitter = { fire: () => listener() }; + return { dispose: () => { } }; + }, + provideLanguageModelChatInfo: async () => currentModels, + sendChatRequest: async () => { throw new Error(); }, + provideTokenCount: async () => { throw new Error(); } + })); + + // Initial resolution + await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' }); + + // Create a promise that resolves when the event fires + const eventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModels(() => { + resolve(); + })); + }); + + // Add a new model + currentModels = [ + ...currentModels, + { + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 2', + vendor: 'test-vendor', + family: 'family2', + version: '1.0', + id: 'model2', + maxInputTokens: 100, + maxOutputTokens: 100, + modelPickerCategory: undefined, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'test-vendor/model2' + } + ]; + + onDidChangeEmitter.fire(); + + await eventPromise; + assert.ok(true, 'Event fired when new model was added'); + }); + + test('fires onChange event when models change without provider emitting change event', async function () { + let callCount = 0; + disposables.add(languageModelsService.registerLanguageModelProvider('test-vendor', { + onDidChange: Event.None, // Provider doesn't emit change events + provideLanguageModelChatInfo: async () => { + callCount++; + if (callCount === 1) { + // First call returns initial model + return [{ + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 1', + vendor: 'test-vendor', + family: 'family1', + version: '1.0', + id: 'model1', + maxInputTokens: 100, + maxOutputTokens: 100, + modelPickerCategory: undefined, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'test-vendor/model1' + }]; + } else { + // Subsequent calls return different model + return [{ + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 2', + vendor: 'test-vendor', + family: 'family2', + version: '2.0', + id: 'model2', + maxInputTokens: 200, + maxOutputTokens: 200, + modelPickerCategory: undefined, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'test-vendor/model2' + }]; + } + }, + sendChatRequest: async () => { throw new Error(); }, + provideTokenCount: async () => { throw new Error(); } + })); + + // Initial resolution + await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' }); + + // Listen for change event + let eventFired = false; + disposables.add(languageModelsService.onDidChangeLanguageModels(() => { + eventFired = true; + })); + + // Call selectLanguageModels again - provider will return different models + await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' }); + + assert.strictEqual(eventFired, true, 'Should fire event when models change even without provider change event'); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.ts index 8b60a218291..fd336e7fb37 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.ts @@ -48,7 +48,7 @@ export class NullLanguageModelsService implements ILanguageModelsService { return; } - async fetchLanguageModelGroups(vendor: string): Promise { + getLanguageModelGroups(vendor: string): ILanguageModelsGroup[] { return []; } From 0ea07d585bde9b8973c17c4134a4219aa4395cad Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 20 Jan 2026 15:56:00 +0100 Subject: [PATCH 262/387] agent sessions - tweaks to time display (#289115) --- .../chat/browser/agentSessions/agentSessionsViewer.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 74f5db7b4cc..50a3b94aea5 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -316,6 +316,10 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre return undefined; } + if (elapsed < 30000) { + return localize('secondsDuration', "now"); + } + return getDurationString(elapsed, useFullTimeWords); } @@ -328,7 +332,7 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre } if (!timeLabel) { - timeLabel = fromNow(session.timing.lastRequestEnded ?? session.timing.lastRequestStarted ?? session.timing.created); + timeLabel = fromNow(session.timing.lastRequestEnded ?? session.timing.lastRequestStarted ?? session.timing.created, true); } return timeLabel; From 33e2f28eec521a42417cb8fc4383745318eed278 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 20 Jan 2026 14:56:08 +0000 Subject: [PATCH 263/387] refactor theme styles for improved UI consistency and add new styles for notifications and sashes --- extensions/theme-2026/themes/2026-light.json | 90 ++++++++++---------- extensions/theme-2026/themes/styles.css | 77 ++++++++++------- 2 files changed, 93 insertions(+), 74 deletions(-) diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index 1248cd2fd9c..93d43d10d5b 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -9,22 +9,22 @@ "descriptionForeground": "#666666", "icon.foreground": "#666666", "focusBorder": "#4466CCFF", - "textBlockQuote.background": "#E9E9E9", - "textBlockQuote.border": "#EEEEEE00", - "textCodeBlock.background": "#E9E9E9", + "textBlockQuote.background": "#EDEDED", + "textBlockQuote.border": "#ECEDEEFF", + "textCodeBlock.background": "#EDEDED", "textLink.foreground": "#3457C0", "textLink.activeForeground": "#3355BA", "textPreformat.foreground": "#666666", - "textSeparator.foreground": "#EEEEEE00", + "textSeparator.foreground": "#EEEEEEFF", "button.background": "#4466CC", "button.foreground": "#FFFFFF", "button.hoverBackground": "#3E61CA", - "button.border": "#EEEEEE00", - "button.secondaryBackground": "#E9E9E9", + "button.border": "#ECEDEEFF", + "button.secondaryBackground": "#EDEDED", "button.secondaryForeground": "#202020", - "button.secondaryHoverBackground": "#E2E2E2", - "checkbox.background": "#E9E9E9", - "checkbox.border": "#EEEEEE00", + "button.secondaryHoverBackground": "#E6E6E6", + "checkbox.background": "#EDEDED", + "checkbox.border": "#ECEDEEFF", "checkbox.foreground": "#202020", "dropdown.background": "#F9F9F9", "dropdown.border": "#D6D7D8", @@ -36,15 +36,15 @@ "input.placeholderForeground": "#999999", "inputOption.activeBackground": "#4466CC33", "inputOption.activeForeground": "#202020", - "inputOption.activeBorder": "#EEEEEE00", + "inputOption.activeBorder": "#ECEDEEFF", "inputValidation.errorBackground": "#F9F9F9", - "inputValidation.errorBorder": "#EEEEEE00", + "inputValidation.errorBorder": "#ECEDEEFF", "inputValidation.errorForeground": "#202020", "inputValidation.infoBackground": "#F9F9F9", - "inputValidation.infoBorder": "#EEEEEE00", + "inputValidation.infoBorder": "#ECEDEEFF", "inputValidation.infoForeground": "#202020", "inputValidation.warningBackground": "#F9F9F9", - "inputValidation.warningBorder": "#EEEEEE00", + "inputValidation.warningBorder": "#ECEDEEFF", "inputValidation.warningForeground": "#202020", "scrollbar.shadow": "#F5F6F84D", "scrollbarSlider.background": "#4466CC33", @@ -55,7 +55,7 @@ "progressBar.background": "#666666", "list.activeSelectionBackground": "#4466CC26", "list.activeSelectionForeground": "#202020", - "list.inactiveSelectionBackground": "#E9E9E9", + "list.inactiveSelectionBackground": "#EDEDED", "list.inactiveSelectionForeground": "#202020", "list.hoverBackground": "#F2F2F2", "list.hoverForeground": "#202020", @@ -70,31 +70,31 @@ "activityBar.background": "#F9F9F9", "activityBar.foreground": "#202020", "activityBar.inactiveForeground": "#666666", - "activityBar.border": "#EEEEEE00", - "activityBar.activeBorder": "#EEEEEE00", + "activityBar.border": "#ECEDEEFF", + "activityBar.activeBorder": "#ECEDEEFF", "activityBar.activeFocusBorder": "#4466CCFF", "activityBarBadge.background": "#4466CC", "activityBarBadge.foreground": "#FFFFFF", "sideBar.background": "#F9F9F9", "sideBar.foreground": "#202020", - "sideBar.border": "#EEEEEE00", + "sideBar.border": "#ECEDEEFF", "sideBarTitle.foreground": "#202020", "sideBarSectionHeader.background": "#F9F9F9", "sideBarSectionHeader.foreground": "#202020", - "sideBarSectionHeader.border": "#EEEEEE00", + "sideBarSectionHeader.border": "#ECEDEEFF", "titleBar.activeBackground": "#F9F9F9", "titleBar.activeForeground": "#424242", "titleBar.inactiveBackground": "#F9F9F9", "titleBar.inactiveForeground": "#666666", - "titleBar.border": "#EEEEEE00", - "menubar.selectionBackground": "#E9E9E9", + "titleBar.border": "#ECEDEEFF", + "menubar.selectionBackground": "#EDEDED", "menubar.selectionForeground": "#202020", "menu.background": "#FCFCFC", "menu.foreground": "#202020", "menu.selectionBackground": "#4466CC26", "menu.selectionForeground": "#202020", "menu.separatorBackground": "#F4F4F4", - "menu.border": "#EEEEEE00", + "menu.border": "#ECEDEEFF", "commandCenter.foreground": "#202020", "commandCenter.activeForeground": "#202020", "commandCenter.background": "#F9F9F9", @@ -106,16 +106,16 @@ "editorLineNumber.activeForeground": "#202123", "editorCursor.foreground": "#202123", "editor.selectionBackground": "#4466CC26", - "editor.inactiveSelectionBackground": "#4466CC80", + "editor.inactiveSelectionBackground": "#4466CC26", "editor.selectionHighlightBackground": "#4466CC1A", "editor.wordHighlightBackground": "#4466CC33", "editor.wordHighlightStrongBackground": "#4466CC33", "editor.findMatchBackground": "#4466CC4D", "editor.findMatchHighlightBackground": "#4466CC26", - "editor.findRangeHighlightBackground": "#E9E9E9", - "editor.hoverHighlightBackground": "#E9E9E9", - "editor.lineHighlightBackground": "#E9E9E9", - "editor.rangeHighlightBackground": "#E9E9E9", + "editor.findRangeHighlightBackground": "#EDEDED", + "editor.hoverHighlightBackground": "#EDEDED", + "editor.lineHighlightBackground": "#EDEDED55", + "editor.rangeHighlightBackground": "#EDEDED", "editorLink.activeForeground": "#4466CC", "editorWhitespace.foreground": "#6666664D", "editorIndentGuide.background": "#F4F4F44D", @@ -123,27 +123,27 @@ "editorRuler.foreground": "#F4F4F4", "editorCodeLens.foreground": "#666666", "editorBracketMatch.background": "#4466CC55", - "editorBracketMatch.border": "#EEEEEE00", + "editorBracketMatch.border": "#ECEDEEFF", "editorWidget.background": "#FCFCFC", - "editorWidget.border": "#EEEEEE00", + "editorWidget.border": "#ECEDEEFF", "editorWidget.foreground": "#202020", "editorSuggestWidget.background": "#FCFCFC", - "editorSuggestWidget.border": "#EEEEEE00", + "editorSuggestWidget.border": "#ECEDEEFF", "editorSuggestWidget.foreground": "#202020", "editorSuggestWidget.highlightForeground": "#202020", "editorSuggestWidget.selectedBackground": "#4466CC26", - "editorHoverWidget.background": "#FCFCFC", - "editorHoverWidget.border": "#EEEEEE00", - "peekView.border": "#EEEEEE00", + "editorHoverWidget.background": "#FCFCFC55", + "editorHoverWidget.border": "#ECEDEEFF", + "peekView.border": "#ECEDEEFF", "peekViewEditor.background": "#F9F9F9", "peekViewEditor.matchHighlightBackground": "#4466CC33", - "peekViewResult.background": "#E9E9E9", + "peekViewResult.background": "#EDEDED", "peekViewResult.fileForeground": "#202020", "peekViewResult.lineForeground": "#666666", "peekViewResult.matchHighlightBackground": "#4466CC33", "peekViewResult.selectionBackground": "#4466CC26", "peekViewResult.selectionForeground": "#202020", - "peekViewTitle.background": "#E9E9E9", + "peekViewTitle.background": "#EDEDED", "peekViewTitleDescription.foreground": "#666666", "peekViewTitleLabel.foreground": "#202020", "editorGutter.background": "#FDFDFD", @@ -151,7 +151,7 @@ "editorGutter.deletedBackground": "#ad0707", "diffEditor.insertedTextBackground": "#587c0c26", "diffEditor.removedTextBackground": "#ad070726", - "editorOverviewRuler.border": "#EEEEEE00", + "editorOverviewRuler.border": "#ECEDEEFF", "editorOverviewRuler.findMatchForeground": "#4466CC99", "editorOverviewRuler.modifiedForeground": "#007acc", "editorOverviewRuler.addedForeground": "#587c0c", @@ -159,13 +159,13 @@ "editorOverviewRuler.errorForeground": "#ad0707", "editorOverviewRuler.warningForeground": "#667309", "panel.background": "#F9F9F9", - "panel.border": "#EEEEEE00", + "panel.border": "#ECEDEEFF", "panelTitle.activeBorder": "#4466CC", "panelTitle.activeForeground": "#202020", "panelTitle.inactiveForeground": "#666666", "statusBar.background": "#F9F9F9", "statusBar.foreground": "#202020", - "statusBar.border": "#EEEEEE00", + "statusBar.border": "#ECEDEEFF", "statusBar.focusBorder": "#4466CCFF", "statusBar.debuggingBackground": "#4466CC", "statusBar.debuggingForeground": "#FFFFFF", @@ -181,8 +181,8 @@ "tab.activeForeground": "#202020", "tab.inactiveBackground": "#F9F9F9", "tab.inactiveForeground": "#666666", - "tab.border": "#EEEEEE00", - "tab.lastPinnedBorder": "#EEEEEE00", + "tab.border": "#ECEDEEFF", + "tab.lastPinnedBorder": "#ECEDEEFF", "tab.activeBorder": "#FBFBFD", "tab.hoverBackground": "#F2F2F2", "tab.hoverForeground": "#202020", @@ -191,24 +191,24 @@ "tab.unfocusedInactiveBackground": "#F9F9F9", "tab.unfocusedInactiveForeground": "#BBBBBB", "editorGroupHeader.tabsBackground": "#F9F9F9", - "editorGroupHeader.tabsBorder": "#EEEEEE00", + "editorGroupHeader.tabsBorder": "#ECEDEEFF", "breadcrumb.foreground": "#666666", "breadcrumb.background": "#FDFDFD", "breadcrumb.focusForeground": "#202020", "breadcrumb.activeSelectionForeground": "#202020", "breadcrumbPicker.background": "#FCFCFC", - "notificationCenter.border": "#EEEEEE00", + "notificationCenter.border": "#ECEDEEFF", "notificationCenterHeader.foreground": "#202020", - "notificationCenterHeader.background": "#E9E9E9", - "notificationToast.border": "#EEEEEE00", + "notificationCenterHeader.background": "#EDEDED", + "notificationToast.border": "#ECEDEEFF", "notifications.foreground": "#202020", "notifications.background": "#FCFCFC", - "notifications.border": "#EEEEEE00", + "notifications.border": "#ECEDEEFF", "notificationLink.foreground": "#4466CC", "extensionButton.prominentBackground": "#4466CC", "extensionButton.prominentForeground": "#FFFFFF", "extensionButton.prominentHoverBackground": "#3E61CA", - "pickerGroup.border": "#EEEEEE00", + "pickerGroup.border": "#ECEDEEFF", "pickerGroup.foreground": "#202020", "quickInput.background": "#FCFCFC", "quickInput.foreground": "#202020", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 589033dccf3..91f1e05826f 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -19,12 +19,26 @@ .monaco-workbench.panel-position-left .part.panel { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); } .monaco-workbench.panel-position-right .part.panel { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); } +/* Sashes - ensure they extend full height and are above other panels */ +.monaco-workbench .monaco-sash { z-index: 45; } +.monaco-workbench .monaco-sash.vertical { z-index: 45; } +.monaco-workbench .monaco-sash.horizontal { z-index: 45; } + +.monaco-workbench .activitybar.left.bordered::before, +.monaco-workbench .activitybar.right.bordered::before { + border: none; +} + /* Editor */ .monaco-workbench .part.editor { position: relative; } .monaco-workbench .part.editor > .content .editor-group-container > .title { box-shadow: none; position: relative; z-index: 10; } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { box-shadow: 0 0 5px rgba(0, 0, 0, 0.10); position: relative; z-index: 5; border-radius: 4px 4px 0 0; border-top: none !important; } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover:not(.active) { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); } +/* Tab border bottom - make transparent */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container { --tabs-border-bottom-color: transparent !important; } +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab { --tab-border-bottom-color: transparent !important; } + /* Title Bar */ .monaco-workbench .part.titlebar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); z-index: 60; position: relative; overflow: visible !important; } .monaco-workbench .part.titlebar .titlebar-container, @@ -54,19 +68,16 @@ .monaco-workbench .quick-input-widget .quick-input-message, /* .monaco-workbench .quick-input-widget .monaco-inputbox, */ .monaco-workbench .quick-input-widget .monaco-list, -.monaco-workbench .quick-input-widget .monaco-list-row { border: none !important; border-color: transparent !important; outline: none !important; } +.monaco-workbench .quick-input-widget .monaco-list-row { border-color: transparent !important; outline: none !important; } .monaco-workbench .quick-input-widget .monaco-inputbox { box-shadow: none !important; background: transparent !important; } .monaco-workbench .quick-input-widget .quick-input-filter .monaco-inputbox { background: rgba(249, 250, 251, 0.4) !important; border-radius: 6px; } .monaco-workbench.vs-dark .quick-input-widget .quick-input-filter .monaco-inputbox, -.monaco-workbench.hc-black .quick-input-widget .quick-input-filter .monaco-inputbox { background: rgba(20, 20, 22, 0.6) !important; } .monaco-workbench .quick-input-widget .monaco-list.list_id_6:not(.drop-target):not(.dragging) .monaco-list-row:hover:not(.selected):not(.focused) { background-color: color-mix(in srgb, var(--vscode-list-hoverBackground) 40%, transparent); } - /* Chat Widget */ .monaco-workbench .interactive-session .chat-input-container { box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.08); border-radius: 6px; } .monaco-workbench .interactive-session .interactive-input-part .chat-editor-container .interactive-input-editor .monaco-editor, -.monaco-workbench .interactive-session .interactive-input-part .chat-editor-container .interactive-input-editor .monaco-editor .monaco-editor-background { background-color: var(--vscode-panel-background, var(--vscode-sideBar-background)) !important; } .monaco-workbench .interactive-session .chat-editing-session .chat-editing-session-container { border-radius: 4px 4px 0 0; } .monaco-workbench .interactive-input-part:has(.chat-editing-session > .chat-editing-session-container) .chat-input-container { border-radius: 0 0 6px 6px; } .monaco-workbench .part.panel .interactive-session, @@ -77,9 +88,25 @@ } /* Notifications */ -.monaco-workbench .notifications-toasts { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); } -.monaco-workbench .notification-toast { box-shadow: none !important; border: none !important; } -.monaco-workbench .notifications-center { border: none !important; } + +.monaco-workbench .notifications-toasts { + box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); + border-radius: 4px; + /* backdrop-filter: blur(20px) saturate(180%); + -webkit-backdrop-filter: blur(20px) saturate(180%); */ +} +.monaco-workbench .notification-toast { box-shadow: none !important; margin: 0 !important;} +.monaco-workbench .notifications-center { + backdrop-filter: blur(20px) saturate(180%); + -webkit-backdrop-filter: blur(20px) saturate(180%); + background-color: rgba(255, 255, 255, 0.6) !important; + +} +.monaco-workbench .notifications-list-container, +.monaco-workbench > .notifications-center > .notifications-center-header, +.monaco-workbench .notifications-list-container .monaco-list-rows { + background: transparent !important; +} /* Context Menus */ .monaco-workbench .monaco-menu .monaco-action-bar.vertical { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 12px; overflow: hidden; backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); } @@ -113,6 +140,9 @@ .monaco-workbench .monaco-editor .peekview-widget .body { background: transparent !important; } .monaco-workbench .monaco-editor .peekview-widget .ref-tree { background: var(--vscode-editor-background, #EDEDED) !important; } + +.monaco-editor .monaco-hover { box-shadow: 0 0 4px rgba(0, 0, 0, 0.18); border-radius: 8px; overflow: hidden; backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); } +.monaco-hover.workbench-hover { backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; } /* Settings */ .monaco-workbench .settings-editor .settings-toc-container { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); } @@ -121,13 +151,12 @@ .monaco-workbench .part.editor .welcomePageContainer .tile:hover { box-shadow: 0 0 8px rgba(0, 0, 0, 0.12); } /* Extensions */ -.monaco-workbench .extensions-list .extension-list-item { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); border: none; border-radius: 6px; margin: 4px 0; } +.monaco-workbench .extensions-list .extension-list-item { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); border: none; } .monaco-workbench .extensions-list .extension-list-item:hover { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10); } /* Breadcrumbs */ .monaco-workbench .part.editor > .content .editor-group-container > .title .breadcrumbs-control { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08) !important; background: rgba(252, 252, 253, 0.65) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; } .monaco-workbench.vs-dark .part.editor > .content .editor-group-container > .title .breadcrumbs-control, -.monaco-workbench.hc-black .part.editor > .content .editor-group-container > .title .breadcrumbs-control { background: rgba(10, 10, 11, 0.85) !important; box-shadow: 0 0 6px rgba(0, 0, 0, 0.08) !important; } /* Input Boxes */ .monaco-workbench .monaco-inputbox, @@ -139,13 +168,9 @@ color: var(--vscode-icon-foreground) !important; } -/* .scm-view .scm-editor { - box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.08); -} */ - /* Buttons */ .monaco-workbench .monaco-button { box-shadow: 0 0 2px rgba(0, 0, 0, 0.06); } -.monaco-workbench .monaco-button:hover { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); } +.monaco-workbench .monaco-button:hover { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); } .monaco-workbench .monaco-button:active { box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); } /* Dropdowns */ @@ -158,26 +183,24 @@ .monaco-workbench .scm-view .scm-provider { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); border-radius: 6px; } /* Debug Toolbar */ -.monaco-workbench .debug-toolbar { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 8px; } +.monaco-workbench .debug-toolbar { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 8px; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; } /* Action Widget */ -.monaco-workbench .action-widget { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14) !important; border: none !important; border-radius: 8px; } +.monaco-workbench .action-widget { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14) !important; border-radius: 8px; } /* Parameter Hints */ .monaco-workbench .monaco-editor .parameter-hints-widget { box-shadow: 0 0 8px rgba(0, 0, 0, 0.12); border: none; border-radius: 12px; overflow: hidden; backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); } .monaco-workbench.vs-dark .monaco-editor .parameter-hints-widget, -.monaco-workbench.hc-black .monaco-editor .parameter-hints-widget { background: rgba(10, 10, 11, 0.85) !important; } .monaco-workbench.vs .monaco-editor .parameter-hints-widget { background: rgba(252, 252, 253, 0.85) !important; } /* Minimap */ .monaco-workbench .monaco-editor .minimap { background: rgba(252, 252, 253, 0.65) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; border-radius: 8px 0 0 8px; } .monaco-workbench .monaco-editor .minimap canvas { opacity: 0.85; } .monaco-workbench.vs-dark .monaco-editor .minimap, -.monaco-workbench.hc-black .monaco-editor .minimap { background: rgba(5, 5, 6, 0.85) !important; box-shadow: 0 0 6px rgba(0, 0, 0, 0.10) !important; } .monaco-workbench .monaco-editor .minimap-shadow-visible { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10); } /* Sticky Scroll */ -.monaco-workbench .monaco-editor .sticky-widget { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10) !important; border-bottom: none !important; border: none !important; background: rgba(252, 252, 253, 0.65) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; border-radius: 0 0 8px 8px !important; } +.monaco-workbench .monaco-editor .sticky-widget { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10) !important; border-bottom: none !important; background: rgba(252, 252, 253, 0.65) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; border-radius: 0 0 8px 8px !important; } .monaco-workbench .monaco-editor .sticky-widget *, .monaco-workbench .monaco-editor .sticky-widget > *, .monaco-workbench .monaco-editor .sticky-widget .sticky-widget-line-numbers, @@ -187,7 +210,6 @@ .monaco-workbench .monaco-editor .sticky-widget .sticky-line-content, .monaco-workbench .monaco-editor .sticky-widget .sticky-line-content:hover { background-color: transparent !important; background: transparent !important; } .monaco-workbench.vs-dark .monaco-editor .sticky-widget, -.monaco-workbench.hc-black .monaco-editor .sticky-widget { background: rgba(10, 10, 11, 0.85) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; box-shadow: 0 0 6px rgba(0, 0, 0, 0.10) !important; } .monaco-workbench .monaco-editor .sticky-widget-focus-preview, .monaco-workbench .monaco-editor .sticky-scroll-focus-line, .monaco-workbench .monaco-editor .focused .sticky-widget, @@ -196,10 +218,6 @@ .monaco-workbench.vs-dark .monaco-editor .sticky-scroll-focus-line, .monaco-workbench.vs-dark .monaco-editor .focused .sticky-widget, .monaco-workbench.vs-dark .monaco-editor:has(.sticky-widget:focus-within) .sticky-widget, -.monaco-workbench.hc-black .monaco-editor .sticky-widget-focus-preview, -.monaco-workbench.hc-black .monaco-editor .sticky-scroll-focus-line, -.monaco-workbench.hc-black .monaco-editor .focused .sticky-widget, -.monaco-workbench.hc-black .monaco-editor:has(.sticky-widget:focus-within) .sticky-widget { background: rgba(10, 10, 11, 0.90) !important; box-shadow: 0 0 8px rgba(0, 0, 0, 0.12) !important; } /* Notebook */ .monaco-workbench .notebookOverlay .monaco-list .cell-focus-indicator { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); border-radius: 6px; } @@ -209,12 +227,10 @@ .monaco-workbench .monaco-editor .inline-chat { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 12px; } /* Command Center */ -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center { box-shadow: 0 0 8px rgba(0, 0, 0, 0.09) !important; border-radius: 8px !important; background: rgba(249, 250, 251, 0.55) !important; backdrop-filter: blur(24px) saturate(150%); -webkit-backdrop-filter: blur(24px) saturate(150%); overflow: visible !important; } -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center:hover { box-shadow: 0 0 10px rgba(0, 0, 0, 0.11) !important; background: rgba(249, 250, 251, 0.70) !important; } +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center { box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.09) !important; border-radius: 8px !important; background: rgba(249, 250, 251, 0.55) !important; backdrop-filter: blur(24px) saturate(150%); -webkit-backdrop-filter: blur(24px) saturate(150%); overflow: visible !important; } +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center:hover { box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.11) !important; background: rgba(249, 250, 251, 0.70) !important; } .monaco-workbench.vs-dark .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center, -.monaco-workbench.hc-black .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center { background: rgba(10, 10, 11, 0.75) !important; backdrop-filter: blur(24px) saturate(150%); -webkit-backdrop-filter: blur(24px) saturate(150%); } .monaco-workbench.vs-dark .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center:hover, -.monaco-workbench.hc-black .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center:hover { background: rgba(15, 15, 17, 0.85) !important; } .monaco-workbench .part.titlebar .command-center .agent-status-pill { box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.08); } @@ -223,6 +239,10 @@ background-color: transparent; } +.monaco-dialog-modal-block .dialog-shadow { + border-radius: 12px; +} + /* Remove Borders */ .monaco-workbench.vs .part.sidebar { border-right: none !important; border-left: none !important; } .monaco-workbench.vs .part.auxiliarybar { border-right: none !important; border-left: none !important; } @@ -230,5 +250,4 @@ .monaco-workbench.vs .part.activitybar { border-right: none !important; border-left: none !important; } .monaco-workbench.vs .part.titlebar { border-bottom: none !important; } .monaco-workbench.vs .part.statusbar { border-top: none !important; } -.monaco-workbench .part.editor > .content .editor-group-container { border: none !important; } .monaco-workbench .pane-composite-part:not(.empty) > .header { border-bottom: none !important; } From 9d2710f48223a1fedbc98785a8397fece5638f55 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:02:13 +0100 Subject: [PATCH 264/387] Add "collapse visible comments" keybinding (#289116) --- .../browser/actions/chatExecuteActions.ts | 4 +++ .../browser/commentThreadZoneWidget.ts | 10 +++++++ .../comments/browser/commentsAccessibility.ts | 2 +- .../comments/browser/commentsController.ts | 26 +++++++++++++++++++ .../browser/commentsEditorContribution.ts | 21 +++++++++++++++ .../comments/common/commentContextKeys.ts | 5 ++++ 6 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index acc95dc6441..1fac6850829 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -798,6 +798,10 @@ export class CancelAction extends Action2 { keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.Escape, + when: ContextKeyExpr.and( + ChatContextKeys.requestInProgress, + ChatContextKeys.remoteJobCreating.negate() + ), win: { primary: KeyMod.Alt | KeyCode.Backspace }, } }); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index ba8d0ada377..85e5fba39ef 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -116,6 +116,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget private _commentThreadWidget!: CommentThreadWidget; private readonly _onDidClose = new Emitter(); private readonly _onDidCreateThread = new Emitter(); + private readonly _onDidChangeExpandedState = new Emitter(); private _isExpanded?: boolean; private _initialCollapsibleState?: languages.CommentThreadCollapsibleState; private _commentGlyph?: CommentGlyphWidget; @@ -185,6 +186,10 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget return this._onDidCreateThread.event; } + public get onDidChangeExpandedState(): Event { + return this._onDidChangeExpandedState.event; + } + public getPosition(): IPosition | undefined { if (this.position) { return this.position; @@ -540,10 +545,14 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget range = new Range(range.startLineNumber + distance, range.startColumn, range.endLineNumber + distance, range.endColumn); } + const wasExpanded = this._isExpanded; this._isExpanded = true; super.show(range ?? new Range(0, 0, 0, 0), heightInLines); this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Expanded; this._refresh(this._commentThreadWidget.getDimensions()); + if (!wasExpanded) { + this._onDidChangeExpandedState.fire(true); + } } async collapseAndFocusRange() { @@ -563,6 +572,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget if (!this._commentThread.comments || !this._commentThread.comments.length) { this.deleteCommentThread(); } + this._onDidChangeExpandedState.fire(false); } super.hide(); } diff --git a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts index 89a885968f0..3eeff582704 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts @@ -20,7 +20,7 @@ export namespace CommentAccessibilityHelpNLS { export const intro = nls.localize('intro', "The editor contains commentable range(s). Some useful commands include:"); export const tabFocus = nls.localize('introWidget', "This widget contains a text area, for composition of new comments, and actions, that can be tabbed to once tab moves focus mode has been enabled with the command Toggle Tab Key Moves Focus{0}.", ``); export const commentCommands = nls.localize('commentCommands', "Some useful comment commands include:"); - export const escape = nls.localize('escape', "- Dismiss Comment (Escape)"); + export const escape = nls.localize('escape', "- Dismiss Comment{0}.", ``); export const nextRange = nls.localize('next', "- Go to Next Commenting Range{0}.", ``); export const previousRange = nls.localize('previous', "- Go to Previous Commenting Range{0}.", ``); export const nextCommentThread = nls.localize('nextCommentThreadKb', "- Go to Next Comment Thread{0}.", ``); diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index b5ef3f9abf7..475634f67c2 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -471,6 +471,7 @@ export class CommentController implements IEditorContribution { private _activeCursorHasCommentingRange: IContextKey; private _activeCursorHasComment: IContextKey; private _activeEditorHasCommentingRange: IContextKey; + private _commentWidgetVisible: IContextKey; private _hasRespondedToEditorChange: boolean = false; constructor( @@ -496,6 +497,7 @@ export class CommentController implements IEditorContribution { this._activeCursorHasCommentingRange = CommentContextKeys.activeCursorHasCommentingRange.bindTo(contextKeyService); this._activeCursorHasComment = CommentContextKeys.activeCursorHasComment.bindTo(contextKeyService); this._activeEditorHasCommentingRange = CommentContextKeys.activeEditorHasCommentingRange.bindTo(contextKeyService); + this._commentWidgetVisible = CommentContextKeys.commentWidgetVisible.bindTo(contextKeyService); if (editor instanceof EmbeddedCodeEditorWidget) { return; @@ -740,6 +742,28 @@ export class CommentController implements IEditorContribution { } } + public async collapseVisibleComments(): Promise { + if (!this.editor) { + return; + } + const visibleRanges = this.editor.getVisibleRanges(); + for (const widget of this._commentWidgets) { + if (widget.expanded && widget.commentThread.range) { + const isVisible = visibleRanges.some(visibleRange => + Range.areIntersectingOrTouching(visibleRange, widget.commentThread.range!) + ); + if (isVisible) { + await widget.collapse(true); + } + } + } + } + + private _updateCommentWidgetVisibleContext(): void { + const hasExpanded = this._commentWidgets.some(widget => widget.expanded); + this._commentWidgetVisible.set(hasExpanded); + } + public expandAll(): void { for (const widget of this._commentWidgets) { widget.expand(); @@ -1074,6 +1098,8 @@ export class CommentController implements IEditorContribution { const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, uniqueOwner, thread, pendingComment ?? continueOnCommentReply?.comment, pendingEdits); await zoneWidget.display(thread.range, shouldReveal); this._commentWidgets.push(zoneWidget); + zoneWidget.onDidChangeExpandedState(() => this._updateCommentWidgetVisibleContext()); + zoneWidget.onDidClose(() => this._updateCommentWidgetVisibleContext()); this.openCommentsView(thread); } diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index 688fd773561..aeb70b11d9a 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -466,6 +466,27 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: CommentCommandId.Hide, + weight: KeybindingWeight.EditorContrib, + primary: KeyMod.CtrlCmd | KeyCode.Escape, + win: { primary: KeyMod.Alt | KeyCode.Backspace }, + when: ContextKeyExpr.and(EditorContextKeys.focus, CommentContextKeys.commentWidgetVisible), + handler: async (accessor, args) => { + const activeCodeEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor(); + const keybindingService = accessor.get(IKeybindingService); + // Unfortunate, but collapsing the comment thread might cause a dialog to show + // If we don't wait for the key up here, then the dialog will consume it and immediately close + await keybindingService.enableKeybindingHoldMode(CommentCommandId.Hide); + if (activeCodeEditor) { + const controller = CommentController.get(activeCodeEditor); + if (controller) { + await controller.collapseVisibleComments(); + } + } + } +}); + export function getActiveEditor(accessor: ServicesAccessor): IActiveCodeEditor | null { let activeTextEditorControl = accessor.get(IEditorService).activeTextEditorControl; diff --git a/src/vs/workbench/contrib/comments/common/commentContextKeys.ts b/src/vs/workbench/contrib/comments/common/commentContextKeys.ts index 2a5d0776c45..a26eee8c8b5 100644 --- a/src/vs/workbench/contrib/comments/common/commentContextKeys.ts +++ b/src/vs/workbench/contrib/comments/common/commentContextKeys.ts @@ -67,6 +67,11 @@ export namespace CommentContextKeys { */ export const commentFocused = new RawContextKey('commentFocused', false, { type: 'boolean', description: nls.localize('commentFocused', "Set when the comment is focused") }); + /** + * A context key that is set when a comment widget is visible in the editor. + */ + export const commentWidgetVisible = new RawContextKey('commentWidgetVisible', false, { type: 'boolean', description: nls.localize('commentWidgetVisible', "Set when a comment widget is visible in the editor") }); + /** * A context key that is set when commenting is enabled. */ From ebaa450e15ddf03d2350a7264a3e931dce0eeb4b Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:05:12 +0100 Subject: [PATCH 265/387] Chat - auto-accept external edits (#288933) * Chat - auto-accept external edits * Trying to track down the test failures --- extensions/git/src/repository.ts | 10 ++++++++++ .../chat/browser/chatEditing/chatEditingSession.ts | 3 +++ 2 files changed, 13 insertions(+) diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index edb16b5f4d0..4b842c07844 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1127,6 +1127,11 @@ export class Repository implements Disposable { return undefined; } + // Since we are inspecting the resource groups + // we have to ensure that the repository state + // is up to date + // await this.status(); + // Ignore path that is inside a merge group if (this.mergeGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) { this.logger.trace(`[Repository][provideOriginalResource] Resource is part of a merge group: ${uri.toString()}`); @@ -3295,6 +3300,11 @@ export class StagedResourceQuickDiffProvider implements QuickDiffProvider { return undefined; } + // Since we are inspecting the resource groups + // we have to ensure that the repository state + // is up to date + // await this._repository.status(); + // Ignore resources that are not in the index group if (!this._repository.indexGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) { this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource is not part of a index group: ${uri.toString()}`); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 5cc56bd9207..a4a9e1ba750 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -677,6 +677,9 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio // Mark as no longer being modified await entry.acceptStreamingEditsEnd(); + // Accept the changes + await entry.accept(); + // Clear external edit mode entry.stopExternalEdit(); } From eeb23ebcf41e9ef8308eeb3eb7ce972ae498f241 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 20 Jan 2026 16:29:30 +0100 Subject: [PATCH 266/387] agent sessions - focus chat if session opened from stacked view (#289120) --- .../chat/browser/agentSessions/agentSessionsControl.ts | 6 ++++-- .../chat/browser/widgetHosts/viewPane/chatViewPane.ts | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index da55fc44340..943aa770a5c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -29,7 +29,7 @@ import { IStyleOverride } from '../../../../../platform/theme/browser/defaultSty import { IAgentSessionsControl } from './agentSessions.js'; import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js'; import { URI } from '../../../../../base/common/uri.js'; -import { openSession } from './agentSessionsOpener.js'; +import { ISessionOpenOptions, openSession } from './agentSessionsOpener.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js'; import { IMouseEvent } from '../../../../../base/browser/mouseEvent.js'; @@ -43,6 +43,7 @@ export interface IAgentSessionsControlOptions extends IAgentSessionsSorterOption getHoverPosition(): HoverPosition; trackActiveEditorSession(): boolean; + overrideSessionOpenOptions?(openEvent: IOpenEvent): ISessionOpenOptions; notifySessionOpened?(resource: URI, widget: IChatWidget): void; } @@ -220,7 +221,8 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo source: this.options.source }); - const widget = await this.instantiationService.invokeFunction(openSession, element, e); + const options = this.options.overrideSessionOpenOptions?.(e) ?? e; + const widget = await this.instantiationService.invokeFunction(openSession, element, options); if (widget) { this.options.notifySessionOpened?.(element.resource, widget); } diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 7ad78b3c131..2036f2ffddc 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -408,6 +408,12 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { trackActiveEditorSession: () => { return !this._widget || this._widget.isEmpty(); // only track and reveal if chat widget is empty }, + overrideSessionOpenOptions: openEvent => { + if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked && !openEvent.sideBySide) { + return { ...openEvent, editorOptions: { ...openEvent.editorOptions, preserveFocus: false /* focus the chat widget when opening from stacked sessions viewer since this closes the stacked viewer */ } }; + } + return openEvent; + }, overrideCompare(sessionA: IAgentSession, sessionB: IAgentSession): number | undefined { // When limited where only few sessions show, sort unread sessions to the top From b345708efbc97c5fe4f3c0b23660a243e164af14 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Tue, 20 Jan 2026 16:35:03 +0100 Subject: [PATCH 267/387] chat.tools still show the old runSubagent tool (#289125) --- .../api/common/extHostLanguageModelTools.ts | 52 +++++++++---------- .../chat/common/tools/builtinTools/tools.ts | 1 + 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index 4d02ef1a57f..f59cbb25ec5 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -7,7 +7,6 @@ import type * as vscode from 'vscode'; import { raceCancellation } from '../../../base/common/async.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { CancellationError } from '../../../base/common/errors.js'; -import { Lazy } from '../../../base/common/lazy.js'; import { IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; import { revive } from '../../../base/common/marshalling.js'; import { generateUuid } from '../../../base/common/uuid.js'; @@ -25,29 +24,8 @@ import * as typeConvert from './extHostTypeConverters.js'; class Tool { private _data: IToolDataDto; - private _apiObject = new Lazy(() => { - const that = this; - return Object.freeze({ - get name() { return that._data.id; }, - get description() { return that._data.modelDescription; }, - get inputSchema() { return that._data.inputSchema; }, - get tags() { return that._data.tags ?? []; }, - get source() { return undefined; } - }); - }); - - private _apiObjectWithChatParticipantAdditions = new Lazy(() => { - const that = this; - const source = typeConvert.LanguageModelToolSource.to(that._data.source); - - return Object.freeze({ - get name() { return that._data.id; }, - get description() { return that._data.modelDescription; }, - get inputSchema() { return that._data.inputSchema; }, - get tags() { return that._data.tags ?? []; }, - get source() { return source; } - }); - }); + private _apiObject: vscode.LanguageModelToolInformation | undefined; + private _apiObjectWithChatParticipantAdditions: vscode.LanguageModelToolInformation | undefined; constructor(data: IToolDataDto) { this._data = data; @@ -55,6 +33,8 @@ class Tool { update(newData: IToolDataDto): void { this._data = newData; + this._apiObject = undefined; + this._apiObjectWithChatParticipantAdditions = undefined; } get data(): IToolDataDto { @@ -62,11 +42,29 @@ class Tool { } get apiObject(): vscode.LanguageModelToolInformation { - return this._apiObject.value; + if (!this._apiObject) { + this._apiObject = Object.freeze({ + name: this._data.id, + description: this._data.modelDescription, + inputSchema: this._data.inputSchema, + tags: this._data.tags ?? [], + source: undefined + }); + } + return this._apiObject; } get apiObjectWithChatParticipantAdditions() { - return this._apiObjectWithChatParticipantAdditions.value; + if (!this._apiObjectWithChatParticipantAdditions) { + this._apiObjectWithChatParticipantAdditions = Object.freeze({ + name: this._data.id, + description: this._data.modelDescription, + inputSchema: this._data.inputSchema, + tags: this._data.tags ?? [], + source: typeConvert.LanguageModelToolSource.to(this._data.source) + }); + } + return this._apiObjectWithChatParticipantAdditions; } } @@ -138,7 +136,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape $onDidChangeTools(tools: IToolDataDto[]): void { - const oldTools = new Set(this._registeredTools.keys()); + const oldTools = new Set(this._allTools.keys()); for (const tool of tools) { oldTools.delete(tool.id); diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts index 4e68b258c85..ad914109af1 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts @@ -40,6 +40,7 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo const registerRunSubagentTool = () => { runSubagentRegistration?.dispose(); toolSetRegistration?.dispose(); + toolsService.flushToolUpdates(); const runSubagentToolData = runSubagentTool.getToolData(); runSubagentRegistration = toolsService.registerTool(runSubagentToolData, runSubagentTool); toolSetRegistration = toolsService.agentToolSet.addTool(runSubagentToolData); From 1f607af990ad6fd7b7ed536e69dbd9410fa339b0 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 20 Jan 2026 17:02:13 +0100 Subject: [PATCH 268/387] enable manage models entry in the model picker for pro and biz users (#289136) --- .../contrib/chat/browser/widget/input/modelPickerActionItem.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts index a6ed9a671a4..8e5feb2fd94 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts @@ -96,6 +96,8 @@ function getModelPickerActionBarActionProvider(commandService: ICommandService, chatEntitlementService.entitlement === ChatEntitlement.Free || chatEntitlementService.entitlement === ChatEntitlement.Pro || chatEntitlementService.entitlement === ChatEntitlement.ProPlus || + chatEntitlementService.entitlement === ChatEntitlement.Business || + chatEntitlementService.entitlement === ChatEntitlement.Enterprise || chatEntitlementService.isInternal ) { additionalActions.push({ From fe035e1862e16a175de2dcc03ff7413ab0b247f1 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 20 Jan 2026 17:11:50 +0100 Subject: [PATCH 269/387] remove unused experimentan (#289061) remove unused experiment --- src/vs/workbench/contrib/chat/common/languageModels.ts | 5 ----- .../contrib/chat/test/common/languageModels.test.ts | 5 ----- 2 files changed, 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index f73289281e2..66a79b87c22 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -21,7 +21,6 @@ import { isString } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { localize } from '../../../../nls.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; @@ -430,7 +429,6 @@ export class LanguageModelsService implements ILanguageModelsService { @ILogService private readonly _logService: ILogService, @IStorageService private readonly _storageService: IStorageService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IConfigurationService private readonly _configurationService: IConfigurationService, @ILanguageModelsConfigurationService private readonly _languageModelsConfigurationService: ILanguageModelsConfigurationService, @IQuickInputService private readonly _quickInputService: IQuickInputService, @ISecretStorageService private readonly _secretStorageService: ISecretStorageService, @@ -567,9 +565,6 @@ export class LanguageModelsService implements ILanguageModelsService { lookupLanguageModel(modelIdentifier: string): ILanguageModelChatMetadata | undefined { const model = this._modelCache.get(modelIdentifier); - if (model && this._configurationService.getValue('chat.experimentalShowAllModels')) { - return { ...model, isUserSelectable: true }; - } if (model && this._modelPickerUserPreferences[modelIdentifier] !== undefined) { return { ...model, isUserSelectable: this._modelPickerUserPreferences[modelIdentifier] }; } diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts index d664d303b8b..a18dc6c8719 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts @@ -19,7 +19,6 @@ import { TestStorageService } from '../../../../test/common/workbenchTestService import { StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { Event } from '../../../../../base/common/event.js'; import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; -import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { ContextKeyExpression } from '../../../../../platform/contextkey/common/contextkey.js'; import { ILanguageModelsConfigurationService } from '../../common/languageModelsConfiguration.js'; import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; @@ -44,7 +43,6 @@ suite('LanguageModels', function () { new NullLogService(), new TestStorageService(), new MockContextKeyService(), - new TestConfigurationService(), new class extends mock() { override onDidChangeLanguageModelGroups = Event.None; override getLanguageModelsProviderGroups() { @@ -259,7 +257,6 @@ suite('LanguageModels - When Clause', function () { new NullLogService(), new TestStorageService(), contextKeyService, - new TestConfigurationService(), new class extends mock() { override onDidChangeLanguageModelGroups = Event.None; }, @@ -327,7 +324,6 @@ suite('LanguageModels - Model Picker Preferences Storage', function () { new NullLogService(), storageService, new MockContextKeyService(), - new TestConfigurationService(), new class extends mock() { override onDidChangeLanguageModelGroups = Event.None; override getLanguageModelsProviderGroups() { @@ -576,7 +572,6 @@ suite('LanguageModels - Model Change Events', function () { new NullLogService(), storageService, new MockContextKeyService(), - new TestConfigurationService(), new class extends mock() { override onDidChangeLanguageModelGroups = Event.None; override getLanguageModelsProviderGroups() { From 254fd048e0fdbdce51d5a6da7b19ce42654533ad Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Tue, 20 Jan 2026 17:24:16 +0100 Subject: [PATCH 270/387] make it possible to hide edit mode. --- .../contrib/chat/browser/actions/chatNewActions.ts | 6 +++++- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 6 ++++++ .../chat/browser/widget/input/modePickerActionItem.ts | 5 ++++- src/vs/workbench/contrib/chat/common/constants.ts | 1 + 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts index 648d7fd1329..cf389342c93 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts @@ -21,7 +21,7 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { IChatEditingSession } from '../../common/editing/chatEditingService.js'; import { IChatService } from '../../common/chatService/chatService.js'; import { localChatSessionType } from '../../common/chatSessionsService.js'; -import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; import { getChatSessionType, LocalChatSessionUri } from '../../common/model/chatUri.js'; import { ChatViewId, IChatWidgetService, isIChatViewViewContext } from '../chat.js'; import { EditingSessionAction, EditingSessionActionContext, getEditingSessionContext } from '../chatEditing/chatEditingActions.js'; @@ -30,6 +30,7 @@ import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; import { ACTION_ID_NEW_CHAT, ACTION_ID_NEW_EDIT_SESSION, CHAT_CATEGORY, handleCurrentEditingSession } from './chatActions.js'; import { clearChatEditor } from './chatClear.js'; import { AgentSessionProviders, AgentSessionsViewerOrientation } from '../agentSessions/agentSessions.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; export interface INewEditSessionActionContext { @@ -294,6 +295,7 @@ async function runNewChatAction( ) { const accessibilityService = accessor.get(IAccessibilityService); const viewsService = accessor.get(IViewsService); + const configurationService = accessor.get(IConfigurationService); const { editingSession, chatWidget: widget } = context ?? {}; if (!widget) { @@ -334,6 +336,8 @@ async function runNewChatAction( if (typeof executeCommandContext.agentMode === 'boolean') { widget.input.setChatMode(executeCommandContext.agentMode ? ChatModeKind.Agent : ChatModeKind.Edit); + } else if (widget.input.currentModeKind === ChatModeKind.Edit && configurationService.getValue(ChatConfiguration.EditModeHidden)) { + widget.input.setChatMode(ChatModeKind.Agent); } if (executeCommandContext.inputValue) { diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 7dfeb8d0044..7f2c4473ab1 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -567,6 +567,12 @@ configurationRegistry.registerConfiguration({ } } }, + [ChatConfiguration.EditModeHidden]: { + type: 'boolean', + description: nls.localize('chat.editMode.hidden', "When enabled, hides the Edit mode from the chat mode picker."), + default: false, + tags: ['experimental'], + }, [ChatConfiguration.EnableMath]: { type: 'boolean', description: nls.localize('chat.mathEnabled.description', "Enable math rendering in chat responses using KaTeX."), diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index 4faa99244a9..d31643faecd 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -108,7 +108,10 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { const modes = chatModeService.getModes(); const currentMode = delegate.currentMode.get(); const agentMode = modes.builtin.find(mode => mode.id === ChatMode.Agent.id); - const otherBuiltinModes = modes.builtin.filter(mode => mode.id !== ChatMode.Agent.id); + + const shouldHideEditMode = configurationService.getValue(ChatConfiguration.EditModeHidden) && chatAgentService.hasToolsAgent && currentMode.id !== ChatMode.Edit.id; + + const otherBuiltinModes = modes.builtin.filter(mode => mode.id !== ChatMode.Agent.id && !(shouldHideEditMode && mode.id === ChatMode.Edit.id)); const customModes = groupBy( modes.custom, mode => isModeConsideredBuiltIn(mode, this._productService) ? 'builtin' : 'custom'); diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 075e980921f..dce1f75f7a0 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -12,6 +12,7 @@ export enum ChatConfiguration { AgentEnabled = 'chat.agent.enabled', AgentStatusEnabled = 'chat.agentsControl.enabled', AgentSessionProjectionEnabled = 'chat.agentSessionProjection.enabled', + EditModeHidden = 'chat.editMode.hidden', Edits2Enabled = 'chat.edits2.enabled', ExtensionToolsEnabled = 'chat.extensionTools.enabled', RepoInfoEnabled = 'chat.repoInfo.enabled', From ac86b00065a26350a9e6e932e4f9ae0dd1d1fcbf Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 20 Jan 2026 17:25:55 +0100 Subject: [PATCH 271/387] Git - Do not provide original resource for hidden repositories (#289128) * Do not provide original resource for hidden repositories * Fix logging message --- extensions/git/src/repository.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 4b842c07844..fbd06340e57 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1127,10 +1127,11 @@ export class Repository implements Disposable { return undefined; } - // Since we are inspecting the resource groups - // we have to ensure that the repository state - // is up to date - // await this.status(); + // Ignore path that is inside a hidden repository + if (this.isHidden === true) { + this.logger.trace(`[Repository][provideOriginalResource] Repository is hidden: ${uri.toString()}`); + return undefined; + } // Ignore path that is inside a merge group if (this.mergeGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) { @@ -3293,6 +3294,12 @@ export class StagedResourceQuickDiffProvider implements QuickDiffProvider { return undefined; } + // Ignore path that is inside a hidden repository + if (this._repository.isHidden === true) { + this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Repository is hidden: ${uri.toString()}`); + return undefined; + } + // Ignore symbolic links const stat = await workspace.fs.stat(uri); if ((stat.type & FileType.SymbolicLink) !== 0) { @@ -3300,11 +3307,6 @@ export class StagedResourceQuickDiffProvider implements QuickDiffProvider { return undefined; } - // Since we are inspecting the resource groups - // we have to ensure that the repository state - // is up to date - // await this._repository.status(); - // Ignore resources that are not in the index group if (!this._repository.indexGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) { this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource is not part of a index group: ${uri.toString()}`); From cb9e108a7d08b477431adf1f00093fd76b08e396 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 20 Jan 2026 17:33:17 +0100 Subject: [PATCH 272/387] fix #283701 (#289134) * fix #283701 * feedback --- .../chatManagement/chatModelsWidget.ts | 3 +- .../contrib/chat/common/languageModels.ts | 86 ++++++-- .../chatModelsViewModel.test.ts | 7 + .../chat/test/common/languageModels.test.ts | 188 +++++++++++++----- .../chat/test/common/languageModels.ts | 4 + 5 files changed, 222 insertions(+), 66 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts index 1dc5e58c8eb..23d1cd4a26c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts @@ -993,6 +993,7 @@ export class ChatModelsWidget extends Disposable { this.createTable(); this._register(this.viewModel.onDidChangeGrouping(() => this.createTable())); this._register(this.chatEntitlementService.onDidChangeEntitlement(() => this.updateAddModelsButton())); + this._register(this.languageModelsService.onDidChangeLanguageModelVendors(() => this.updateAddModelsButton())); } private createTable(): void { @@ -1168,7 +1169,6 @@ export class ChatModelsWidget extends Disposable { this.table.setFocus([selectedEntryIndex]); this.table.setSelection([selectedEntryIndex]); } - this.updateAddModelsButton(); })); this.tableDisposables.add(this.table.onDidOpen(async ({ element, browserEvent }) => { @@ -1205,6 +1205,7 @@ export class ChatModelsWidget extends Disposable { && entitlement !== ChatEntitlement.Available && entitlement !== ChatEntitlement.Business && entitlement !== ChatEntitlement.Enterprise); + this.addButton.enabled = supportsAddingModels && configurableVendors.length > 0; this.dropdownActions = configurableVendors.map(vendor => toAction({ diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 66a79b87c22..d5abc6281ef 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -285,7 +285,7 @@ export interface ILanguageModelsService { readonly _serviceBrand: undefined; - // TODO @lramos15 - Make this a richer event in the future. Right now it just indicates some change happened, but not what + readonly onDidChangeLanguageModelVendors: Event; readonly onDidChangeLanguageModels: Event; updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void; @@ -306,6 +306,8 @@ export interface ILanguageModelsService { registerLanguageModelProvider(vendor: string, provider: ILanguageModelChatProvider): IDisposable; + deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any sendChatRequest(modelId: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise; @@ -415,6 +417,9 @@ export class LanguageModelsService implements ILanguageModelsService { private readonly _providers = new Map(); private readonly _vendors = new Map(); + private readonly _onDidChangeLanguageModelVendors = this._store.add(new Emitter()); + readonly onDidChangeLanguageModelVendors = this._onDidChangeLanguageModelVendors.event; + private readonly _modelsGroups = new Map(); private readonly _modelCache = new Map(); private readonly _resolveLMSequencer = new SequencerByKey(); @@ -440,11 +445,11 @@ export class LanguageModelsService implements ILanguageModelsService { this._store.add(this.onDidChangeLanguageModels(() => this._hasUserSelectableModels.set(this._modelCache.size > 0 && Array.from(this._modelCache.values()).some(model => model.isUserSelectable)))); this._store.add(this._languageModelsConfigurationService.onDidChangeLanguageModelGroups(changedGroups => this._onDidChangeLanguageModelGroups(changedGroups))); - this._store.add(languageModelChatProviderExtensionPoint.setHandler((extensions) => { + this._store.add(languageModelChatProviderExtensionPoint.setHandler((extensions, { added, removed }) => { + const addedVendors: IUserFriendlyLanguageModel[] = []; + const removedVendors: IUserFriendlyLanguageModel[] = []; - this._vendors.clear(); - - for (const extension of extensions) { + for (const extension of added) { for (const item of Iterable.wrap(extension.value)) { if (this._vendors.has(item.vendor)) { extension.collector.error(localize('vscode.extension.contributes.languageModels.vendorAlreadyRegistered', "The vendor '{0}' is already registered and cannot be registered twice", item.vendor)); @@ -458,21 +463,76 @@ export class LanguageModelsService implements ILanguageModelsService { extension.collector.error(localize('vscode.extension.contributes.languageModels.whitespaceVendor', "The vendor field cannot start or end with whitespace.")); continue; } - this._vendors.set(item.vendor, item); - // Have some models we want from this vendor, so activate the extension - if (this._hasStoredModelForVendor(item.vendor)) { - this._extensionService.activateByEvent(`onLanguageModelChatProvider:${item.vendor}`); - } + addedVendors.push(item); } } - for (const [vendor, _] of this._providers) { - if (!this._vendors.has(vendor)) { - this._providers.delete(vendor); + + for (const extension of removed) { + for (const item of Iterable.wrap(extension.value)) { + removedVendors.push(item); } } + + this.deltaLanguageModelChatProviderDescriptors(addedVendors, removedVendors); })); } + deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void { + const addedVendorIds: string[] = []; + const removedVendorIds: string[] = []; + + for (const item of added) { + if (this._vendors.has(item.vendor)) { + this._logService.error(`The vendor '${item.vendor}' is already registered and cannot be registered twice`); + continue; + } + if (isFalsyOrWhitespace(item.vendor)) { + this._logService.error('The vendor field cannot be empty.'); + continue; + } + if (item.vendor.trim() !== item.vendor) { + this._logService.error('The vendor field cannot start or end with whitespace.'); + continue; + } + // Cast to IUserFriendlyLanguageModel - fill in optional properties with undefined + const vendor: IUserFriendlyLanguageModel = { + vendor: item.vendor, + displayName: item.displayName, + configuration: item.configuration, + managementCommand: item.managementCommand, + when: item.when + }; + this._vendors.set(item.vendor, vendor); + addedVendorIds.push(item.vendor); + // Have some models we want from this vendor, so activate the extension + if (this._hasStoredModelForVendor(item.vendor)) { + this._extensionService.activateByEvent(`onLanguageModelChatProvider:${item.vendor}`); + } + } + + for (const item of removed) { + this._vendors.delete(item.vendor); + this._providers.delete(item.vendor); + this._clearModelCache(item.vendor); + removedVendorIds.push(item.vendor); + } + + for (const [vendor, _] of this._providers) { + if (!this._vendors.has(vendor)) { + this._providers.delete(vendor); + } + } + + if (addedVendorIds.length > 0 || removedVendorIds.length > 0) { + this._onDidChangeLanguageModelVendors.fire([...addedVendorIds, ...removedVendorIds]); + if (removedVendorIds.length > 0) { + for (const vendor of removedVendorIds) { + this._onLanguageModelChange.fire(vendor); + } + } + } + } + private async _onDidChangeLanguageModelGroups(changedGroups: readonly ILanguageModelsProviderGroup[]): Promise { const changedVendors = new Set(changedGroups.map(g => g.vendor)); await Promise.all(Array.from(changedVendors).map(vendor => this._resolveAllLanguageModels(vendor, true))); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts index 0e3ebc1a642..f30faad4f26 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts @@ -25,6 +25,9 @@ class MockLanguageModelsService implements ILanguageModelsService { private readonly _onDidChangeLanguageModels = new Emitter(); readonly onDidChangeLanguageModels = this._onDidChangeLanguageModels.event; + private readonly _onDidChangeLanguageModelVendors = new Emitter(); + readonly onDidChangeLanguageModelVendors = this._onDidChangeLanguageModelVendors.event; + addVendor(vendor: IUserFriendlyLanguageModel): void { this.vendors.push(vendor); this.modelsByVendor.set(vendor.vendor, []); @@ -56,6 +59,10 @@ class MockLanguageModelsService implements ILanguageModelsService { throw new Error('Method not implemented.'); } + deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void { + throw new Error('Method not implemented.'); + } + updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void { const metadata = this.models.get(modelIdentifier); if (metadata) { diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts index a18dc6c8719..760563d637f 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts @@ -10,9 +10,8 @@ import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { mock } from '../../../../../base/test/common/mock.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { NullLogService } from '../../../../../platform/log/common/log.js'; -import { ChatMessageRole, languageModelChatProviderExtensionPoint, LanguageModelsService, IChatMessage, IChatResponsePart, ILanguageModelChatMetadata } from '../../common/languageModels.js'; +import { ChatMessageRole, LanguageModelsService, IChatMessage, IChatResponsePart, ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; -import { ExtensionsRegistry } from '../../../../services/extensions/common/extensionsRegistry.js'; import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../common/widget/input/modelPickerWidget.js'; import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { TestStorageService } from '../../../../test/common/workbenchTestServices.js'; @@ -53,17 +52,10 @@ suite('LanguageModels', function () { new TestSecretStorageService(), ); - const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!; - - ext.acceptUsers([{ - description: { ...nullExtensionDescription }, - value: { vendor: 'test-vendor' }, - collector: null! - }, { - description: { ...nullExtensionDescription }, - value: { vendor: 'actual-vendor' }, - collector: null! - }]); + languageModels.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'test-vendor', displayName: 'Test Vendor', configuration: undefined, managementCommand: undefined, when: undefined }, + { vendor: 'actual-vendor', displayName: 'Actual Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); store.add(languageModels.registerLanguageModelProvider('test-vendor', { onDidChange: Event.None, @@ -189,12 +181,9 @@ suite('LanguageModels', function () { })); // Register the extension point for the actual vendor - const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!; - ext.acceptUsers([{ - description: { ...nullExtensionDescription }, - value: { vendor: 'actual-vendor' }, - collector: null! - }]); + languageModels.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'actual-vendor', displayName: 'Actual Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); const models = await languageModels.selectLanguageModels({ id: 'actual-lm' }); assert.ok(models.length === 1); @@ -264,21 +253,11 @@ suite('LanguageModels - When Clause', function () { new TestSecretStorageService(), ); - const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!; - - ext.acceptUsers([{ - description: { ...nullExtensionDescription }, - value: { vendor: 'visible-vendor', displayName: 'Visible Vendor' }, - collector: null! - }, { - description: { ...nullExtensionDescription }, - value: { vendor: 'conditional-vendor', displayName: 'Conditional Vendor', when: 'testKey' }, - collector: null! - }, { - description: { ...nullExtensionDescription }, - value: { vendor: 'hidden-vendor', displayName: 'Hidden Vendor', when: 'falseKey' }, - collector: null! - }]); + languageModelsWithWhen.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'visible-vendor', displayName: 'Visible Vendor', configuration: undefined, managementCommand: undefined, when: undefined }, + { vendor: 'conditional-vendor', displayName: 'Conditional Vendor', configuration: undefined, managementCommand: undefined, when: 'testKey' }, + { vendor: 'hidden-vendor', displayName: 'Hidden Vendor', configuration: undefined, managementCommand: undefined, when: 'falseKey' } + ], []); }); teardown(function () { @@ -304,6 +283,7 @@ suite('LanguageModels - When Clause', function () { const vendors = languageModelsWithWhen.getVendors(); assert.ok(!vendors.some(v => v.vendor === 'hidden-vendor'), 'hidden-vendor should be hidden when falseKey is false'); }); + }); suite('LanguageModels - Model Picker Preferences Storage', function () { @@ -335,12 +315,9 @@ suite('LanguageModels - Model Picker Preferences Storage', function () { ); // Register vendor1 used in most tests - const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!; - ext.acceptUsers([{ - description: { ...nullExtensionDescription }, - value: { vendor: 'vendor1' }, - collector: null! - }]); + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'vendor1', displayName: 'Vendor 1', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); disposables.add(languageModelsService.registerLanguageModelProvider('vendor1', { onDidChange: Event.None, @@ -446,12 +423,9 @@ suite('LanguageModels - Model Picker Preferences Storage', function () { test('only fires onChange event for affected vendors', async function () { // Register vendor2 - const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!; - ext.acceptUsers([{ - description: { ...nullExtensionDescription }, - value: { vendor: 'vendor2' }, - collector: null! - }]); + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'vendor2', displayName: 'Vendor 2', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); disposables.add(languageModelsService.registerLanguageModelProvider('vendor2', { onDidChange: Event.None, @@ -556,12 +530,6 @@ suite('LanguageModels - Model Change Events', function () { setup(async function () { storageService = new TestStorageService(); - const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!; - ext.acceptUsers([{ - description: { ...nullExtensionDescription }, - value: { vendor: 'test-vendor' }, - collector: null! - }]); languageModelsService = new LanguageModelsService( new class extends mock() { @@ -581,6 +549,11 @@ suite('LanguageModels - Model Change Events', function () { new class extends mock() { }, new TestSecretStorageService(), ); + + // Register the vendor first + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'test-vendor', displayName: 'Test Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); }); teardown(function () { @@ -898,3 +871,114 @@ suite('LanguageModels - Model Change Events', function () { assert.strictEqual(eventFired, true, 'Should fire event when models change even without provider change event'); }); }); + +suite('LanguageModels - Vendor Change Events', function () { + + let languageModelsService: LanguageModelsService; + const disposables = new DisposableStore(); + + setup(function () { + languageModelsService = new LanguageModelsService( + new class extends mock() { + override activateByEvent(name: string) { + return Promise.resolve(); + } + }, + new NullLogService(), + new TestStorageService(), + new MockContextKeyService(), + new TestConfigurationService(), + new class extends mock() { + override onDidChangeLanguageModelGroups = Event.None; + override getLanguageModelsProviderGroups() { + return []; + } + }, + new class extends mock() { }, + new TestSecretStorageService(), + ); + }); + + teardown(function () { + languageModelsService.dispose(); + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('fires onDidChangeLanguageModelVendors when a vendor is added', async function () { + const eventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModelVendors(vendors => resolve(vendors))); + }); + + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'added-vendor', displayName: 'Added Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); + + const vendors = await eventPromise; + assert.ok(vendors.includes('added-vendor')); + }); + + test('fires onDidChangeLanguageModelVendors when a vendor is removed', async function () { + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'removed-vendor', displayName: 'Removed Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); + + const eventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModelVendors(vendors => resolve(vendors))); + }); + + languageModelsService.deltaLanguageModelChatProviderDescriptors([], [ + { vendor: 'removed-vendor', displayName: 'Removed Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ]); + + const vendors = await eventPromise; + assert.ok(vendors.includes('removed-vendor')); + }); + + test('fires onDidChangeLanguageModelVendors when multiple vendors are added and removed', async function () { + // Add multiple vendors + const addEventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModelVendors(vendors => resolve(vendors))); + }); + + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'vendor-a', displayName: 'Vendor A', configuration: undefined, managementCommand: undefined, when: undefined }, + { vendor: 'vendor-b', displayName: 'Vendor B', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); + + const addedVendors = await addEventPromise; + assert.ok(addedVendors.includes('vendor-a')); + assert.ok(addedVendors.includes('vendor-b')); + + // Remove one vendor + const removeEventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModelVendors(vendors => resolve(vendors))); + }); + + languageModelsService.deltaLanguageModelChatProviderDescriptors([], [ + { vendor: 'vendor-a', displayName: 'Vendor A', configuration: undefined, managementCommand: undefined, when: undefined } + ]); + + const removedVendors = await removeEventPromise; + assert.ok(removedVendors.includes('vendor-a')); + }); + + test('does not fire onDidChangeLanguageModelVendors when no vendors are added or removed', async function () { + // Add initial vendor + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'stable-vendor', displayName: 'Stable Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); + + // Listen for change event + let eventFired = false; + disposables.add(languageModelsService.onDidChangeLanguageModelVendors(() => { + eventFired = true; + })); + + // Call with empty arrays - should not fire event + languageModelsService.deltaLanguageModelChatProviderDescriptors([], []); + + assert.strictEqual(eventFired, false, 'Should not fire event when vendor list is unchanged'); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.ts index fd336e7fb37..57c83ec7131 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.ts @@ -18,7 +18,11 @@ export class NullLanguageModelsService implements ILanguageModelsService { return Disposable.None; } + deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void { + } + onDidChangeLanguageModels = Event.None; + onDidChangeLanguageModelVendors = Event.None; updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void { return; From 6d297b78525a7d5ffbc8b5ed37e6840a3a979dc7 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 20 Jan 2026 17:37:04 +0100 Subject: [PATCH 273/387] fix: use right configuration file (#289149) --- .../chat/browser/languageModelsConfigurationService.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts b/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts index cde93d3d618..3424791911d 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts @@ -282,13 +282,11 @@ export class ChatLanguageModelsDataContribution extends Disposable implements IW constructor( @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, - @IUserDataProfileService userDataProfileService: IUserDataProfileService, - @IUriIdentityService uriIdentityService: IUriIdentityService, + @ILanguageModelsConfigurationService languageModelsConfigurationService: ILanguageModelsConfigurationService, ) { super(); - const modelsConfigurationFile = uriIdentityService.extUri.joinPath(userDataProfileService.currentProfile.location, 'models.json'); const registry = Registry.as(JSONExtensions.JSONContribution); - this._register(registry.registerSchemaAssociation(languageModelsSchemaId, modelsConfigurationFile.toString())); + this._register(registry.registerSchemaAssociation(languageModelsSchemaId, languageModelsConfigurationService.configurationFile.toString())); this.updateSchema(registry); this._register(this.languageModelsService.onDidChangeLanguageModels(() => this.updateSchema(registry))); From 519d4877a3c42445703f7f20c86e9385eb062b56 Mon Sep 17 00:00:00 2001 From: minkyung Date: Wed, 21 Jan 2026 01:41:29 +0900 Subject: [PATCH 274/387] fix: Screencast Mode - keyboard overlay timeout (#238860) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: keyboard overlay timeout in screencast mode Co-authored-by: João Moreno --- src/vs/workbench/browser/actions/developerActions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/browser/actions/developerActions.ts b/src/vs/workbench/browser/actions/developerActions.ts index 1fda3db0826..14f5cb830f7 100644 --- a/src/vs/workbench/browser/actions/developerActions.ts +++ b/src/vs/workbench/browser/actions/developerActions.ts @@ -297,7 +297,7 @@ class ToggleScreencastModeAction extends Action2 { keyboardMarker.innerText = ''; append(keyboardMarker, $('span.key', {}, `Backspace`)); } - clearKeyboardScheduler.schedule(); + clearKeyboardScheduler.schedule(keyboardMarkerTimeout); })); disposables.add(onCompositionEnd.event(e => { @@ -315,7 +315,7 @@ class ToggleScreencastModeAction extends Action2 { } else { imeBackSpace = true; } - clearKeyboardScheduler.schedule(); + clearKeyboardScheduler.schedule(keyboardMarkerTimeout); return; } @@ -381,7 +381,7 @@ class ToggleScreencastModeAction extends Action2 { } length++; - clearKeyboardScheduler.schedule(); + clearKeyboardScheduler.schedule(keyboardMarkerTimeout); })); ToggleScreencastModeAction.disposable = disposables; From 1e99235c90d64499cf85a3394da6208b7ffb81a4 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Tue, 20 Jan 2026 17:42:22 +0100 Subject: [PATCH 275/387] add filterFontDecoration to decorationProvider (#289146) --- ...colorizedBracketPairsDecorationProvider.ts | 7 ++-- .../editor/common/model/decorationProvider.ts | 4 +- src/vs/editor/common/model/textModel.ts | 8 ++-- .../tokenizationFontDecorationsProvider.ts | 37 ++++++++++--------- 4 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/colorizedBracketPairsDecorationProvider.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/colorizedBracketPairsDecorationProvider.ts index 2b431cb64bd..66e78d57cb0 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/colorizedBracketPairsDecorationProvider.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/colorizedBracketPairsDecorationProvider.ts @@ -42,7 +42,7 @@ export class ColorizedBracketPairsDecorationProvider extends Disposable implemen //#endregion - getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[] { + getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean, filterFontDecorations?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[] { if (onlyMinimapDecorations) { // Bracket pair colorization decorations are not rendered in the minimap return []; @@ -70,7 +70,7 @@ export class ColorizedBracketPairsDecorationProvider extends Disposable implemen return result; } - getAllDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[] { + getAllDecorations(ownerId?: number, filterOutValidation?: boolean, filterFontDecorations?: boolean): IModelDecoration[] { if (ownerId === undefined) { return []; } @@ -80,7 +80,8 @@ export class ColorizedBracketPairsDecorationProvider extends Disposable implemen return this.getDecorationsInRange( new Range(1, 1, this.textModel.getLineCount(), 1), ownerId, - filterOutValidation + filterOutValidation, + filterFontDecorations ); } } diff --git a/src/vs/editor/common/model/decorationProvider.ts b/src/vs/editor/common/model/decorationProvider.ts index e8154b7277b..e28ef209de8 100644 --- a/src/vs/editor/common/model/decorationProvider.ts +++ b/src/vs/editor/common/model/decorationProvider.ts @@ -15,14 +15,14 @@ export interface DecorationProvider { * @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors). * @return An array with the decorations */ - getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean): IModelDecoration[]; + getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean, filterFontDecorations?: boolean): IModelDecoration[]; /** * Gets all the decorations as an array. * @param ownerId If set, it will ignore decorations belonging to other owners. * @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors). */ - getAllDecorations(ownerId?: number, filterOutValidation?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[]; + getAllDecorations(ownerId?: number, filterOutValidation?: boolean, filterFontDecorations?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[]; } diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 1953c170203..f38bd4218d3 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1819,8 +1819,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const range = new Range(startLineNumber, 1, endLineNumber, endColumn); const decorations = this._getDecorationsInRange(range, ownerId, filterOutValidation, filterFontDecorations, onlyMarginDecorations); - pushMany(decorations, this._decorationProvider.getDecorationsInRange(range, ownerId, filterOutValidation)); - pushMany(decorations, this._fontTokenDecorationsProvider.getDecorationsInRange(range, ownerId, filterOutValidation)); + pushMany(decorations, this._decorationProvider.getDecorationsInRange(range, ownerId, filterOutValidation, filterFontDecorations)); + pushMany(decorations, this._fontTokenDecorationsProvider.getDecorationsInRange(range, ownerId, filterOutValidation, filterFontDecorations)); return decorations; } @@ -1828,8 +1828,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const validatedRange = this.validateRange(range); const decorations = this._getDecorationsInRange(validatedRange, ownerId, filterOutValidation, filterFontDecorations, onlyMarginDecorations); - pushMany(decorations, this._decorationProvider.getDecorationsInRange(validatedRange, ownerId, filterOutValidation, onlyMinimapDecorations)); - pushMany(decorations, this._fontTokenDecorationsProvider.getDecorationsInRange(validatedRange, ownerId, filterOutValidation, onlyMinimapDecorations)); + pushMany(decorations, this._decorationProvider.getDecorationsInRange(validatedRange, ownerId, filterOutValidation, filterFontDecorations, onlyMinimapDecorations)); + pushMany(decorations, this._fontTokenDecorationsProvider.getDecorationsInRange(validatedRange, ownerId, filterOutValidation, filterFontDecorations, onlyMinimapDecorations)); return decorations; } diff --git a/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts b/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts index 0481e1507fc..ef7e2d3bfb4 100644 --- a/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts +++ b/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts @@ -118,31 +118,34 @@ export class TokenizationFontDecorationProvider extends Disposable implements De this._onDidChangeFont.fire(affectedLineFonts); } - public getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[] { + public getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean, filterFontDecorations?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[] { const startOffsetOfRange = this.textModel.getOffsetAt(range.getStartPosition()); const endOffsetOfRange = this.textModel.getOffsetAt(range.getEndPosition()); const annotations = this._fontAnnotatedString.getAnnotationsIntersecting(new OffsetRange(startOffsetOfRange, endOffsetOfRange)); const decorations: IModelDecoration[] = []; for (const annotation of annotations) { - const annotationStartPosition = this.textModel.getPositionAt(annotation.range.start); - const annotationEndPosition = this.textModel.getPositionAt(annotation.range.endExclusive); - const range = Range.fromPositions(annotationStartPosition, annotationEndPosition); const anno = annotation.annotation; - const className = classNameForFontTokenDecorations(anno.fontToken.fontFamily ?? '', anno.fontToken.fontSizeMultiplier ?? 0); const affectsFont = !!(anno.fontToken.fontFamily || anno.fontToken.fontSizeMultiplier); - const id = anno.decorationId; - decorations.push({ - id: id, - options: { - description: 'FontOptionDecoration', - inlineClassName: className, - lineHeight: anno.fontToken.lineHeightMultiplier, - affectsFont - }, - ownerId: 0, - range - }); + if (!(affectsFont && filterFontDecorations)) { + const annotationStartPosition = this.textModel.getPositionAt(annotation.range.start); + const annotationEndPosition = this.textModel.getPositionAt(annotation.range.endExclusive); + const range = Range.fromPositions(annotationStartPosition, annotationEndPosition); + const anno = annotation.annotation; + const className = classNameForFontTokenDecorations(anno.fontToken.fontFamily ?? '', anno.fontToken.fontSizeMultiplier ?? 0); + const id = anno.decorationId; + decorations.push({ + id: id, + options: { + description: 'FontOptionDecoration', + inlineClassName: className, + lineHeight: anno.fontToken.lineHeightMultiplier, + affectsFont + }, + ownerId: 0, + range + }); + } } return decorations; } From c302ab7f27d91fcd52e557fb4e857c03931d23b4 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 21 Jan 2026 03:43:12 +1100 Subject: [PATCH 276/387] Hide the chat context widget for non-local sessions (background, cloud and other session providers) (#289131) --- .../contrib/chat/browser/widget/input/chatContextUsageWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts index 15fe5e4c7a6..6af972fcf3a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts @@ -123,7 +123,7 @@ export class ChatContextUsageWidget extends Disposable { this._currentModel = model; this._modelListener.clear(); - if (model) { + if (model && !model.contributedChatSession) { this._modelListener.value = model.onDidChange(() => { this._updateScheduler.schedule(); }); From 0c90923342ea128685b20bbc996dad62b3af97c6 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 20 Jan 2026 18:10:52 +0100 Subject: [PATCH 277/387] fix compliation (#289153) --- src/vs/workbench/contrib/chat/test/common/languageModels.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts index 760563d637f..48e2b40d1f1 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts @@ -887,7 +887,6 @@ suite('LanguageModels - Vendor Change Events', function () { new NullLogService(), new TestStorageService(), new MockContextKeyService(), - new TestConfigurationService(), new class extends mock() { override onDidChangeLanguageModelGroups = Event.None; override getLanguageModelsProviderGroups() { From 1bda099568ea292762db54341730c5417234d676 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 20 Jan 2026 10:53:49 -0800 Subject: [PATCH 278/387] More sessionId -> sessionResource adoption For #274403 --- .../browser/tools/languageModelToolsService.ts | 7 ++++--- .../chatContentParts/chatProgressContentPart.ts | 3 ++- .../contrib/chat/common/model/chatViewModel.ts | 14 -------------- .../chat/common/tools/languageModelToolsService.ts | 2 +- .../electron-browser/builtInTools/fetchPageTool.ts | 5 ++--- .../common/tools/mockLanguageModelToolsService.ts | 3 ++- 6 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index a37156c6a62..0dccf919d02 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -43,6 +43,7 @@ import { ChatConfiguration } from '../../common/constants.js'; import { ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.js'; import { CountTokensCallback, createToolSchemaUri, IBeginToolCallOptions, ILanguageModelToolsService, IPreparedToolInvocation, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, SpecedToolAliases, stringifyPromptTsxPart, ToolDataSource, ToolSet, VSCodeToolReference } from '../../common/tools/languageModelToolsService.js'; import { getToolConfirmationAlert } from '../accessibility/chatAccessibilityProvider.js'; +import { URI } from '../../../../../base/common/uri.js'; const jsonSchemaRegistry = Registry.as(JSONContributionRegistry.Extensions.JSONContribution); @@ -83,7 +84,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo private readonly _onDidChangeTools = this._register(new Emitter()); readonly onDidChangeTools = this._onDidChangeTools.event; - private readonly _onDidPrepareToolCallBecomeUnresponsive = this._register(new Emitter<{ sessionId: string; toolData: IToolData }>()); + private readonly _onDidPrepareToolCallBecomeUnresponsive = this._register(new Emitter<{ sessionResource: URI; toolData: IToolData }>()); readonly onDidPrepareToolCallBecomeUnresponsive = this._onDidPrepareToolCallBecomeUnresponsive.event; /** Throttle tools updates because it sends all tools and runs on context key updates */ @@ -542,9 +543,9 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo timeout(3000, token).then(() => 'timeout'), preparePromise ]); - if (raceResult === 'timeout') { + if (raceResult === 'timeout' && dto.context) { this._onDidPrepareToolCallBecomeUnresponsive.fire({ - sessionId: dto.context?.sessionId ?? '', + sessionResource: dto.context.sessionResource, toolData: tool.data }); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.ts index e9436e9ad65..f13918be0b9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.ts @@ -25,6 +25,7 @@ import { AccessibilityWorkbenchSettingId } from '../../../../accessibility/brows import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { HoverStyle } from '../../../../../../base/browser/ui/hover/hover.js'; import { ILanguageModelToolsService } from '../../../common/tools/languageModelToolsService.js'; +import { isEqual } from '../../../../../../base/common/resources.js'; export class ChatProgressContentPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; @@ -165,7 +166,7 @@ export class ChatWorkingProgressContentPart extends ChatProgressContentPart impl }; super(progressMessage, chatContentMarkdownRenderer, context, undefined, undefined, undefined, undefined, instantiationService, chatMarkdownAnchorService, configurationService); this._register(languageModelToolsService.onDidPrepareToolCallBecomeUnresponsive(e => { - if (context.element.sessionId === e.sessionId) { + if (isEqual(context.element.sessionResource, e.sessionResource)) { this.updateMessage(new MarkdownString(localize('toolCallUnresponsive', "Waiting for tool '{0}' to respond...", e.toolData.displayName))); } })); diff --git a/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts index d434ea202e8..d36593cb3bf 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts @@ -75,8 +75,6 @@ export interface IChatViewModel { export interface IChatRequestViewModel { readonly id: string; - /** @deprecated */ - readonly sessionId: string; readonly sessionResource: URI; /** This ID updates every time the underlying data changes */ readonly dataId: string; @@ -187,8 +185,6 @@ export interface IChatResponseViewModel { readonly model: IChatResponseModel; readonly id: string; readonly session: IChatViewModel; - /** @deprecated */ - readonly sessionId: string; readonly sessionResource: URI; /** This ID updates every time the underlying data changes */ readonly dataId: string; @@ -366,11 +362,6 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return `${this.id}_${this._model.version + (this._model.response?.isComplete ? 1 : 0)}`; } - /** @deprecated */ - get sessionId() { - return this._model.session.sessionId; - } - get sessionResource() { return this._model.session.sessionResource; } @@ -462,11 +453,6 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi (this.isLast ? '_last' : ''); } - /** @deprecated */ - get sessionId() { - return this._model.session.sessionId; - } - get sessionResource(): URI { return this._model.session.sessionResource; } diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts index 7d7e0c86fd9..4fba3afc859 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts @@ -390,7 +390,7 @@ export interface ILanguageModelToolsService { readonly readToolSet: ToolSet; readonly agentToolSet: ToolSet; readonly onDidChangeTools: Event; - readonly onDidPrepareToolCallBecomeUnresponsive: Event<{ readonly sessionId: string; readonly toolData: IToolData }>; + readonly onDidPrepareToolCallBecomeUnresponsive: Event<{ readonly sessionResource: URI; readonly toolData: IToolData }>; registerToolData(toolData: IToolData): IDisposable; registerToolImplementation(id: string, tool: IToolImpl): IDisposable; registerTool(toolData: IToolData, tool: IToolImpl): IDisposable; diff --git a/src/vs/workbench/contrib/chat/electron-browser/builtInTools/fetchPageTool.ts b/src/vs/workbench/contrib/chat/electron-browser/builtInTools/fetchPageTool.ts index 43406a4e929..d59f63292d7 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/builtInTools/fetchPageTool.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/builtInTools/fetchPageTool.ts @@ -16,7 +16,6 @@ import { IWebContentExtractorService, WebContentExtractResult } from '../../../. import { detectEncodingFromBuffer } from '../../../../services/textfile/common/encoding.js'; import { ITrustedDomainService } from '../../../url/browser/trustedDomainService.js'; import { IChatService } from '../../common/chatService/chatService.js'; -import { LocalChatSessionUri } from '../../common/model/chatUri.js'; import { ChatImageMimeType } from '../../common/languageModels.js'; import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, IToolResultDataPart, IToolResultTextPart, ToolDataSource, ToolProgress } from '../../common/tools/languageModelToolsService.js'; import { InternalFetchWebPageToolId } from '../../common/tools/builtinTools/tools.js'; @@ -219,8 +218,8 @@ export class FetchWebPageTool implements IToolImpl { } let confirmationNotNeededReason: string | undefined; - if (context.chatSessionId) { - const model = this._chatService.getSession(LocalChatSessionUri.forSession(context.chatSessionId)); + if (context.chatSessionResource) { + const model = this._chatService.getSession(context.chatSessionResource); const userMessages = model?.getRequests().map(r => r.message.text.toLowerCase()); let urlsMentionedInPrompt = false; for (const uri of urlsNeedingConfirmation) { diff --git a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts index 7ee177ea731..73df30cb679 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts @@ -14,6 +14,7 @@ import { IVariableReference } from '../../../common/chatModes.js'; import { IChatToolInvocation } from '../../../common/chatService/chatService.js'; import { ChatRequestToolReferenceEntry } from '../../../common/attachments/chatVariableEntries.js'; import { CountTokensCallback, IBeginToolCallOptions, ILanguageModelToolsService, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolDataSource, ToolSet } from '../../../common/tools/languageModelToolsService.js'; +import { URI } from '../../../../../../base/common/uri.js'; export class MockLanguageModelToolsService implements ILanguageModelToolsService { _serviceBrand: undefined; @@ -25,7 +26,7 @@ export class MockLanguageModelToolsService implements ILanguageModelToolsService constructor() { } readonly onDidChangeTools: Event = Event.None; - readonly onDidPrepareToolCallBecomeUnresponsive: Event<{ sessionId: string; toolData: IToolData }> = Event.None; + readonly onDidPrepareToolCallBecomeUnresponsive: Event<{ sessionResource: URI; toolData: IToolData }> = Event.None; registerToolData(toolData: IToolData): IDisposable { return Disposable.None; From 653e4d8064de49d22c98efe745c3d0f747f0c353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Ruales?= <1588988+jruales@users.noreply.github.com> Date: Tue, 20 Jan 2026 11:01:11 -0800 Subject: [PATCH 279/387] Integrated Browser: Fix buggy Right Click -> Copy into New Window (#288207) --- .../electron-browser/browserEditor.ts | 31 ++++++++++++++----- .../electron-browser/browserEditorInput.ts | 18 ++++++++++- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts index ccececc4b40..3604b8b75eb 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts @@ -5,7 +5,7 @@ import './media/browser.css'; import { localize } from '../../../../nls.js'; -import { $, addDisposableListener, disposableWindowInterval, EventType, isHTMLElement, registerExternalFocusChecker, scheduleAtNextAnimationFrame } from '../../../../base/browser/dom.js'; +import { $, addDisposableListener, Dimension, disposableWindowInterval, EventType, IDomPosition, isHTMLElement, registerExternalFocusChecker } from '../../../../base/browser/dom.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { RawContextKey, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -29,7 +29,7 @@ import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js import { BrowserOverlayManager } from './overlayManager.js'; import { getZoomFactor, onDidChangeZoomLevel } from '../../../../base/browser/browser.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { WorkbenchHoverDelegate } from '../../../../platform/hover/browser/hover.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; @@ -250,6 +250,11 @@ export class BrowserEditor extends EditorPane { // Register external focus checker so that cross-window focus logic knows when // this browser view has focus (since it's outside the normal DOM tree). this._register(registerExternalFocusChecker(() => this._model?.focused ?? false)); + + // Automatically call layoutBrowserContainer() when the browser container changes size + const resizeObserver = new ResizeObserver(async () => this.layoutBrowserContainer()); + resizeObserver.observe(this._browserContainer); + this._register(toDisposable(() => resizeObserver.disconnect())); } override async setInput(input: BrowserEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { @@ -354,7 +359,7 @@ export class BrowserEditor extends EditorPane { // Listen for zoom level changes and update browser view zoom factor this._inputDisposables.add(onDidChangeZoomLevel(targetWindowId => { if (targetWindowId === this.window.vscodeWindowId) { - this.layout(); + this.layoutBrowserContainer(); } })); // Capture screenshot periodically (once per second) to keep background updated @@ -365,11 +370,8 @@ export class BrowserEditor extends EditorPane { )); this.updateErrorDisplay(); - this.layout(); + this.layoutBrowserContainer(); await this._model.setVisible(this.shouldShowView); - - // Sometimes the element has not been inserted into the DOM yet. Ensure layout after next animation frame. - scheduleAtNextAnimationFrame(this.window, () => this.layout()); } protected override setEditorVisible(visible: boolean): void { @@ -675,7 +677,20 @@ export class BrowserEditor extends EditorPane { } } - override layout(): void { + override layout(_dimension: Dimension, _position?: IDomPosition): void { + // no-op: layout is handled in layoutBrowserContainer() + } + + /** + * This should be called whenever .browser-container changes in size, or when + * there could be any elements, such as the command palette, overlapping with it. + * + * Note that we don't call layoutBrowserContainer() from layout() but instead rely on using a ResizeObserver and on + * making direct calls to it. This is because we have seen cases where the getBoundingClientRect() values of + * the .browser-container element are not correct during layout() calls, especially during "Move into New Window" + * and "Copy into New Window" operations into a different monitor. + */ + layoutBrowserContainer(): void { if (this._model) { this.checkOverlays(); diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditorInput.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditorInput.ts index 57d42830dd2..bc23c8a501d 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditorInput.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditorInput.ts @@ -7,6 +7,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { truncate } from '../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; import { BrowserViewUri } from '../../../../platform/browserView/common/browserViewUri.js'; import { EditorInputCapabilities, IEditorSerializer, IUntypedEditorInput } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; @@ -56,7 +57,8 @@ export class BrowserEditorInput extends EditorInput { options: IBrowserEditorInputData, @IThemeService private readonly themeService: IThemeService, @IBrowserViewWorkbenchService private readonly browserViewWorkbenchService: IBrowserViewWorkbenchService, - @ILifecycleService private readonly lifecycleService: ILifecycleService + @ILifecycleService private readonly lifecycleService: ILifecycleService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); this._id = options.id; @@ -205,6 +207,20 @@ export class BrowserEditorInput extends EditorInput { return false; } + /** + * Creates a copy of this browser editor input with a new unique ID, creating an independent browser view with no linked state. + * This is used during Copy into New Window. + */ + override copy(): EditorInput { + const currentUrl = this._model?.url ?? this._initialData.url; + return this.instantiationService.createInstance(BrowserEditorInput, { + id: generateUuid(), + url: currentUrl, + title: this._model?.title ?? this._initialData.title, + favicon: this._model?.favicon ?? this._initialData.favicon + }); + } + override toUntyped(): IUntypedEditorInput { return { resource: this.resource, From a766d18464ddcdf85eeb4118af9ae063fa5325f9 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 20 Jan 2026 14:08:52 -0500 Subject: [PATCH 280/387] Add hover options for `ActionList`, implement for chat model picker (#288426) --- .../actionWidget/browser/actionList.ts | 93 ++++++++++++++++++- .../actionWidget/browser/actionWidget.ts | 9 +- .../browser/actionWidgetDropdown.ts | 7 +- .../chatManagement.contribution.ts | 31 +++++++ .../chatManagement/chatManagementEditor.ts | 4 + .../widget/input/modelPickerActionItem.ts | 14 ++- 6 files changed, 150 insertions(+), 8 deletions(-) diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 7d48eaa295e..b895a0ef908 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -9,7 +9,7 @@ import { IListAccessibilityProvider, List } from '../../../base/browser/ui/list/ import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js'; import { Codicon } from '../../../base/common/codicons.js'; import { ResolvedKeybinding } from '../../../base/common/keybindings.js'; -import { Disposable } from '../../../base/common/lifecycle.js'; +import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; import { OS } from '../../../base/common/platform.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import './actionWidget.css'; @@ -19,6 +19,10 @@ import { IKeybindingService } from '../../keybinding/common/keybinding.js'; import { defaultListStyles } from '../../theme/browser/defaultStyles.js'; import { asCssVariable } from '../../theme/common/colorRegistry.js'; import { ILayoutService } from '../../layout/browser/layoutService.js'; +import { IHoverService } from '../../hover/browser/hover.js'; +import { MarkdownString } from '../../../base/common/htmlContent.js'; +import { HoverPosition } from '../../../base/browser/ui/hover/hoverWidget.js'; +import { IHoverAction, IHoverWidget } from '../../../base/browser/ui/hover/hover.js'; export const acceptSelectedActionCommand = 'acceptSelectedCodeAction'; export const previewSelectedActionCommand = 'previewSelectedCodeAction'; @@ -30,6 +34,20 @@ export interface IActionListDelegate { onFocus?(action: T | undefined): void; } +/** + * Optional hover configuration shown when focusing/hovering over an action list item. + */ +export interface IActionListItemHover { + /** + * Content to display in the hover. + */ + readonly content?: string; + /** + * Actions to show in the hover. + */ + readonly actions?: IHoverAction[]; +} + export interface IActionListItem { readonly item?: T; readonly kind: ActionListItemKind; @@ -37,6 +55,10 @@ export interface IActionListItem { readonly disabled?: boolean; readonly label?: string; readonly description?: string; + /** + * Optional hover configuration shown when focusing/hovering over the item. + */ + readonly hover?: IActionListItemHover; readonly keybinding?: ResolvedKeybinding; canPreview?: boolean | undefined; readonly hideIcon?: boolean; @@ -179,6 +201,9 @@ class ActionItemRenderer implements IListRenderer, IAction data.container.title = element.tooltip; } else if (element.disabled) { data.container.title = element.label; + } else if (element.hover?.content || element.hover?.actions) { + // Don't show tooltip when hover content is configured - the rich hover will show instead + data.container.title = ''; } else if (actionTitle && previewTitle) { if (this._supportsPreview && element.canPreview) { data.container.title = localize({ key: 'label-preview', comment: ['placeholders are keybindings, e.g "F2 to Apply, Shift+F2 to Preview"'] }, "{0} to Apply, {1} to Preview", actionTitle, previewTitle); @@ -225,6 +250,8 @@ export class ActionList extends Disposable { private readonly cts = this._register(new CancellationTokenSource()); + private hover: { index: number; hover: IHoverWidget } | undefined; + constructor( user: string, preview: boolean, @@ -234,6 +261,7 @@ export class ActionList extends Disposable { @IContextViewService private readonly _contextViewService: IContextViewService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @ILayoutService private readonly _layoutService: ILayoutService, + @IHoverService private readonly _hoverService: IHoverService, ) { super(); this.domNode = document.createElement('div'); @@ -298,6 +326,9 @@ export class ActionList extends Disposable { this._register(this._list.onDidChangeFocus(() => this.onFocus())); this._register(this._list.onDidChangeSelection(e => this.onListSelection(e))); + // Ensure hover is hidden when ActionList is disposed + this._register(toDisposable(() => this.hideHover())); + this._allMenuItems = items; this._list.splice(0, this._list.length, this._allMenuItems); @@ -313,6 +344,7 @@ export class ActionList extends Disposable { hide(didCancel?: boolean): void { this._delegate.onHide(didCancel); this.cts.cancel(); + this.hideHover(); this._contextViewService.hideContextView(); } @@ -331,8 +363,7 @@ export class ActionList extends Disposable { } else { // For finding width dynamically (not using resize observer) const itemWidths: number[] = this._allMenuItems.map((_, index): number => { - // eslint-disable-next-line no-restricted-syntax - const element = this.domNode.ownerDocument.getElementById(this._list.getElementID(index)); + const element = this._getRowElement(index); if (element) { element.style.width = 'auto'; const width = element.getBoundingClientRect().width; @@ -393,6 +424,15 @@ export class ActionList extends Disposable { } } + private hideHover() { + if (this.hover) { + if (!this.hover.hover.isDisposed) { + this.hover.hover.dispose(); + } + this.hover = undefined; + } + } + private onFocus() { const focused = this._list.getFocus(); if (focused.length === 0) { @@ -401,10 +441,53 @@ export class ActionList extends Disposable { const focusIndex = focused[0]; const element = this._list.element(focusIndex); this._delegate.onFocus?.(element.item); + + // Show hover on focus change + this._showHoverForElement(element, focusIndex); + } + + private _getRowElement(index: number): HTMLElement | null { + // eslint-disable-next-line no-restricted-syntax + return this.domNode.ownerDocument.getElementById(this._list.getElementID(index)); + } + + private _showHoverForElement(element: IActionListItem, index: number): void { + // Hide any existing hover when moving to a different item + if (this.hover) { + if (this.hover.index === index && !this.hover.hover.isDisposed) { + return; + } + this.hideHover(); + } + + // Show hover if the element has hover content or actions + if ((element.hover?.content || element.hover?.actions) && this.focusCondition(element)) { + // The List widget separates data models from DOM elements, so we need to + // look up the actual DOM node to use as the hover target. + const rowElement = this._getRowElement(index); + if (rowElement) { + const markdown = element.hover.content ? new MarkdownString(element.hover.content) : undefined; + const hover = this._hoverService.showInstantHover({ + content: markdown ?? '', + target: rowElement, + actions: element.hover.actions, + additionalClasses: ['action-widget-hover'], + position: { + hoverPosition: HoverPosition.LEFT, + forcePosition: false, + }, + appearance: { + showPointer: true, + }, + }); + this.hover = hover ? { index, hover } : undefined; + } + } } private async onListHover(e: IListMouseEvent>) { const element = e.element; + if (element && element.item && this.focusCondition(element)) { if (this._delegate.onHover && !element.disabled && element.kind === ActionListItemKind.Action) { const result = await this._delegate.onHover(element.item, this.cts.token); @@ -413,9 +496,9 @@ export class ActionList extends Disposable { if (e.index) { this._list.splice(e.index, 1, [element]); } - } - this._list.setFocus(typeof e.index === 'number' ? [e.index] : []); + this._list.setFocus(typeof e.index === 'number' ? [e.index] : []); + } } private onListClick(e: IListMouseEvent>): void { diff --git a/src/vs/platform/actionWidget/browser/actionWidget.ts b/src/vs/platform/actionWidget/browser/actionWidget.ts index 5ab460e6790..21b49245beb 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.ts +++ b/src/vs/platform/actionWidget/browser/actionWidget.ts @@ -141,7 +141,14 @@ class ActionWidgetService extends Disposable implements IActionWidgetService { widget.style.width = `${width}px`; const focusTracker = renderDisposables.add(dom.trackFocus(element)); - renderDisposables.add(focusTracker.onDidBlur(() => this.hide(true))); + renderDisposables.add(focusTracker.onDidBlur(() => { + // Don't hide if focus moved to a hover that belongs to this action widget + const activeElement = dom.getActiveElement(); + if (activeElement?.closest('.action-widget-hover')) { + return; + } + this.hide(true); + })); return renderDisposables; } diff --git a/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts b/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts index 2b5897deff6..2021c617ce4 100644 --- a/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts +++ b/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts @@ -6,7 +6,7 @@ import { IActionWidgetService } from './actionWidget.js'; import { IAction } from '../../../base/common/actions.js'; import { BaseDropdown, IActionProvider, IBaseDropdownOptions } from '../../../base/browser/ui/dropdown/dropdown.js'; -import { ActionListItemKind, IActionListDelegate, IActionListItem } from './actionList.js'; +import { ActionListItemKind, IActionListDelegate, IActionListItem, IActionListItemHover } from './actionList.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import { Codicon } from '../../../base/common/codicons.js'; import { getActiveElement, isHTMLElement } from '../../../base/browser/dom.js'; @@ -17,6 +17,10 @@ export interface IActionWidgetDropdownAction extends IAction { category?: { label: string; order: number; showHeader?: boolean }; icon?: ThemeIcon; description?: string; + /** + * Optional flyout hover configuration shown when focusing/hovering over the action. + */ + hover?: IActionListItemHover; } // TODO @lramos15 - Should we just make IActionProvider templated? @@ -103,6 +107,7 @@ export class ActionWidgetDropdown extends BaseDropdown { item: action, tooltip: action.tooltip, description: action.description, + hover: action.hover, kind: ActionListItemKind.Action, canPreview: false, group: { title: '', icon: action.icon ?? ThemeIcon.fromId(action.checked ? Codicon.check.id : Codicon.blank.id) }, diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagement.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagement.contribution.ts index f3c901e717c..dd6e1d34efc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagement.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagement.contribution.ts @@ -117,6 +117,37 @@ function sanitizeOpenManageCopilotEditorArgs(input: unknown): IOpenManageCopilot }; } +registerAction2(class extends Action2 { + constructor() { + super({ + id: MANAGE_CHAT_COMMAND_ID, + title: localize2('openAiManagement', "Manage Language Models"), + category: CHAT_CATEGORY, + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or( + ChatContextKeys.Entitlement.planFree, + ChatContextKeys.Entitlement.planPro, + ChatContextKeys.Entitlement.planProPlus, + ChatContextKeys.Entitlement.internal + )), + f1: true, + }); + } + async run(accessor: ServicesAccessor, args: string | IOpenManageCopilotEditorActionOptions) { + const editorGroupsService = accessor.get(IEditorGroupsService); + const editorService = accessor.get(IEditorService); + args = sanitizeOpenManageCopilotEditorArgs(args); + await editorGroupsService.activeGroup.openEditor(new ModelsManagementEditorInput(), { pinned: true }); + + // If a query was provided, search for it in the models widget + if (args.query) { + const activeEditorPane = editorService.activeEditorPane; + if (activeEditorPane instanceof ModelsManagementEditor) { + activeEditorPane.search(args.query); + } + } + } +}); + class ChatManagementActionsContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.chatManagementActions'; diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagementEditor.ts index 36355c7870b..1226a59d0f6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagementEditor.ts @@ -94,6 +94,10 @@ export class ModelsManagementEditor extends EditorPane { clearSearch(): void { this.modelsWidget?.clearSearch(); } + + search(query: string): void { + this.modelsWidget?.search(query); + } } export const chatManagementSashBorder = registerColor('chatManagement.sashBorder', PANEL_BORDER, localize('chatManagementSashBorder', "The color of the Chat Management editor splitview sash border.")); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts index 8e5feb2fd94..d95f6b56d75 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts @@ -57,12 +57,23 @@ function modelDelegateToWidgetActionsProvider(delegate: IModelPickerDelegate, te checked: true, category: DEFAULT_MODEL_PICKER_CATEGORY, class: undefined, + description: localize('chat.modelPicker.auto.detail', "Best for your request based on capacity and performance."), tooltip: localize('chat.modelPicker.auto', "Auto"), label: localize('chat.modelPicker.auto', "Auto"), + hover: { content: localize('chat.modelPicker.auto.description', "Automatically selects the best model for your task based on context and complexity.") }, run: () => { } } satisfies IActionWidgetDropdownAction]; } return models.map(model => { + // Build hover content combining tooltip and rate info + const hoverParts: string[] = []; + if (model.metadata.tooltip) { + hoverParts.push(model.metadata.tooltip); + } + if (model.metadata.detail) { + hoverParts.push(localize('chat.modelPicker.rateDescription', "Rate is counted at {0}.", model.metadata.detail)); + } + const hoverContent = hoverParts.length > 0 ? hoverParts.join(' ') : undefined; return { id: model.metadata.id, enabled: true, @@ -71,7 +82,8 @@ function modelDelegateToWidgetActionsProvider(delegate: IModelPickerDelegate, te category: model.metadata.modelPickerCategory || DEFAULT_MODEL_PICKER_CATEGORY, class: undefined, description: model.metadata.detail, - tooltip: model.metadata.tooltip ?? model.metadata.name, + tooltip: hoverContent ? '' : model.metadata.name, + hover: hoverContent ? { content: hoverContent } : undefined, label: model.metadata.name, run: () => { const previousModel = delegate.getCurrentModel(); From 31bc1ffd6fbb3ecba4c35e501878a6adc102f7f3 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 20 Jan 2026 11:15:10 -0800 Subject: [PATCH 281/387] Fix tests --- .../contrib/chat/common/tools/languageModelToolsService.ts | 2 +- .../tools/builtinTools/fetchPageTool.test.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts index 4fba3afc859..a153f3b8425 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts @@ -162,7 +162,7 @@ export interface IToolInvocationPreparationContext { chatRequestId?: string; /** @deprecated Use {@link chatSessionResource} instead */ chatSessionId?: string; - chatSessionResource?: URI; + chatSessionResource: URI | undefined; chatInteractionId?: string; } diff --git a/src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts b/src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts index efcb9f37fe1..acee94cc06c 100644 --- a/src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts +++ b/src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts @@ -18,6 +18,7 @@ import { InternalFetchWebPageToolId } from '../../../../common/tools/builtinTool import { MockChatService } from '../../../common/chatService/mockChatService.js'; import { upcastDeepPartial } from '../../../../../../../base/test/common/mock.js'; import { IChatService } from '../../../../common/chatService/chatService.js'; +import { LocalChatSessionUri } from '../../../../common/model/chatUri.js'; class TestWebContentExtractorService implements IWebContentExtractorService { _serviceBrand: undefined; @@ -191,7 +192,7 @@ suite('FetchWebPageTool', () => { ); const preparation = await tool.prepareToolInvocation( - { parameters: { urls: ['https://valid.com', 'test://valid/resource', 'invalid://invalid'] } }, + { parameters: { urls: ['https://valid.com', 'test://valid/resource', 'invalid://invalid'] }, chatSessionResource: undefined }, CancellationToken.None ); @@ -229,7 +230,7 @@ suite('FetchWebPageTool', () => { ); const preparation1 = await tool.prepareToolInvocation( - { parameters: { urls: ['https://example.com'] }, chatSessionId: 'a' }, + { parameters: { urls: ['https://example.com'] }, chatSessionResource: LocalChatSessionUri.forSession('a') }, CancellationToken.None ); @@ -237,7 +238,7 @@ suite('FetchWebPageTool', () => { assert.strictEqual(preparation1.confirmationMessages?.title, undefined); const preparation2 = await tool.prepareToolInvocation( - { parameters: { urls: ['https://other.com'] }, chatSessionId: 'a' }, + { parameters: { urls: ['https://other.com'] }, chatSessionResource: LocalChatSessionUri.forSession('a') }, CancellationToken.None ); From a02f43ba07f734a944483aac6de76ee003d97542 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 20 Jan 2026 11:53:00 -0800 Subject: [PATCH 282/387] Minor chat optimizations and fixes (#289181) --- .../contrib/chat/browser/widget/chatWidget.ts | 51 +++++++++++-------- .../browser/widget/input/chatInputPart.ts | 4 +- .../widgetHosts/viewPane/chatViewPane.ts | 4 +- .../contrib/chat/common/model/chatModel.ts | 4 ++ .../inlineChat/browser/inlineChatWidget.ts | 2 +- 5 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 642cc3bea38..482687545f5 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -584,7 +584,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } get contentHeight(): number { - return this.input.inputPartHeight.get() + this.listWidget.contentHeight + this.chatSuggestNextWidget.height; + return this.input.height.get() + this.listWidget.contentHeight + this.chatSuggestNextWidget.height; } get attachmentModel(): ChatAttachmentModel { @@ -1652,6 +1652,18 @@ export class ChatWidget extends Disposable implements IChatWidget { this.styles, true ); + this._register(autorun(reader => { + this.inlineInputPart.height.read(reader); + if (!this.listWidget) { + // This is set up before the list/renderer are created + return; + } + + const editedRequest = this.listWidget.getTemplateDataForRequestId(this.viewModel?.editing?.id); + if (isRequestVM(editedRequest?.currentElement) && this.viewModel?.editing) { + this.listWidget.updateItemHeightOnRender(editedRequest?.currentElement, editedRequest); + } + })); } else { this.inputPartDisposable.value = this.instantiationService.createInstance(ChatInputPart, this.location, @@ -1659,6 +1671,19 @@ export class ChatWidget extends Disposable implements IChatWidget { this.styles, false ); + this._register(autorun(reader => { + this.inputPart.height.read(reader); + if (!this.listWidget) { + // This is set up before the list/renderer are created + return; + } + + if (this.bodyDimension) { + this.layout(this.bodyDimension.height, this.bodyDimension.width); + } + + this._onDidChangeContentHeight.fire(); + })); } this.input.render(container, '', this); @@ -1709,24 +1734,6 @@ export class ChatWidget extends Disposable implements IChatWidget { }, }); })); - this._register(autorun(reader => { - this.input.inputPartHeight.read(reader); - if (!this.listWidget) { - // This is set up before the list/renderer are created - return; - } - - const editedRequest = this.listWidget.getTemplateDataForRequestId(this.viewModel?.editing?.id); - if (isRequestVM(editedRequest?.currentElement) && this.viewModel?.editing) { - this.listWidget.updateItemHeightOnRender(editedRequest?.currentElement, editedRequest); - } - - if (this.bodyDimension) { - this.layout(this.bodyDimension.height, this.bodyDimension.width); - } - - this._onDidChangeContentHeight.fire(); - })); this._register(this.inputEditor.onDidChangeModelContent(() => { this.parsedChatRequest = undefined; this.updateChatInputContext(); @@ -2207,7 +2214,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.inputPart.layout(width); - const inputHeight = this.inputPart.inputPartHeight.get(); + const inputHeight = this.inputPart.height.get(); const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height; const lastElementVisible = this.listWidget.isScrolledToBottom; const lastItem = this.listWidget.lastItem; @@ -2256,7 +2263,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const possibleMaxHeight = (this._dynamicMessageLayoutData?.maxHeight ?? maxHeight); const width = this.bodyDimension?.width ?? this.container.offsetWidth; this.input.layout(width); - const inputPartHeight = this.input.inputPartHeight.get(); + const inputPartHeight = this.input.height.get(); const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height; const newHeight = Math.min(renderHeight + diff, possibleMaxHeight - inputPartHeight - chatSuggestNextWidgetHeight); this.layout(newHeight + inputPartHeight + chatSuggestNextWidgetHeight, width); @@ -2301,7 +2308,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const width = this.bodyDimension?.width ?? this.container.offsetWidth; this.input.layout(width); - const inputHeight = this.input.inputPartHeight.get(); + const inputHeight = this.input.height.get(); const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height; const totalMessages = this.viewModel.getItems(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 7ba16c7bd47..c9176cd8416 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -271,7 +271,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private readonly _widgetController = this._register(new MutableDisposable()); private readonly _contextUsageWidget = this._register(new MutableDisposable()); - readonly inputPartHeight = observableValue(this, 0); + readonly height = observableValue(this, 0); private _inputEditor!: CodeEditorWidget; private _inputEditorElement!: HTMLElement; @@ -2026,7 +2026,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const inputResizeObserver = this._register(new dom.DisposableResizeObserver(() => { const newHeight = this.container.offsetHeight; - this.inputPartHeight.set(newHeight, undefined); + this.height.set(newHeight, undefined); })); inputResizeObserver.observe(this.container); } diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 2036f2ffddc..0e334221dc7 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -661,7 +661,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { // When showing sessions stacked, adjust the height of the sessions list to make room for chat input this._register(autorun(reader => { - chatWidget.input.inputPartHeight.read(reader); + chatWidget.input.height.read(reader); if (this.sessionsViewerVisible && this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { this.relayout(); } @@ -966,7 +966,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { let availableSessionsHeight = height - this.sessionsTitleContainer.offsetHeight; if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { - availableSessionsHeight -= Math.max(ChatViewPane.MIN_CHAT_WIDGET_HEIGHT, this._widget?.input?.inputPartHeight.get() ?? 0); + availableSessionsHeight -= Math.max(ChatViewPane.MIN_CHAT_WIDGET_HEIGHT, this._widget?.input?.height.get() ?? 0); } else { availableSessionsHeight -= this.sessionsNewButtonContainer?.offsetHeight ?? 0; } diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index bae8c889b1d..875f3abf283 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -864,6 +864,10 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel } public set shouldBeRemovedOnSend(disablement: IChatRequestDisablement | undefined) { + if (this._shouldBeRemovedOnSend === disablement) { + return; + } + this._shouldBeRemovedOnSend = disablement; this._onDidChange.fire(defaultChatResponseModelChangeReason); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index d88b121fadd..fcf646a5109 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -393,7 +393,7 @@ export class InlineChatWidget { let value = this.contentHeight; value -= this._chatWidget.contentHeight; - value += Math.min(this._chatWidget.input.inputPartHeight.get() + maxWidgetOutputHeight, this._chatWidget.contentHeight); + value += Math.min(this._chatWidget.input.height.get() + maxWidgetOutputHeight, this._chatWidget.contentHeight); return value; } From 7ab682a71c10d9632db8037d8847746f2f61e7d3 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 20 Jan 2026 21:15:54 +0100 Subject: [PATCH 283/387] agent sessions - add verbose logging to figure out issues (#289192) --- .../agentSessions/agentSessionsModel.ts | 214 +++++++++++++++++- 1 file changed, 207 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index 29b7f2d8714..fad1044c603 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -14,10 +14,13 @@ import { ResourceMap } from '../../../../../base/common/map.js'; import { MarshalledId } from '../../../../../base/common/marshallingIds.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI, UriComponents } from '../../../../../base/common/uri.js'; +import { localize } from '../../../../../nls.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../../platform/log/common/log.js'; +import { ILogService, LogLevel } from '../../../../../platform/log/common/log.js'; +import { Registry } from '../../../../../platform/registry/common/platform.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; +import { Extensions, IOutputChannelRegistry, IOutputService } from '../../../../services/output/common/output.js'; import { ChatSessionStatus as AgentSessionStatus, IChatSessionFileChange, IChatSessionItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus } from '../../common/chatSessionsService.js'; import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName } from './agentSessions.js'; @@ -181,6 +184,195 @@ export function isMarshalledAgentSessionContext(thing: unknown): thing is IMarsh //#endregion +//#region Sessions Logger + +const agentSessionsOutputChannelId = 'agentSessionsOutput'; +const agentSessionsOutputChannelLabel = localize('agentSessionsOutput', "Agent Sessions"); + +function statusToString(status: AgentSessionStatus): string { + switch (status) { + case AgentSessionStatus.Failed: return 'Failed'; + case AgentSessionStatus.Completed: return 'Completed'; + case AgentSessionStatus.InProgress: return 'InProgress'; + case AgentSessionStatus.NeedsInput: return 'NeedsInput'; + default: return `Unknown(${status})`; + } +} + +interface ISessionToStateEntry { + status: AgentSessionStatus; + inProgressTime?: number; + finishedOrFailedTime?: number; +} + +class AgentSessionsLogger extends Disposable { + + constructor( + private readonly getSessionsData: () => { + sessions: Iterable; + sessionStates: ResourceMap; + mapSessionToState: ResourceMap; + }, + @ILogService private readonly logService: ILogService, + @IOutputService private readonly outputService: IOutputService, + ) { + super(); + + this.registerOutputChannel(); + this.registerListeners(); + } + + private registerOutputChannel(): void { + Registry.as(Extensions.OutputChannels).registerChannel({ + id: agentSessionsOutputChannelId, + label: agentSessionsOutputChannelLabel, + log: false + }); + } + + private registerListeners(): void { + this._register(this.logService.onDidChangeLogLevel(level => { + if (level === LogLevel.Trace) { + this.logIfTrace('Log level changed to trace'); + } + })); + } + + logIfTrace(reason: string): void { + if (this.logService.getLevel() !== LogLevel.Trace) { + return; + } + + this.logAllSessions(reason); + this.logSessionStates(); + this.logMapSessionToState(); + } + + private logAllSessions(reason: string): void { + const channel = this.outputService.getChannel(agentSessionsOutputChannelId); + if (!channel) { + return; + } + + const { sessions, sessionStates } = this.getSessionsData(); + + const lines: string[] = []; + lines.push(`=== Agent Sessions (${reason}) ===`); + + let count = 0; + for (const session of sessions) { + count++; + const state = sessionStates.get(session.resource); + + lines.push(`--- Session: ${session.label} ---`); + lines.push(` Resource: ${session.resource.toString()}`); + lines.push(` Provider Type: ${session.providerType}`); + lines.push(` Provider Label: ${session.providerLabel}`); + lines.push(` Status: ${statusToString(session.status)}`); + lines.push(` Icon: ${session.icon.id}`); + + if (session.description) { + lines.push(` Description: ${typeof session.description === 'string' ? session.description : session.description.value}`); + } + if (session.badge) { + lines.push(` Badge: ${typeof session.badge === 'string' ? session.badge : session.badge.value}`); + } + if (session.tooltip) { + lines.push(` Tooltip: ${typeof session.tooltip === 'string' ? session.tooltip : session.tooltip.value}`); + } + + // Timing info + lines.push(` Timing:`); + lines.push(` Created: ${session.timing.created ? new Date(session.timing.created).toISOString() : 'N/A'}`); + lines.push(` Last Request Started: ${session.timing.lastRequestStarted ? new Date(session.timing.lastRequestStarted).toISOString() : 'N/A'}`); + lines.push(` Last Request Ended: ${session.timing.lastRequestEnded ? new Date(session.timing.lastRequestEnded).toISOString() : 'N/A'}`); + if (session.timing.inProgressTime) { + lines.push(` In Progress Time: ${new Date(session.timing.inProgressTime).toISOString()}`); + } + if (session.timing.finishedOrFailedTime) { + lines.push(` Finished/Failed Time: ${new Date(session.timing.finishedOrFailedTime).toISOString()}`); + } + + // Changes info + if (session.changes) { + const summary = getAgentChangesSummary(session.changes); + if (summary) { + lines.push(` Changes: ${summary.files} files, +${summary.insertions} -${summary.deletions}`); + } + } + + // Our state (read/unread, archived) + lines.push(` State:`); + lines.push(` Archived (provider): ${session.archived ?? 'N/A'}`); + lines.push(` Archived (computed): ${session.isArchived()}`); + lines.push(` Archived (stored): ${state?.archived ?? 'N/A'}`); + lines.push(` Read: ${session.isRead()}`); + lines.push(` Read date (stored): ${state?.read ? new Date(state.read).toISOString() : 'N/A'}`); + + lines.push(''); + } + + lines.unshift(`Total sessions: ${count}`, ''); + + lines.push(`=== End Agent Sessions ===`); + + channel.append(lines.join('\n') + '\n'); + } + + private logSessionStates(): void { + const channel = this.outputService.getChannel(agentSessionsOutputChannelId); + if (!channel) { + return; + } + + const { sessionStates } = this.getSessionsData(); + + const lines: string[] = []; + lines.push(`=== Session States ===`); + lines.push(`Total stored states: ${sessionStates.size}`); + lines.push(''); + + for (const [resource, state] of sessionStates) { + lines.push(`URI: ${resource.toString()}`); + lines.push(` Archived: ${state.archived}`); + lines.push(` Read: ${state.read ? new Date(state.read).toISOString() : '0 (unread)'}`); + lines.push(''); + } + + lines.push(`=== End Session States ===`); + + channel.append(lines.join('\n') + '\n'); + } + + private logMapSessionToState(): void { + const channel = this.outputService.getChannel(agentSessionsOutputChannelId); + if (!channel) { + return; + } + + const { mapSessionToState } = this.getSessionsData(); + + const lines: string[] = []; + lines.push(`=== Map Session To State (Status Tracking) ===`); + lines.push(`Total entries: ${mapSessionToState.size}`); + lines.push(''); + + for (const [resource, state] of mapSessionToState) { + lines.push(`URI: ${resource.toString()}`); + lines.push(` Status: ${statusToString(state.status)}`); + lines.push(` In Progress Time: ${state.inProgressTime ? new Date(state.inProgressTime).toISOString() : 'N/A'}`); + lines.push(` Finished/Failed Time: ${state.finishedOrFailedTime ? new Date(state.finishedOrFailedTime).toISOString() : 'N/A'}`); + lines.push(''); + } + + lines.push(`=== End Map Session To State ===`); + + channel.append(lines.join('\n') + '\n'); + } +} + +//#endregion + export class AgentSessionsModel extends Disposable implements IAgentSessionsModel { private readonly _onWillResolve = this._register(new Emitter()); @@ -206,13 +398,13 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode }>(); private readonly cache: AgentSessionsCache; + private readonly logger: AgentSessionsLogger; constructor( @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService private readonly storageService: IStorageService, - @ILogService private readonly logService: ILogService, ) { super(); @@ -225,7 +417,18 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } this.sessionStates = this.cache.loadSessionStates(); + this.logger = this._register(this.instantiationService.createInstance( + AgentSessionsLogger, + () => ({ + sessions: this._sessions.values(), + sessionStates: this.sessionStates, + mapSessionToState: this.mapSessionToState + }) + )); + this.logger.logIfTrace('Loaded cached sessions'); + this.registerListeners(); + } private registerListeners(): void { @@ -273,8 +476,6 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode const providersToResolve = Array.from(this.providersToResolve); this.providersToResolve.clear(); - this.logService.trace(`[agent sessions] Resolving agent sessions for providers: ${providersToResolve.map(p => p ?? 'all').join(', ')}`); - const mapSessionContributionToType = new Map(); for (const contribution of this.chatSessionsService.getAllChatSessionContributions()) { mapSessionContributionToType.set(contribution.type, contribution); @@ -290,8 +491,6 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode const sessions = new ResourceMap(); for (const { chatSessionType, items: providerSessions } of providerResults) { - this.logService.trace(`[agent sessions] Resolved ${providerSessions.length} agent sessions for provider ${chatSessionType}`); - resolvedProviders.add(chatSessionType); if (token.isCancellationRequested) { @@ -398,7 +597,6 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } this._sessions = sessions; - this.logService.trace(`[agent sessions] Total resolved agent sessions:`, Array.from(this._sessions.values())); for (const [resource] of this.mapSessionToState) { if (!sessions.has(resource)) { @@ -412,6 +610,8 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } } + this.logger.logIfTrace('Sessions resolved from providers'); + this._onDidChangeSessions.fire(); } From 501401e1854bf766e0c4612be5bde4981c678c2a Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Tue, 20 Jan 2026 12:16:26 -0800 Subject: [PATCH 284/387] refactor: update chat session handling to use sessionResource instead of sessionId (#289178) --- .../chat/common/chatService/chatService.ts | 1 - .../tools/builtinTools/manageTodoListTool.ts | 36 +++++++++---------- .../chatResponseAccessibleView.test.ts | 2 -- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 92041da7586..eb181252df9 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -773,7 +773,6 @@ export interface IChatSubagentToolInvocationData { export interface IChatTodoListContent { kind: 'todoList'; - sessionId: string; todoList: Array<{ id: string; title: string; diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts index 88638500cd3..895a6c537f5 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts @@ -23,7 +23,6 @@ import { IChatTodo, IChatTodoListService } from '../chatTodoListService.js'; import { localize } from '../../../../../../nls.js'; import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { URI } from '../../../../../../base/common/uri.js'; -import { chatSessionResourceToId, LocalChatSessionUri } from '../../model/chatUri.js'; export const ManageTodoListToolToolId = 'manage_todo_list'; @@ -81,7 +80,6 @@ interface IManageTodoListToolInputParams { title: string; status: 'not-started' | 'in-progress' | 'completed'; }>; - chatSessionId?: string; } export class ManageTodoListTool extends Disposable implements IToolImpl { @@ -97,17 +95,23 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { // eslint-disable-next-line @typescript-eslint/no-explicit-any async invoke(invocation: IToolInvocation, _countTokens: any, _progress: any, _token: CancellationToken): Promise { const args = invocation.parameters as IManageTodoListToolInputParams; - // For: #263001 Use default sessionId - const DEFAULT_TODO_SESSION_ID = 'default'; - const chatSessionId = invocation.context?.sessionId ?? args.chatSessionId ?? DEFAULT_TODO_SESSION_ID; + const chatSessionResource = invocation.context?.sessionResource; + if (!chatSessionResource) { + return { + content: [{ + kind: 'text', + value: 'Error: No session resource available' + }] + }; + } this.logService.debug(`ManageTodoListTool: Invoking with options ${JSON.stringify(args)}`); try { if (args.operation === 'read') { - return this.handleReadOperation(LocalChatSessionUri.forSession(chatSessionId)); + return this.handleReadOperation(chatSessionResource); } else { - return this.handleWriteOperation(args, LocalChatSessionUri.forSession(chatSessionId)); + return this.handleWriteOperation(args, chatSessionResource); } } catch (error) { @@ -123,11 +127,12 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { async prepareToolInvocation(context: IToolInvocationPreparationContext, _token: CancellationToken): Promise { const args = context.parameters as IManageTodoListToolInputParams; - // For: #263001 Use default sessionId - const DEFAULT_TODO_SESSION_ID = 'default'; - const chatSessionId = context.chatSessionId ?? args.chatSessionId ?? DEFAULT_TODO_SESSION_ID; + const chatSessionResource = context.chatSessionResource; + if (!chatSessionResource) { + return undefined; + } - const currentTodoItems = this.chatTodoListService.getTodos(LocalChatSessionUri.forSession(chatSessionId)); + const currentTodoItems = this.chatTodoListService.getTodos(chatSessionResource); let message: string | undefined; if (args.operation === 'read') { @@ -147,7 +152,6 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { pastTenseMessage: new MarkdownString(message ?? localize('todo.updatedList', "Updated todo list")), toolSpecificData: { kind: 'todoList', - sessionId: chatSessionId, todoList: todoList } }; @@ -222,8 +226,7 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { operation: 'read', notStartedCount: statusCounts.notStartedCount, inProgressCount: statusCounts.inProgressCount, - completedCount: statusCounts.completedCount, - chatSessionId: chatSessionResourceToId(chatSessionResource) + completedCount: statusCounts.completedCount } ); @@ -276,8 +279,7 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { operation: 'write', notStartedCount: statusCounts.notStartedCount, inProgressCount: statusCounts.inProgressCount, - completedCount: statusCounts.completedCount, - chatSessionId: chatSessionResourceToId(chatSessionResource) + completedCount: statusCounts.completedCount } ); @@ -349,7 +351,6 @@ type TodoListToolInvokedEvent = { notStartedCount: number; inProgressCount: number; completedCount: number; - chatSessionId: string | undefined; }; type TodoListToolInvokedClassification = { @@ -357,7 +358,6 @@ type TodoListToolInvokedClassification = { notStartedCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of tasks with not-started status.' }; inProgressCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of tasks with in-progress status.' }; completedCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of tasks with completed status.' }; - chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat session that the tool was used within, if applicable.' }; owner: 'bhavyaus'; comment: 'Provides insight into the usage of the todo list tool including detailed task status distribution.'; }; diff --git a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts index dcb88c2a6ec..728883ddfe1 100644 --- a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts @@ -100,7 +100,6 @@ suite('ChatResponseAccessibleView', () => { test('returns todo list description for todoList data', () => { const todoData: IChatTodoListContent = { kind: 'todoList', - sessionId: 'session-1', todoList: [ { id: '1', title: 'Task 1', status: 'in-progress' }, { id: '2', title: 'Task 2', status: 'completed' } @@ -117,7 +116,6 @@ suite('ChatResponseAccessibleView', () => { test('returns empty for empty todo list', () => { const todoData: IChatTodoListContent = { kind: 'todoList', - sessionId: 'session-1', todoList: [] }; assert.strictEqual(getToolSpecificDataDescription(todoData), ''); From 73f17ed78709d9ff89fd6bb0ce428babec22b009 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 20 Jan 2026 15:20:59 -0500 Subject: [PATCH 285/387] action list hover follow ups (#289185) --- .../actionWidget/browser/actionList.ts | 13 +++----- .../chatManagement.contribution.ts | 31 ------------------- .../widget/input/modelPickerActionItem.ts | 10 +----- 3 files changed, 5 insertions(+), 49 deletions(-) diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index b895a0ef908..a33540a913d 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -22,7 +22,7 @@ import { ILayoutService } from '../../layout/browser/layoutService.js'; import { IHoverService } from '../../hover/browser/hover.js'; import { MarkdownString } from '../../../base/common/htmlContent.js'; import { HoverPosition } from '../../../base/browser/ui/hover/hoverWidget.js'; -import { IHoverAction, IHoverWidget } from '../../../base/browser/ui/hover/hover.js'; +import { IHoverWidget } from '../../../base/browser/ui/hover/hover.js'; export const acceptSelectedActionCommand = 'acceptSelectedCodeAction'; export const previewSelectedActionCommand = 'previewSelectedCodeAction'; @@ -42,10 +42,6 @@ export interface IActionListItemHover { * Content to display in the hover. */ readonly content?: string; - /** - * Actions to show in the hover. - */ - readonly actions?: IHoverAction[]; } export interface IActionListItem { @@ -201,7 +197,7 @@ class ActionItemRenderer implements IListRenderer, IAction data.container.title = element.tooltip; } else if (element.disabled) { data.container.title = element.label; - } else if (element.hover?.content || element.hover?.actions) { + } else if (element.hover?.content) { // Don't show tooltip when hover content is configured - the rich hover will show instead data.container.title = ''; } else if (actionTitle && previewTitle) { @@ -460,8 +456,8 @@ export class ActionList extends Disposable { this.hideHover(); } - // Show hover if the element has hover content or actions - if ((element.hover?.content || element.hover?.actions) && this.focusCondition(element)) { + // Show hover if the element has hover content + if (element.hover?.content && this.focusCondition(element)) { // The List widget separates data models from DOM elements, so we need to // look up the actual DOM node to use as the hover target. const rowElement = this._getRowElement(index); @@ -470,7 +466,6 @@ export class ActionList extends Disposable { const hover = this._hoverService.showInstantHover({ content: markdown ?? '', target: rowElement, - actions: element.hover.actions, additionalClasses: ['action-widget-hover'], position: { hoverPosition: HoverPosition.LEFT, diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagement.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagement.contribution.ts index dd6e1d34efc..f3c901e717c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagement.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagement.contribution.ts @@ -117,37 +117,6 @@ function sanitizeOpenManageCopilotEditorArgs(input: unknown): IOpenManageCopilot }; } -registerAction2(class extends Action2 { - constructor() { - super({ - id: MANAGE_CHAT_COMMAND_ID, - title: localize2('openAiManagement', "Manage Language Models"), - category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or( - ChatContextKeys.Entitlement.planFree, - ChatContextKeys.Entitlement.planPro, - ChatContextKeys.Entitlement.planProPlus, - ChatContextKeys.Entitlement.internal - )), - f1: true, - }); - } - async run(accessor: ServicesAccessor, args: string | IOpenManageCopilotEditorActionOptions) { - const editorGroupsService = accessor.get(IEditorGroupsService); - const editorService = accessor.get(IEditorService); - args = sanitizeOpenManageCopilotEditorArgs(args); - await editorGroupsService.activeGroup.openEditor(new ModelsManagementEditorInput(), { pinned: true }); - - // If a query was provided, search for it in the models widget - if (args.query) { - const activeEditorPane = editorService.activeEditorPane; - if (activeEditorPane instanceof ModelsManagementEditor) { - activeEditorPane.search(args.query); - } - } - } -}); - class ChatManagementActionsContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.chatManagementActions'; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts index d95f6b56d75..3d5234a740b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts @@ -65,15 +65,7 @@ function modelDelegateToWidgetActionsProvider(delegate: IModelPickerDelegate, te } satisfies IActionWidgetDropdownAction]; } return models.map(model => { - // Build hover content combining tooltip and rate info - const hoverParts: string[] = []; - if (model.metadata.tooltip) { - hoverParts.push(model.metadata.tooltip); - } - if (model.metadata.detail) { - hoverParts.push(localize('chat.modelPicker.rateDescription', "Rate is counted at {0}.", model.metadata.detail)); - } - const hoverContent = hoverParts.length > 0 ? hoverParts.join(' ') : undefined; + const hoverContent = model.metadata.tooltip; return { id: model.metadata.id, enabled: true, From b158a78b1ce99e7a88f44a915cb30952f226ec68 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 20 Jan 2026 21:25:32 +0100 Subject: [PATCH 286/387] Agent Sessions - add view all changes action to sessions list (#289142) * Agent Sessions - add view all changes action to sessions list * Hide menu if the session does not have any changes --- .../browser/agentSessions/agentSessionsViewer.ts | 1 + .../agentSessions/media/agentsessionsviewer.css | 2 +- .../browser/chatEditing/chatEditingActions.ts | 16 ++++++++++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 50a3b94aea5..481f4bed2f1 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -192,6 +192,7 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre } } template.diffContainer.classList.toggle('has-diff', hasDiff); + ChatContextKeys.hasAgentSessionChanges.bindTo(template.contextKeyService).set(hasDiff); // Badge let hasBadge = false; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css index 190ef9eceb1..846c2ff8fd9 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css @@ -59,7 +59,7 @@ .monaco-list-row:hover .agent-session-title-toolbar, .monaco-list-row.focused .agent-session-title-toolbar { - width: 22px; + width: 44px; .monaco-toolbar { display: block; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 30404affd13..5d245b48a1c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -35,6 +35,7 @@ import { isChatTreeItem, isRequestVM, isResponseVM } from '../../common/model/ch import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; import { CHAT_CATEGORY } from '../actions/chatActions.js'; import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js'; +import { IAgentSession, isAgentSession } from '../agentSessions/agentSessionsModel.js'; export abstract class EditingSessionAction extends Action2 { @@ -323,18 +324,29 @@ export class ViewAllSessionChangesAction extends Action2 { group: 'navigation', order: 10, when: ChatContextKeys.hasAgentSessionChanges + }, + { + id: MenuId.AgentSessionItemToolbar, + group: 'navigation', + order: 0, + when: ChatContextKeys.hasAgentSessionChanges } ], }); } - override async run(accessor: ServicesAccessor, sessionResource?: URI): Promise { + override async run(accessor: ServicesAccessor, sessionOrSessionResource?: URI | IAgentSession): Promise { const agentSessionsService = accessor.get(IAgentSessionsService); const commandService = accessor.get(ICommandService); - if (!URI.isUri(sessionResource)) { + + if (!URI.isUri(sessionOrSessionResource) && !isAgentSession(sessionOrSessionResource)) { return; } + const sessionResource = URI.isUri(sessionOrSessionResource) + ? sessionOrSessionResource + : sessionOrSessionResource.resource; + const session = agentSessionsService.getSession(sessionResource); const changes = session?.changes; if (!(changes instanceof Array)) { From 22e81e3086ddb433c39d57e78a20aa52f83643c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Ruales?= <1588988+jruales@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:49:17 -0800 Subject: [PATCH 287/387] Allow setting simpleBrowser.useIntegratedBrowser to be targeted by ExP (#289207) Allow setting simpleBrowser.useIntegratedBrowser to be targeted by ExP framework --- extensions/simple-browser/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/simple-browser/package.json b/extensions/simple-browser/package.json index 8b361f66f61..0d558eeebf6 100644 --- a/extensions/simple-browser/package.json +++ b/extensions/simple-browser/package.json @@ -58,7 +58,8 @@ "markdownDescription": "%configuration.useIntegratedBrowser.description%", "scope": "application", "tags": [ - "experimental" + "experimental", + "onExP" ] } } From ac24b5477f94aa1b05e09da833a6a5dec07b37f6 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Tue, 20 Jan 2026 12:50:25 -0800 Subject: [PATCH 288/387] Tracing for sessions timing --- .../chat/browser/agentSessions/agentSessionsModel.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index 29b7f2d8714..4ca31bc3dec 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -324,6 +324,12 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode // No previous state, just add it if (!state) { + const isInProgress = isSessionInProgressStatus(status); + let inProgressTime: number | undefined; + if (isInProgress) { + inProgressTime = Date.now(); + this.logService.trace(`[agent sessions] Setting inProgressTime for session ${session.resource.toString()} to ${inProgressTime} (status: ${status})`); + } this.mapSessionToState.set(session.resource, { status, inProgressTime: isSessionInProgressStatus(status) ? Date.now() : undefined, // this is not accurate but best effort @@ -368,6 +374,8 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } } + this.logService.trace(`[agent sessions] Resolved session ${session.resource.toString()} with timings: created=${created}, lastRequestStarted=${lastRequestStarted}, lastRequestEnded=${lastRequestEnded}`); + sessions.set(session.resource, this.toAgentSession({ providerType: chatSessionType, providerLabel, From 679ec21c93f9cd97e47eb08ffc028945cff8d8b2 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:00:12 -0800 Subject: [PATCH 289/387] Use session resource to get telemetry id For #274403 --- .../contrib/chat/browser/tools/languageModelToolsService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index 0dccf919d02..2d29e9c5c9e 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -44,6 +44,7 @@ import { ILanguageModelToolsConfirmationService } from '../../common/tools/langu import { CountTokensCallback, createToolSchemaUri, IBeginToolCallOptions, ILanguageModelToolsService, IPreparedToolInvocation, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, SpecedToolAliases, stringifyPromptTsxPart, ToolDataSource, ToolSet, VSCodeToolReference } from '../../common/tools/languageModelToolsService.js'; import { getToolConfirmationAlert } from '../accessibility/chatAccessibilityProvider.js'; import { URI } from '../../../../../base/common/uri.js'; +import { chatSessionResourceToId } from '../../common/model/chatUri.js'; const jsonSchemaRegistry = Registry.as(JSONContributionRegistry.Extensions.JSONContribution); @@ -490,7 +491,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo 'languageModelToolInvoked', { result: 'success', - chatSessionId: dto.context?.sessionId, + chatSessionId: dto.context?.sessionResource ? chatSessionResourceToId(dto.context.sessionResource) : undefined, toolId: tool.data.id, toolExtensionId: tool.data.source.type === 'extension' ? tool.data.source.extensionId.value : undefined, toolSourceKind: tool.data.source.type, From 2002893fdb1d88f6ba9207c8b591e0d7771af625 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:02:03 -0800 Subject: [PATCH 290/387] Use session resource for edit file tool For #274403 --- .../contrib/chat/common/tools/builtinTools/editFileTool.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/editFileTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/editFileTool.ts index 8a582a9210d..f83c3c4e7a8 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/editFileTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/editFileTool.ts @@ -14,7 +14,6 @@ import { ICodeMapperService } from '../../editing/chatCodeMapperService.js'; import { ChatModel } from '../../model/chatModel.js'; import { IChatService } from '../../chatService/chatService.js'; import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress } from '../languageModelToolsService.js'; -import { LocalChatSessionUri } from '../../model/chatUri.js'; export const ExtensionEditToolId = 'vscode_editFile'; export const InternalEditToolId = 'vscode_editFile_internal'; @@ -48,7 +47,7 @@ export class EditTool implements IToolImpl { const fileUri = URI.revive(parameters.uri); const uri = CellUri.parse(fileUri)?.notebook || fileUri; - const model = this.chatService.getSession(LocalChatSessionUri.forSession(invocation.context?.sessionId)) as ChatModel; + const model = this.chatService.getSession(invocation.context.sessionResource) as ChatModel; const request = model.getRequests().at(-1)!; model.acceptResponseProgress(request, { From 0572e9f23373c60ef02b1f2505363a1c19563d8b Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Tue, 20 Jan 2026 13:11:42 -0800 Subject: [PATCH 291/387] Tracing chat session status --- .../chat/browser/agentSessions/localAgentSessionsProvider.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/localAgentSessionsProvider.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/localAgentSessionsProvider.ts index 8b2b3947efb..985cc88e959 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/localAgentSessionsProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/localAgentSessionsProvider.ts @@ -15,6 +15,7 @@ import { IChatModel } from '../../common/model/chatModel.js'; import { convertLegacyChatSessionTiming, IChatDetail, IChatService, ResponseModelState } from '../../common/chatService/chatService.js'; import { ChatSessionStatus, IChatSessionItem, IChatSessionItemProvider, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js'; import { getChatSessionType } from '../../common/model/chatUri.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; interface IChatSessionItemWithProvider extends IChatSessionItem { readonly provider: IChatSessionItemProvider; @@ -35,6 +36,7 @@ export class LocalAgentsSessionsProvider extends Disposable implements IChatSess constructor( @IChatService private readonly chatService: IChatService, @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, + @ILogService private readonly logService: ILogService, ) { super(); @@ -126,10 +128,12 @@ export class LocalAgentsSessionsProvider extends Disposable implements IChatSess private modelToStatus(model: IChatModel): ChatSessionStatus | undefined { if (model.requestInProgress.get()) { + this.logService.trace(`[agent sessions] Session ${model.sessionResource.toString()} request is in progress.`); return ChatSessionStatus.InProgress; } const lastRequest = model.getRequests().at(-1); + this.logService.trace(`[agent sessions] Session ${model.sessionResource.toString()} last request response: state ${lastRequest?.response?.state}, isComplete ${lastRequest?.response?.isComplete}, isCanceled ${lastRequest?.response?.isCanceled}, error: ${lastRequest?.response?.result?.errorDetails?.message}.`); if (lastRequest?.response) { if (lastRequest.response.state === ResponseModelState.NeedsInput) { return ChatSessionStatus.NeedsInput; From a461babf461798f8e5873394ac0fb699885f1fa5 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Tue, 20 Jan 2026 22:29:47 +0100 Subject: [PATCH 292/387] Fixes https://github.com/microsoft/vscode/issues/277133 (#289198) --- .../browser/telemetry/arcTelemetryReporter.ts | 10 +++------- .../browser/telemetry/arcTelemetrySender.ts | 4 ++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetryReporter.ts b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetryReporter.ts index 63bd4058042..387563fbd4a 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetryReporter.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetryReporter.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { TimeoutTimer } from '../../../../../base/common/async.js'; -import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; import { IObservableWithChange, IObservable, runOnChange } from '../../../../../base/common/observable.js'; import { BaseStringEdit } from '../../../../../editor/common/core/edits/stringEdit.js'; import { StringText } from '../../../../../editor/common/core/text/abstractText.js'; @@ -25,17 +25,13 @@ export class ArcTelemetryReporter extends Disposable { private readonly _gitRepo: IObservable, private readonly _trackedEdit: BaseStringEdit, private readonly _sendTelemetryEvent: (res: ArcTelemetryReporterData) => void, - private readonly _onBeforeDispose: () => void, + private readonly _dispose: () => void, @ITelemetryService private readonly _telemetryService: ITelemetryService ) { super(); this._arcTracker = new ArcTracker(this._documentValueBeforeTrackedEdit, this._trackedEdit); - this._store.add(toDisposable(() => { - this._onBeforeDispose(); - })); - this._store.add(runOnChange(this._document.value, (_val, _prevVal, changes) => { const edit = BaseStringEdit.composeOrUndefined(changes.map(c => c.edit)); if (edit) { @@ -54,7 +50,7 @@ export class ArcTelemetryReporter extends Disposable { this._report(timeMs); } else { this._reportAfter(timeMs, i === this._timesMs.length - 1 ? () => { - this.dispose(); + this._dispose(); } : undefined); } } diff --git a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts index d369742b365..faa9c30d06c 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts @@ -95,7 +95,7 @@ export class EditTelemetryReportInlineEditArcSender extends Disposable { ...forwardToChannelIf(isCopilotLikeExtension(data.$extensionId)), }); }, () => { - this._store.deleteAndLeak(reporter); + this._store.delete(reporter); })); })); } @@ -255,7 +255,7 @@ export class EditTelemetryReportEditArcForChatOrInlineChatSender extends Disposa ...forwardToChannelIf(isCopilotLikeExtension(data.props.$extensionId)), }); }, () => { - this._store.deleteAndLeak(reporter); + this._store.delete(reporter); })); })); } From 93faffe9ef5a26224a1bf3008f988b3145a4fb5a Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 20 Jan 2026 13:49:29 -0800 Subject: [PATCH 293/387] Merge pull request #289217 from microsoft/connor4312/fix-app-rerenders mcp: only rerender apps when visibility changes --- .../chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts index 98545c506cd..a1414f7cf79 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts @@ -116,10 +116,6 @@ export class ChatMcpAppSubPart extends BaseChatToolInvocationSubPart { this._model.remount(); } })); - - this._register(onDidRemount(() => { - this._model.remount(); - })); } private _handleLoadStateChange(container: HTMLElement, loadState: McpAppLoadState): void { From c058856ceaa277794c8ae05b1e22ec29769fa5f8 Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Tue, 20 Jan 2026 14:20:21 -0800 Subject: [PATCH 294/387] Add agent diagnostics metrics (#289209) --- src/vs/platform/diagnostics/node/diagnosticsService.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/vs/platform/diagnostics/node/diagnosticsService.ts b/src/vs/platform/diagnostics/node/diagnosticsService.ts index 5a424bb9ff0..5695a7125d2 100644 --- a/src/vs/platform/diagnostics/node/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/node/diagnosticsService.ts @@ -66,6 +66,12 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P { tag: 'agent.md', filePattern: /^agent\.md$/i }, { tag: 'agents.md', filePattern: /^agents\.md$/i }, { tag: 'claude.md', filePattern: /^claude\.md$/i }, + { tag: 'claude-settings', filePattern: /^settings\.json$/i, relativePathPattern: /^\.claude$/i }, + { tag: 'claude-settings-local', filePattern: /^settings\.local\.json$/i, relativePathPattern: /^\.claude$/i }, + { tag: 'claude-mcp', filePattern: /^mcp\.json$/i, relativePathPattern: /^\.claude$/i }, + { tag: 'claude-commands-dir', filePattern: /\.md$/i, relativePathPattern: /^\.claude[\/\\]commands$/i }, + { tag: 'claude-skills-dir', filePattern: /^SKILL\.md$/i, relativePathPattern: /^\.claude[\/\\]skills[\/\\]/i }, + { tag: 'claude-rules-dir', filePattern: /\.md$/i, relativePathPattern: /^\.claude[\/\\]rules$/i }, { tag: 'gemini.md', filePattern: /^gemini\.md$/i }, { tag: 'copilot-instructions.md', filePattern: /^copilot\-instructions\.md$/i, relativePathPattern: /^\.github$/i }, ]; From 2fd8c70fa170261f429573a48762011480fccf6b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 21 Jan 2026 09:45:55 +1100 Subject: [PATCH 295/387] mcp: expose MCP server definitions to ext host (#288798) * Expose MCP server definitions to ext host * Fixes * Update src/vs/workbench/api/common/extHostTypeConverters.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/vs/workbench/api/browser/mainThreadMcp.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Address review comments * address review comments * Reuse McpServerDefinition.Serialized instead of custom DTOs (#289165) * Initial plan * Reuse McpServerDefinition.Serialized instead of custom DTO interfaces Co-authored-by: DonJayamanne <1948812+DonJayamanne@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: DonJayamanne <1948812+DonJayamanne@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> --- extensions/vscode-api-tests/package.json | 1 + .../common/extensionsApiProposals.ts | 3 ++ src/vs/workbench/api/browser/mainThreadMcp.ts | 33 +++++++++++++++- .../workbench/api/common/extHost.api.impl.ts | 8 ++++ .../workbench/api/common/extHost.protocol.ts | 6 +++ src/vs/workbench/api/common/extHostMcp.ts | 38 +++++++++++++++++++ .../api/common/extHostTypeConverters.ts | 27 ++++++++++++- .../vscode.proposed.mcpServerDefinitions.d.ts | 31 +++++++++++++++ 8 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.mcpServerDefinitions.d.ts diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 5c308453ec5..525f6195420 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -26,6 +26,7 @@ "fsChunks", "interactive", "languageStatusText", + "mcpServerDefinitions", "nativeWindowHandle", "notebookDeprecated", "notebookLiveShare", diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index d9bb1d86281..cf9ff526955 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -290,6 +290,9 @@ const _allApiProposals = { markdownAlertSyntax: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.markdownAlertSyntax.d.ts', }, + mcpServerDefinitions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mcpServerDefinitions.d.ts', + }, mcpToolDefinitions: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mcpToolDefinitions.d.ts', }, diff --git a/src/vs/workbench/api/browser/mainThreadMcp.ts b/src/vs/workbench/api/browser/mainThreadMcp.ts index e0b0fe9410b..5dede3a3d9c 100644 --- a/src/vs/workbench/api/browser/mainThreadMcp.ts +++ b/src/vs/workbench/api/browser/mainThreadMcp.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { mapFindFirst } from '../../../base/common/arraysFind.js'; -import { disposableTimeout } from '../../../base/common/async.js'; +import { disposableTimeout, RunOnceScheduler } from '../../../base/common/async.js'; import { CancellationError } from '../../../base/common/errors.js'; import { Emitter } from '../../../base/common/event.js'; import { Disposable, DisposableMap, DisposableStore, MutableDisposable } from '../../../base/common/lifecycle.js'; -import { ISettableObservable, observableValue } from '../../../base/common/observable.js'; +import { autorun, ISettableObservable, observableValue } from '../../../base/common/observable.js'; import Severity from '../../../base/common/severity.js'; import { URI } from '../../../base/common/uri.js'; import * as nls from '../../../nls.js'; @@ -98,6 +98,35 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { return launch; }, })); + + // Subscribe to MCP server definition changes and notify ext host + const onDidChangeMcpServerDefinitionsTrigger = this._register(new RunOnceScheduler(() => this._proxy.$onDidChangeMcpServerDefinitions(), 500)); + this._register(autorun(reader => { + const collections = this._mcpRegistry.collections.read(reader); + // Read all server definitions to track changes + for (const collection of collections) { + collection.serverDefinitions.read(reader); + } + // Notify ext host that definitions changed (it will re-fetch if needed) + if (!onDidChangeMcpServerDefinitionsTrigger.isScheduled()) { + onDidChangeMcpServerDefinitionsTrigger.schedule(); + } + })); + } + + /** Returns all MCP server definitions known to the editor. */ + $getMcpServerDefinitions(): Promise { + const collections = this._mcpRegistry.collections.get(); + const allServers: McpServerDefinition.Serialized[] = []; + + for (const collection of collections) { + const servers = collection.serverDefinitions.get(); + for (const server of servers) { + allServers.push(McpServerDefinition.toSerialized(server)); + } + } + + return Promise.resolve(allServers); } $upsertMcpCollection(collection: McpCollectionDefinition.FromExtHost, serversDto: McpServerDefinition.Serialized[]): void { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 38849ed2a4a..61bda2cda0d 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1630,6 +1630,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerMcpServerDefinitionProvider(id, provider) { return extHostMcp.registerMcpConfigurationProvider(extension, id, provider); }, + onDidChangeMcpServerDefinitions: (...args) => { + checkProposedApiEnabled(extension, 'mcpServerDefinitions'); + return _asExtensionEvent(extHostMcp.onDidChangeMcpServerDefinitions)(...args); + }, + get mcpServerDefinitions() { + checkProposedApiEnabled(extension, 'mcpServerDefinitions'); + return extHostMcp.mcpServerDefinitions; + }, onDidChangeChatRequestTools(...args) { checkProposedApiEnabled(extension, 'chatParticipantAdditions'); return _asExtensionEvent(extHostChatAgents2.onDidChangeChatRequestTools)(...args); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 498dc1cd162..f6be2fb99ef 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3163,6 +3163,8 @@ export interface IStartMcpOptions { errorOnUserInteraction?: boolean; } + + export interface ExtHostMcpShape { $substituteVariables(workspaceFolder: UriComponents | undefined, value: McpServerLaunch.Serialized): Promise; $resolveMcpLaunch(collectionId: string, label: string): Promise; @@ -3170,6 +3172,8 @@ export interface ExtHostMcpShape { $stopMcp(id: number): void; $sendMessage(id: number, message: string): void; $waitForInitialCollectionProviders(): Promise; + /** Notification that MCP server definitions have changed. ExtHost should re-fetch. */ + $onDidChangeMcpServerDefinitions(): void; } export interface IMcpAuthenticationDetails { @@ -3210,6 +3214,8 @@ export interface MainThreadMcpShape { $getTokenFromServerMetadata(id: number, authDetails: IMcpAuthenticationDetails, options?: IMcpAuthenticationOptions): Promise; $getTokenForProviderId(id: number, providerId: string, scopes: string[], options?: IMcpAuthenticationOptions): Promise; $logMcpAuthSetup(data: IAuthMetadataSource): void; + /** Returns all MCP server definitions known to the editor. */ + $getMcpServerDefinitions(): Promise; } export interface MainThreadDataChannelsShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index 5079b4f9c8f..d9aacf2fa91 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import { DeferredPromise, raceCancellationError, Sequencer, timeout } from '../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js'; import { CancellationError } from '../../../base/common/errors.js'; +import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; import { AUTH_SCOPE_SEPARATOR, fetchAuthorizationServerMetadata, fetchResourceMetadata, getDefaultMetadataForUrl, IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata, parseWWWAuthenticateHeader, scopesMatch } from '../../../base/common/oauth.js'; import { SSEParser } from '../../../base/common/sseParser.js'; @@ -33,6 +34,12 @@ export const IExtHostMpcService = createDecorator('IExtHostM export interface IExtHostMpcService extends ExtHostMcpShape { registerMcpConfigurationProvider(extension: IExtensionDescription, id: string, provider: vscode.McpServerDefinitionProvider): IDisposable; + + /** Event that fires when the set of MCP server definitions changes. */ + readonly onDidChangeMcpServerDefinitions: Event; + + /** Returns all MCP server definitions known to the editor. */ + readonly mcpServerDefinitions: readonly vscode.McpServerDefinition[]; } const serverDataValidation = vObj({ @@ -65,6 +72,12 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService servers: vscode.McpServerDefinition[]; }>(); + // MCP server definitions synced from main thread + private readonly _onDidChangeMcpServerDefinitions = this._register(new Emitter()); + readonly onDidChangeMcpServerDefinitions: Event = this._onDidChangeMcpServerDefinitions.event; + private _mcpServerDefinitions: readonly vscode.McpServerDefinition[] = []; + private _mcpServerDefinitionsInitialized = false; + constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @ILogService protected readonly _logService: ILogService, @@ -76,6 +89,31 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService this._proxy = extHostRpc.getProxy(MainContext.MainThreadMcp); } + /** Returns all MCP server definitions known to the editor. */ + get mcpServerDefinitions(): readonly vscode.McpServerDefinition[] { + if (!this._mcpServerDefinitionsInitialized) { + this._mcpServerDefinitionsInitialized = true; + // Fetch asynchronously in background and update when ready + this._proxy.$getMcpServerDefinitions().then(dtos => { + this._mcpServerDefinitions = dtos.map(dto => Convert.McpServerDefinition.to(dto)); + this._onDidChangeMcpServerDefinitions.fire(); + }).catch(err => this._logService.error('Failed to fetch MCP server definitions:', err)); + } + return this._mcpServerDefinitions; + } + + /** Called by main thread to notify that MCP server definitions have changed. */ + $onDidChangeMcpServerDefinitions(): void { + if (!this._mcpServerDefinitionsInitialized) { + return; + } + // Re-fetch from main thread + this._proxy.$getMcpServerDefinitions().then(dtos => { + this._mcpServerDefinitions = dtos.map(dto => Convert.McpServerDefinition.to(dto)); + this._onDidChangeMcpServerDefinitions.fire(); + }).catch(err => this._logService.error('Failed to fetch MCP server definitions:', err)); + } + $startMcp(id: number, opts: IStartMcpOptions): void { this._startMcp(id, McpServerLaunch.fromSerialized(opts.launch), opts.defaultCwd && URI.revive(opts.defaultCwd), opts.errorOnUserInteraction); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index c22c4e08f77..0ef0f0979e2 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -50,7 +50,7 @@ import { IToolInvocationContext, IToolResult, IToolResultInputOutputDetails, ITo import * as chatProvider from '../../contrib/chat/common/languageModels.js'; import { IChatMessageDataPart, IChatResponseDataPart, IChatResponsePromptTsxPart, IChatResponseTextPart } from '../../contrib/chat/common/languageModels.js'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from '../../contrib/debug/common/debug.js'; -import { McpServerLaunch, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; +import { McpServerDefinition as McpServerDefinitionType, McpServerLaunch, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; import * as notebooks from '../../contrib/notebook/common/notebookCommon.js'; import { CellEditType } from '../../contrib/notebook/common/notebookCommon.js'; import { ICellRange } from '../../contrib/notebook/common/notebookRange.js'; @@ -3768,6 +3768,31 @@ export namespace McpServerDefinition { } ); } + + /** Converts from the IPC DTO to the API type. */ + export function to(dto: McpServerDefinitionType.Serialized): vscode.McpServerDefinition { + const launch = McpServerLaunch.fromSerialized(dto.launch); + if (launch.type === McpServerTransportType.HTTP) { + return new types.McpHttpServerDefinition( + dto.label, + launch.uri, + Object.fromEntries(launch.headers), + dto.cacheNonce === '$$NONE' ? undefined : dto.cacheNonce, + ); + } else { + const result = new types.McpStdioServerDefinition( + dto.label, + launch.command, + [...launch.args], + Object.fromEntries(Object.entries(launch.env).map(([key, value]) => [key, value === null ? null : String(value)])), + dto.cacheNonce === '$$NONE' ? undefined : dto.cacheNonce, + ); + if (launch.cwd) { + result.cwd = URI.file(launch.cwd); + } + return result; + } + } } export namespace SourceControlInputBoxValidationType { diff --git a/src/vscode-dts/vscode.proposed.mcpServerDefinitions.d.ts b/src/vscode-dts/vscode.proposed.mcpServerDefinitions.d.ts new file mode 100644 index 00000000000..c0d4dc2b702 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.mcpServerDefinitions.d.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/288777 @DonJayamanne + + /** + * Namespace for language model related functionality. + */ + export namespace lm { + /** + * All MCP server definitions known to the editor. This includes + * servers defined in user and workspace mcp.json files as well as those + * provided by extensions. + * + * Consumers should listen to {@link onDidChangeMcpServerDefinitions} and + * re-read this property when it fires. + */ + export const mcpServerDefinitions: readonly McpServerDefinition[]; + + /** + * Event that fires when the set of MCP server definitions changes. + * This can be due to additions, deletions, or modifications of server + * definitions from any source. + */ + export const onDidChangeMcpServerDefinitions: Event; + } +} From d0818b2f495d6e1809d834f94879a4f53fc92c94 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Tue, 20 Jan 2026 23:57:59 +0100 Subject: [PATCH 296/387] Adopt hover for session type picker and mode picker --- .../actionWidget/browser/actionList.ts | 33 +++++-------------- .../browser/agentSessions/agentSessions.ts | 15 +++++++++ .../delegationSessionPickerActionItem.ts | 2 +- .../widget/input/modePickerActionItem.ts | 6 ++-- .../input/sessionTargetPickerActionItem.ts | 11 ++++--- 5 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index a33540a913d..2fc8c36ed69 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -9,7 +9,7 @@ import { IListAccessibilityProvider, List } from '../../../base/browser/ui/list/ import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js'; import { Codicon } from '../../../base/common/codicons.js'; import { ResolvedKeybinding } from '../../../base/common/keybindings.js'; -import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { Disposable, MutableDisposable } from '../../../base/common/lifecycle.js'; import { OS } from '../../../base/common/platform.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import './actionWidget.css'; @@ -246,7 +246,7 @@ export class ActionList extends Disposable { private readonly cts = this._register(new CancellationTokenSource()); - private hover: { index: number; hover: IHoverWidget } | undefined; + private _hover = this._register(new MutableDisposable()); constructor( user: string, @@ -322,9 +322,6 @@ export class ActionList extends Disposable { this._register(this._list.onDidChangeFocus(() => this.onFocus())); this._register(this._list.onDidChangeSelection(e => this.onListSelection(e))); - // Ensure hover is hidden when ActionList is disposed - this._register(toDisposable(() => this.hideHover())); - this._allMenuItems = items; this._list.splice(0, this._list.length, this._allMenuItems); @@ -340,7 +337,7 @@ export class ActionList extends Disposable { hide(didCancel?: boolean): void { this._delegate.onHide(didCancel); this.cts.cancel(); - this.hideHover(); + this._hover.clear(); this._contextViewService.hideContextView(); } @@ -420,15 +417,6 @@ export class ActionList extends Disposable { } } - private hideHover() { - if (this.hover) { - if (!this.hover.hover.isDisposed) { - this.hover.hover.dispose(); - } - this.hover = undefined; - } - } - private onFocus() { const focused = this._list.getFocus(); if (focused.length === 0) { @@ -448,13 +436,7 @@ export class ActionList extends Disposable { } private _showHoverForElement(element: IActionListItem, index: number): void { - // Hide any existing hover when moving to a different item - if (this.hover) { - if (this.hover.index === index && !this.hover.hover.isDisposed) { - return; - } - this.hideHover(); - } + let newHover: IHoverWidget | undefined; // Show hover if the element has hover content if (element.hover?.content && this.focusCondition(element)) { @@ -463,7 +445,7 @@ export class ActionList extends Disposable { const rowElement = this._getRowElement(index); if (rowElement) { const markdown = element.hover.content ? new MarkdownString(element.hover.content) : undefined; - const hover = this._hoverService.showInstantHover({ + newHover = this._hoverService.showDelayedHover({ content: markdown ?? '', target: rowElement, additionalClasses: ['action-widget-hover'], @@ -474,10 +456,11 @@ export class ActionList extends Disposable { appearance: { showPointer: true, }, - }); - this.hover = hover ? { index, hover } : undefined; + }, { groupId: `actionListHover` }); } } + + this._hover.value = newHover; } private async onListHover(e: IListMouseEvent>) { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts index 4cc9edf8ac9..474f1a2530a 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts @@ -86,6 +86,21 @@ export function getAgentCanContinueIn(provider: AgentSessionProviders): boolean } } +export function getAgentSessionProviderDescription(provider: AgentSessionProviders): string { + switch (provider) { + case AgentSessionProviders.Local: + return localize('chat.session.providerDescription.local', "Run tasks within VS Code chat. The agent iterates via chat and works interactively to implement changes on your main workspace."); + case AgentSessionProviders.Background: + return localize('chat.session.providerDescription.background', "Delegate tasks to a background agent running locally on your machine. The agent iterates via chat and works asynchronously in a Git worktree to implement changes isolated from your main workspace using the GitHub Copilot CLI."); + case AgentSessionProviders.Cloud: + return localize('chat.session.providerDescription.cloud', "Delegate tasks to the GitHub Copilot coding agent. The agent iterates via chat and works asynchronously in the cloud to implement changes and pull requests as needed."); + case AgentSessionProviders.Claude: + return localize('chat.session.providerDescription.claude', "Delegate tasks to the Claude SDK running locally on your machine. The agent iterates via chat and works asynchronously to implement changes."); + case AgentSessionProviders.Codex: + return localize('chat.session.providerDescription.codex', "Opens a new Codex session in the editor. Codex sessions can be managed from the chat sessions view."); + } +} + export enum AgentSessionsViewerOrientation { Stacked = 1, SideBySide, diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts index 9e562c53e3d..0e3b54a72eb 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts @@ -90,7 +90,7 @@ export class DelegationSessionPickerActionItem extends SessionTypePickerActionIt id: 'newChatSession', class: undefined, label: localize('chat.newChatSession', "New Chat Session"), - tooltip: localize('chat.newChatSession.tooltip', "Create a new chat session"), + tooltip: '', checked: false, icon: Codicon.plus, enabled: true, diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index d31643faecd..9843dab12eb 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -76,7 +76,8 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { class: isDisabledViaPolicy ? 'disabled-by-policy' : undefined, enabled: !isDisabledViaPolicy, checked: !isDisabledViaPolicy && currentMode.id === mode.id, - tooltip, + tooltip: '', + hover: { content: tooltip }, run: async () => { if (isDisabledViaPolicy) { return; // Block interaction if disabled by policy @@ -97,7 +98,8 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { const makeActionFromCustomMode = (mode: IChatMode, currentMode: IChatMode): IActionWidgetDropdownAction => { return { ...makeAction(mode, currentMode), - tooltip: mode.description.get() ?? chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, mode.kind)?.description ?? action.tooltip, + tooltip: '', + hover: { content: mode.description.get() ?? chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, mode.kind)?.description ?? action.tooltip }, icon: mode.icon.get() ?? (isModeConsideredBuiltIn(mode, this._productService) ? builtinDefaultIcon : undefined), category: agentModeDisabledViaPolicy ? policyDisabledCategory : customCategory }; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts index bc378fc83fe..584f6948a84 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts @@ -17,7 +17,7 @@ import { IContextKeyService } from '../../../../../../platform/contextkey/common import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; import { IChatSessionsService } from '../../../common/chatSessionsService.js'; -import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName, isFirstPartyAgentSessionProvider } from '../../agentSessions/agentSessions.js'; +import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderDescription, getAgentSessionProviderIcon, getAgentSessionProviderName, isFirstPartyAgentSessionProvider } from '../../agentSessions/agentSessions.js'; import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; import { ISessionTypePickerDelegate } from '../../chat.js'; import { IActionProvider } from '../../../../../../base/browser/ui/dropdown/dropdown.js'; @@ -25,7 +25,7 @@ import { IActionProvider } from '../../../../../../base/browser/ui/dropdown/drop export interface ISessionTypeItem { type: AgentSessionProviders; label: string; - description: string; + hoverDescription: string; commandId: string; } @@ -66,12 +66,13 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { ...action, id: sessionTypeItem.commandId, label: sessionTypeItem.label, - tooltip: sessionTypeItem.description, checked: currentType === sessionTypeItem.type, icon: getAgentSessionProviderIcon(sessionTypeItem.type), enabled: this._isSessionTypeEnabled(sessionTypeItem.type), category: this._getSessionCategory(sessionTypeItem), description: this._getSessionDescription(sessionTypeItem), + tooltip: '', + hover: { content: sessionTypeItem.hoverDescription }, run: async () => { this._run(sessionTypeItem); }, @@ -141,7 +142,7 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { const localSessionItem: ISessionTypeItem = { type: AgentSessionProviders.Local, label: getAgentSessionProviderName(AgentSessionProviders.Local), - description: localize('chat.sessionTarget.local.description', "Local chat session"), + hoverDescription: getAgentSessionProviderDescription(AgentSessionProviders.Local), commandId: `workbench.action.chat.openNewChatSessionInPlace.${AgentSessionProviders.Local}`, }; @@ -157,7 +158,7 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { agentSessionItems.push({ type: agentSessionType, label: getAgentSessionProviderName(agentSessionType), - description: contribution.description, + hoverDescription: getAgentSessionProviderDescription(agentSessionType), commandId: contribution.canDelegate ? `workbench.action.chat.openNewChatSessionInPlace.${contribution.type}` : `workbench.action.chat.openNewChatSessionExternal.${contribution.type}`, From f42b4b1b22faa660b888ac6ec32f7341b456f03b Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 20 Jan 2026 14:58:02 -0800 Subject: [PATCH 297/387] chat: remove fragile timeout in revealSessionIfAlreadyOpen (#289232) Removes the 500ms timeout fallback that was a hack to wait for focus transfer to auxiliary windows. Instead, only waits for focus transfer when preserveFocus is false, since preserveFocus: true means the window won't get focus and the layout service won't fire onDidChangeActiveContainer. - Remove unused raceCancellablePromises import - Add preserveFocus check to avoid waiting when focus won't transfer - Remove timeout(500) that could cause UI to appear in wrong location Fixes https://github.com/microsoft/vscode/issues/279738 (Commit message generated by Copilot) --- .../contrib/chat/browser/widget/chatWidgetService.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts index 2deb9859f16..b9bb9dbadf8 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../../base/browser/dom.js'; -import { raceCancellablePromises, timeout } from '../../../../../base/common/async.js'; +import { timeout } from '../../../../../base/common/async.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { combinedDisposable, Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { isEqual } from '../../../../../base/common/resources.js'; @@ -153,11 +153,8 @@ export class ChatWidgetService extends Disposable implements IChatWidgetService const isGroupActive = () => dom.getWindow(this.layoutService.activeContainer).vscodeWindowId === existingEditorWindowId; let ensureFocusTransfer: Promise | undefined; - if (!isGroupActive()) { - ensureFocusTransfer = raceCancellablePromises([ - timeout(500), - Event.toPromise(Event.once(Event.filter(this.layoutService.onDidChangeActiveContainer, isGroupActive))), - ]); + if (!isGroupActive() && !options?.preserveFocus) { + ensureFocusTransfer = Event.toPromise(Event.once(Event.filter(this.layoutService.onDidChangeActiveContainer, isGroupActive))); } const pane = await existingEditor.group.openEditor(existingEditor.editor, options); From 7d397574506a82d9d4238b706816a9361558901b Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Wed, 21 Jan 2026 07:05:53 +0800 Subject: [PATCH 298/387] switch edit icon to pencil (#289231) --- .../widget/chatContentParts/chatThinkingContentPart.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index 66a6558bf95..80f5c17750c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -64,7 +64,7 @@ export function getToolInvocationIcon(toolId: string): ThemeIcon { lowerToolId.includes('edit') || lowerToolId.includes('create') ) { - return Codicon.wand; + return Codicon.pencil; } if ( @@ -634,7 +634,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen let icon: ThemeIcon; if (isMarkdownEdit) { - icon = Codicon.wand; + icon = Codicon.pencil; } else if (isTerminalTool) { const terminalData = (toolInvocationOrMarkdown as IChatToolInvocation | IChatToolInvocationSerialized).toolSpecificData as { kind: 'terminal'; terminalCommandState?: { exitCode?: number } }; const exitCode = terminalData?.terminalCommandState?.exitCode; From ac06186ae14632e9c8fae9b60fc3f6e595d3f0bc Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:09:30 -0800 Subject: [PATCH 299/387] Add `sessionResource` to proposed apis For #274403 Replacing chatSessionId in a few API proposals. Keeping around the old fields for now until we can adopt this everywhere --- .../api/common/extHostLanguageModelTools.ts | 4 ++++ .../workbench/api/common/extHostTypeConverters.ts | 1 + .../chat/common/tools/languageModelToolsService.ts | 2 ++ .../vscode.proposed.chatParticipantAdditions.d.ts | 2 ++ .../vscode.proposed.chatParticipantPrivate.d.ts | 13 ++++++++++++- 5 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index f59cbb25ec5..9970d364349 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -20,6 +20,7 @@ import { Dto, SerializableObjectWithBuffers } from '../../services/extensions/co import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js'; import { ExtHostLanguageModels } from './extHostLanguageModels.js'; import * as typeConvert from './extHostTypeConverters.js'; +import { URI } from '../../../base/common/uri.js'; class Tool { @@ -184,6 +185,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape options.chatRequestId = dto.chatRequestId; options.chatInteractionId = dto.chatInteractionId; options.chatSessionId = dto.context?.sessionId; + options.chatSessionResource = URI.revive(dto.context?.sessionResource); options.subAgentInvocationId = dto.subAgentInvocationId; } @@ -262,6 +264,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape rawInput: context.rawInput, chatRequestId: context.chatRequestId, chatSessionId: context.chatSessionId, + chatSessionResource: context.chatSessionResource, chatInteractionId: context.chatInteractionId }; @@ -285,6 +288,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape input: context.parameters, chatRequestId: context.chatRequestId, chatSessionId: context.chatSessionId, + chatSessionResource: context.chatSessionResource, chatInteractionId: context.chatInteractionId }; if (item.tool.prepareInvocation) { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 0ef0f0979e2..b49df82b3bb 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -3148,6 +3148,7 @@ export namespace ChatAgentRequest { enableCommandDetection: request.enableCommandDetection ?? true, isParticipantDetected: request.isParticipantDetected ?? false, sessionId, + sessionResource: request.sessionResource, references: variableReferences .map(v => ChatPromptReference.to(v, diagnostics, logService)) .filter(isDefined), diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts index a153f3b8425..f1fc3f1d8ef 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts @@ -294,7 +294,9 @@ export interface IToolInvocationStreamContext { toolCallId: string; rawInput: unknown; chatRequestId?: string; + /** @deprecated Use {@link chatSessionResource} instead */ chatSessionId?: string; + chatSessionResource?: URI; chatInteractionId?: string; } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 83235b8fea7..6dbf88c6895 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -695,7 +695,9 @@ declare module 'vscode' { readonly rawInput?: unknown; readonly chatRequestId?: string; + /** @deprecated Use {@link chatSessionResource} instead */ readonly chatSessionId?: string; + readonly chatSessionResource?: Uri; readonly chatInteractionId?: string; } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index 4196e31d903..4ab722c122e 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -61,10 +61,17 @@ declare module 'vscode' { readonly attempt: number; /** - * The session identifier for this chat request + * The session identifier for this chat request. + * + * @deprecated Use {@link chatSessionResource} instead. */ readonly sessionId: string; + /** + * The resource URI for the chat session this request belongs to. + */ + readonly sessionResource: Uri; + /** * If automatic command detection is enabled. */ @@ -239,7 +246,9 @@ declare module 'vscode' { export interface LanguageModelToolInvocationOptions { chatRequestId?: string; + /** @deprecated Use {@link chatSessionResource} instead */ chatSessionId?: string; + chatSessionResource?: Uri; chatInteractionId?: string; terminalCommand?: string; /** @@ -254,7 +263,9 @@ declare module 'vscode' { */ input: T; chatRequestId?: string; + /** @deprecated Use {@link chatSessionResource} instead */ chatSessionId?: string; + chatSessionResource?: Uri; chatInteractionId?: string; } From 820726c739f5ce1652037c3272fd382d0e17c8ed Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Wed, 21 Jan 2026 07:44:34 +0800 Subject: [PATCH 300/387] make thinking scrollable (#289227) * make fixed scrolling scrollable * set default back to fixedscrolling * fix comments * address some comments * fix scroll when done --- .../contrib/chat/browser/chat.contribution.ts | 2 +- .../chatThinkingContentPart.ts | 102 +++++++++++++++++- .../media/chatThinkingContent.css | 8 ++ 3 files changed, 107 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 7f2c4473ab1..08a5b32efaf 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -811,7 +811,7 @@ configurationRegistry.registerConfiguration({ }, [ChatConfiguration.ThinkingStyle]: { type: 'string', - default: 'collapsedPreview', + default: 'fixedScrolling', enum: ['collapsed', 'collapsedPreview', 'fixedScrolling'], enumDescriptions: [ nls.localize('chat.agent.thinkingMode.collapsed', "Thinking parts will be collapsed by default."), diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index 80f5c17750c..73a7bcb62c2 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -5,6 +5,8 @@ import { $, clearNode, hide } from '../../../../../../base/browser/dom.js'; import { alert } from '../../../../../../base/browser/ui/aria/aria.js'; +import { DomScrollableElement } from '../../../../../../base/browser/ui/scrollbar/scrollableElement.js'; +import { ScrollbarVisibility } from '../../../../../../base/common/scrollable.js'; import { IChatMarkdownContent, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService/chatService.js'; import { IChatContentPartRenderContext, IChatContentPart } from './chatContentParts.js'; import { IChatRendererContent } from '../../../common/model/chatViewModel.js'; @@ -94,6 +96,7 @@ interface ILazyItem { toolInvocationOrMarkdown?: IChatToolInvocation | IChatToolInvocationSerialized | IChatMarkdownContent; originalParent?: HTMLElement; } +const THINKING_SCROLL_MAX_HEIGHT = 200; export class ChatThinkingContentPart extends ChatCollapsibleContentPart implements IChatContentPart { public readonly codeblocks: undefined; @@ -108,6 +111,8 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen private markdownResult: IRenderedMarkdown | undefined; private wrapper!: HTMLElement; private fixedScrollingMode: boolean = false; + private autoScrollEnabled: boolean = true; + private scrollableElement: DomScrollableElement | undefined; private lastExtractedTitle: string | undefined; private extractedTitles: string[] = []; private toolInvocationCount: number = 0; @@ -234,10 +239,95 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen this.wrapper.appendChild(this.textContainer); this.renderMarkdown(this.currentThinkingValue); } + + // wrap content in scrollable element for fixed scrolling mode + if (this.fixedScrollingMode) { + this.scrollableElement = this._register(new DomScrollableElement(this.wrapper, { + vertical: ScrollbarVisibility.Auto, + horizontal: ScrollbarVisibility.Hidden, + handleMouseWheel: true, + alwaysConsumeMouseWheel: false + })); + this._register(this.scrollableElement.onScroll(e => this.handleScroll(e.scrollTop))); + + this._register(this._onDidChangeHeight.event(() => { + setTimeout(() => this.scrollToBottomIfEnabled(), 0); + })); + + setTimeout(() => this.scrollToBottomIfEnabled(), 0); + + this.updateDropdownClickability(); + return this.scrollableElement.getDomNode(); + } + this.updateDropdownClickability(); return this.wrapper; } + private handleScroll(scrollTop: number): void { + if (!this.scrollableElement) { + return; + } + + const scrollDimensions = this.scrollableElement.getScrollDimensions(); + const maxScrollTop = scrollDimensions.scrollHeight - scrollDimensions.height; + const isAtBottom = maxScrollTop <= 0 || scrollTop >= maxScrollTop - 10; + + if (isAtBottom) { + this.autoScrollEnabled = true; + } else { + this.autoScrollEnabled = false; + } + } + + private scrollToBottomIfEnabled(): void { + if (!this.scrollableElement || !this.autoScrollEnabled) { + return; + } + + const isCollapsed = this.domNode.classList.contains('chat-used-context-collapsed'); + if (!isCollapsed) { + return; + } + + const contentHeight = this.wrapper.scrollHeight; + const viewportHeight = Math.min(contentHeight, THINKING_SCROLL_MAX_HEIGHT); + + this.scrollableElement.setScrollDimensions({ + width: this.scrollableElement.getDomNode().clientWidth, + scrollWidth: this.wrapper.scrollWidth, + height: viewportHeight, + scrollHeight: contentHeight + }); + + if (contentHeight > viewportHeight) { + this.scrollableElement.setScrollPosition({ scrollTop: contentHeight - viewportHeight }); + } + } + + /** + * updates scroll dimensions when streaming is complete. + */ + private updateScrollDimensionsForCompletion(): void { + if (!this.scrollableElement || !this.fixedScrollingMode) { + return; + } + + const contentHeight = this.wrapper.scrollHeight; + const viewportHeight = Math.min(contentHeight, THINKING_SCROLL_MAX_HEIGHT); + + this.scrollableElement.setScrollDimensions({ + width: this.scrollableElement.getDomNode().clientWidth, + scrollWidth: this.wrapper.scrollWidth, + height: viewportHeight, + scrollHeight: contentHeight + }); + + if (contentHeight <= THINKING_SCROLL_MAX_HEIGHT) { + this.scrollableElement.setScrollPosition({ scrollTop: 0 }); + } + } + private renderMarkdown(content: string, reuseExisting?: boolean): void { // Guard against rendering after disposal to avoid leaking disposables if (this._store.isDisposed) { @@ -339,8 +429,8 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen this.currentThinkingValue = next; this.renderMarkdown(next, reuseExisting); - if (this.fixedScrollingMode && this.wrapper) { - this.wrapper.scrollTop = this.wrapper.scrollHeight; + if (this.fixedScrollingMode && this.scrollableElement) { + setTimeout(() => this.scrollToBottomIfEnabled(), 0); } const extractedTitle = extractTitleFromThinkingContent(raw); @@ -379,6 +469,10 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen this._collapseButton.icon = Codicon.check; } + // Update scroll dimensions now that streaming is complete + // This removes unnecessary scrollbar when content fits + this.updateScrollDimensionsForCompletion(); + this.updateDropdownClickability(); if (this.content.generatedTitle) { @@ -649,8 +743,8 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen this.wrapper.appendChild(itemWrapper); - if (this.fixedScrollingMode && this.wrapper) { - this.wrapper.scrollTop = this.wrapper.scrollHeight; + if (this.fixedScrollingMode && this.scrollableElement) { + setTimeout(() => this.scrollToBottomIfEnabled(), 0); } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css index 4d6484b28e5..c24a307a50f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css @@ -191,6 +191,10 @@ .interactive-session .interactive-response .value .chat-thinking-fixed-mode { outline: none; + &.chat-used-context-collapsed > .monaco-scrollable-element:has(.chat-used-context-list.chat-thinking-collapsible:not(.chat-thinking-streaming)) { + display: none; + } + &.chat-used-context-collapsed .chat-used-context-list.chat-thinking-collapsible.chat-thinking-streaming { max-height: 200px; overflow: hidden; @@ -207,6 +211,10 @@ max-height: none; overflow: visible; } + + .chat-thinking-tool-wrapper .chat-used-context:not(.chat-used-context-collapsed) .chat-used-context-list { + display: block; + } } .editor-instance .interactive-session .interactive-response .value .chat-thinking-box .chat-thinking-item ::before { From c1acc69e0042f025c3830d7b0a0098f20bcee8c5 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 21 Jan 2026 00:56:12 +0100 Subject: [PATCH 301/387] Workbench - update editor content menu design (#289245) * Workbench - update editor content menu design * Pull request feedback --- .../floatingMenu/browser/floatingMenu.css | 47 +++++++++------- .../floatingMenu/browser/floatingMenu.ts | 55 +++++++++++++++---- 2 files changed, 71 insertions(+), 31 deletions(-) diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css index 930c962b888..5221366ed32 100644 --- a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css @@ -4,41 +4,48 @@ *--------------------------------------------------------------------------------------------*/ .floating-menu-overlay-widget { - padding: 0px; - color: var(--vscode-button-foreground); - background-color: var(--vscode-button-background); - border-radius: 2px; + padding: 2px 4px; + color: var(--vscode-foreground); + background-color: var(--vscode-editorWidget-background); + border-radius: 6px; border: 1px solid var(--vscode-contrastBorder); display: flex; align-items: center; + justify-content: center; + gap: 4px; z-index: 10; box-shadow: 0 2px 8px var(--vscode-widget-shadow); overflow: hidden; - .action-item > .action-label { - padding: 5px; - font-size: 12px; - border-radius: 2px; + .actions-container { + gap: 4px; } - .action-item > .action-label.codicon, .action-item .codicon { - color: var(--vscode-button-foreground); + .action-item > .action-label { + padding: 4px 6px; + font-size: 11px; + line-height: 14px; + border-radius: 4px; } .action-item > .action-label.codicon:not(.separator) { - padding-top: 6px; - padding-bottom: 6px; + color: var(--vscode-foreground); + width: 22px; + height: 22px; + padding: 0; + font-size: 16px; + line-height: 22px; + display: flex; + align-items: center; + justify-content: center; } - .action-item:first-child > .action-label { - padding-left: 7px; + .action-item.primary > .action-label { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); } - .action-item:last-child > .action-label { - padding-right: 7px; - } - - .action-item .action-label.separator { - background-color: var(--vscode-button-separator); + .action-item.primary > .action-label:hover { + background-color: var(--vscode-button-hoverBackground) !important; } } diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts index 56f8117ef61..a8af1c1b93d 100644 --- a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts @@ -5,7 +5,7 @@ import { h } from '../../../../base/browser/dom.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { autorun, constObservable, observableFromEvent } from '../../../../base/common/observable.js'; +import { autorun, constObservable, derived, observableFromEvent } from '../../../../base/common/observable.js'; import { MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { IMenuService, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; @@ -29,11 +29,35 @@ export class FloatingEditorToolbar extends Disposable implements IEditorContribu const editorObs = this._register(observableCodeEditor(editor)); const menu = this._register(menuService.createMenu(MenuId.EditorContent, editor.contextKeyService)); - const menuIsEmptyObs = observableFromEvent(this, menu.onDidChange, () => menu.getActions().length === 0); + const menuActionsObs = observableFromEvent(this, menu.onDidChange, () => menu.getActions()); + const menuPrimaryActionIdObs = derived(reader => { + const menuActions = menuActionsObs.read(reader); + if (menuActions.length === 0) { + return undefined; + } + + // Navigation group + const navigationGroup = menuActions + .find((group) => group[0] === 'navigation'); + + // First action in navigation group + if (navigationGroup && navigationGroup[1].length > 0) { + return navigationGroup[1][0].id; + } + + // Fallback to first group/action + for (const [, actions] of menuActions) { + if (actions.length > 0) { + return actions[0].id; + } + } + + return undefined; + }); this._register(autorun(reader => { - const menuIsEmpty = menuIsEmptyObs.read(reader); - if (menuIsEmpty) { + const menuPrimaryActionId = menuPrimaryActionIdObs.read(reader); + if (!menuPrimaryActionId) { return; } @@ -41,7 +65,7 @@ export class FloatingEditorToolbar extends Disposable implements IEditorContribu // Set height explicitly to ensure that the floating menu element // is rendered in the lower right corner at the correct position. - container.root.style.height = '28px'; + container.root.style.height = '26px'; // Toolbar const toolbar = instantiationService.createInstance(MenuWorkbenchToolBar, container.root, MenuId.EditorContent, { @@ -50,15 +74,24 @@ export class FloatingEditorToolbar extends Disposable implements IEditorContribu return undefined; } - const keybinding = keybindingService.lookupKeybinding(action.id); - if (!keybinding) { - return undefined; - } - return instantiationService.createInstance(class extends MenuEntryActionViewItem { + override render(container: HTMLElement): void { + super.render(container); + + // Highlight primary action + if (action.id === menuPrimaryActionId) { + this.element?.classList.add('primary'); + } + } + protected override updateLabel(): void { + const keybinding = keybindingService.lookupKeybinding(action.id); + const keybindingLabel = keybinding ? keybinding.getLabel() : undefined; + if (this.options.label && this.label) { - this.label.textContent = `${this._commandAction.label} (${keybinding.getLabel()})`; + this.label.textContent = keybindingLabel + ? `${this._commandAction.label} (${keybindingLabel})` + : this._commandAction.label; } } }, action, { ...options, keybindingNotRenderedWithLabel: true }); From 63c5c4c5322e5cb91b163c083daaeb25dfc6417b Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 20 Jan 2026 15:56:48 -0800 Subject: [PATCH 302/387] mcp: eager push server definitions to ext hosts (#289248) Cleanup #288798 --- src/vs/workbench/api/browser/mainThreadMcp.ts | 9 ++++---- .../workbench/api/common/extHost.protocol.ts | 5 +---- src/vs/workbench/api/common/extHostMcp.ts | 21 +++---------------- 3 files changed, 9 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadMcp.ts b/src/vs/workbench/api/browser/mainThreadMcp.ts index 5dede3a3d9c..f09b6b867fe 100644 --- a/src/vs/workbench/api/browser/mainThreadMcp.ts +++ b/src/vs/workbench/api/browser/mainThreadMcp.ts @@ -100,7 +100,7 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { })); // Subscribe to MCP server definition changes and notify ext host - const onDidChangeMcpServerDefinitionsTrigger = this._register(new RunOnceScheduler(() => this._proxy.$onDidChangeMcpServerDefinitions(), 500)); + const onDidChangeMcpServerDefinitionsTrigger = this._register(new RunOnceScheduler(() => this._publishServerDefinitions(), 500)); this._register(autorun(reader => { const collections = this._mcpRegistry.collections.read(reader); // Read all server definitions to track changes @@ -112,10 +112,11 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { onDidChangeMcpServerDefinitionsTrigger.schedule(); } })); + + onDidChangeMcpServerDefinitionsTrigger.schedule(); } - /** Returns all MCP server definitions known to the editor. */ - $getMcpServerDefinitions(): Promise { + private _publishServerDefinitions() { const collections = this._mcpRegistry.collections.get(); const allServers: McpServerDefinition.Serialized[] = []; @@ -126,7 +127,7 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { } } - return Promise.resolve(allServers); + this._proxy.$onDidChangeMcpServerDefinitions(allServers); } $upsertMcpCollection(collection: McpCollectionDefinition.FromExtHost, serversDto: McpServerDefinition.Serialized[]): void { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index f6be2fb99ef..6945985fc2c 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3172,8 +3172,7 @@ export interface ExtHostMcpShape { $stopMcp(id: number): void; $sendMessage(id: number, message: string): void; $waitForInitialCollectionProviders(): Promise; - /** Notification that MCP server definitions have changed. ExtHost should re-fetch. */ - $onDidChangeMcpServerDefinitions(): void; + $onDidChangeMcpServerDefinitions(servers: McpServerDefinition.Serialized[]): void; } export interface IMcpAuthenticationDetails { @@ -3214,8 +3213,6 @@ export interface MainThreadMcpShape { $getTokenFromServerMetadata(id: number, authDetails: IMcpAuthenticationDetails, options?: IMcpAuthenticationOptions): Promise; $getTokenForProviderId(id: number, providerId: string, scopes: string[], options?: IMcpAuthenticationOptions): Promise; $logMcpAuthSetup(data: IAuthMetadataSource): void; - /** Returns all MCP server definitions known to the editor. */ - $getMcpServerDefinitions(): Promise; } export interface MainThreadDataChannelsShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index d9aacf2fa91..e148c5d0628 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -76,7 +76,6 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService private readonly _onDidChangeMcpServerDefinitions = this._register(new Emitter()); readonly onDidChangeMcpServerDefinitions: Event = this._onDidChangeMcpServerDefinitions.event; private _mcpServerDefinitions: readonly vscode.McpServerDefinition[] = []; - private _mcpServerDefinitionsInitialized = false; constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @@ -91,27 +90,13 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService /** Returns all MCP server definitions known to the editor. */ get mcpServerDefinitions(): readonly vscode.McpServerDefinition[] { - if (!this._mcpServerDefinitionsInitialized) { - this._mcpServerDefinitionsInitialized = true; - // Fetch asynchronously in background and update when ready - this._proxy.$getMcpServerDefinitions().then(dtos => { - this._mcpServerDefinitions = dtos.map(dto => Convert.McpServerDefinition.to(dto)); - this._onDidChangeMcpServerDefinitions.fire(); - }).catch(err => this._logService.error('Failed to fetch MCP server definitions:', err)); - } return this._mcpServerDefinitions; } /** Called by main thread to notify that MCP server definitions have changed. */ - $onDidChangeMcpServerDefinitions(): void { - if (!this._mcpServerDefinitionsInitialized) { - return; - } - // Re-fetch from main thread - this._proxy.$getMcpServerDefinitions().then(dtos => { - this._mcpServerDefinitions = dtos.map(dto => Convert.McpServerDefinition.to(dto)); - this._onDidChangeMcpServerDefinitions.fire(); - }).catch(err => this._logService.error('Failed to fetch MCP server definitions:', err)); + $onDidChangeMcpServerDefinitions(servers: McpServerDefinition.Serialized[]): void { + this._mcpServerDefinitions = servers.map(dto => Convert.McpServerDefinition.to(dto)); + this._onDidChangeMcpServerDefinitions.fire(); } $startMcp(id: number, opts: IStartMcpOptions): void { From 5e019f25d35e8a02dbb25a8cbd278309376fda25 Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:10:54 -0800 Subject: [PATCH 303/387] Fix navigation to `localhost:` in integrated browser (#289253) --- .../contrib/browserView/electron-browser/browserEditor.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts index 3604b8b75eb..2fbb832ecce 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts @@ -467,9 +467,11 @@ export class BrowserEditor extends EditorPane { if (this._model) { this.group.pinEditor(this.input); // pin editor on navigation - const scheme = URL.parse(url)?.protocol; - if (!scheme) { - // If no scheme provided, default to http (to support localhost etc -- sites will generally upgrade to https) + // Special case localhost URLs (e.g., "localhost:3000") to add http:// + if (/^localhost(:|\/|$)/i.test(url)) { + url = 'http://' + url; + } else if (!URL.parse(url)?.protocol) { + // If no scheme provided, default to http (sites will generally upgrade to https) url = 'http://' + url; } From b00da6f941e7cacf6553789902e7b7d6bc3b5194 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Tue, 20 Jan 2026 16:18:53 -0800 Subject: [PATCH 304/387] Privacy footer for agents welcome page --- .../browser/agentSessionsWelcome.ts | 46 +++++++++++++++++++ .../browser/media/agentSessionsWelcome.css | 22 +++++++++ 2 files changed, 68 insertions(+) diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index e437e7144ef..9c4b78fa776 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -40,6 +40,7 @@ import { AgentSessionsWelcomeEditorOptions, AgentSessionsWelcomeInput } from './ import { IChatService } from '../../chat/common/chatService/chatService.js'; import { IChatModel } from '../../chat/common/model/chatModel.js'; import { ISessionTypePickerDelegate } from '../../chat/browser/chat.js'; +import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; import { AgentSessionsControl, IAgentSessionsControlOptions } from '../../chat/browser/agentSessions/agentSessionsControl.js'; import { IAgentSessionsFilter } from '../../chat/browser/agentSessions/agentSessionsViewer.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; @@ -79,6 +80,7 @@ export class AgentSessionsWelcomePage extends EditorPane { @IProductService private readonly productService: IProductService, @IWalkthroughsService private readonly walkthroughsService: IWalkthroughsService, @IChatService private readonly chatService: IChatService, + @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, ) { super(AgentSessionsWelcomePage.ID, group, telemetryService, themeService, storageService); @@ -349,7 +351,51 @@ export class AgentSessionsWelcomePage extends EditorPane { } } + private buildPrivacyNotice(container: HTMLElement): void { + // TOS/Privacy notice for users who are not signed in - reusing walkthrough card design + if (!this.chatEntitlementService.anonymous) { + return; + } + + const providers = this.productService.defaultChatAgent?.provider; + if (!providers || !this.productService.defaultChatAgent?.termsStatementUrl || !this.productService.defaultChatAgent?.privacyStatementUrl) { + return; + } + + const tosCard = append(container, $('.agentSessionsWelcome-walkthroughCard.agentSessionsWelcome-tosCard')); + + // Icon + const iconContainer = append(tosCard, $('.agentSessionsWelcome-walkthroughCard-icon')); + iconContainer.appendChild(renderIcon(Codicon.commentDiscussion)); + + // Content + const content = append(tosCard, $('.agentSessionsWelcome-walkthroughCard-content')); + const title = append(content, $('.agentSessionsWelcome-walkthroughCard-title')); + title.textContent = localize('tosTitle', "AI Feature Trial is Active"); + + const desc = append(content, $('.agentSessionsWelcome-walkthroughCard-description')); + desc.textContent = localize( + { key: 'tosDescription', comment: ['{Locked="]"}'] }, + "By continuing, you agree to {0}'s ", + providers.default.name + ); + const termsLink = append(desc, $('a.agentSessionsWelcome-tosLink')); + termsLink.textContent = localize('terms', "Terms"); + termsLink.href = this.productService.defaultChatAgent.termsStatementUrl; + termsLink.target = '_blank'; + desc.appendChild(document.createTextNode(localize('and', " and "))); + const privacyLink = append(desc, $('a.agentSessionsWelcome-tosLink')); + privacyLink.textContent = localize('privacyStatement', "Privacy statement"); + privacyLink.href = this.productService.defaultChatAgent.privacyStatementUrl; + privacyLink.target = '_blank'; + desc.appendChild(document.createTextNode('.')); + } + private buildFooter(container: HTMLElement): void { + + // Privacy notice + this.buildPrivacyNotice(container); + // Learning link const learningLink = append(container, $('button.agentSessionsWelcome-footerLink')); learningLink.appendChild(renderIcon(Codicon.mortarBoard)); diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css b/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css index 35d56a9feab..2309a4ba97d 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css @@ -358,3 +358,25 @@ width: 16px; height: 16px; } + +/* TOS/Privacy card - extends walkthrough card with TOS-specific overrides */ +.agentSessionsWelcome-tosCard { + width: 100%; + max-width: 800px; + margin-bottom: 16px; + box-sizing: border-box; + cursor: default; +} + +.agentSessionsWelcome-tosCard:hover { + background-color: var(--vscode-welcomePage-tileBackground); +} + +.agentSessionsWelcome-tosLink { + color: var(--vscode-textLink-foreground); + text-decoration: none; +} + +.agentSessionsWelcome-tosLink:hover { + text-decoration: underline; +} From dbe5ad87338d268ff42947d5c86a9c5ca373aa30 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Tue, 20 Jan 2026 16:21:29 -0800 Subject: [PATCH 305/387] Review comment --- .../contrib/chat/browser/agentSessions/agentSessionsModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index 4ca31bc3dec..c170c5eb3a3 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -332,7 +332,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } this.mapSessionToState.set(session.resource, { status, - inProgressTime: isSessionInProgressStatus(status) ? Date.now() : undefined, // this is not accurate but best effort + inProgressTime, }); } From 2c85d171468ab2b1a42ff3ad70ec5339df4d28ef Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:28:42 -0800 Subject: [PATCH 306/387] Obey sendElementsToChat feature flag (#289255) --- .../browserView/electron-browser/browserViewActions.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts index 6b8991df48f..e4ee6bdb3f0 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts @@ -145,18 +145,19 @@ class AddElementToChatAction extends Action2 { static readonly ID = 'workbench.action.browser.addElementToChat'; constructor() { + const enabled = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('config.chat.sendElementsToChat.enabled', true)); super({ id: AddElementToChatAction.ID, title: localize2('browser.addElementToChatAction', 'Add Element to Chat'), icon: Codicon.inspect, - f1: true, - precondition: ChatContextKeys.enabled, + f1: false, + precondition: enabled, toggled: CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE, menu: { id: MenuId.BrowserActionsToolbar, group: 'actions', order: 1, - when: ChatContextKeys.enabled + when: enabled }, keybinding: [{ when: BROWSER_EDITOR_ACTIVE, From d58526e4612f0de0015ffd13950e16372c6276b8 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:36:21 -0800 Subject: [PATCH 307/387] Merge pull request #289251 from microsoft/joshspicer/agent-status-widget-updates 'Agent Status' widget tweaks --- .../agentSessionProjectionActions.ts | 20 + .../agentSessionProjectionService.ts | 2 +- .../agentSessionsExperiments.contribution.ts | 19 +- .../experiments/agentTitleBarStatusService.ts | 9 +- .../experiments/agentTitleBarStatusWidget.ts | 414 +++++++++++------- .../media/agenttitlebarstatuswidget.css | 172 +++++++- .../contrib/chat/browser/chat.contribution.ts | 10 +- .../contrib/chat/common/constants.ts | 1 + 8 files changed, 461 insertions(+), 186 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionActions.ts index cc74b7234b5..15182db4d4c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionActions.ts @@ -108,3 +108,23 @@ export class ToggleAgentStatusAction extends ToggleTitleBarConfigAction { } //#endregion + +//#region Toggle Unified Agents Bar + +export class ToggleUnifiedAgentsBarAction extends ToggleTitleBarConfigAction { + constructor() { + super( + ChatConfiguration.UnifiedAgentsBar, + localize('toggle.unifiedAgentsBar', 'Unified Agents Bar'), + localize('toggle.unifiedAgentsBarDescription', "Toggle Unified Agents Bar, replacing the classic command center search box."), 7, + ContextKeyExpr.and( + ChatContextKeys.enabled, + IsCompactTitleBarContext.negate(), + ChatContextKeys.supported, + ContextKeyExpr.has(`config.${ChatConfiguration.AgentStatusEnabled}`) + ) + ); + } +} + +//#endregion diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts index e798dc3ce8a..2755033c631 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts @@ -262,7 +262,7 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS this.layoutService.mainContainer.classList.add('agent-session-projection-active'); // Update the agent status to show session mode - this.agentTitleBarStatusService.enterSessionMode(session.resource.toString(), session.label); + this.agentTitleBarStatusService.enterSessionMode(session.resource, session.label); if (!wasActive) { this._onDidChangeProjectionMode.fire(true); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts index 153e27cfde7..5a167c4584e 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts @@ -6,13 +6,14 @@ import { registerSingleton, InstantiationType } from '../../../../../../platform/instantiation/common/extensions.js'; import { MenuId, MenuRegistry, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; import { IAgentSessionProjectionService, AgentSessionProjectionService, AgentSessionProjectionOpenerContribution } from './agentSessionProjectionService.js'; -import { EnterAgentSessionProjectionAction, ExitAgentSessionProjectionAction, ToggleAgentStatusAction } from './agentSessionProjectionActions.js'; +import { EnterAgentSessionProjectionAction, ExitAgentSessionProjectionAction, ToggleAgentStatusAction, ToggleUnifiedAgentsBarAction } from './agentSessionProjectionActions.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../../common/contributions.js'; import { AgentTitleBarStatusRendering } from './agentTitleBarStatusWidget.js'; import { AgentTitleBarStatusService, IAgentTitleBarStatusService } from './agentTitleBarStatusService.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { localize } from '../../../../../../nls.js'; import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { ProductQualityContext } from '../../../../../../platform/contextkey/common/contextkeys.js'; import { ChatConfiguration } from '../../../common/constants.js'; // #region Agent Session Projection & Status @@ -20,6 +21,7 @@ import { ChatConfiguration } from '../../../common/constants.js'; registerAction2(EnterAgentSessionProjectionAction); registerAction2(ExitAgentSessionProjectionAction); registerAction2(ToggleAgentStatusAction); +registerAction2(ToggleUnifiedAgentsBarAction); registerSingleton(IAgentSessionProjectionService, AgentSessionProjectionService, InstantiationType.Delayed); registerSingleton(IAgentTitleBarStatusService, AgentTitleBarStatusService, InstantiationType.Delayed); @@ -43,6 +45,21 @@ MenuRegistry.appendMenuItem(MenuId.AgentsTitleBarControlMenu, { title: localize('openChat', "Open Chat"), }, when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentStatusEnabled}`), + order: 1 +}); + +// Toggle for Unified Agents Bar (Insiders only) +MenuRegistry.appendMenuItem(MenuId.AgentsTitleBarControlMenu, { + command: { + id: `toggle.${ChatConfiguration.UnifiedAgentsBar}`, + title: localize('toggleUnifiedAgentsBar', "Unified Agents Bar"), + toggled: ContextKeyExpr.has(`config.${ChatConfiguration.UnifiedAgentsBar}`), + }, + when: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${ChatConfiguration.AgentStatusEnabled}`), + ProductQualityContext.notEqualsTo('stable') + ), + order: 10 }); //#endregion diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusService.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusService.ts index 0c8389b4060..f6a1c08ad1d 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusService.ts @@ -5,6 +5,7 @@ import { Emitter, Event } from '../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../../base/common/uri.js'; import { createDecorator } from '../../../../../../platform/instantiation/common/instantiation.js'; //#region Agent Status Mode @@ -17,7 +18,7 @@ export enum AgentStatusMode { } export interface IAgentStatusSessionInfo { - readonly sessionId: string; + readonly sessionResource: URI; readonly title: string; } @@ -52,7 +53,7 @@ export interface IAgentTitleBarStatusService { * Enter session mode, showing the session title and escape button. * Used by Agent Session Projection when entering a focused session view. */ - enterSessionMode(sessionId: string, title: string): void; + enterSessionMode(sessionResource: URI, title: string): void; /** * Exit session mode, returning to the default mode with workspace name and stats. @@ -88,8 +89,8 @@ export class AgentTitleBarStatusService extends Disposable implements IAgentTitl private readonly _onDidChangeSessionInfo = this._register(new Emitter()); readonly onDidChangeSessionInfo = this._onDidChangeSessionInfo.event; - enterSessionMode(sessionId: string, title: string): void { - const newInfo: IAgentStatusSessionInfo = { sessionId, title }; + enterSessionMode(sessionResource: URI, title: string): void { + const newInfo: IAgentStatusSessionInfo = { sessionResource, title }; const modeChanged = this._mode !== AgentStatusMode.Session; this._mode = AgentStatusMode.Session; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts index bb4bdaf6386..0d1ced311e9 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts @@ -18,7 +18,7 @@ import { ExitAgentSessionProjectionAction } from './agentSessionProjectionAction import { IAgentSessionsService } from '../agentSessionsService.js'; import { AgentSessionStatus, IAgentSession, isSessionInProgressStatus } from '../agentSessionsModel.js'; import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js'; -import { IAction, SubmenuAction } from '../../../../../../base/common/actions.js'; +import { IAction, SubmenuAction, toAction } from '../../../../../../base/common/actions.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import { IBrowserWorkbenchEnvironmentService } from '../../../../../services/environment/browser/environmentService.js'; @@ -29,9 +29,10 @@ import { Schemas } from '../../../../../../base/common/network.js'; import { renderAsPlaintext } from '../../../../../../base/browser/markdownRenderer.js'; import { openSession } from '../agentSessionsOpener.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { IMenuService, MenuId, SubmenuItemAction } from '../../../../../../platform/actions/common/actions.js'; +import { IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from '../../../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { HiddenItemStrategy, WorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; +import { DropdownWithPrimaryActionViewItem } from '../../../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js'; import { createActionViewItem } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; import { FocusAgentSessionsAction } from '../agentSessionsActions.js'; @@ -42,9 +43,13 @@ import { mainWindow } from '../../../../../../base/browser/window.js'; import { LayoutSettings } from '../../../../../services/layout/browser/layoutService.js'; import { ChatConfiguration } from '../../../common/constants.js'; -// Action triggered when clicking the main pill - change this to modify the primary action -const ACTION_ID = 'workbench.action.quickchat.toggle'; -const SEARCH_BUTTON_ACITON_ID = 'workbench.action.quickOpenWithModes'; +// Action IDs +const QUICK_CHAT_ACTION_ID = 'workbench.action.quickchat.toggle'; +const TOGGLE_CHAT_ACTION_ID = 'workbench.action.chat.toggle'; +const QUICK_OPEN_ACTION_ID = 'workbench.action.quickOpenWithModes'; + +// Storage key for filter state +const FILTER_STORAGE_KEY = 'agentSessions.filterExcludes.agentsessionsviewerfiltersubmenu'; const NLS_EXTENSION_HOST = localize('devExtensionWindowTitlePrefix', "[Extension Development Host]"); const TITLE_DIRTY = '\u25cf '; @@ -60,8 +65,6 @@ const TITLE_DIRTY = '\u25cf '; */ export class AgentTitleBarStatusWidget extends BaseActionViewItem { - private static readonly _quickOpenCommandId = 'workbench.action.quickOpenWithModes'; - private _container: HTMLElement | undefined; private readonly _dynamicDisposables = this._register(new DisposableStore()); @@ -71,9 +74,15 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { /** Cached render state to avoid unnecessary DOM rebuilds */ private _lastRenderState: string | undefined; + /** Guard to prevent re-entrant rendering */ + private _isRendering = false; + /** Reusable menu for CommandCenterCenter items (e.g., debug toolbar) */ private readonly _commandCenterMenu; + /** Menu for ChatTitleBarMenu items (same as chat controls dropdown) */ + private readonly _chatTitleBarMenu; + constructor( action: IAction, options: IBaseActionViewItemOptions | undefined, @@ -91,12 +100,16 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IStorageService private readonly storageService: IStorageService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(undefined, action, options); // Create menu for CommandCenterCenter to get items like debug toolbar this._commandCenterMenu = this._register(this.menuService.createMenu(MenuId.CommandCenterCenter, this.contextKeyService)); + // Create menu for ChatTitleBarMenu to show in sparkle section dropdown + this._chatTitleBarMenu = this._register(this.menuService.createMenu(MenuId.ChatTitleBarMenu, this.contextKeyService)); + // Re-render when control mode or session info changes this._register(this.agentTitleBarStatusService.onDidChangeMode(() => { this._render(); @@ -128,6 +141,19 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { this._lastRenderState = undefined; // Force re-render this._render(); })); + + // Re-render when storage changes (e.g., filter state changes from sessions view) + this._register(this.storageService.onDidChangeValue(StorageScope.PROFILE, 'agentSessions.filterExcludes.agentsessionsviewerfiltersubmenu', this._store)(() => { + this._render(); + })); + + // Re-render when enhanced setting changes + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(ChatConfiguration.UnifiedAgentsBar)) { + this._lastRenderState = undefined; // Force re-render + this._render(); + } + })); } override render(container: HTMLElement): void { @@ -144,57 +170,78 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { return; } - // Compute current render state to avoid unnecessary DOM rebuilds - const mode = this.agentTitleBarStatusService.mode; - const sessionInfo = this.agentTitleBarStatusService.sessionInfo; - const { activeSessions, unreadSessions, attentionNeededSessions } = this._getSessionStats(); - - // Get attention session info for state computation - const attentionSession = attentionNeededSessions.length > 0 - ? [...attentionNeededSessions].sort((a, b) => { - const timeA = a.timing.lastRequestStarted ?? a.timing.created; - const timeB = b.timing.lastRequestStarted ?? b.timing.created; - return timeB - timeA; - })[0] - : undefined; - - const attentionText = attentionSession?.description - ? (typeof attentionSession.description === 'string' - ? attentionSession.description - : renderAsPlaintext(attentionSession.description)) - : attentionSession?.label; - - const label = this._getLabel(); - - // Build state key for comparison - const stateKey = JSON.stringify({ - mode, - sessionTitle: sessionInfo?.title, - activeCount: activeSessions.length, - unreadCount: unreadSessions.length, - attentionCount: attentionNeededSessions.length, - attentionText, - label, - }); - - // Skip re-render if state hasn't changed - if (this._lastRenderState === stateKey) { + if (this._isRendering) { return; } - this._lastRenderState = stateKey; + this._isRendering = true; - // Clear existing content - reset(this._container); + try { + // Compute current render state to avoid unnecessary DOM rebuilds + const mode = this.agentTitleBarStatusService.mode; + const sessionInfo = this.agentTitleBarStatusService.sessionInfo; + const { activeSessions, unreadSessions, attentionNeededSessions } = this._getSessionStats(); - // Clear previous disposables for dynamic content - this._dynamicDisposables.clear(); + // Get attention session info for state computation + const attentionSession = attentionNeededSessions.length > 0 + ? [...attentionNeededSessions].sort((a, b) => { + const timeA = a.timing.lastRequestStarted ?? a.timing.created; + const timeB = b.timing.lastRequestStarted ?? b.timing.created; + return timeB - timeA; + })[0] + : undefined; - if (this.agentTitleBarStatusService.mode === AgentStatusMode.Session) { - // Agent Session Projection mode - show session title + close button - this._renderSessionMode(this._dynamicDisposables); - } else { - // Default mode - show copilot pill with optional in-progress indicator - this._renderChatInputMode(this._dynamicDisposables); + const attentionText = attentionSession?.description + ? (typeof attentionSession.description === 'string' + ? attentionSession.description + : renderAsPlaintext(attentionSession.description)) + : attentionSession?.label; + + const label = this._getLabel(); + + // Get current filter state for state key + const { isFilteredToUnread, isFilteredToInProgress } = this._getCurrentFilterState(); + + // Check if enhanced mode is enabled + const isEnhanced = this.configurationService.getValue(ChatConfiguration.UnifiedAgentsBar) === true; + + // Build state key for comparison + const stateKey = JSON.stringify({ + mode, + sessionTitle: sessionInfo?.title, + activeCount: activeSessions.length, + unreadCount: unreadSessions.length, + attentionCount: attentionNeededSessions.length, + attentionText, + label, + isFilteredToUnread, + isFilteredToInProgress, + isEnhanced, + }); + + // Skip re-render if state hasn't changed + if (this._lastRenderState === stateKey) { + return; + } + this._lastRenderState = stateKey; + + // Clear existing content + reset(this._container); + + // Clear previous disposables for dynamic content + this._dynamicDisposables.clear(); + + if (this.agentTitleBarStatusService.mode === AgentStatusMode.Session) { + // Agent Session Projection mode - show session title + close button + this._renderSessionMode(this._dynamicDisposables); + } else if (isEnhanced) { + // Enhanced mode - show full pill with label + status badge + this._renderChatInputMode(this._dynamicDisposables); + } else { + // Basic mode - show only the status badge (sparkle + unread/active counts) + this._renderBadgeOnlyMode(this._dynamicDisposables); + } + } finally { + this._isRendering = false; } } @@ -313,7 +360,7 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { if (this._displayedSession) { return localize('openSessionTooltip', "Open session: {0}", this._displayedSession.label); } - const kbForTooltip = this.keybindingService.lookupKeybinding(ACTION_ID)?.getLabel(); + const kbForTooltip = this.keybindingService.lookupKeybinding(QUICK_CHAT_ACTION_ID)?.getLabel(); return kbForTooltip ? localize('askTooltip', "Open Quick Chat ({0})", kbForTooltip) : localize('askTooltip2', "Open Quick Chat"); @@ -375,6 +422,21 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { this._renderStatusBadge(disposables, activeSessions, unreadSessions); } + /** + * Render badge-only mode - just the status badge without the full pill. + * Used when Agent Status is enabled but Enhanced Agent Status is not. + */ + private _renderBadgeOnlyMode(disposables: DisposableStore): void { + if (!this._container) { + return; + } + + const { activeSessions, unreadSessions } = this._getSessionStats(); + + // Status badge only - no pill, no command center toolbar + this._renderStatusBadge(disposables, activeSessions, unreadSessions); + } + // #endregion // #region Reusable Components @@ -394,7 +456,7 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { for (const [, actions] of this._commandCenterMenu.getActions({ shouldForwardArgs: true })) { for (const action of actions) { // Filter out the quick open action - we provide our own search UI - if (action.id === AgentTitleBarStatusWidget._quickOpenCommandId) { + if (action.id === QUICK_OPEN_ACTION_ID) { continue; } // For submenus (like debug toolbar), add the submenu actions @@ -450,7 +512,7 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { // Setup hover const hoverDelegate = getDefaultHoverDelegate('mouse'); - const searchKb = this.keybindingService.lookupKeybinding(SEARCH_BUTTON_ACITON_ID)?.getLabel(); + const searchKb = this.keybindingService.lookupKeybinding(QUICK_OPEN_ACTION_ID)?.getLabel(); const searchTooltip = searchKb ? localize('openQuickOpenTooltip', "Go to File ({0})", searchKb) : localize('openQuickOpenTooltip2', "Go to File"); @@ -460,7 +522,7 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { disposables.add(addDisposableListener(searchButton, EventType.CLICK, (e) => { e.preventDefault(); e.stopPropagation(); - this.commandService.executeCommand(SEARCH_BUTTON_ACITON_ID); + this.commandService.executeCommand(QUICK_OPEN_ACTION_ID); })); // Keyboard handler @@ -468,15 +530,15 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); - this.commandService.executeCommand(SEARCH_BUTTON_ACITON_ID); + this.commandService.executeCommand(QUICK_OPEN_ACTION_ID); } })); } /** * Render the status badge showing in-progress and/or unread session counts. - * Shows split UI with both indicators when both types exist. - * When no notifications, shows a chat sparkle icon. + * Shows split UI with sparkle icon on left, then unread and active indicators. + * Always renders the sparkle icon section. */ private _renderStatusBadge(disposables: DisposableStore, activeSessions: IAgentSession[], unreadSessions: IAgentSession[]): void { if (!this._container) { @@ -485,7 +547,6 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { const hasActiveSessions = activeSessions.length > 0; const hasUnreadSessions = unreadSessions.length > 0; - const hasContent = hasActiveSessions || hasUnreadSessions; // Auto-clear filter if the filtered category becomes empty this._clearFilterIfCategoryEmpty(hasUnreadSessions, hasActiveSessions); @@ -493,15 +554,52 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { const badge = $('div.agent-status-badge'); this._container.appendChild(badge); - // When no notifications, hide the badge - if (!hasContent) { - badge.classList.add('empty'); - return; + // Sparkle dropdown button section (always visible on left) - proper button with dropdown menu + const sparkleContainer = $('span.agent-status-badge-section.sparkle'); + badge.appendChild(sparkleContainer); + + // Get menu actions for dropdown + const menuActions: IAction[] = []; + for (const [, actions] of this._chatTitleBarMenu.getActions({ shouldForwardArgs: true })) { + menuActions.push(...actions); } + // Create primary action (toggle chat) + const primaryAction = this.instantiationService.createInstance(MenuItemAction, { + id: TOGGLE_CHAT_ACTION_ID, + title: localize('toggleChat', "Toggle Chat"), + icon: Codicon.chatSparkle, + }, undefined, undefined, undefined, undefined); + + // Create dropdown action (empty label prevents default tooltip - we have our own hover) + const dropdownAction = toAction({ + id: 'agentStatus.sparkle.dropdown', + label: '', + run() { } + }); + + // Create the dropdown with primary action button + const sparkleDropdown = this.instantiationService.createInstance( + DropdownWithPrimaryActionViewItem, + primaryAction, + dropdownAction, + menuActions, + 'agent-status-sparkle-dropdown', + { skipTelemetry: true } + ); + sparkleDropdown.render(sparkleContainer); + disposables.add(sparkleDropdown); + + // Hover delegate for status sections + const hoverDelegate = getDefaultHoverDelegate('mouse'); + // Unread section (blue dot + count) if (hasUnreadSessions) { + const { isFilteredToUnread } = this._getCurrentFilterState(); const unreadSection = $('span.agent-status-badge-section.unread'); + if (isFilteredToUnread) { + unreadSection.classList.add('filtered'); + } unreadSection.setAttribute('role', 'button'); unreadSection.tabIndex = 0; const unreadIcon = $('span.agent-status-icon'); @@ -525,11 +623,21 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { this._openSessionsWithFilter('unread'); } })); + + // Hover tooltip for unread section + const unreadTooltip = unreadSessions.length === 1 + ? localize('unreadSessionsTooltip1', "{0} unread session", unreadSessions.length) + : localize('unreadSessionsTooltip', "{0} unread sessions", unreadSessions.length); + disposables.add(this.hoverService.setupManagedHover(hoverDelegate, unreadSection, unreadTooltip)); } // In-progress section (session-in-progress icon + count) if (hasActiveSessions) { + const { isFilteredToInProgress } = this._getCurrentFilterState(); const activeSection = $('span.agent-status-badge-section.active'); + if (isFilteredToInProgress) { + activeSection.classList.add('filtered'); + } activeSection.setAttribute('role', 'button'); activeSection.tabIndex = 0; const runningIcon = $('span.agent-status-icon'); @@ -553,24 +661,14 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { this._openSessionsWithFilter('inProgress'); } })); + + // Hover tooltip for active section + const activeTooltip = activeSessions.length === 1 + ? localize('activeSessionsTooltip1', "{0} session in progress", activeSessions.length) + : localize('activeSessionsTooltip', "{0} sessions in progress", activeSessions.length); + disposables.add(this.hoverService.setupManagedHover(hoverDelegate, activeSection, activeTooltip)); } - // Setup hover with combined tooltip - const hoverDelegate = getDefaultHoverDelegate('mouse'); - disposables.add(this.hoverService.setupManagedHover(hoverDelegate, badge, () => { - const parts: string[] = []; - if (hasUnreadSessions) { - parts.push(unreadSessions.length === 1 - ? localize('unreadSessionsTooltip1', "{0} unread session", unreadSessions.length) - : localize('unreadSessionsTooltip', "{0} unread sessions", unreadSessions.length)); - } - if (hasActiveSessions) { - parts.push(activeSessions.length === 1 - ? localize('activeSessionsTooltip1', "{0} session in progress", activeSessions.length) - : localize('activeSessionsTooltip', "{0} sessions in progress", activeSessions.length)); - } - return parts.join(', '); - })); } /** @@ -578,107 +676,99 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { * For example, if filtered to "unread" but no unread sessions exist, clear the filter. */ private _clearFilterIfCategoryEmpty(hasUnreadSessions: boolean, hasActiveSessions: boolean): void { - const FILTER_STORAGE_KEY = 'agentSessions.filterExcludes.agentsessionsviewerfiltersubmenu'; - - const currentFilterStr = this.storageService.get(FILTER_STORAGE_KEY, StorageScope.PROFILE); - if (!currentFilterStr) { - return; - } - - let currentFilter: { providers: string[]; states: AgentSessionStatus[]; archived: boolean; read: boolean } | undefined; - try { - currentFilter = JSON.parse(currentFilterStr); - } catch { - return; - } - - if (!currentFilter) { - return; - } - - // Detect if filtered to unread (read=true excludes read sessions, leaving only unread) - const isFilteredToUnread = currentFilter.read === true && currentFilter.states.length === 0; - // Detect if filtered to in-progress (2 excluded states = Completed + Failed) - const isFilteredToInProgress = currentFilter.states?.length === 2 && currentFilter.read === false; + const { isFilteredToUnread, isFilteredToInProgress } = this._getCurrentFilterState(); // Clear filter if filtered category is now empty if ((isFilteredToUnread && !hasUnreadSessions) || (isFilteredToInProgress && !hasActiveSessions)) { - const clearedFilter = { - providers: [], - states: [], - archived: true, - read: false - }; - this.storageService.store(FILTER_STORAGE_KEY, JSON.stringify(clearedFilter), StorageScope.PROFILE, StorageTarget.USER); + this._clearFilter(); } } + /** + * Get the current filter state from storage. + */ + private _getCurrentFilterState(): { isFilteredToUnread: boolean; isFilteredToInProgress: boolean } { + const filter = this._getStoredFilter(); + if (!filter) { + return { isFilteredToUnread: false, isFilteredToInProgress: false }; + } + + // Detect if filtered to unread (read=true excludes read sessions, leaving only unread) + const isFilteredToUnread = filter.read === true && filter.states.length === 0; + // Detect if filtered to in-progress (2 excluded states = Completed + Failed) + const isFilteredToInProgress = filter.states?.length === 2 && filter.read === false; + + return { isFilteredToUnread, isFilteredToInProgress }; + } + + /** + * Get the stored filter object from storage. + */ + private _getStoredFilter(): { providers: string[]; states: AgentSessionStatus[]; archived: boolean; read: boolean } | undefined { + const filterStr = this.storageService.get(FILTER_STORAGE_KEY, StorageScope.PROFILE); + if (!filterStr) { + return undefined; + } + try { + return JSON.parse(filterStr); + } catch { + return undefined; + } + } + + /** + * Store a filter object to storage. + */ + private _storeFilter(filter: { providers: string[]; states: AgentSessionStatus[]; archived: boolean; read: boolean }): void { + this.storageService.store(FILTER_STORAGE_KEY, JSON.stringify(filter), StorageScope.PROFILE, StorageTarget.USER); + } + + /** + * Clear all filters (reset to default). + */ + private _clearFilter(): void { + this._storeFilter({ + providers: [], + states: [], + archived: true, + read: false + }); + } + /** * Opens the agent sessions view with a specific filter applied, or clears filter if already applied. * @param filterType 'unread' to show only unread sessions, 'inProgress' to show only in-progress sessions */ private _openSessionsWithFilter(filterType: 'unread' | 'inProgress'): void { - const FILTER_STORAGE_KEY = 'agentSessions.filterExcludes.agentsessionsviewerfiltersubmenu'; - - // Check current filter to see if we should toggle off - const currentFilterStr = this.storageService.get(FILTER_STORAGE_KEY, StorageScope.PROFILE); - let currentFilter: { providers: string[]; states: AgentSessionStatus[]; archived: boolean; read: boolean } | undefined; - if (currentFilterStr) { - try { - currentFilter = JSON.parse(currentFilterStr); - } catch { - // Ignore parse errors - } - } - - // Determine if the current filter matches what we're clicking - const isCurrentlyFilteredToUnread = currentFilter?.read === true && currentFilter.states.length === 0; - const isCurrentlyFilteredToInProgress = currentFilter?.states?.length === 2 && currentFilter.read === false; - - // Build filter excludes based on filter type - let excludes: { providers: string[]; states: AgentSessionStatus[]; archived: boolean; read: boolean }; + const { isFilteredToUnread, isFilteredToInProgress } = this._getCurrentFilterState(); + // Toggle filter based on current state if (filterType === 'unread') { - if (isCurrentlyFilteredToUnread) { - // Toggle off - clear all filters - excludes = { - providers: [], - states: [], - archived: true, - read: false - }; + if (isFilteredToUnread) { + this._clearFilter(); } else { // Exclude read sessions to show only unread - excludes = { + this._storeFilter({ providers: [], states: [], archived: true, - read: true // exclude read sessions - }; + read: true + }); } } else { - if (isCurrentlyFilteredToInProgress) { - // Toggle off - clear all filters - excludes = { - providers: [], - states: [], - archived: true, - read: false - }; + if (isFilteredToInProgress) { + this._clearFilter(); } else { // Exclude Completed and Failed to show InProgress and NeedsInput - excludes = { + this._storeFilter({ providers: [], states: [AgentSessionStatus.Completed, AgentSessionStatus.Failed], archived: true, read: false - }; + }); } } - // Store the filter - this.storageService.store(FILTER_STORAGE_KEY, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER); - // Open the sessions view this.commandService.executeCommand(FocusAgentSessionsAction.id); } @@ -732,7 +822,7 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { if (this._displayedSession) { this.instantiationService.invokeFunction(openSession, this._displayedSession); } else { - this.commandService.executeCommand(ACTION_ID); + this.commandService.executeCommand(QUICK_CHAT_ACTION_ID); } } @@ -834,7 +924,7 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { * Provides custom rendering for the agent status in the command center. * Uses IActionViewItemService to render a custom AgentStatusWidget * for the AgentsControlMenu submenu. - * Also adds a CSS class to the workbench when agent status is enabled. + * Also adds CSS classes to the workbench based on settings. */ export class AgentTitleBarStatusRendering extends Disposable implements IWorkbenchContribution { @@ -854,20 +944,28 @@ export class AgentTitleBarStatusRendering extends Disposable implements IWorkben return instantiationService.createInstance(AgentTitleBarStatusWidget, action, options); }, undefined)); - // Add/remove CSS class on workbench based on setting - // Also force enable command center when agent status is enabled + // Add/remove CSS classes on workbench based on settings + // Force enable command center and disable chat controls when agent status is enabled const updateClass = () => { const enabled = configurationService.getValue(ChatConfiguration.AgentStatusEnabled) === true; + const enhanced = configurationService.getValue(ChatConfiguration.UnifiedAgentsBar) === true; + mainWindow.document.body.classList.toggle('agent-status-enabled', enabled); + mainWindow.document.body.classList.toggle('unified-agents-bar', enabled && enhanced); // Force enable command center when agent status is enabled if (enabled && configurationService.getValue(LayoutSettings.COMMAND_CENTER) !== true) { configurationService.updateValue(LayoutSettings.COMMAND_CENTER, true); } + + // Turn off chat controls when agent status is enabled (they would be duplicates) + if (enabled && configurationService.getValue('chat.commandCenter.enabled') === true) { + configurationService.updateValue('chat.commandCenter.enabled', false); + } }; updateClass(); this._register(configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(ChatConfiguration.AgentStatusEnabled)) { + if (e.affectsConfiguration(ChatConfiguration.AgentStatusEnabled) || e.affectsConfiguration(ChatConfiguration.UnifiedAgentsBar)) { updateClass(); } })); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css index e4af6e88de4..0dfed92e7b6 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css @@ -3,30 +3,56 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* Hide command center search box when agent status enabled */ -.agent-status-enabled .command-center .action-item.command-center-center { +/* + * Command Center Integration + * Hide default search box when enhanced agent status replaces it. + */ +.unified-agents-bar .command-center .action-item.command-center-center { display: none !important; } -/* Give agent status same width as search box */ -.agent-status-enabled .command-center .action-item.agent-status-container { +.agent-status-enabled .command-center { + overflow: visible !important; +} + +/* + * Enhanced mode layout - full-width pill replacing command center search. + */ +.unified-agents-bar .command-center .action-item.agent-status-container { width: 38vw; max-width: 600px; display: flex; flex-direction: row; flex-wrap: nowrap; align-items: center; - justify-content: center; + justify-content: flex-start; } +/* + * Badge-only mode layout - compact badge next to command center. + */ +.agent-status-enabled:not(.unified-agents-bar) .command-center .action-item.agent-status-container { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + overflow: visible; +} + +/* + * Container - holds pill and/or badge. + * Right padding reserves space for badge chevron expansion. + */ .agent-status-container { display: flex; flex-direction: row; flex-wrap: nowrap; align-items: center; - justify-content: center; + justify-content: flex-start; gap: 4px; + padding-right: 22px; -webkit-app-region: no-drag; + overflow: visible; } /* Pill - shared styles */ @@ -283,26 +309,27 @@ align-items: center; } -/* Status badge (separate rectangle on right of pill) */ +/* + * Status Badge + * Split UI showing: [Sparkle + Chevron] | [Unread] | [Active] + * Expands rightward when chevron appears on hover. + */ .agent-status-badge { display: flex; align-items: center; gap: 0; height: 22px; border-radius: 6px; - overflow: hidden; - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent); - border: 1px solid color-mix(in srgb, var(--vscode-progressBar-background) 50%, transparent); + background-color: var(--vscode-quickInput-background); + border: 1px solid var(--vscode-commandCenter-border, transparent); flex-shrink: 0; -webkit-app-region: no-drag; + position: relative; + overflow: visible; + margin-left: auto; } -/* Empty badge - completely hidden */ -.agent-status-badge.empty { - display: none; -} - -/* Badge section (for split UI) */ +/* Badge sections - clickable segments with hover states */ .agent-status-badge-section { display: flex; align-items: center; @@ -310,9 +337,29 @@ padding: 0 8px; height: 100%; position: relative; + cursor: pointer; } -/* Separator between sections */ +.agent-status-badge-section:hover { + background-color: var(--vscode-chat-requestBubbleBackground); +} + +.agent-status-badge-section:focus { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + +/* Active filter state - highlighted when filter is applied */ +.agent-status-badge-section.filtered { + background-color: var(--vscode-inputOption-activeBackground); +} + +.agent-status-badge-section.filtered:hover { + background-color: var(--vscode-inputOption-activeBackground); + filter: brightness(1.1); +} + +/* Vertical separator between badge sections */ .agent-status-badge-section + .agent-status-badge-section::before { content: ''; position: absolute; @@ -320,10 +367,95 @@ top: 4px; bottom: 4px; width: 1px; - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 40%, transparent); + background-color: var(--vscode-commandCenter-border, rgba(128, 128, 128, 0.35)); } -/* Unread section styling */ +/* Sparkle section - primary chat action with expandable chevron */ +.agent-status-badge-section.sparkle { + color: var(--vscode-foreground); + gap: 0; + padding: 0; +} + +/* Disable hover on sparkle section itself since children handle it */ +.agent-status-badge-section.sparkle:hover { + background-color: transparent; +} + +/* Dropdown button inside sparkle section */ +.agent-status-badge-section.sparkle .monaco-dropdown-with-primary { + display: flex; + align-items: center; + height: 100%; +} + +.agent-status-badge-section.sparkle .action-container { + display: flex; + align-items: center; + justify-content: center; + padding: 0 6px; + height: 100%; +} + +.agent-status-badge-section.sparkle .action-container:hover { + background-color: var(--vscode-chat-requestBubbleBackground); +} + +.agent-status-badge-section.sparkle .action-container:focus-within { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + +.agent-status-badge-section.sparkle .action-container .action-label { + display: flex; + align-items: center; + justify-content: center; + background: none !important; + width: 16px; + height: 16px; +} + +/* + * Chevron dropdown - slides out from sparkle section on hover. + * Badge expands rightward into reserved container padding. + */ +.agent-status-badge-section.sparkle .dropdown-action-container { + display: flex; + align-items: center; + justify-content: center; + width: 0; + height: 100%; + overflow: hidden; + transition: width 0.15s ease-out 0.3s; /* Linger 300ms before collapsing */ +} + +.agent-status-badge-section.sparkle:hover .dropdown-action-container, +.agent-status-badge-section.sparkle .dropdown-action-container:hover { + width: 22px; + transition: width 0.15s ease-out 0.1s; +} + +.agent-status-badge-section.sparkle .dropdown-action-container:hover { + background-color: var(--vscode-chat-requestBubbleBackground); +} + +.agent-status-badge-section.sparkle .dropdown-action-container:focus-within { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + +.agent-status-badge-section.sparkle .dropdown-action-container .action-label { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + font-size: 18px; + background-position: center !important; + background-size: 18px !important; +} + +/* Unread section - blue dot indicator with count */ .agent-status-badge-section.unread { color: var(--vscode-foreground); } @@ -333,7 +465,7 @@ color: var(--vscode-notificationsInfoIcon-foreground); } -/* Active/in-progress section styling */ +/* Active section - in-progress indicator with count */ .agent-status-badge-section.active { color: var(--vscode-foreground); } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 08a5b32efaf..2e6bc1c1251 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -194,7 +194,13 @@ configurationRegistry.registerConfiguration({ }, [ChatConfiguration.AgentStatusEnabled]: { type: 'boolean', - markdownDescription: nls.localize('chat.agentsControl.enabled', "Controls whether the Agent Status is shown in the title bar command center, replacing the search box with quick access to chat sessions. Enabling this setting will automatically enable {0}.", '`#window.commandCenter#`'), + markdownDescription: nls.localize('chat.agentsControl.enabled', "Controls whether the Agent Status indicator is shown in the title bar command center. Enabling this setting will automatically enable {0}.", '`#window.commandCenter#`'), + default: false, + tags: ['experimental'] + }, + [ChatConfiguration.UnifiedAgentsBar]: { + type: 'boolean', + markdownDescription: nls.localize('chat.unifiedAgentsBar.enabled', "When enabled alongside {0}, replaces the command center search box with a unified chat and search widget.", '`#chat.agentsControl.enabled#`'), default: false, tags: ['experimental'] }, @@ -202,7 +208,7 @@ configurationRegistry.registerConfiguration({ type: 'boolean', markdownDescription: nls.localize('chat.agentSessionProjection.enabled', "Controls whether Agent Session Projection mode is enabled for reviewing agent sessions in a focused workspace."), default: false, - tags: ['experimental'] + tags: ['experimental'], }, 'chat.implicitContext.enabled': { type: 'object', diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index dce1f75f7a0..93219c0793e 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -11,6 +11,7 @@ import { RawContextKey } from '../../../../platform/contextkey/common/contextkey export enum ChatConfiguration { AgentEnabled = 'chat.agent.enabled', AgentStatusEnabled = 'chat.agentsControl.enabled', + UnifiedAgentsBar = 'chat.unifiedAgentsBar.enabled', AgentSessionProjectionEnabled = 'chat.agentSessionProjection.enabled', EditModeHidden = 'chat.editMode.hidden', Edits2Enabled = 'chat.edits2.enabled', From 4a8af5d7f90ad49e7416fd3a7ceb0f1fa014997e Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega <48293249+osortega@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:36:28 -0800 Subject: [PATCH 308/387] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../welcomeAgentSessions/browser/agentSessionsWelcome.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index 9c4b78fa776..4c4ff736417 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -358,7 +358,7 @@ export class AgentSessionsWelcomePage extends EditorPane { } const providers = this.productService.defaultChatAgent?.provider; - if (!providers || !this.productService.defaultChatAgent?.termsStatementUrl || !this.productService.defaultChatAgent?.privacyStatementUrl) { + if (!providers || !providers.default || !this.productService.defaultChatAgent?.termsStatementUrl || !this.productService.defaultChatAgent?.privacyStatementUrl) { return; } From 29ddeaa23f47798133495047574b0dad45ed72ba Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega <48293249+osortega@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:36:49 -0800 Subject: [PATCH 309/387] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../welcomeAgentSessions/browser/agentSessionsWelcome.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index 4c4ff736417..53ff33b49a7 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -383,11 +383,13 @@ export class AgentSessionsWelcomePage extends EditorPane { termsLink.textContent = localize('terms', "Terms"); termsLink.href = this.productService.defaultChatAgent.termsStatementUrl; termsLink.target = '_blank'; + termsLink.rel = 'noopener'; desc.appendChild(document.createTextNode(localize('and', " and "))); const privacyLink = append(desc, $('a.agentSessionsWelcome-tosLink')); privacyLink.textContent = localize('privacyStatement', "Privacy statement"); privacyLink.href = this.productService.defaultChatAgent.privacyStatementUrl; privacyLink.target = '_blank'; + privacyLink.rel = 'noopener'; desc.appendChild(document.createTextNode('.')); } From a5d6f5405e0e4f02eec5ef6a588edbdf9ce33351 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Tue, 20 Jan 2026 16:41:45 -0800 Subject: [PATCH 310/387] Test update --- .../agentSessions/agentSessionViewModel.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts index db0468452a4..d2e2a9b6379 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts @@ -22,6 +22,7 @@ import { MenuId } from '../../../../../../platform/actions/common/actions.js'; import { ILifecycleService } from '../../../../../services/lifecycle/common/lifecycle.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; +import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../../../browser/agentSessions/agentSessions.js'; suite('Agent Sessions', () => { @@ -46,6 +47,7 @@ suite('Agent Sessions', () => { instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, mockLifecycleService); + instantiationService.stub(ILogService, new NullLogService()); }); teardown(() => { @@ -1238,6 +1240,7 @@ suite('Agent Sessions', () => { instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + instantiationService.stub(ILogService, new NullLogService()); }); teardown(() => { @@ -1400,6 +1403,7 @@ suite('Agent Sessions', () => { instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + instantiationService.stub(ILogService, new NullLogService()); }); teardown(() => { @@ -1830,6 +1834,7 @@ suite('Agent Sessions', () => { instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + instantiationService.stub(ILogService, new NullLogService()); }); teardown(() => { @@ -2008,6 +2013,7 @@ suite('Agent Sessions', () => { const mockChatSessionsService = new MockChatSessionsService(); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + instantiationService.stub(ILogService, new NullLogService()); const provider: IChatSessionItemProvider = { chatSessionType: AgentSessionProviders.Local, @@ -2035,6 +2041,7 @@ suite('Agent Sessions', () => { const mockChatSessionsService = new MockChatSessionsService(); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + instantiationService.stub(ILogService, new NullLogService()); const provider: IChatSessionItemProvider = { chatSessionType: AgentSessionProviders.Background, @@ -2062,6 +2069,7 @@ suite('Agent Sessions', () => { const mockChatSessionsService = new MockChatSessionsService(); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + instantiationService.stub(ILogService, new NullLogService()); const provider: IChatSessionItemProvider = { chatSessionType: AgentSessionProviders.Cloud, @@ -2089,6 +2097,7 @@ suite('Agent Sessions', () => { const mockChatSessionsService = new MockChatSessionsService(); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + instantiationService.stub(ILogService, new NullLogService()); const customIcon = ThemeIcon.fromId('beaker'); const provider: IChatSessionItemProvider = { @@ -2120,6 +2129,7 @@ suite('Agent Sessions', () => { const mockChatSessionsService = new MockChatSessionsService(); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + instantiationService.stub(ILogService, new NullLogService()); const provider: IChatSessionItemProvider = { chatSessionType: 'custom-type', @@ -2153,6 +2163,7 @@ suite('Agent Sessions', () => { instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, mockLifecycleService); + instantiationService.stub(ILogService, new NullLogService()); }); teardown(() => { From b6768f08a61e61c851764d2aacf681e0d98b1215 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 00:43:57 +0000 Subject: [PATCH 311/387] Initial plan From 5b84902b0dd8792f903a727c46c6e742853a3706 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Tue, 20 Jan 2026 16:46:12 -0800 Subject: [PATCH 312/387] Do not open the chat widget if welcome page is agent sessions --- .../contrib/chat/browser/actions/chatGettingStarted.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts b/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts index 663a8f4054d..d78a88982b2 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts @@ -12,6 +12,7 @@ import { IExtensionManagementService, InstallOperation } from '../../../../../pl import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IDefaultChatAgent } from '../../../../../base/common/product.js'; import { IChatWidgetService } from '../chat.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; export class ChatGettingStartedContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.chatGettingStarted'; @@ -25,6 +26,7 @@ export class ChatGettingStartedContribution extends Disposable implements IWorkb @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IStorageService private readonly storageService: IStorageService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(); @@ -63,8 +65,12 @@ export class ChatGettingStartedContribution extends Disposable implements IWorkb private async onDidInstallChat() { - // Open Chat view - this.chatWidgetService.revealWidget(); + // Don't reveal if user prefers the agent sessions welcome page + const startupEditor = this.configurationService.getValue('workbench.startupEditor'); + if (startupEditor !== 'agentSessionsWelcomePage') { + // Open Chat view + this.chatWidgetService.revealWidget(); + } // Only do this once this.storageService.store(ChatGettingStartedContribution.hideWelcomeView, true, StorageScope.APPLICATION, StorageTarget.MACHINE); From 0ab69cd73d320b464f047226b8cce28b58bbe453 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 20 Jan 2026 16:47:28 -0800 Subject: [PATCH 313/387] Use proper virtual file system for prompt files provider extension API (#289234) --- src/vs/base/browser/markdownRenderer.ts | 1 + .../api/common/extHostApiCommands.ts | 5 +- .../api/common/extHostChatAgents2.ts | 32 ++- src/vs/workbench/api/common/extHostTypes.ts | 2 +- .../contrib/chat/browser/chat.contribution.ts | 1 + .../chatPromptFileSystemProvider.ts | 119 ++++++++ .../promptSyntax/chatPromptContentStore.ts | 12 +- .../chatPromptFilesContribution.ts | 38 ++- .../chatPromptFileSystemProvider.test.ts | 257 ++++++++++++++++++ .../chatPromptContentStore.test.ts | 48 ++++ .../vscode.proposed.chatPromptFiles.d.ts | 14 +- 11 files changed, 508 insertions(+), 21 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptFileSystemProvider.ts create mode 100644 src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptFileSystemProvider.test.ts diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index e3f20d96726..b5006737fb6 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -600,6 +600,7 @@ function getDomSanitizerConfig(mdStrConfig: MdStrConfig, options: MarkdownSaniti Schemas.vscodeRemote, Schemas.vscodeRemoteResource, Schemas.vscodeNotebookCell, + Schemas.vscodeChatPrompt, // For links that are handled entirely by the action handler Schemas.internal, ]; diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 12fe6f307c7..a04bbd05075 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -6,7 +6,7 @@ import { isFalsyOrEmpty } from '../../../base/common/arrays.js'; import { VSBuffer } from '../../../base/common/buffer.js'; import { Schemas, matchesSomeScheme } from '../../../base/common/network.js'; -import { URI, UriComponents } from '../../../base/common/uri.js'; +import { URI } from '../../../base/common/uri.js'; import { IPosition } from '../../../editor/common/core/position.js'; import { IRange } from '../../../editor/common/core/range.js'; import { ISelection } from '../../../editor/common/core/selection.js'; @@ -23,6 +23,7 @@ import { TransientCellMetadata, TransientDocumentMetadata } from '../../contrib/ import * as search from '../../contrib/search/common/search.js'; import type * as vscode from 'vscode'; import { PromptsType } from '../../contrib/chat/common/promptSyntax/promptTypes.js'; +import type { IExtensionPromptFileResult } from '../../contrib/chat/common/promptSyntax/chatPromptFilesContribution.js'; //#region --- NEW world @@ -560,7 +561,7 @@ const newCommands: ApiCommand[] = [ new ApiCommand( 'vscode.extensionPromptFileProvider', '_listExtensionPromptFiles', 'Get all extension-contributed prompt files (custom agents, instructions, and prompt files).', [], - new ApiCommandResult<{ uri: UriComponents; type: PromptsType }[], { uri: vscode.Uri; type: PromptsType }[]>( + new ApiCommandResult( 'A promise that resolves to an array of objects containing uri and type.', (value) => { if (!value) { diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 6c32e537504..a55f2238694 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -574,20 +574,42 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS } // Convert ChatResourceDescriptor to IPromptFileResource format - return resources?.map(r => this.convertChatResourceDescriptorToPromptFileResource(r.resource, providerData.extension.identifier.value)); + return resources?.map(r => this.convertChatResourceDescriptorToPromptFileResource(r.resource, providerData.extension.identifier.value, type)); } /** * Creates a virtual URI for a prompt file. + * Format varies by type: + * - Skills: /${extensionId}/skills/${id}/SKILL.md + * - Agents: /${extensionId}/agents/${id}.agent.md + * - Instructions: /${extensionId}/instructions/${id}.instructions.md + * - Prompts: /${extensionId}/prompts/${id}.prompt.md */ - createVirtualPromptUri(id: string, extensionId: string): URI { + createVirtualPromptUri(id: string, extensionId: string, type: PromptsType): URI { + let path: string; + switch (type) { + case PromptsType.skill: + path = `/${extensionId}/skills/${id}/SKILL.md`; + break; + case PromptsType.agent: + path = `/${extensionId}/agents/${id}.agent.md`; + break; + case PromptsType.instructions: + path = `/${extensionId}/instructions/${id}.instructions.md`; + break; + case PromptsType.prompt: + path = `/${extensionId}/prompts/${id}.prompt.md`; + break; + default: + throw new Error(`Unsupported PromptsType: ${type}`); + } return URI.from({ scheme: Schemas.vscodeChatPrompt, - path: `/${extensionId}/${id}` + path }); } - convertChatResourceDescriptorToPromptFileResource(resource: vscode.ChatResourceDescriptor | vscode.ChatResourceUriDescriptor, extensionId: string): IPromptFileResource { + convertChatResourceDescriptorToPromptFileResource(resource: vscode.ChatResourceDescriptor, extensionId: string, type: PromptsType): IPromptFileResource { if (URI.isUri(resource)) { // Plain URI return { uri: resource }; @@ -595,7 +617,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS // { id, content } return { content: resource.content, - uri: this.createVirtualPromptUri(resource.id, extensionId), + uri: this.createVirtualPromptUri(resource.id, extensionId, type), isEditable: undefined }; } else if ('uri' in resource && URI.isUri(resource.uri)) { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 9e19f40e259..1efc19c563c 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3906,6 +3906,6 @@ export class PromptFileChatResource implements vscode.PromptFileChatResource { @es5ClassCompat export class SkillChatResource implements vscode.SkillChatResource { - constructor(public readonly resource: vscode.ChatResourceUriDescriptor) { } + constructor(public readonly resource: vscode.ChatResourceDescriptor) { } } //#endregion diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 2e6bc1c1251..d0ad59d06c1 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -95,6 +95,7 @@ import { ChatMarkdownAnchorService, IChatMarkdownAnchorService } from './widget/ import { ChatContextPickService, IChatContextPickService } from './attachments/chatContextPickService.js'; import { ChatInputBoxContentProvider } from './widget/input/editor/chatEditorInputContentProvider.js'; import { ChatPromptContentProvider } from './promptSyntax/chatPromptContentProvider.js'; +import './promptSyntax/chatPromptFileSystemProvider.js'; import { ChatEditingEditorAccessibility } from './chatEditing/chatEditingEditorAccessibility.js'; import { registerChatEditorActions } from './chatEditing/chatEditingEditorActions.js'; import { ChatEditingEditorContextKeys } from './chatEditing/chatEditingEditorContextKeys.js'; diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptFileSystemProvider.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptFileSystemProvider.ts new file mode 100644 index 00000000000..daee1c26006 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptFileSystemProvider.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * 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, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { VSBuffer } from '../../../../../base/common/buffer.js'; +import { IFileDeleteOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileType, IFileWriteOptions, IFileSystemProvider, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions, createFileSystemProviderError, FileSystemProviderErrorCode, IFileService } from '../../../../../platform/files/common/files.js'; +import { IChatPromptContentStore } from '../../common/promptSyntax/chatPromptContentStore.js'; +import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../../common/contributions.js'; + +/** + * File system provider for virtual chat prompt files created with inline content. + * These URIs have the scheme 'vscode-chat-prompt' and retrieve their content + * from the {@link IChatPromptContentStore} which maintains an in-memory map + * of content indexed by URI. + * + * This enables external extensions to use VS Code's file system API to read + * these virtual prompt files. + */ +export class ChatPromptFileSystemProvider implements IFileSystemProvider, IFileSystemProviderWithFileReadWriteCapability { + + get capabilities() { + return FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.Readonly; + } + + constructor( + private readonly chatPromptContentStore: IChatPromptContentStore + ) { } + + //#region Supported File Operations + + async stat(resource: URI): Promise { + const content = this.chatPromptContentStore.getContent(resource); + if (content === undefined) { + throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound); + } + + const size = VSBuffer.fromString(content).byteLength; + + return { + type: FileType.File, + ctime: 0, + mtime: 0, + size + }; + } + + async readFile(resource: URI): Promise { + const content = this.chatPromptContentStore.getContent(resource); + if (content === undefined) { + throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound); + } + + return VSBuffer.fromString(content).buffer; + } + + //#endregion + + //#region Unsupported File Operations + + readonly onDidChangeCapabilities = Event.None; + readonly onDidChangeFile = Event.None; + + async writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise { + throw createFileSystemProviderError('not allowed', FileSystemProviderErrorCode.NoPermissions); + } + + async mkdir(resource: URI): Promise { + throw createFileSystemProviderError('not allowed', FileSystemProviderErrorCode.NoPermissions); + } + + async readdir(resource: URI): Promise<[string, FileType][]> { + return []; + } + + async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise { + throw createFileSystemProviderError('not allowed', FileSystemProviderErrorCode.NoPermissions); + } + + async delete(resource: URI, opts: IFileDeleteOptions): Promise { + throw createFileSystemProviderError('not allowed', FileSystemProviderErrorCode.NoPermissions); + } + + watch(resource: URI, opts: IWatchOptions): IDisposable { + return Disposable.None; + } + + //#endregion +} + +/** + * Workbench contribution that registers the chat prompt file system provider. + */ +export class ChatPromptFileSystemProviderContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.chatPromptFileSystemProvider'; + + constructor( + @IFileService fileService: IFileService, + @IChatPromptContentStore chatPromptContentStore: IChatPromptContentStore + ) { + super(); + + this._register(fileService.registerProvider( + Schemas.vscodeChatPrompt, + new ChatPromptFileSystemProvider(chatPromptContentStore) + )); + } +} + +registerWorkbenchContribution2( + ChatPromptFileSystemProviderContribution.ID, + ChatPromptFileSystemProviderContribution, + WorkbenchPhase.Eventually +); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptContentStore.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptContentStore.ts index 30de1ac7e0c..a28402f8897 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptContentStore.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptContentStore.ts @@ -44,8 +44,16 @@ export class ChatPromptContentStore extends Disposable implements IChatPromptCon super(); } + /** + * Normalizes a URI by stripping query and fragment for consistent lookup. + * Query parameters like vscodeLinkType are metadata for rendering, not content identification. + */ + private normalizeUri(uri: URI): string { + return uri.with({ query: '', fragment: '' }).toString(); + } + registerContent(uri: URI, content: string): { dispose: () => void } { - const key = uri.toString(); + const key = this.normalizeUri(uri); this._contentMap.set(key, content); const dispose = () => { @@ -56,7 +64,7 @@ export class ChatPromptContentStore extends Disposable implements IChatPromptCon } getContent(uri: URI): string | undefined { - return this._contentMap.get(uri.toString()); + return this._contentMap.get(this.normalizeUri(uri)); } override dispose(): void { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts index 0eecdcb0d05..8e9d41c3db7 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts @@ -10,8 +10,11 @@ import { localize } from '../../../../../nls.js'; import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js'; -import { IPromptsService } from './service/promptsService.js'; +import { IPromptsService, PromptsStorage } from './service/promptsService.js'; import { PromptsType } from './promptTypes.js'; +import { UriComponents } from '../../../../../base/common/uri.js'; +import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; interface IRawChatFileContribution { readonly path: string; @@ -126,3 +129,36 @@ export class ChatPromptFilesExtensionPointHandler implements IWorkbenchContribut }); } } + +/** + * Result type for the extension prompt file provider command. + */ +export interface IExtensionPromptFileResult { + readonly uri: UriComponents; + readonly type: PromptsType; +} + +/** + * Register the command to list all extension-contributed prompt files. + */ +CommandsRegistry.registerCommand('_listExtensionPromptFiles', async (accessor): Promise => { + const promptsService = accessor.get(IPromptsService); + + // Get extension prompt files for all prompt types in parallel + const [agents, instructions, prompts, skills] = await Promise.all([ + promptsService.listPromptFiles(PromptsType.agent, CancellationToken.None), + promptsService.listPromptFiles(PromptsType.instructions, CancellationToken.None), + promptsService.listPromptFiles(PromptsType.prompt, CancellationToken.None), + promptsService.listPromptFiles(PromptsType.skill, CancellationToken.None), + ]); + + // Combine all files and collect extension-contributed ones + const result: IExtensionPromptFileResult[] = []; + for (const file of [...agents, ...instructions, ...prompts, ...skills]) { + if (file.storage === PromptsStorage.extension) { + result.push({ uri: file.uri.toJSON(), type: file.type }); + } + } + + return result; +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptFileSystemProvider.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptFileSystemProvider.test.ts new file mode 100644 index 00000000000..f43e53d2cce --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptFileSystemProvider.test.ts @@ -0,0 +1,257 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { URI } from '../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { VSBuffer } from '../../../../../../base/common/buffer.js'; +import { FileSystemProviderErrorCode, toFileSystemProviderErrorCode } from '../../../../../../platform/files/common/files.js'; +import { ChatPromptFileSystemProvider } from '../../../browser/promptSyntax/chatPromptFileSystemProvider.js'; +import { ChatPromptContentStore } from '../../../common/promptSyntax/chatPromptContentStore.js'; + +suite('ChatPromptFileSystemProvider', () => { + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let contentStore: ChatPromptContentStore; + let provider: ChatPromptFileSystemProvider; + + setup(() => { + contentStore = testDisposables.add(new ChatPromptContentStore()); + provider = new ChatPromptFileSystemProvider(contentStore); + }); + + suite('stat', () => { + test('returns stat for registered content', async () => { + const uri = URI.parse('vscode-chat-prompt:/.agent.md/test-agent'); + const content = '# Test Agent\nThis is test content.'; + + testDisposables.add(contentStore.registerContent(uri, content)); + + const stat = await provider.stat(uri); + + assert.strictEqual(stat.type, 1); // FileType.File + assert.strictEqual(stat.size, VSBuffer.fromString(content).byteLength); + }); + + test('throws FileNotFound for unregistered URI', async () => { + const uri = URI.parse('vscode-chat-prompt:/.agent.md/missing'); + + await assert.rejects( + () => provider.stat(uri), + (err: Error & { code?: string }) => { + return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.FileNotFound; + }, + 'Should throw FileNotFound error' + ); + }); + + test('returns correct size for empty content', async () => { + const uri = URI.parse('vscode-chat-prompt:/.prompt.md/empty'); + + testDisposables.add(contentStore.registerContent(uri, '')); + + const stat = await provider.stat(uri); + + assert.strictEqual(stat.size, 0); + }); + + test('returns correct size for unicode content', async () => { + const uri = URI.parse('vscode-chat-prompt:/.instructions.md/unicode'); + const content = '日本語テスト 🎉'; + + testDisposables.add(contentStore.registerContent(uri, content)); + + const stat = await provider.stat(uri); + + assert.strictEqual(stat.size, VSBuffer.fromString(content).byteLength); + }); + }); + + suite('readFile', () => { + test('returns content for registered URI', async () => { + const uri = URI.parse('vscode-chat-prompt:/.agent.md/test-agent'); + const content = '# Test Agent\nThis is test content.'; + + testDisposables.add(contentStore.registerContent(uri, content)); + + const result = await provider.readFile(uri); + + assert.strictEqual(VSBuffer.wrap(result).toString(), content); + }); + + test('throws FileNotFound for unregistered URI', async () => { + const uri = URI.parse('vscode-chat-prompt:/.agent.md/missing'); + + await assert.rejects( + () => provider.readFile(uri), + (err: Error & { code?: string }) => { + return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.FileNotFound; + }, + 'Should throw FileNotFound error' + ); + }); + + test('returns empty buffer for empty content', async () => { + const uri = URI.parse('vscode-chat-prompt:/.prompt.md/empty'); + + testDisposables.add(contentStore.registerContent(uri, '')); + + const result = await provider.readFile(uri); + + assert.strictEqual(result.byteLength, 0); + }); + + test('preserves unicode content', async () => { + const uri = URI.parse('vscode-chat-prompt:/.instructions.md/unicode'); + const content = '日本語テスト 🎉\n\n```typescript\nconst greeting = "こんにちは";\n```'; + + testDisposables.add(contentStore.registerContent(uri, content)); + + const result = await provider.readFile(uri); + + assert.strictEqual(VSBuffer.wrap(result).toString(), content); + }); + + test('handles content with special markdown characters', async () => { + const uri = URI.parse('vscode-chat-prompt:/.agent.md/markdown'); + const content = '# Heading\n\n- List item\n- Another item\n\n> Blockquote\n\n```\ncode block\n```'; + + testDisposables.add(contentStore.registerContent(uri, content)); + + const result = await provider.readFile(uri); + + assert.strictEqual(VSBuffer.wrap(result).toString(), content); + }); + }); + + suite('content lifecycle', () => { + test('readFile fails after content is disposed', async () => { + const uri = URI.parse('vscode-chat-prompt:/.agent.md/lifecycle-test'); + const content = 'Temporary content'; + + const registration = contentStore.registerContent(uri, content); + + // Verify content is readable + const result = await provider.readFile(uri); + assert.strictEqual(VSBuffer.wrap(result).toString(), content); + + // Dispose the content + registration.dispose(); + + // Now reading should fail + await assert.rejects( + () => provider.readFile(uri), + (err: Error & { code?: string }) => { + return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.FileNotFound; + } + ); + }); + + test('stat fails after content is disposed', async () => { + const uri = URI.parse('vscode-chat-prompt:/.prompt.md/lifecycle-stat'); + const content = 'Content for stat test'; + + const registration = contentStore.registerContent(uri, content); + + // Verify stat works + const stat = await provider.stat(uri); + assert.strictEqual(stat.size, VSBuffer.fromString(content).byteLength); + + // Dispose the content + registration.dispose(); + + // Now stat should fail + await assert.rejects( + () => provider.stat(uri), + (err: Error & { code?: string }) => { + return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.FileNotFound; + } + ); + }); + }); + + suite('URI normalization', () => { + test('readFile succeeds when URI has query parameters', async () => { + const baseUri = URI.parse('vscode-chat-prompt:/.agent.md/query-test'); + const content = 'Content for query test'; + + testDisposables.add(contentStore.registerContent(baseUri, content)); + + // Read with query parameters + const uriWithQuery = baseUri.with({ query: 'vscodeLinkType=prompt' }); + const result = await provider.readFile(uriWithQuery); + + assert.strictEqual(VSBuffer.wrap(result).toString(), content); + }); + + test('stat succeeds when URI has fragment', async () => { + const baseUri = URI.parse('vscode-chat-prompt:/.instructions.md/fragment-test'); + const content = 'Content for fragment test'; + + testDisposables.add(contentStore.registerContent(baseUri, content)); + + // Stat with fragment + const uriWithFragment = baseUri.with({ fragment: 'section1' }); + const stat = await provider.stat(uriWithFragment); + + assert.strictEqual(stat.size, VSBuffer.fromString(content).byteLength); + }); + }); + + suite('unsupported operations', () => { + test('writeFile throws NoPermissions error', async () => { + const uri = URI.parse('vscode-chat-prompt:/.agent.md/write-test'); + + await assert.rejects( + () => provider.writeFile(uri, new Uint8Array(), { create: true, overwrite: true, unlock: false, atomic: false }), + (err: Error & { code?: string }) => { + return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.NoPermissions; + } + ); + }); + + test('mkdir throws NoPermissions error', async () => { + const uri = URI.parse('vscode-chat-prompt:/test-dir'); + + await assert.rejects( + () => provider.mkdir(uri), + (err: Error & { code?: string }) => { + return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.NoPermissions; + } + ); + }); + + test('delete throws NoPermissions error', async () => { + const uri = URI.parse('vscode-chat-prompt:/.agent.md/delete-test'); + + await assert.rejects( + () => provider.delete(uri, { recursive: false, useTrash: false, atomic: false }), + (err: Error & { code?: string }) => { + return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.NoPermissions; + } + ); + }); + + test('rename throws NoPermissions error', async () => { + const from = URI.parse('vscode-chat-prompt:/.agent.md/rename-from'); + const to = URI.parse('vscode-chat-prompt:/.agent.md/rename-to'); + + await assert.rejects( + () => provider.rename(from, to, { overwrite: false }), + (err: Error & { code?: string }) => { + return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.NoPermissions; + } + ); + }); + + test('readdir returns empty array', async () => { + const uri = URI.parse('vscode-chat-prompt:/'); + + const result = await provider.readdir(uri); + + assert.deepStrictEqual(result, []); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/chatPromptContentStore.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/chatPromptContentStore.test.ts index 3d3236124b9..75f4496a0e3 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/chatPromptContentStore.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/chatPromptContentStore.test.ts @@ -142,4 +142,52 @@ suite('ChatPromptContentStore', () => { // Should be retrievable with equivalent URI assert.strictEqual(store.getContent(uri2), content); }); + + test('getContent normalizes URI by stripping query parameters', () => { + const baseUri = URI.parse('vscode-chat-prompt:/.agent.md/normalize-test'); + const content = 'Normalized content'; + + const disposable = store.registerContent(baseUri, content); + testDisposables.add(disposable); + + // Should retrieve content when queried with extra query parameters + const uriWithQuery = baseUri.with({ query: 'vscodeLinkType=prompt' }); + assert.strictEqual(store.getContent(uriWithQuery), content); + }); + + test('getContent normalizes URI by stripping fragment', () => { + const baseUri = URI.parse('vscode-chat-prompt:/.instructions.md/fragment-test'); + const content = 'Content with fragment lookup'; + + const disposable = store.registerContent(baseUri, content); + testDisposables.add(disposable); + + // Should retrieve content when queried with fragment + const uriWithFragment = baseUri.with({ fragment: 'section1' }); + assert.strictEqual(store.getContent(uriWithFragment), content); + }); + + test('getContent normalizes URI by stripping both query and fragment', () => { + const baseUri = URI.parse('vscode-chat-prompt:/.prompt.md/full-normalize'); + const content = 'Fully normalized content'; + + const disposable = store.registerContent(baseUri, content); + testDisposables.add(disposable); + + // Should retrieve content when queried with both query and fragment + const uriWithBoth = baseUri.with({ query: 'vscodeLinkType=skill&foo=bar', fragment: 'heading' }); + assert.strictEqual(store.getContent(uriWithBoth), content); + }); + + test('registerContent normalizes URI so content registered with query is found without it', () => { + const uriWithQuery = URI.parse('vscode-chat-prompt:/.agent.md/register-with-query?vscodeLinkType=agent'); + const content = 'Content registered with query'; + + const disposable = store.registerContent(uriWithQuery, content); + testDisposables.add(disposable); + + // Should retrieve content using base URI without query + const baseUri = uriWithQuery.with({ query: '' }); + assert.strictEqual(store.getContent(baseUri), content); + }); }); diff --git a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts index 8e35b35ba92..f97cb106c10 100644 --- a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts +++ b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts @@ -8,18 +8,12 @@ declare module 'vscode' { // #region Resource Classes - /** - * Describes a chat resource URI with optional editability. - */ - export type ChatResourceUriDescriptor = - | Uri - | { uri: Uri; isEditable?: boolean }; - /** * Describes a chat resource file. */ export type ChatResourceDescriptor = - | ChatResourceUriDescriptor + | Uri + | { uri: Uri; isEditable?: boolean } | { id: string; content: string; @@ -80,14 +74,14 @@ declare module 'vscode' { /** * The skill resource descriptor. */ - readonly resource: ChatResourceUriDescriptor; + readonly resource: ChatResourceDescriptor; /** * Creates a new skill resource from the specified resource URI pointing to SKILL.md. * The parent folder name needs to match the name of the skill in the frontmatter. * @param resource The chat resource descriptor. */ - constructor(resource: ChatResourceUriDescriptor); + constructor(resource: ChatResourceDescriptor); } // #endregion From c6d89825e84d2f7a8c189d31f8f0b4326e4b5c69 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 00:50:42 +0000 Subject: [PATCH 314/387] Use MarkdownString for localization with links following VS Code patterns Co-authored-by: osortega <48293249+osortega@users.noreply.github.com> --- .../browser/agentSessionsWelcome.ts | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index 53ff33b49a7..392494ace5d 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -45,6 +45,8 @@ import { AgentSessionsControl, IAgentSessionsControlOptions } from '../../chat/b import { IAgentSessionsFilter } from '../../chat/browser/agentSessions/agentSessionsViewer.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { IResolvedWalkthrough, IWalkthroughsService } from '../../welcomeGettingStarted/browser/gettingStartedService.js'; +import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; const configurationKey = 'workbench.startupEditor'; const MAX_SESSIONS = 6; @@ -81,6 +83,7 @@ export class AgentSessionsWelcomePage extends EditorPane { @IWalkthroughsService private readonly walkthroughsService: IWalkthroughsService, @IChatService private readonly chatService: IChatService, @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, + @IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService, ) { super(AgentSessionsWelcomePage.ID, group, telemetryService, themeService, storageService); @@ -374,23 +377,19 @@ export class AgentSessionsWelcomePage extends EditorPane { title.textContent = localize('tosTitle', "AI Feature Trial is Active"); const desc = append(content, $('.agentSessionsWelcome-walkthroughCard-description')); - desc.textContent = localize( - { key: 'tosDescription', comment: ['{Locked="]"}'] }, - "By continuing, you agree to {0}'s ", - providers.default.name + const descriptionMarkdown = new MarkdownString( + localize( + { key: 'tosDescription', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, + "By continuing, you agree to {0}'s [Terms]({2}) and [Privacy Statement]({3}).", + providers.default.name, + providers.default.name, + this.productService.defaultChatAgent.termsStatementUrl, + this.productService.defaultChatAgent.privacyStatementUrl + ), + { isTrusted: true } ); - const termsLink = append(desc, $('a.agentSessionsWelcome-tosLink')); - termsLink.textContent = localize('terms', "Terms"); - termsLink.href = this.productService.defaultChatAgent.termsStatementUrl; - termsLink.target = '_blank'; - termsLink.rel = 'noopener'; - desc.appendChild(document.createTextNode(localize('and', " and "))); - const privacyLink = append(desc, $('a.agentSessionsWelcome-tosLink')); - privacyLink.textContent = localize('privacyStatement', "Privacy statement"); - privacyLink.href = this.productService.defaultChatAgent.privacyStatementUrl; - privacyLink.target = '_blank'; - privacyLink.rel = 'noopener'; - desc.appendChild(document.createTextNode('.')); + const renderedMarkdown = this.markdownRendererService.render(descriptionMarkdown); + desc.appendChild(renderedMarkdown.element); } private buildFooter(container: HTMLElement): void { From 1d088083e213687fcdb2f161fd5b2ed1d75a0c92 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 00:55:03 +0000 Subject: [PATCH 315/387] Remove duplicate provider name parameter Co-authored-by: osortega <48293249+osortega@users.noreply.github.com> --- .../welcomeAgentSessions/browser/agentSessionsWelcome.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index 392494ace5d..a94e8202194 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -379,9 +379,8 @@ export class AgentSessionsWelcomePage extends EditorPane { const desc = append(content, $('.agentSessionsWelcome-walkthroughCard-description')); const descriptionMarkdown = new MarkdownString( localize( - { key: 'tosDescription', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, - "By continuing, you agree to {0}'s [Terms]({2}) and [Privacy Statement]({3}).", - providers.default.name, + { key: 'tosDescription', comment: ['{Locked="]({1})"}', '{Locked="]({2})"}'] }, + "By continuing, you agree to {0}'s [Terms]({1}) and [Privacy Statement]({2}).", providers.default.name, this.productService.defaultChatAgent.termsStatementUrl, this.productService.defaultChatAgent.privacyStatementUrl From 58db9f8def19bd03efc61050f8a9f14b5fa4df35 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 20 Jan 2026 17:35:19 -0800 Subject: [PATCH 316/387] chat: combine adjacent code blocks in MCP tool output (#289272) Combines adjacent text code blocks in ChatToolOutputContentSubPart to reduce the number of editor models and listeners created when tools return multiple separate output parts. When an MCP tool outputs many lines (e.g., 100 lines), they are now merged into a single code block instead of creating 100 separate blocks, which significantly improves responsiveness and prevents listener leak warnings. - Modified createOutputContents() to group consecutive code parts - Modified addCodeBlock() to accept an array of parts and combine their text content with newline separators - Reduces model/listener creation for high-volume MCP tool outputs Fixes https://github.com/microsoft/vscode/issues/279624 (Commit message generated by Copilot) --- .../chatToolOutputContentSubPart.ts | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolOutputContentSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolOutputContentSubPart.ts index a468794b3a0..63af33033b7 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolOutputContentSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolOutputContentSubPart.ts @@ -75,7 +75,12 @@ export class ChatToolOutputContentSubPart extends Disposable { for (let i = 0; i < this.parts.length; i++) { const part = this.parts[i]; if (part.kind === 'code') { - this.addCodeBlock(part, container); + // Collect adjacent code parts and combine their contents + const codeParts = [part]; + while (i + 1 < this.parts.length && this.parts[i + 1].kind === 'code') { + codeParts.push(this.parts[++i] as IChatCollapsibleIOCodePart); + } + this.addCodeBlock(codeParts, container); continue; } @@ -153,22 +158,27 @@ export class ChatToolOutputContentSubPart extends Disposable { toolbar.context = { parts } satisfies IChatToolOutputResourceToolbarContext; } - private addCodeBlock(part: IChatCollapsibleIOCodePart, container: HTMLElement) { - if (part.title) { + private addCodeBlock(parts: IChatCollapsibleIOCodePart[], container: HTMLElement): void { + const firstPart = parts[0]; + if (firstPart.title) { const title = dom.$('div.chat-confirmation-widget-title'); - const renderedTitle = this._register(this._markdownRendererService.render(this.toMdString(part.title))); + const renderedTitle = this._register(this._markdownRendererService.render(this.toMdString(firstPart.title))); title.appendChild(renderedTitle.element); container.appendChild(title); } + // Combine text from all adjacent code parts + const combinedText = parts.map(p => p.textModel.getValue()).join('\n'); + firstPart.textModel.setValue(combinedText); + const data: ICodeBlockData = { - languageId: part.languageId, - textModel: Promise.resolve(part.textModel), - codeBlockIndex: part.codeBlockInfo.codeBlockIndex, + languageId: firstPart.languageId, + textModel: Promise.resolve(firstPart.textModel), + codeBlockIndex: firstPart.codeBlockInfo.codeBlockIndex, codeBlockPartIndex: 0, element: this.context.element, parentContextKeyService: this.contextKeyService, - renderOptions: part.options, + renderOptions: firstPart.options, chatSessionResource: this.context.element.sessionResource, }; const editorReference = this._register(this.context.editorPool.get()); @@ -176,7 +186,7 @@ export class ChatToolOutputContentSubPart extends Disposable { this._register(editorReference.object.onDidChangeContentHeight(() => this._onDidChangeHeight.fire())); container.appendChild(editorReference.object.element); this._editorReferences.push(editorReference); - this.codeblocks.push(part.codeBlockInfo); + this.codeblocks.push(firstPart.codeBlockInfo); } layout(width: number): void { From 749a2535d5f112ad29d6246d9fe2caea31ddd572 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Tue, 20 Jan 2026 17:12:59 -0800 Subject: [PATCH 317/387] Missing stub --- .../test/browser/agentSessions/agentSessionViewModel.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts index d2e2a9b6379..865206ad723 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts @@ -784,6 +784,7 @@ suite('Agent Sessions', () => { mockChatSessionsService = new MockChatSessionsService(); instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); instantiationService.stub(IChatSessionsService, mockChatSessionsService); + instantiationService.stub(ILogService, new NullLogService()); }); teardown(() => { @@ -2205,6 +2206,7 @@ suite('Agent Sessions', () => { mockChatSessionsService = new MockChatSessionsService(); instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); instantiationService.stub(IChatSessionsService, mockChatSessionsService); + instantiationService.stub(ILogService, new NullLogService()); }); teardown(() => { From 2b033f7321237c367465a03109ab0f67ce805389 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Tue, 20 Jan 2026 17:51:24 -0800 Subject: [PATCH 318/387] So silly --- .../browser/agentSessions/agentSessionsModel.ts | 4 ++-- .../agentSessions/agentSessionViewModel.test.ts | 13 ------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index 950a5549b9f..23970c73a7a 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -527,7 +527,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode let inProgressTime: number | undefined; if (isInProgress) { inProgressTime = Date.now(); - this.logService.trace(`[agent sessions] Setting inProgressTime for session ${session.resource.toString()} to ${inProgressTime} (status: ${status})`); + this.logger.logIfTrace(`[agent sessions] Setting inProgressTime for session ${session.resource.toString()} to ${inProgressTime} (status: ${status})`); } this.mapSessionToState.set(session.resource, { status, @@ -573,7 +573,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } } - this.logService.trace(`[agent sessions] Resolved session ${session.resource.toString()} with timings: created=${created}, lastRequestStarted=${lastRequestStarted}, lastRequestEnded=${lastRequestEnded}`); + this.logger.logIfTrace(`[agent sessions] Resolved session ${session.resource.toString()} with timings: created=${created}, lastRequestStarted=${lastRequestStarted}, lastRequestEnded=${lastRequestEnded}`); sessions.set(session.resource, this.toAgentSession({ providerType: chatSessionType, diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts index 865206ad723..db0468452a4 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts @@ -22,7 +22,6 @@ import { MenuId } from '../../../../../../platform/actions/common/actions.js'; import { ILifecycleService } from '../../../../../services/lifecycle/common/lifecycle.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; -import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../../../browser/agentSessions/agentSessions.js'; suite('Agent Sessions', () => { @@ -47,7 +46,6 @@ suite('Agent Sessions', () => { instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, mockLifecycleService); - instantiationService.stub(ILogService, new NullLogService()); }); teardown(() => { @@ -784,7 +782,6 @@ suite('Agent Sessions', () => { mockChatSessionsService = new MockChatSessionsService(); instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); instantiationService.stub(IChatSessionsService, mockChatSessionsService); - instantiationService.stub(ILogService, new NullLogService()); }); teardown(() => { @@ -1241,7 +1238,6 @@ suite('Agent Sessions', () => { instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); - instantiationService.stub(ILogService, new NullLogService()); }); teardown(() => { @@ -1404,7 +1400,6 @@ suite('Agent Sessions', () => { instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); - instantiationService.stub(ILogService, new NullLogService()); }); teardown(() => { @@ -1835,7 +1830,6 @@ suite('Agent Sessions', () => { instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); - instantiationService.stub(ILogService, new NullLogService()); }); teardown(() => { @@ -2014,7 +2008,6 @@ suite('Agent Sessions', () => { const mockChatSessionsService = new MockChatSessionsService(); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); - instantiationService.stub(ILogService, new NullLogService()); const provider: IChatSessionItemProvider = { chatSessionType: AgentSessionProviders.Local, @@ -2042,7 +2035,6 @@ suite('Agent Sessions', () => { const mockChatSessionsService = new MockChatSessionsService(); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); - instantiationService.stub(ILogService, new NullLogService()); const provider: IChatSessionItemProvider = { chatSessionType: AgentSessionProviders.Background, @@ -2070,7 +2062,6 @@ suite('Agent Sessions', () => { const mockChatSessionsService = new MockChatSessionsService(); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); - instantiationService.stub(ILogService, new NullLogService()); const provider: IChatSessionItemProvider = { chatSessionType: AgentSessionProviders.Cloud, @@ -2098,7 +2089,6 @@ suite('Agent Sessions', () => { const mockChatSessionsService = new MockChatSessionsService(); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); - instantiationService.stub(ILogService, new NullLogService()); const customIcon = ThemeIcon.fromId('beaker'); const provider: IChatSessionItemProvider = { @@ -2130,7 +2120,6 @@ suite('Agent Sessions', () => { const mockChatSessionsService = new MockChatSessionsService(); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); - instantiationService.stub(ILogService, new NullLogService()); const provider: IChatSessionItemProvider = { chatSessionType: 'custom-type', @@ -2164,7 +2153,6 @@ suite('Agent Sessions', () => { instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); instantiationService.stub(IChatSessionsService, mockChatSessionsService); instantiationService.stub(ILifecycleService, mockLifecycleService); - instantiationService.stub(ILogService, new NullLogService()); }); teardown(() => { @@ -2206,7 +2194,6 @@ suite('Agent Sessions', () => { mockChatSessionsService = new MockChatSessionsService(); instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); instantiationService.stub(IChatSessionsService, mockChatSessionsService); - instantiationService.stub(ILogService, new NullLogService()); }); teardown(() => { From 980d45aba830f70d933a82569bbdb793cfbf0a6a Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Tue, 20 Jan 2026 18:00:03 -0800 Subject: [PATCH 319/387] Hash custom agent names (#289279) --- .../chat/browser/actions/chatExecuteActions.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 1fac6850829..d85ecf4321b 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../../base/common/codicons.js'; +import { hash } from '../../../../../base/common/hash.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { basename } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; @@ -27,6 +28,7 @@ import { IChatService } from '../../common/chatService/chatService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind, } from '../../common/constants.js'; import { ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { ILanguageModelToolsService } from '../../common/tools/languageModelToolsService.js'; +import { PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { IChatSessionsService } from '../../common/chatSessionsService.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; import { getAgentSessionProvider, AgentSessionProviders } from '../agentSessions/agentSessions.js'; @@ -325,9 +327,18 @@ class ToggleChatModeAction extends Action2 { const toolsCount = switchToMode.customTools?.get()?.length ?? 0; const handoffsCount = switchToMode.handOffs?.get()?.length ?? 0; + // Hash names for user/workspace modes to only instrument non-user agent names + const getModeNameForTelemetry = (mode: IChatMode): string => { + const modeStorage = mode.source?.storage; + if (modeStorage === PromptsStorage.local || modeStorage === PromptsStorage.user) { + return String(hash(mode.name.get())); + } + return mode.name.get(); + }; + telemetryService.publicLog2('chat.modeChange', { - fromMode: currentMode.name.get(), - mode: switchToMode.name.get(), + fromMode: getModeNameForTelemetry(currentMode), + mode: getModeNameForTelemetry(switchToMode), requestCount: requestCount, storage, extensionId, From 51a7a0fe2f59cf41ac97ffa8741acea8154956d5 Mon Sep 17 00:00:00 2001 From: Simon Siefke Date: Wed, 21 Jan 2026 03:13:13 +0100 Subject: [PATCH 320/387] fix: memory leak in folder configuration (#279230) * fix: memory leak in folder configuration * pass resourcemap --- .../configuration/browser/configuration.ts | 4 ++++ .../configuration/browser/configurationService.ts | 15 ++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/services/configuration/browser/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts index c4027279f98..0defa533935 100644 --- a/src/vs/workbench/services/configuration/browser/configuration.ts +++ b/src/vs/workbench/services/configuration/browser/configuration.ts @@ -1080,4 +1080,8 @@ export class FolderConfiguration extends Disposable { this.cachedFolderConfiguration.updateConfiguration(settingsContent, standAloneConfigurationContents); } } + + public addRelated(disposable: IDisposable): void { + this._register(disposable); + } } diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index ecbfc79a45a..e9e0189165b 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -7,7 +7,7 @@ import { URI } from '../../../../base/common/uri.js'; import { Event, Emitter } from '../../../../base/common/event.js'; import { ResourceMap } from '../../../../base/common/map.js'; import { equals } from '../../../../base/common/objects.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, DisposableStore } from '../../../../base/common/lifecycle.js'; import { Queue, Barrier, Promises, Delayer, Throttler } from '../../../../base/common/async.js'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js'; import { IWorkspaceContextService, Workspace as BaseWorkspace, WorkbenchState, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, WorkspaceFolder, toWorkspaceFolder, isWorkspaceFolder, IWorkspaceFoldersWillChangeEvent, IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier, IAnyWorkspaceIdentifier } from '../../../../platform/workspace/common/workspace.js'; @@ -77,7 +77,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat private readonly localUserConfiguration: UserConfiguration; private readonly remoteUserConfiguration: RemoteUserConfiguration | null = null; private readonly workspaceConfiguration: WorkspaceConfiguration; - private cachedFolderConfigs: ResourceMap; + private cachedFolderConfigs: DisposableMap = this._register(new DisposableMap(new ResourceMap())); private readonly workspaceEditingQueue: Queue; private readonly _onDidChangeConfiguration: Emitter = this._register(new Emitter()); @@ -131,7 +131,6 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat this.applicationConfigurationDisposables = this._register(new DisposableStore()); this.createApplicationConfiguration(); this.localUserConfiguration = this._register(new UserConfiguration(userDataProfileService.currentProfile.settingsResource, userDataProfileService.currentProfile.tasksResource, userDataProfileService.currentProfile.mcpResource, { scopes: getLocalUserConfigurationScopes(userDataProfileService.currentProfile, !!remoteAuthority) }, fileService, uriIdentityService, logService)); - this.cachedFolderConfigs = new ResourceMap(); this._register(this.localUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onLocalUserConfigurationChanged(userConfiguration))); if (remoteAuthority) { const remoteUserConfiguration = this.remoteUserConfiguration = this._register(new RemoteUserConfiguration(remoteAuthority, configurationCache, fileService, uriIdentityService, remoteAgentService, logService)); @@ -686,7 +685,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat private async loadConfiguration(applicationConfigurationModel: ConfigurationModel, userConfigurationModel: ConfigurationModel, remoteUserConfigurationModel: ConfigurationModel, trigger: boolean): Promise { // reset caches - this.cachedFolderConfigs = new ResourceMap(); + this.cachedFolderConfigs.clearAndDisposeAll(); const folders = this.workspace.folders; const folderConfigurations = await this.loadFolderConfigurations(folders); @@ -942,9 +941,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat // Remove the configurations of deleted folders for (const key of this.cachedFolderConfigs.keys()) { if (!this.workspace.folders.filter(folder => folder.uri.toString() === key.toString())[0]) { - const folderConfiguration = this.cachedFolderConfigs.get(key); - folderConfiguration!.dispose(); - this.cachedFolderConfigs.delete(key); + this.cachedFolderConfigs.deleteAndDispose(key); changes.push(this._configuration.compareAndDeleteFolderConfiguration(key)); } } @@ -964,8 +961,8 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat let folderConfiguration = this.cachedFolderConfigs.get(folder.uri); if (!folderConfiguration) { folderConfiguration = new FolderConfiguration(!this.initialized, folder, FOLDER_CONFIG_FOLDER_NAME, this.getWorkbenchState(), this.isWorkspaceTrusted, this.fileService, this.uriIdentityService, this.logService, this.configurationCache); - this._register(folderConfiguration.onDidChange(() => this.onWorkspaceFolderConfigurationChanged(folder))); - this.cachedFolderConfigs.set(folder.uri, this._register(folderConfiguration)); + folderConfiguration.addRelated(folderConfiguration.onDidChange(() => this.onWorkspaceFolderConfigurationChanged(folder))); + this.cachedFolderConfigs.set(folder.uri, folderConfiguration); } return folderConfiguration.loadConfiguration(); })]); From 729e9d13e44607c4182fd15eba872fe2974eccdd Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Wed, 21 Jan 2026 10:24:55 +0800 Subject: [PATCH 321/387] better instructions for ai headers (#289259) --- .../chatThinkingContentPart.ts | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index 73a7bcb62c2..7fe9cfeb374 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -537,12 +537,41 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen context = this.currentThinkingValue.substring(0, 1000); } - const prompt = `Summarize the following actions in 6-7 words using past tense. Be very concise - focus on the main action only. No subjects, quotes, or punctuation. + const prompt = `Summarize the following actions concisely (6-10 words) using past tense. Follow these rules strictly: - Examples: - - "Preparing to create new page file, Read HomePage.tsx, Creating new TypeScript file" → "Created new page file" - - "Searching for files, Reading configuration, Analyzing dependencies" → "Analyzed project structure" - - "Invoked terminal command, Checked build output, Fixed errors" → "Ran build and fixed errors" + GENERAL: + - The actions may include tool calls (file edits, reads, searches, terminal commands) AND non-tool reasoning/analysis + - Summarize ALL actions, not just tool calls. If there's reasoning or analysis without tool calls, summarize that too + - Examples of non-tool actions: "Analyzing code structure", "Planning implementation", "Reviewing dependencies" + + RULES FOR TOOL CALLS: + 1. If the SAME file was both edited AND read: Start with "Read and edited " + 2. If exactly ONE file was edited: Start with "Edited " (include actual filename) + 3. If exactly ONE file was read: Start with "Read " (include actual filename) + 4. If MULTIPLE files were edited: Start with "Edited X files" + 5. If MULTIPLE files were read: Start with "Read X files" + 6. If BOTH edits AND reads occurred on DIFFERENT files: Start with "Edited and read " if one each, otherwise "Edited X files and read Y files" + 7. For searches: Say "searched for " with the actual search term, NOT "searched for files" + 8. After the file info, you may add a brief summary of other actions (e.g., ran terminal, searched for X) if space permits + 9. NEVER say "1 file" - always use the actual filename when there's only one file + + EXAMPLES: + - "Read HomePage.tsx, Edited HomePage.tsx" → "Read and edited HomePage.tsx" + - "Edited HomePage.tsx" → "Edited HomePage.tsx" + - "Read config.json, Read package.json" → "Read 2 files" + - "Edited App.tsx, Read utils.ts" → "Edited App.tsx and read utils.ts" + - "Edited App.tsx, Read utils.ts, Read types.ts" → "Edited App.tsx and read 2 files" + - "Edited index.ts, Edited styles.css, Ran terminal command" → "Edited 2 files and ran command" + - "Read README.md, Searched for AuthService" → "Read README.md and searched for AuthService" + - "Searched for login, Searched for authentication" → "Searched for login and authentication" + - "Edited api.ts, Edited models.ts, Read schema.json" → "Edited 2 files and read schema.json" + - "Edited Button.tsx, Edited Button.css, Edited index.ts" → "Edited 3 files" + - "Searched codebase for error handling" → "Searched for error handling" + - "Grep search for useState, Read App.tsx" → "Read App.tsx and searched for useState" + - "Analyzing component architecture" → "Analyzed component architecture" + - "Planning refactor strategy, Read utils.ts" → "Planned refactor and read utils.ts" + + No quotes, no trailing punctuation. Never say "searched for files" - always include the actual search term. Actions: ${context}`; From 691296005d662a60579fbfb68a37e4f77cc3f013 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 20 Jan 2026 19:11:27 -0800 Subject: [PATCH 322/387] Clean up chat row layout (#289238) * Minor chat optimizations and fixes * Remove updateItemHeightOnRender in favor of a ResizeObserver * Remove other unneeded onDidChangeHeight emitters * Fix autoscroll- maybe this fixes our autoscroll issues. But it does fix the flickering I get when expanding items, due to waiting for the resize observer then waiting multiple animation frames for the scroll event to be handled * Clean up * More onDidChangeHeights cleanup * Cleanup * Fixes * Restore onDidChangeHeight for thinking part --- src/vs/base/browser/dom.ts | 7 +- .../chatChangesSummaryPart.ts | 5 - .../chatCollapsibleContentPart.ts | 10 -- .../chatCollapsibleMarkdownContentPart.ts | 4 +- .../chatConfirmationContentPart.ts | 7 - .../chatConfirmationWidget.ts | 1 - .../chatElicitationContentPart.ts | 8 - .../chatErrorConfirmationPart.ts | 8 +- .../chatExtensionsContentPart.ts | 6 +- .../chatMarkdownContentPart.ts | 20 +-- .../chatMcpServersInteractionContentPart.ts | 11 -- .../chatMultiDiffContentPart.ts | 11 +- .../chatPullRequestContentPart.ts | 4 - .../chatContentParts/chatQuotaExceededPart.ts | 6 - .../chatSubagentContentPart.ts | 30 +--- .../chatContentParts/chatTaskContentPart.ts | 8 +- .../chatTextEditContentPart.ts | 11 +- .../chatThinkingContentPart.ts | 9 +- .../chatContentParts/chatTodoListWidget.ts | 15 +- .../chatToolInputOutputContentPart.ts | 11 +- .../chatToolOutputContentSubPart.ts | 11 +- .../chatContentParts/chatTreeContentPart.ts | 9 +- .../widget/chatContentParts/codeBlockPart.ts | 6 +- .../abstractToolConfirmationSubPart.ts | 1 - .../chatExtensionsInstallToolSubPart.ts | 2 - .../chatInputOutputMarkdownProgressPart.ts | 1 - .../toolInvocationParts/chatMcpAppSubPart.ts | 3 - .../chatResultListSubPart.ts | 1 - .../chatTerminalToolConfirmationSubPart.ts | 12 +- .../chatTerminalToolProgressPart.ts | 6 +- .../chatToolConfirmationSubPart.ts | 5 - .../chatToolInvocationPart.ts | 8 - .../chatToolInvocationSubPart.ts | 3 - .../toolInvocationParts/chatToolOutputPart.ts | 2 - .../chatToolPostExecuteConfirmationPart.ts | 1 - .../chatToolStreamingSubPart.ts | 3 - .../chat/browser/widget/chatListRenderer.ts | 136 ++++------------- .../chat/browser/widget/chatListWidget.ts | 139 +++++++----------- .../contrib/chat/browser/widget/chatWidget.ts | 16 -- 39 files changed, 124 insertions(+), 433 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 36848442d36..6dbacac2bd6 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -2064,12 +2064,9 @@ export class DisposableResizeObserver extends Disposable { this._register(toDisposable(() => this.observer.disconnect())); } - observe(target: Element, options?: ResizeObserverOptions): void { + observe(target: Element, options?: ResizeObserverOptions): IDisposable { this.observer.observe(target, options); - } - - unobserve(target: Element): void { - this.observer.unobserve(target); + return toDisposable(() => this.observer.unobserve(target)); } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatChangesSummaryPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatChangesSummaryPart.ts index ca58382b997..7dcf175a3f4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatChangesSummaryPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatChangesSummaryPart.ts @@ -8,7 +8,6 @@ import { $ } from '../../../../../../base/browser/dom.js'; import { ButtonWithIcon } from '../../../../../../base/browser/ui/button/button.js'; import { IListRenderer, IListVirtualDelegate } from '../../../../../../base/browser/ui/list/list.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { Emitter } from '../../../../../../base/common/event.js'; import { Iterable } from '../../../../../../base/common/iterator.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { autorun, IObservable } from '../../../../../../base/common/observable.js'; @@ -41,9 +40,6 @@ export class ChatCheckpointFileChangesSummaryContentPart extends Disposable impl public readonly ELEMENT_HEIGHT = 22; public readonly MAX_ITEMS_SHOWN = 6; - private readonly _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - private readonly diffsBetweenRequests = new Map>(); private fileChangesDiffsObservable: IObservable; @@ -101,7 +97,6 @@ export class ChatCheckpointFileChangesSummaryContentPart extends Disposable impl const setExpansionState = () => { viewListButton.icon = this.isCollapsed ? Codicon.chevronRight : Codicon.chevronDown; this.domNode.classList.toggle('chat-file-changes-collapsed', this.isCollapsed); - this._onDidChangeHeight.fire(); }; setExpansionState(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts index 69311635b2c..95456c91374 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts @@ -7,7 +7,6 @@ import { $ } from '../../../../../../base/browser/dom.js'; import { ButtonWithIcon } from '../../../../../../base/browser/ui/button/button.js'; import { HoverStyle } from '../../../../../../base/browser/ui/hover/hover.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { Emitter } from '../../../../../../base/common/event.js'; import { IMarkdownString, MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { Disposable, IDisposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; import { autorun, IObservable, observableValue } from '../../../../../../base/common/observable.js'; @@ -28,9 +27,6 @@ export abstract class ChatCollapsibleContentPart extends Disposable implements I private _domNode?: HTMLElement; private readonly _renderedTitleWithWidgets = this._register(new MutableDisposable()); - protected readonly _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - protected readonly hasFollowingContent: boolean; protected _isExpanded = observableValue(this, false); protected _collapseButton: ButtonWithIcon | undefined; @@ -100,12 +96,6 @@ export abstract class ChatCollapsibleContentPart extends Disposable implements I collapseButton.icon = this._overrideIcon.read(r) ?? (expanded ? Codicon.chevronDown : Codicon.chevronRight); this._domNode?.classList.toggle('chat-used-context-collapsed', !expanded); this.updateAriaLabel(collapseButton.element, typeof referencesLabel === 'string' ? referencesLabel : referencesLabel.value, expanded); - - if (this._domNode?.isConnected) { - queueMicrotask(() => { - this._onDidChangeHeight.fire(); - }); - } })); const content = this.initContent(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleMarkdownContentPart.ts index 1d4d8b9095d..c7f48ae4024 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleMarkdownContentPart.ts @@ -37,9 +37,7 @@ export class ChatCollapsibleMarkdownContentPart extends ChatCollapsibleContentPa if (this.markdownContent) { this.contentElement = $('.chat-collapsible-markdown-body'); - const rendered = this._register(this.chatContentMarkdownRenderer.render(new MarkdownString(this.markdownContent), { - asyncRenderCallback: () => this._onDidChangeHeight.fire(), - })); + const rendered = this._register(this.chatContentMarkdownRenderer.render(new MarkdownString(this.markdownContent))); this.contentElement.appendChild(rendered.element); wrapper.appendChild(this.contentElement); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatConfirmationContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatConfirmationContentPart.ts index 69d2b040db4..07ec74d925f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatConfirmationContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatConfirmationContentPart.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter } from '../../../../../../base/common/event.js'; import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; import { localize } from '../../../../../../nls.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; @@ -17,9 +16,6 @@ import { IChatContentPart, IChatContentPartRenderContext } from './chatContentPa export class ChatConfirmationContentPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; - private readonly _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - constructor( confirmation: IChatConfirmation, context: IChatContentPartRenderContext, @@ -43,8 +39,6 @@ export class ChatConfirmationContentPart extends Disposable implements IChatCont const confirmationWidget = this._register(this.instantiationService.createInstance(SimpleChatConfirmationWidget, context, { title: confirmation.title, buttons, message: confirmation.message })); confirmationWidget.setShowButtons(!confirmation.isUsed); - this._register(confirmationWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); - this._register(confirmationWidget.onDidClick(async e => { if (isResponseVM(element)) { const prompt = `${e.label}: "${confirmation.title}"`; @@ -63,7 +57,6 @@ export class ChatConfirmationContentPart extends Disposable implements IChatCont if (await this.chatService.sendRequest(element.sessionResource, prompt, options)) { confirmation.isUsed = true; confirmationWidget.setShowButtons(false); - this._onDidChangeHeight.fire(); } } })); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatConfirmationWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatConfirmationWidget.ts index d0fea511292..b58a0d67dc3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatConfirmationWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatConfirmationWidget.ts @@ -423,7 +423,6 @@ abstract class BaseChatConfirmationWidget extends Disposable { } satisfies IChatMarkdownContentPartOptions, )); renderFileWidgets(part.domNode, this.instantiationService, this.chatMarkdownAnchorService, this._store); - this._register(part.onDidChangeHeight(() => this._onDidChangeHeight.fire())); this.markdownContentPart.value = part; element = part.domNode; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatElicitationContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatElicitationContentPart.ts index 3f57d9627af..0018491307d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatElicitationContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatElicitationContentPart.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter } from '../../../../../../base/common/event.js'; import { IMarkdownString, isMarkdownString, MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { Disposable, IDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { autorun } from '../../../../../../base/common/observable.js'; @@ -22,9 +21,6 @@ import { IAction } from '../../../../../../base/common/actions.js'; export class ChatElicitationContentPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; - private readonly _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - private readonly _confirmWidget: ChatConfirmationWidget; public get codeblocks() { @@ -88,8 +84,6 @@ export class ChatElicitationContentPart extends Disposable implements IChatConte this._confirmWidget = confirmationWidget; confirmationWidget.setShowButtons(elicitation.kind === 'elicitation2' && elicitation.state.get() === ElicitationState.Pending); - this._register(confirmationWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); - this._register(confirmationWidget.onDidClick(async e => { if (elicitation.kind !== 'elicitation2') { return; @@ -111,8 +105,6 @@ export class ChatElicitationContentPart extends Disposable implements IChatConte confirmationWidget.setShowButtons(false); confirmationWidget.updateMessage(this.getMessageToRender(elicitation)); - - this._onDidChangeHeight.fire(); })); this.domNode = confirmationWidget.domNode; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatErrorConfirmationPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatErrorConfirmationPart.ts index f9dea3c88e3..fc89ccc8aa3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatErrorConfirmationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatErrorConfirmationPart.ts @@ -5,7 +5,6 @@ import * as dom from '../../../../../../base/browser/dom.js'; import { Button, IButtonOptions } from '../../../../../../base/browser/ui/button/button.js'; -import { Emitter } from '../../../../../../base/common/event.js'; import { IMarkdownString } from '../../../../../../base/common/htmlContent.js'; import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; import { IMarkdownRenderer } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; @@ -22,9 +21,6 @@ const $ = dom.$; export class ChatErrorConfirmationContentPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; - private readonly _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - constructor( kind: ChatErrorLevel, content: IMarkdownString, @@ -62,9 +58,7 @@ export class ChatErrorConfirmationContentPart extends Disposable implements ICha const widget = chatWidgetService.getWidgetBySessionResource(element.sessionResource); options.userSelectedModelId = widget?.input.currentLanguageModel; Object.assign(options, widget?.getModeRequestOptions()); - if (await chatService.sendRequest(element.sessionResource, prompt, options)) { - this._onDidChangeHeight.fire(); - } + await chatService.sendRequest(element.sessionResource, prompt, options); })); }); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatExtensionsContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatExtensionsContentPart.ts index 5576bf98c9d..8d101f1c2fb 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatExtensionsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatExtensionsContentPart.ts @@ -5,7 +5,7 @@ import './media/chatExtensionsContent.css'; import * as dom from '../../../../../../base/browser/dom.js'; -import { Emitter, Event } from '../../../../../../base/common/event.js'; +import { Event } from '../../../../../../base/common/event.js'; import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ExtensionsList, getExtensions } from '../../../../extensions/browser/extensionsViewer.js'; @@ -22,9 +22,6 @@ import { localize } from '../../../../../../nls.js'; export class ChatExtensionsContentPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; - private _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - public get codeblocks(): IChatCodeBlockInfo[] { return []; } @@ -53,7 +50,6 @@ export class ChatExtensionsContentPart extends Disposable implements IChatConten } list.setModel(new PagedModel(extensions)); list.layout(); - this._onDidChangeHeight.fire(); }); } 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 6485bd8f9dc..d060546f925 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts @@ -13,7 +13,6 @@ import { DomScrollableElement } from '../../../../../../base/browser/ui/scrollba import { coalesce } from '../../../../../../base/common/arrays.js'; import { findLast } from '../../../../../../base/common/arraysFind.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { Emitter } from '../../../../../../base/common/event.js'; import { Lazy } from '../../../../../../base/common/lazy.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { autorun, autorunSelfDisposable, derived } from '../../../../../../base/common/observable.js'; @@ -92,9 +91,6 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP private readonly allRefs: IDisposableReference[] = []; - private readonly _onDidChangeHeight = this._register(new Emitter()); - readonly onDidChangeHeight = this._onDidChangeHeight.event; - private readonly _codeblocks: IMarkdownPartCodeBlockInfo[] = []; public get codeblocks(): IChatCodeBlockInfo[] { return this._codeblocks; @@ -200,14 +196,12 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP dispose: () => diffPart.dispose() }; this.allRefs.push(ref); - this._register(diffPart.onDidChangeContentHeight(() => this._onDidChangeHeight.fire())); orderedDisposablesList.push(ref); return diffPart.element; } } if (languageId === 'vscode-extensions') { const chatExtensions = this._register(instantiationService.createInstance(ChatExtensionsContentPart, { kind: 'extensions', extensions: text.split(',') })); - this._register(chatExtensions.onDidChangeHeight(() => this._onDidChangeHeight.fire())); return chatExtensions.domNode; } const globalIndex = globalCodeBlockIndexStart++; @@ -249,10 +243,6 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP const ref = this.renderCodeBlock(codeBlockInfo, text, isCodeBlockComplete, currentWidth); this.allRefs.push(ref); - // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) - // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) - this._register(ref.object.onDidChangeContentHeight(() => this._onDidChangeHeight.fire())); - const ownerMarkdownPartId = this.codeblocksPartId; const info: IMarkdownPartCodeBlockInfo = new class implements IMarkdownPartCodeBlockInfo { readonly ownerMarkdownPartId = ownerMarkdownPartId; @@ -284,7 +274,6 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP this.codeBlockModelCollection.update(codeBlockInfo.element.sessionResource, codeBlockInfo.element, codeBlockInfo.codeBlockIndex, { text, languageId: codeBlockInfo.languageId, isComplete: isCodeBlockComplete }).then((e) => { // Update the existing object's codemapperUri this._codeblocks[codeBlockInfo.codeBlockPartIndex].codemapperUri = e.codemapperUri; - this._onDidChangeHeight.fire(); }); } this.allRefs.push(ref); @@ -311,7 +300,6 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP return ref.object.element; } }, - asyncRenderCallback: () => this._onDidChangeHeight.fire(), markedOptions: markedOpts, markedExtensions, ...markdownRenderOptions, @@ -373,9 +361,6 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP console.error('Failed to load MarkedKatexSupport extension:', e); }).finally(() => { doRenderMarkdown(); - if (!this._store.isDisposed) { - this._onDidChangeHeight.fire(); - } }); } else { doRenderMarkdown(); @@ -401,13 +386,10 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP this.codeBlockModelCollection.update(data.element.sessionResource, data.element, data.codeBlockIndex, { text, languageId: data.languageId, isComplete }).then((e) => { // Update the existing object's codemapperUri this._codeblocks[data.codeBlockPartIndex].codemapperUri = e.codemapperUri; - this._onDidChangeHeight.fire(); }); } - editorInfo.render(data, currentWidth).then(() => { - this._onDidChangeHeight.fire(); - }); + editorInfo.render(data, currentWidth); return ref; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMcpServersInteractionContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMcpServersInteractionContentPart.ts index 9ef33d8fa8a..3e7c9c91667 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMcpServersInteractionContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMcpServersInteractionContentPart.ts @@ -6,7 +6,6 @@ import * as dom from '../../../../../../base/browser/dom.js'; import { RunOnceScheduler } from '../../../../../../base/common/async.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { Emitter } from '../../../../../../base/common/event.js'; import { escapeMarkdownSyntaxTokens, createMarkdownCommandLink, MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { Lazy } from '../../../../../../base/common/lazy.js'; import { Disposable, IDisposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; @@ -28,8 +27,6 @@ import './media/chatMcpServersInteractionContent.css'; export class ChatMcpServersInteractionContentPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; - private readonly _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; private workingProgressPart: ChatProgressContentPart | undefined; private interactionContainer: HTMLElement | undefined; @@ -104,8 +101,6 @@ export class ChatMcpServersInteractionContentPart extends Disposable implements this.interactionContainer.remove(); this.interactionContainer = undefined; } - - this._onDidChangeHeight.fire(); } private createServerCommandLinks(servers: Array<{ id: string; label: string }>): string { @@ -146,8 +141,6 @@ export class ChatMcpServersInteractionContentPart extends Disposable implements )); this.domNode.appendChild(this.workingProgressPart.domNode); } - - this._onDidChangeHeight.fire(); } private renderInteractionRequired(serversRequiringInteraction: Array<{ id: string; label: string; errorMessage?: string }>): void { @@ -181,7 +174,6 @@ export class ChatMcpServersInteractionContentPart extends Disposable implements : localize('mcp.start.multiple', 'The MCP servers {0} may have new tools and require interaction to start. [Start them now?]({1})', links, '#start'); const str = new MarkdownString(content, { isTrusted: true }); const messageMd = this.interactionMd.value = this._markdownRendererService.render(str, { - asyncRenderCallback: () => this._onDidChangeHeight.fire(), actionHandler: (content) => { if (!content.startsWith('command:')) { this._start(startLink!); @@ -221,7 +213,6 @@ export class ChatMcpServersInteractionContentPart extends Disposable implements for (let i = 0; i < serversToStart.length; i++) { const serverInfo = serversToStart[i]; startLink.textContent = localize('mcp.starting', "Starting {0}...", serverInfo.label); - this._onDidChangeHeight.fire(); const server = this.mcpService.servers.get().find(s => s.definition.id === serverInfo.id); if (server) { @@ -242,8 +233,6 @@ export class ChatMcpServersInteractionContentPart extends Disposable implements startLink.style.pointerEvents = ''; startLink.style.opacity = ''; startLink.textContent = 'Start now?'; - } finally { - this._onDidChangeHeight.fire(); } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMultiDiffContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMultiDiffContentPart.ts index b310d8a20f1..15ae7996728 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMultiDiffContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMultiDiffContentPart.ts @@ -7,7 +7,7 @@ import * as dom from '../../../../../../base/browser/dom.js'; import { ButtonWithIcon } from '../../../../../../base/browser/ui/button/button.js'; import { IListRenderer, IListVirtualDelegate } from '../../../../../../base/browser/ui/list/list.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { Emitter, Event } from '../../../../../../base/common/event.js'; +import { Event } from '../../../../../../base/common/event.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { MarshalledId } from '../../../../../../base/common/marshallingIds.js'; import { autorun, constObservable, IObservable, isObservable } from '../../../../../../base/common/observable.js'; @@ -48,9 +48,6 @@ const MAX_ITEMS_SHOWN = 6; export class ChatMultiDiffContentPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; - private readonly _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - private list!: WorkbenchList; private isCollapsed: boolean = false; private readonly readOnly: boolean; @@ -93,7 +90,6 @@ export class ChatMultiDiffContentPart extends Disposable implements IChatContent const setExpansionState = () => { viewListButton.icon = this.isCollapsed ? Codicon.chevronRight : Codicon.chevronDown; this.domNode.classList.toggle('chat-file-changes-collapsed', this.isCollapsed); - this._onDidChangeHeight.fire(); }; setExpansionState(); @@ -150,7 +146,7 @@ export class ChatMultiDiffContentPart extends Disposable implements IChatContent $mid: MarshalledId.Uri }; - const toolbar = disposables.add(nestedInsta.createInstance( + disposables.add(nestedInsta.createInstance( MenuWorkbenchToolBar, buttonsContainer, MenuId.ChatMultiDiffContext, @@ -165,8 +161,6 @@ export class ChatMultiDiffContentPart extends Disposable implements IChatContent } )); - disposables.add(toolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); - return disposables; } @@ -231,7 +225,6 @@ export class ChatMultiDiffContentPart extends Disposable implements IChatContent const height = Math.min(items.length, MAX_ITEMS_SHOWN) * ELEMENT_HEIGHT; this.list.layout(height); listContainer.style.height = `${height}px`; - this._onDidChangeHeight.fire(); })); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatPullRequestContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatPullRequestContentPart.ts index 200c92b9789..bd557d60550 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatPullRequestContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatPullRequestContentPart.ts @@ -5,7 +5,6 @@ import './media/chatPullRequestContent.css'; import * as dom from '../../../../../../base/browser/dom.js'; -import { Emitter } from '../../../../../../base/common/event.js'; import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; import { IChatPullRequestContent } from '../../../common/chatService/chatService.js'; import { IChatRendererContent } from '../../../common/model/chatViewModel.js'; @@ -21,9 +20,6 @@ import { renderAsPlaintext } from '../../../../../../base/browser/markdownRender export class ChatPullRequestContentPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; - private _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - constructor( private readonly pullRequestContent: IChatPullRequestContent, @IOpenerService private readonly openerService: IOpenerService diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuotaExceededPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuotaExceededPart.ts index 3d51867e4a4..cb964b6db6e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuotaExceededPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuotaExceededPart.ts @@ -7,7 +7,6 @@ import * as dom from '../../../../../../base/browser/dom.js'; import { Button } from '../../../../../../base/browser/ui/button/button.js'; import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../../../base/common/actions.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { Emitter } from '../../../../../../base/common/event.js'; import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; @@ -41,9 +40,6 @@ export class ChatQuotaExceededPart extends Disposable implements IChatContentPar readonly domNode: HTMLElement; - private readonly _onDidChangeHeight = this._register(new Emitter()); - readonly onDidChangeHeight = this._onDidChangeHeight.event; - constructor( element: IChatResponseViewModel, private readonly content: IChatErrorDetailsPart, @@ -101,8 +97,6 @@ export class ChatQuotaExceededPart extends Disposable implements IChatContentPar retryButton.element.classList.add('chat-quota-error-secondary-button'); retryButton.label = localize('clickToContinue', "Click to Retry"); - this._onDidChangeHeight.fire(); - this._register(retryButton.onDidClick(() => { const widget = chatWidgetService.getWidgetBySessionResource(element.sessionResource); if (!widget) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts index a2ffe19a43a..f23c0333600 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../../../base/browser/dom.js'; -import { $, AnimationFrameScheduler } from '../../../../../../base/browser/dom.js'; +import { $, AnimationFrameScheduler, DisposableResizeObserver } from '../../../../../../base/browser/dom.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { rcut } from '../../../../../../base/common/strings.js'; @@ -159,6 +159,10 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen // Scheduler for coalescing layout operations this.layoutScheduler = this._register(new AnimationFrameScheduler(this.domNode, () => this.performLayout())); + // Use ResizeObserver to trigger layout when wrapper content changes + const resizeObserver = this._register(new DisposableResizeObserver(() => this.layoutScheduler.schedule())); + this._register(resizeObserver.observe(this.wrapper)); + // Render the prompt section at the start if available (must be after wrapper is initialized) this.renderPromptSection(); @@ -221,7 +225,6 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen this.context, this.chatContentMarkdownRenderer )); - this._register(collapsiblePart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); // Wrap in a container for chain of thought line styling this.promptContainer = $('.chat-thinking-tool-wrapper.chat-subagent-section'); @@ -250,7 +253,6 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen this.finalizeTitle(); // Collapse when done this.setExpanded(false); - this._onDidChangeHeight.fire(); } public finalizeTitle(): void { @@ -360,7 +362,6 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen this.context, this.chatContentMarkdownRenderer )); - this._register(collapsiblePart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); // Wrap in a container for chain of thought line styling this.resultContainer = $('.chat-thinking-tool-wrapper.chat-subagent-section'); @@ -373,8 +374,6 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen if (this.wrapper.style.display === 'none') { this.wrapper.style.display = ''; } - - this._onDidChangeHeight.fire(); } /** @@ -425,21 +424,6 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen ); this._register(part); - this._register(part.onDidChangeHeight(() => { - this.layoutScheduler.schedule(); - this._onDidChangeHeight.fire(); - })); - - // Watch for tool completion to update height when label changes - if (toolInvocation.kind === 'toolInvocation') { - this._register(autorun(r => { - const state = toolInvocation.state.read(r); - if (state.type === IChatToolInvocation.StateKind.Completed) { - this._onDidChangeHeight.fire(); - } - })); - } - return part; } @@ -504,8 +488,6 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen this.pendingResultText = undefined; this.doRenderResultText(resultText); } - - this._onDidChangeHeight.fire(); } private performLayout(): void { @@ -522,8 +504,6 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen const scrollHeight = this.wrapper.scrollHeight; this.wrapper.scrollTop = scrollHeight; } - - this._onDidChangeHeight.fire(); } hasSameContent(other: IChatRendererContent, _followingContent: IChatRendererContent[], _element: ChatTreeItem): boolean { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTaskContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTaskContentPart.ts index b96749b7cdf..b112e4bd616 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTaskContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTaskContentPart.ts @@ -4,19 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../../../base/browser/dom.js'; -import { Event } from '../../../../../../base/common/event.js'; import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; -import { IMarkdownRenderer } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { IChatProgressRenderableResponseContent } from '../../../common/model/chatModel.js'; +import { IMarkdownRenderer } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; import { IChatTask, IChatTaskSerialized } from '../../../common/chatService/chatService.js'; +import { IChatProgressRenderableResponseContent } from '../../../common/model/chatModel.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; import { ChatProgressContentPart } from './chatProgressContentPart.js'; import { ChatCollapsibleListContentPart, CollapsibleListPool } from './chatReferencesContentPart.js'; export class ChatTaskContentPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; - public readonly onDidChangeHeight: Event; private isSettled: boolean; @@ -34,7 +32,6 @@ export class ChatTaskContentPart extends Disposable implements IChatContentPart const refsPart = this._register(instantiationService.createInstance(ChatCollapsibleListContentPart, task.progress, task.content.value, context, contentReferencesListPool, undefined)); this.domNode = dom.$('.chat-progress-task'); this.domNode.appendChild(refsPart.domNode); - this.onDidChangeHeight = refsPart.onDidChangeHeight; } else { const isSettled = task.kind === 'progressTask' ? task.isSettled() : @@ -43,7 +40,6 @@ export class ChatTaskContentPart extends Disposable implements IChatContentPart const showSpinner = !isSettled && !context.element.isComplete; const progressPart = this._register(instantiationService.createInstance(ChatProgressContentPart, task, chatContentMarkdownRenderer, context, showSpinner, true, undefined, undefined)); this.domNode = progressPart.domNode; - this.onDidChangeHeight = Event.None; } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTextEditContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTextEditContentPart.ts index 959ef8b2347..4305cc54d72 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTextEditContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTextEditContentPart.ts @@ -5,7 +5,7 @@ import * as dom from '../../../../../../base/browser/dom.js'; import { CancellationTokenSource } from '../../../../../../base/common/cancellation.js'; -import { Emitter, Event } from '../../../../../../base/common/event.js'; +import { Event } from '../../../../../../base/common/event.js'; import { Disposable, IDisposable, IReference, RefCountedDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../../base/common/network.js'; import { isEqual } from '../../../../../../base/common/resources.js'; @@ -43,9 +43,6 @@ export class ChatTextEditContentPart extends Disposable implements IChatContentP public readonly domNode: HTMLElement; private readonly comparePart: IDisposableReference | undefined; - private readonly _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - constructor( chatTextEdit: IChatTextEditGroup, context: IChatContentPartRenderContext, @@ -86,12 +83,6 @@ export class ChatTextEditContentPart extends Disposable implements IChatContentP this.comparePart = this._register(diffEditorPool.get()); - // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) - // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) - this._register(this.comparePart.object.onDidChangeContentHeight(() => { - this._onDidChangeHeight.fire(); - })); - const data: ICodeCompareBlockData = { element, edit: chatTextEdit, diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index 7fe9cfeb374..6ed7bee0fb8 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -24,6 +24,7 @@ import { localize } from '../../../../../../nls.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { Lazy } from '../../../../../../base/common/lazy.js'; +import { Emitter } from '../../../../../../base/common/event.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; import { autorun } from '../../../../../../base/common/observable.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; @@ -102,6 +103,9 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen public readonly codeblocks: undefined; public readonly codeblocksPartId: undefined; + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + private id: string | undefined; private content: IChatThinkingPart; private currentThinkingValue: string; @@ -188,15 +192,16 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen } })); - // Materialize lazy items when first expanded this._register(autorun(r => { + // Materialize lazy items when first expanded if (this._isExpanded.read(r) && !this.hasExpandedOnce && this.lazyItems.length > 0) { this.hasExpandedOnce = true; for (const item of this.lazyItems) { this.materializeLazyItem(item); } - this._onDidChangeHeight.fire(); } + // Fire when expanded/collapsed + this._onDidChangeHeight.fire(); })); if (this._collapseButton && !this.streamingCompleted && !this.element.isComplete) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTodoListWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTodoListWidget.ts index 57585c981b5..c58fdfc1206 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTodoListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTodoListWidget.ts @@ -8,16 +8,15 @@ import { Button } from '../../../../../../base/browser/ui/button/button.js'; import { IconLabel } from '../../../../../../base/browser/ui/iconLabel/iconLabel.js'; import { IListRenderer, IListVirtualDelegate } from '../../../../../../base/browser/ui/list/list.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { Emitter, Event } from '../../../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { isEqual } from '../../../../../../base/common/resources.js'; +import { URI } from '../../../../../../base/common/uri.js'; import { localize } from '../../../../../../nls.js'; import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { WorkbenchList } from '../../../../../../platform/list/browser/listService.js'; -import { IChatTodoListService, IChatTodo } from '../../../common/tools/chatTodoListService.js'; import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; -import { URI } from '../../../../../../base/common/uri.js'; -import { isEqual } from '../../../../../../base/common/resources.js'; +import { IChatTodo, IChatTodoListService } from '../../../common/tools/chatTodoListService.js'; class TodoListDelegate implements IListVirtualDelegate { getHeight(element: IChatTodo): number { @@ -113,9 +112,6 @@ class TodoListRenderer implements IListRenderer { export class ChatTodoListWidget extends Disposable { public readonly domNode: HTMLElement; - private readonly _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight: Event = this._onDidChangeHeight.event; - private _isExpanded: boolean = false; private _userManuallyExpanded: boolean = false; private expandoButton!: Button; @@ -150,7 +146,6 @@ export class ChatTodoListWidget extends Disposable { private hideWidget(): void { this.domNode.style.display = 'none'; - this._onDidChangeHeight.fire(); } private createChatTodoWidget(): HTMLElement { @@ -257,7 +252,6 @@ export class ChatTodoListWidget extends Disposable { this.domNode.classList.add('has-todos'); this.renderTodoList(todoList); this.domNode.style.display = 'block'; - this._onDidChangeHeight.fire(); } private renderTodoList(todoList: IChatTodo[]): void { @@ -313,7 +307,6 @@ export class ChatTodoListWidget extends Disposable { this.expandIcon.classList.add('codicon-chevron-right'); this.updateTitleElement(this.titleElement, todoList); - this._onDidChangeHeight.fire(); } } @@ -330,8 +323,6 @@ export class ChatTodoListWidget extends Disposable { const todoList = this.chatTodoListService.getTodos(this._currentSessionResource); this.updateTitleElement(this.titleElement, todoList); } - - this._onDidChangeHeight.fire(); } private clearAllTodos(): void { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolInputOutputContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolInputOutputContentPart.ts index 9f244f116f0..9426697b2a0 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolInputOutputContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolInputOutputContentPart.ts @@ -7,7 +7,6 @@ import * as dom from '../../../../../../base/browser/dom.js'; import { ButtonWithIcon } from '../../../../../../base/browser/ui/button/button.js'; import { HoverStyle } from '../../../../../../base/browser/ui/hover/hover.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { Emitter } from '../../../../../../base/common/event.js'; import { IMarkdownString } from '../../../../../../base/common/htmlContent.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { autorun, ISettableObservable, observableValue } from '../../../../../../base/common/observable.js'; @@ -52,9 +51,6 @@ export interface IChatCollapsibleOutputData { } export class ChatCollapsibleInputOutputContentPart extends Disposable { - private readonly _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - private _currentWidth: number = 0; private readonly _editorReferences: IDisposableReference[] = []; private readonly _titlePart: ChatQueryTitlePart; @@ -106,14 +102,12 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable { this.domNode = container.root; container.root.appendChild(elements.root); - const titlePart = this._titlePart = this._register(_instantiationService.createInstance( + this._titlePart = this._register(_instantiationService.createInstance( ChatQueryTitlePart, titleEl.root, title, subtitle, )); - this._register(titlePart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); - const spacer = document.createElement('span'); spacer.style.flexGrow = '1'; @@ -144,7 +138,6 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable { ? Codicon.check : ThemeIcon.modify(Codicon.loading, 'spin'); elements.root.classList.toggle('collapsed', !value); - this._onDidChangeHeight.fire(); })); const toggle = (e: Event) => { @@ -203,7 +196,6 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable { output.parts, )); this._outputSubPart = outputSubPart; - this._register(outputSubPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); contents.output.appendChild(outputSubPart.domNode); } @@ -223,7 +215,6 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable { }; const editorReference = this._register(this.context.editorPool.get()); editorReference.object.render(data, this._currentWidth || 300); - this._register(editorReference.object.onDidChangeContentHeight(() => this._onDidChangeHeight.fire())); container.appendChild(editorReference.object.element); this._editorReferences.push(editorReference); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolOutputContentSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolOutputContentSubPart.ts index 63af33033b7..700882b896c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolOutputContentSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolOutputContentSubPart.ts @@ -5,7 +5,6 @@ import * as dom from '../../../../../../base/browser/dom.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { Emitter } from '../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { basename, joinPath } from '../../../../../../base/common/resources.js'; import { URI } from '../../../../../../base/common/uri.js'; @@ -40,9 +39,6 @@ import { ChatCollapsibleIOPart, IChatCollapsibleIOCodePart, IChatCollapsibleIODa * This is used by both ChatCollapsibleInputOutputContentPart and ChatToolPostExecuteConfirmationPart. */ export class ChatToolOutputContentSubPart extends Disposable { - private readonly _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - private _currentWidth: number = 0; private readonly _editorReferences: IDisposableReference[] = []; public readonly domNode: HTMLElement; @@ -106,7 +102,7 @@ export class ChatToolOutputContentSubPart extends Disposable { dom.h('.chat-collapsible-io-resource-actions@actions'), ]); - this.fillInResourceGroup(parts, el.items, el.actions).then(() => this._onDidChangeHeight.fire()); + this.fillInResourceGroup(parts, el.items, el.actions); container.appendChild(el.root); return el.root; @@ -122,6 +118,10 @@ export class ChatToolOutputContentSubPart extends Disposable { } })); + if (this._store.isDisposed) { + return; + } + const attachments = this._register(this._instantiationService.createInstance( ChatAttachmentsContentPart, { @@ -183,7 +183,6 @@ export class ChatToolOutputContentSubPart extends Disposable { }; const editorReference = this._register(this.context.editorPool.get()); editorReference.object.render(data, this._currentWidth || 300); - this._register(editorReference.object.onDidChangeContentHeight(() => this._onDidChangeHeight.fire())); container.appendChild(editorReference.object.element); this._editorReferences.push(editorReference); this.codeblocks.push(firstPart.codeBlockInfo); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTreeContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTreeContentPart.ts index 598fe1f0867..2efa11c044c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTreeContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTreeContentPart.ts @@ -9,7 +9,7 @@ import { ITreeCompressionDelegate } from '../../../../../../base/browser/ui/tree import { ICompressedTreeNode } from '../../../../../../base/browser/ui/tree/compressedObjectTreeModel.js'; import { ICompressibleTreeRenderer } from '../../../../../../base/browser/ui/tree/objectTree.js'; import { IAsyncDataSource, ITreeNode } from '../../../../../../base/browser/ui/tree/tree.js'; -import { Emitter, Event } from '../../../../../../base/common/event.js'; +import { Event } from '../../../../../../base/common/event.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../../../base/common/lifecycle.js'; import { localize } from '../../../../../../nls.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; @@ -31,9 +31,6 @@ const $ = dom.$; export class ChatTreeContentPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; - private readonly _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - public readonly onDidFocus: Event; private tree: WorkbenchCompressibleAsyncDataTree; @@ -54,9 +51,6 @@ export class ChatTreeContentPart extends Disposable implements IChatContentPart this.openerService.open(e.element.uri); } })); - this._register(this.tree.onDidChangeCollapseState(() => { - this._onDidChangeHeight.fire(); - })); this._register(this.tree.onContextMenu((e) => { e.browserEvent.preventDefault(); e.browserEvent.stopPropagation(); @@ -65,7 +59,6 @@ export class ChatTreeContentPart extends Disposable implements IChatContentPart this.tree.setInput(data).then(() => { if (!ref.isStale()) { this.tree.layout(); - this._onDidChangeHeight.fire(); } }); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/codeBlockPart.ts index 35b7d57c59e..45b20d570ee 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/codeBlockPart.ts @@ -56,7 +56,7 @@ import { ServiceCollection } from '../../../../../../platform/instantiation/comm import { ILabelService } from '../../../../../../platform/label/common/label.js'; import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; import { ResourceLabel } from '../../../../../browser/labels.js'; -import { ResourceContextKey } from '../../../../../common/contextkeys.js'; +import { StaticResourceContextKey } from '../../../../../common/contextkeys.js'; import { AccessibilityVerbositySettingId } from '../../../../accessibility/browser/accessibilityConfiguration.js'; import { InspectEditorTokensController } from '../../../../codeEditor/browser/inspectEditorTokens/inspectEditorTokens.js'; import { MenuPreventer } from '../../../../codeEditor/browser/menuPreventer.js'; @@ -169,7 +169,7 @@ export class CodeBlockPart extends Disposable { private isDisposed = false; - private resourceContextKey: ResourceContextKey; + private resourceContextKey: StaticResourceContextKey; private get verticalPadding(): number { return this.currentCodeBlockData?.renderOptions?.verticalPadding ?? defaultCodeblockPadding; @@ -190,7 +190,7 @@ export class CodeBlockPart extends Disposable { super(); this.element = $('.interactive-result-code-block'); - this.resourceContextKey = this._register(instantiationService.createInstance(ResourceContextKey)); + this.resourceContextKey = instantiationService.createInstance(StaticResourceContextKey); this.contextKeyService = this._register(contextKeyService.createScoped(this.element)); const scopedInstantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService]))); const editorElement = dom.append(this.element, $('.interactive-result-editor')); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/abstractToolConfirmationSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/abstractToolConfirmationSubPart.ts index 2d6e9558ff9..19312386303 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/abstractToolConfirmationSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/abstractToolConfirmationSubPart.ts @@ -105,7 +105,6 @@ export abstract class AbstractToolConfirmationSubPart extends BaseChatToolInvoca this.chatWidgetService.getWidgetBySessionResource(this.context.element.sessionResource)?.focusInput(); })); - this._register(confirmWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); this._register(toDisposable(() => hasToolConfirmation.reset())); this.domNode = confirmWidget.domNode; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatExtensionsInstallToolSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatExtensionsInstallToolSubPart.ts index ce673417560..efbe69369f7 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatExtensionsInstallToolSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatExtensionsInstallToolSubPart.ts @@ -52,7 +52,6 @@ export class ExtensionsInstallConfirmationWidgetSubPart extends BaseChatToolInvo const extensionsContent = toolInvocation.toolSpecificData; this.domNode = dom.$(''); const chatExtensionsContentPart = this._register(instantiationService.createInstance(ChatExtensionsContentPart, extensionsContent)); - this._register(chatExtensionsContentPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); dom.append(this.domNode, chatExtensionsContentPart.domNode); const state = toolInvocation.state.get(); @@ -90,7 +89,6 @@ export class ExtensionsInstallConfirmationWidgetSubPart extends BaseChatToolInvo } )); this._confirmWidget = confirmWidget; - this._register(confirmWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); dom.append(this.domNode, confirmWidget.domNode); this._register(confirmWidget.onDidClick(button => { IChatToolInvocation.confirmWith(toolInvocation, button.data); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatInputOutputMarkdownProgressPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatInputOutputMarkdownProgressPart.ts index 0d7a1188a3a..2bb403aedd5 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatInputOutputMarkdownProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatInputOutputMarkdownProgressPart.ts @@ -129,7 +129,6 @@ export class ChatInputOutputMarkdownProgressPart extends BaseChatToolInvocationS ChatInputOutputMarkdownProgressPart._expandedByDefault.get(toolInvocation) ?? false, )); this._codeblocks.push(...collapsibleListPart.codeblocks); - this._register(collapsibleListPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); this._register(toDisposable(() => ChatInputOutputMarkdownProgressPart._expandedByDefault.set(toolInvocation, collapsibleListPart.expanded))); const progressObservable = toolInvocation.kind === 'toolInvocation' ? toolInvocation.state.map((s, r) => s.type === IChatToolInvocation.StateKind.Executing ? s.progress.read(r) : undefined) : undefined; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts index a1414f7cf79..98a2cef2f07 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts @@ -108,7 +108,6 @@ export class ChatMcpAppSubPart extends BaseChatToolInvocationSubPart { // Subscribe to model height changes this._register(this._model.onDidChangeHeight(() => { this._updateContainerHeight(); - this._onDidChangeHeight.fire(); })); this._register(context.onDidChangeVisibility(visible => { @@ -150,7 +149,6 @@ export class ChatMcpAppSubPart extends BaseChatToolInvocationSubPart { case 'loaded': { // Show the webview container container.style.display = ''; - this._onDidChangeHeight.fire(); break; } case 'error': { @@ -190,6 +188,5 @@ export class ChatMcpAppSubPart extends BaseChatToolInvocationSubPart { container.appendChild(errorNode); this._errorNode = errorNode; - this._onDidChangeHeight.fire(); } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatResultListSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatResultListSubPart.ts index 4b8b00155ef..af3c5c31ef7 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatResultListSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatResultListSubPart.ts @@ -41,7 +41,6 @@ export class ChatResultListSubPart extends BaseChatToolInvocationSubPart { getToolApprovalMessage(toolInvocation), )); collapsibleListPart.icon = Codicon.check; - this._register(collapsibleListPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); this.domNode = collapsibleListPart.domNode; } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts index 45ef359a7ee..d7826880faa 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts @@ -11,7 +11,7 @@ import { asArray } from '../../../../../../../base/common/arrays.js'; import { Codicon } from '../../../../../../../base/common/codicons.js'; import { ErrorNoTelemetry } from '../../../../../../../base/common/errors.js'; import { createCommandUri, MarkdownString, type IMarkdownString } from '../../../../../../../base/common/htmlContent.js'; -import { thenIfNotDisposed, thenRegisterOrDispose, toDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { thenRegisterOrDispose, toDisposable } from '../../../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../../../base/common/network.js'; import Severity from '../../../../../../../base/common/severity.js'; import { isObject } from '../../../../../../../base/common/types.js'; @@ -32,17 +32,17 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../../../ import { IPreferencesService } from '../../../../../../services/preferences/common/preferences.js'; import { ITerminalChatService } from '../../../../../terminal/browser/terminal.js'; import { TerminalContribCommandId, TerminalContribSettingId } from '../../../../../terminal/terminalContribExports.js'; -import { migrateLegacyTerminalToolSpecificData } from '../../../../common/chat.js'; import { ChatContextKeys } from '../../../../common/actions/chatContextKeys.js'; +import { migrateLegacyTerminalToolSpecificData } from '../../../../common/chat.js'; import { IChatToolInvocation, ToolConfirmKind, type IChatTerminalToolInvocationData, type ILegacyChatTerminalToolInvocationData } from '../../../../common/chatService/chatService.js'; import type { CodeBlockModelCollection } from '../../../../common/widget/codeBlockModelCollection.js'; import { AcceptToolConfirmationActionId, SkipToolConfirmationActionId } from '../../../actions/chatToolActions.js'; import { IChatCodeBlockInfo, IChatWidgetService } from '../../../chat.js'; -import { ICodeBlockRenderOptions } from '../codeBlockPart.js'; import { ChatCustomConfirmationWidget, IChatConfirmationButton } from '../chatConfirmationWidget.js'; import { EditorPool } from '../chatContentCodePools.js'; import { IChatContentPartRenderContext } from '../chatContentParts.js'; import { ChatMarkdownContentPart } from '../chatMarkdownContentPart.js'; +import { ICodeBlockRenderOptions } from '../codeBlockPart.js'; import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; export const enum TerminalToolConfirmationStorageKeys { @@ -161,7 +161,7 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS )); thenRegisterOrDispose(textModelService.createModelReference(model.uri), this._store); const editor = this._register(this.editorPool.get()); - const renderPromise = editor.object.render({ + editor.object.render({ codeBlockIndex: this.codeBlockStartIndex, codeBlockPartIndex: 0, element: this.context.element, @@ -170,7 +170,6 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS textModel: Promise.resolve(model), chatSessionResource: this.context.element.sessionResource }, this.currentWidthDelegate()); - this._register(thenIfNotDisposed(renderPromise, () => this._onDidChangeHeight.fire())); this.codeblocks.push({ codeBlockIndex: this.codeBlockStartIndex, codemapperUri: undefined, @@ -183,7 +182,6 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS }); this._register(editor.object.onDidChangeContentHeight(() => { editor.object.layout(this.currentWidthDelegate()); - this._onDidChangeHeight.fire(); })); this._register(model.onDidChangeContent(e => { const currentValue = model.getValue(); @@ -388,7 +386,6 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS this.chatWidgetService.getWidgetBySessionResource(this.context.element.sessionResource)?.focusInput(); } })); - this._register(confirmWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); this.domNode = confirmWidget.domNode; } @@ -457,6 +454,5 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS { codeBlockRenderOptions }, )); append(container, part.domNode); - this._register(part.onDidChangeHeight(() => this._onDidChangeHeight.fire())); } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts index 4117c381f8d..44958482fe4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts @@ -298,12 +298,11 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart )); this._register(titlePart.onDidChangeHeight(() => { this._decoration.update(); - this._onDidChangeHeight.fire(); })); this._outputView = this._register(this._instantiationService.createInstance( ChatTerminalToolOutputSection, - () => this._onDidChangeHeight.fire(), + () => { }, () => this._ensureTerminalInstance(), () => this._getResolvedCommand(), () => this._terminalData.terminalCommandOutput, @@ -355,7 +354,6 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart }; this.markdownPart = this._register(_instantiationService.createInstance(ChatMarkdownContentPart, chatMarkdownContent, context, editorPool, false, codeBlockStartIndex, renderer, {}, currentWidthDelegate(), codeBlockModelCollection, markdownOptions)); - this._register(this.markdownPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); elements.message.append(this.markdownPart.domNode); const progressPart = this._register(_instantiationService.createInstance(ChatProgressSubPart, elements.container, this.getIcon(), terminalData.autoApproveInfo)); @@ -398,8 +396,6 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart )); this._thinkingCollapsibleWrapper = wrapper; - this._register(wrapper.onDidChangeHeight(() => this._onDidChangeHeight.fire())); - return wrapper.domNode; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts index 150d5bd1beb..6d619657239 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts @@ -249,7 +249,6 @@ export class ToolConfirmationSubPart extends AbstractToolConfirmationSubPart { }); this._register(editor.object.onDidChangeContentHeight(() => { editor.object.layout(this.currentWidthDelegate()); - this._onDidChangeHeight.fire(); })); this._register(model.onDidChangeContent(e => { try { @@ -286,13 +285,11 @@ export class ToolConfirmationSubPart extends AbstractToolConfirmationSubPart { const show = messageSeeMoreObserver.getHeight() > SHOW_MORE_MESSAGE_HEIGHT_TRIGGER; if (elements.messageContainer.classList.contains('can-see-more') !== show) { elements.messageContainer.classList.toggle('can-see-more', show); - this._onDidChangeHeight.fire(); } }; this._register(dom.addDisposableListener(elements.showMore, 'click', () => { elements.messageContainer.classList.toggle('can-see-more', false); - this._onDidChangeHeight.fire(); messageSeeMoreObserver.dispose(); })); @@ -341,8 +338,6 @@ export class ToolConfirmationSubPart extends AbstractToolConfirmationSubPart { renderFileWidgets(part.domNode, this.instantiationService, this.chatMarkdownAnchorService, this._store); container.append(part.domNode); - this._register(part.onDidChangeHeight(() => this._onDidChangeHeight.fire())); - return part; } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts index 5e079e24834..1fa9051e6d4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts @@ -33,9 +33,6 @@ import { ChatToolStreamingSubPart } from './chatToolStreamingSubPart.js'; export class ChatToolInvocationPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; - private _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - public get codeblocks(): IChatCodeBlockInfo[] { const codeblocks = this.subPart?.codeblocks ?? []; if (this.mcpAppPart) { @@ -100,9 +97,7 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa subPartDomNode.replaceWith(this.subPart.domNode); subPartDomNode = this.subPart.domNode; - partStore.add(this.subPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); partStore.add(this.subPart.onNeedsRerender(render)); - this._onDidChangeHeight.fire(); }; const mcpAppRenderData = this.getMcpAppRenderData(); @@ -126,13 +121,10 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa )); appDomNode.replaceWith(this.mcpAppPart.domNode); appDomNode = this.mcpAppPart.domNode; - r.store.add(this.mcpAppPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); } else { this.mcpAppPart = undefined; dom.clearNode(appDomNode); } - - this._onDidChangeHeight.fire(); })); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationSubPart.ts index c6f9cbf585f..036d5e01d63 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationSubPart.ts @@ -17,9 +17,6 @@ export abstract class BaseChatToolInvocationSubPart extends Disposable { protected _onNeedsRerender = this._register(new Emitter()); public readonly onNeedsRerender = this._onNeedsRerender.event; - protected _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - public abstract codeblocks: IChatCodeBlockInfo[]; private readonly _codeBlocksPartId = 'tool-' + (BaseChatToolInvocationSubPart.idPool++); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolOutputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolOutputPart.ts index a796242c2f3..6d903ad98fb 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolOutputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolOutputPart.ts @@ -117,9 +117,7 @@ export class ChatToolOutputSubPart extends BaseChatToolInvocationSubPart { progressPart.domNode.remove(); - this._onDidChangeHeight.fire(); this._register(renderedItem.onDidChangeHeight(newHeight => { - this._onDidChangeHeight.fire(); partState.height = newHeight; })); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPostExecuteConfirmationPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPostExecuteConfirmationPart.ts index 1c5af92390c..c05ce8635c3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPostExecuteConfirmationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPostExecuteConfirmationPart.ts @@ -267,7 +267,6 @@ export class ChatToolPostExecuteConfirmationPart extends AbstractToolConfirmatio )); this._codeblocks.push(...outputSubPart.codeblocks); - this._register(outputSubPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); outputSubPart.domNode.classList.add('tool-postconfirm-display'); return outputSubPart.domNode; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolStreamingSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolStreamingSubPart.ts index 389a8bacda6..3e3e3cc12d0 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolStreamingSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolStreamingSubPart.ts @@ -90,9 +90,6 @@ export class ChatToolStreamingSubPart extends BaseChatToolInvocationSubPart { )); dom.reset(container, part.domNode); - - // Notify parent that content has changed - this._onDidChangeHeight.fire(); })); return container; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index ea5647b20a4..c5ad85e3af6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -10,7 +10,7 @@ import { IActionViewItemOptions } from '../../../../../base/browser/ui/actionbar import { alert } from '../../../../../base/browser/ui/aria/aria.js'; import { DropdownMenuActionViewItem, IDropdownMenuActionViewItemOptions } from '../../../../../base/browser/ui/dropdown/dropdownActionViewItem.js'; import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; -import { IListElementRenderDetails, IListVirtualDelegate } from '../../../../../base/browser/ui/list/list.js'; +import { CachedListVirtualDelegate, IListElementRenderDetails } from '../../../../../base/browser/ui/list/list.js'; import { ITreeNode, ITreeRenderer } from '../../../../../base/browser/ui/tree/tree.js'; import { IAction } from '../../../../../base/common/actions.js'; import { coalesce, distinct } from '../../../../../base/common/arrays.js'; @@ -533,6 +533,29 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + if (!template.currentElement) { + return; + } + + const entry = entries[0]; + if (entry) { + const height = entry.borderBoxSize.at(0)?.blockSize; + if (height === 0 || !height || !template.rowContainer.isConnected) { + // Don't fire for changes that happen from the row being removed from the DOM + return; + } + + const normalizedHeight = Math.ceil(height); + template.currentElement.currentRenderedHeight = normalizedHeight; + if (template.currentElement !== this._elementBeingRendered) { + this._onDidChangeItemHeight.fire({ element: template.currentElement, height: normalizedHeight }); + } + } + })); + templateDisposables.add(resizeObserver.observe(rowContainer)); + return template; } @@ -788,8 +811,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - // Have to recompute the height here because codeblock rendering is currently async and it may have changed. - // If it becomes properly sync, then this could be removed. - if (templateData.rowContainer.isConnected) { - element.currentRenderedHeight = templateData.rowContainer.offsetHeight; - this._onDidChangeItemHeight.fire({ element, height: element.currentRenderedHeight }); - } - disposable.dispose(); - })); - } - } - - private updateItemHeight(templateData: IChatListItemTemplate): void { - if (!templateData.currentElement || templateData.currentElement === this._elementBeingRendered) { - return; - } - - if (templateData.rowContainer.isConnected) { - const newHeight = templateData.rowContainer.offsetHeight; - templateData.currentElement.currentRenderedHeight = newHeight; - this._onDidChangeItemHeight.fire({ element: templateData.currentElement, height: newHeight }); - } } /** @@ -1011,13 +1001,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - this.updateItemHeight(templateData); - })); return subagentPart; } @@ -1558,7 +1532,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.updateItemHeight(templateData))); return renderedError; } else if (content.errorDetails.isRateLimited && this.chatEntitlementService.anonymous) { const renderedError = this.instantiationService.createInstance(ChatAnonymousRateLimitedPart, content); @@ -1566,7 +1539,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.updateItemHeight(templateData))); return errorConfirmation; } else { const level = content.errorDetails.level ?? ChatErrorLevel.Error; @@ -1590,10 +1562,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - this.updateItemHeight(templateData); - })); - if (isResponseVM(context.element)) { const fileTreeFocusInfo = { treeDataId: data.uri.toString(), @@ -1619,17 +1587,11 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - this.updateItemHeight(templateData); - })); return multiDiffPart; } private renderContentReferencesListData(references: IChatReferences, labelOverride: string | undefined, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): ChatCollapsibleListContentPart { const referencesPart = this.instantiationService.createInstance(ChatUsedReferencesListContentPart, references.references, labelOverride, context, this._contentReferencesListPool, { expandedWhenEmptyResponse: checkModeOption(this.delegate.currentChatMode(), this.rendererOptions.referencesExpandedWhenEmptyResponse) }); - referencesPart.addDisposable(referencesPart.onDidChangeHeight(() => { - this.updateItemHeight(templateData); - })); return referencesPart; } @@ -1689,9 +1651,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { lazilyCreatedPart = this.instantiationService.createInstance(ChatToolInvocationPart, toolInvocation, context, this.chatContentMarkdownRenderer, this._contentReferencesListPool, this._toolEditorPool, () => this._currentLayoutWidth.get(), this._toolInvocationCodeBlockCollection, this._announcedToolProgressKeys, codeBlockStartIndex); - lazilyCreatedPart.addDisposable(lazilyCreatedPart.onDidChangeHeight(() => { - this.updateItemHeight(templateData); - })); this.handleRenderedCodeblocks(context.element, lazilyCreatedPart, codeBlockStartIndex); // watch for streaming -> confirmation transition to finalize thinking @@ -1732,9 +1691,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - this.updateItemHeight(templateData); - })); } return thinkingPart; @@ -1767,13 +1723,11 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.updateItemHeight(templateData))); return part; } private renderPullRequestContent(pullRequestContent: IChatPullRequestContent, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): IChatContentPart | undefined { const part = this.instantiationService.createInstance(ChatPullRequestContentPart, pullRequestContent); - part.addDisposable(part.onDidChangeHeight(() => this.updateItemHeight(templateData))); return part; } @@ -1783,16 +1737,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - this.updateItemHeight(templateData); - })); return taskPart; } private renderConfirmation(context: IChatContentPartRenderContext, confirmation: IChatConfirmation, templateData: IChatListItemTemplate): IChatContentPart { const part = this.instantiationService.createInstance(ChatConfirmationContentPart, confirmation, context); - part.addDisposable(part.onDidChangeHeight(() => this.updateItemHeight(templateData))); return part; } @@ -1802,13 +1752,11 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.updateItemHeight(templateData))); return part; } private renderChangesSummary(content: IChatChangesSummaryPart, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): IChatContentPart { const part = this.instantiationService.createInstance(ChatCheckpointFileChangesSummaryContentPart, content, context); - part.addDisposable(part.onDidChangeHeight(() => { this.updateItemHeight(templateData); })); return part; } @@ -1822,11 +1770,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - textEditPart.layout(this._currentLayoutWidth.get()); - this.updateItemHeight(templateData); - })); - return textEditPart; } @@ -1885,11 +1828,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - markdownPart.layout(this._currentLayoutWidth.get()); - this.updateItemHeight(templateData); - })); - this.handleRenderedCodeblocks(element, markdownPart, codeBlockStartIndex); const collapsedToolsMode = this.configService.getValue('chat.agent.thinking.collapsedTools'); @@ -1913,9 +1851,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - this.updateItemHeight(templateData); - })); } return thinkingPart; @@ -1961,7 +1896,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.updateItemHeight(templateData))); lastPart = itemPart; } } @@ -1975,7 +1909,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.updateItemHeight(templateData))); return part; } @@ -2021,25 +1954,16 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { +export class ChatListDelegate extends CachedListVirtualDelegate { constructor( private readonly defaultElementHeight: number, - @ILogService private readonly logService: ILogService - ) { } - - private _traceLayout(method: string, message: string) { - if (forceVerboseLayoutTracing) { - this.logService.info(`ChatListDelegate#${method}: ${message}`); - } else { - this.logService.trace(`ChatListDelegate#${method}: ${message}`); - } + ) { + super(); } - getHeight(element: ChatTreeItem): number { - const kind = isRequestVM(element) ? 'request' : 'response'; - const height = element.currentRenderedHeight ?? this.defaultElementHeight; - this._traceLayout('getHeight', `${kind}, height=${height}`); - return height; + protected estimateHeight(element: ChatTreeItem): number { + // currentRenderedHeight is not load-bearing here- probably if it's ever set, then the superclass cache will have the height. + return element.currentRenderedHeight ?? this.defaultElementHeight; } getTemplateId(element: ChatTreeItem): string { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts index 39ea0c348ea..c26ee8b2f81 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts @@ -4,36 +4,36 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../../base/browser/dom.js'; -import { Button } from '../../../../../base/browser/ui/button/button.js'; import { IMouseWheelEvent } from '../../../../../base/browser/mouseEvent.js'; +import { Button } from '../../../../../base/browser/ui/button/button.js'; import { ITreeContextMenuEvent, ITreeElement, ITreeFilter } from '../../../../../base/browser/ui/tree/tree.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { FuzzyScore } from '../../../../../base/common/filters.js'; -import { Disposable, MutableDisposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { ScrollEvent } from '../../../../../base/common/scrollable.js'; import { URI } from '../../../../../base/common/uri.js'; -import { IContextKeyService, IContextKey } from '../../../../../platform/contextkey/common/contextkey.js'; +import { MenuId } from '../../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; import { WorkbenchObjectTree } from '../../../../../platform/list/browser/listService.js'; -import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; -import { MenuId } from '../../../../../platform/actions/common/actions.js'; -import { asCssVariable, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground } from '../../../../../platform/theme/common/colorRegistry.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; +import { asCssVariable, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground } from '../../../../../platform/theme/common/colorRegistry.js'; +import { katexContainerClassName } from '../../../markdown/common/markedKatexExtension.js'; +import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; +import { IChatFollowup, IChatSendRequestOptions, IChatService } from '../../common/chatService/chatService.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; +import { IChatRequestModeInfo } from '../../common/model/chatModel.js'; +import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js'; +import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; import { ChatAccessibilityProvider } from '../accessibility/chatAccessibilityProvider.js'; import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions } from '../chat.js'; -import { katexContainerClassName } from '../../../markdown/common/markedKatexExtension.js'; -import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; -import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; -import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js'; -import { IChatRequestModeInfo } from '../../common/model/chatModel.js'; -import { IChatFollowup, IChatSendRequestOptions, IChatService } from '../../common/chatService/chatService.js'; -import { ChatEditorOptions } from './chatOptions.js'; -import { ChatListDelegate, ChatListItemRenderer, IChatListItemTemplate, IChatRendererDelegate } from './chatListRenderer.js'; import { CodeBlockPart } from './chatContentParts/codeBlockPart.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { ChatListDelegate, ChatListItemRenderer, IChatListItemTemplate, IChatRendererDelegate } from './chatListRenderer.js'; +import { ChatEditorOptions } from './chatOptions.js'; export interface IChatListWidgetStyles { listForeground?: string; @@ -188,7 +188,6 @@ export class ChatListWidget extends Disposable { private _viewModel: IChatViewModel | undefined; private _visible = true; private _lastItem: ChatTreeItem | undefined; - private _previousScrollHeight: number = 0; private _mostRecentlyFocusedItemIndex: number = -1; private _scrollLock: boolean = true; private _settingChangeCounter: number = 0; @@ -196,7 +195,6 @@ export class ChatListWidget extends Disposable { private readonly _container: HTMLElement; private readonly _scrollDownButton: Button; - private readonly _scrollAnimationFrameDisposable = this._register(new MutableDisposable()); private readonly _lastItemIdContextKey: IContextKey; private readonly _location: ChatAgentLocation | undefined; @@ -323,9 +321,7 @@ export class ChatListWidget extends Disposable { })); this._register(this._renderer.onDidChangeItemHeight(e => { - if (this._tree.hasElement(e.element) && this._visible) { - this._tree.updateElementHeight(e.element, e.height); - } + this._updateElementHeight(e.element, e.height); // If the second-to-last item's height changed, update the last item's min height const secondToLastItem = this._viewModel?.getItems().at(-2); @@ -417,7 +413,6 @@ export class ChatListWidget extends Disposable { // Handle content height changes (fires high-level event, internal scroll handling) this._register(this._tree.onDidChangeContentHeight(() => { - this.handleContentHeightChange(); this._onDidChangeContentHeight.fire(); })); @@ -459,25 +454,6 @@ export class ChatListWidget extends Disposable { //#region Internal event handlers - /** - * Handle content height changes - auto-scroll if needed. - */ - private handleContentHeightChange(): void { - if (!this.hasScrollHeightChanged()) { - return; - } - const rendering = this._lastItem && isResponseVM(this._lastItem) && this._lastItem.renderData; - if (!rendering || this.scrollLock) { - if (this.wasLastElementVisible()) { - this._scrollAnimationFrameDisposable.value = dom.scheduleAtNextAnimationFrame(dom.getWindow(this._container), () => { - this.scrollToEnd(); - }, 0); - } - } - - this.updatePreviousScrollHeight(); - } - /** * Update scroll-down button visibility based on scroll position and scroll lock. */ @@ -549,28 +525,30 @@ export class ChatListWidget extends Disposable { const editing = this._viewModel.editing; const checkpoint = this._viewModel.model?.checkpoint; - this._tree.setChildren(null, treeItems, { - diffIdentityProvider: { - getId: (element) => { - return element.dataId + - // If a response is in the process of progressive rendering, we need to ensure that it will - // be re-rendered so progressive rendering is restarted, even if the model wasn't updated. - `${isResponseVM(element) && element.renderData ? `_${this._visibleChangeCount}` : ''}` + - // Re-render once content references are loaded - (isResponseVM(element) ? `_${element.contentReferences.length}` : '') + - // Re-render if element becomes hidden due to undo/redo - `_${element.shouldBeRemovedOnSend ? `${element.shouldBeRemovedOnSend.afterUndoStop || '1'}` : '0'}` + - // Re-render if we have an element currently being edited - `_${editing ? '1' : '0'}` + - // Re-render if we have an element currently being checkpointed - `_${checkpoint ? '1' : '0'}` + - // Re-render all if invoked by setting change - `_setting${this._settingChangeCounter}` + - // Rerender request if we got new content references in the response - // since this may change how we render the corresponding attachments in the request - (isRequestVM(element) && element.contentReferences ? `_${element.contentReferences?.length}` : ''); - }, - } + this._withPersistedAutoScroll(() => { + this._tree.setChildren(null, treeItems, { + diffIdentityProvider: { + getId: (element) => { + return element.dataId + + // If a response is in the process of progressive rendering, we need to ensure that it will + // be re-rendered so progressive rendering is restarted, even if the model wasn't updated. + `${isResponseVM(element) && element.renderData ? `_${this._visibleChangeCount}` : ''}` + + // Re-render once content references are loaded + (isResponseVM(element) ? `_${element.contentReferences.length}` : '') + + // Re-render if element becomes hidden due to undo/redo + `_${element.shouldBeRemovedOnSend ? `${element.shouldBeRemovedOnSend.afterUndoStop || '1'}` : '0'}` + + // Re-render if we have an element currently being edited + `_${editing ? '1' : '0'}` + + // Re-render if we have an element currently being checkpointed + `_${checkpoint ? '1' : '0'}` + + // Re-render all if invoked by setting change + `_setting${this._settingChangeCounter}` + + // Rerender request if we got new content references in the response + // since this may change how we render the corresponding attachments in the request + (isRequestVM(element) && element.contentReferences ? `_${element.contentReferences?.length}` : ''); + }, + } + }); }); } @@ -652,9 +630,11 @@ export class ChatListWidget extends Disposable { /** * Update the height of an element. */ - updateElementHeight(element: ChatTreeItem, height?: number): void { + private _updateElementHeight(element: ChatTreeItem, height?: number): void { if (this._tree.hasElement(element) && this._visible) { - this._tree.updateElementHeight(element, height); + this._withPersistedAutoScroll(() => { + this._tree.updateElementHeight(element, height); + }); } } @@ -721,18 +701,12 @@ export class ChatListWidget extends Disposable { } } - private hasScrollHeightChanged(): boolean { - return this._tree.scrollHeight !== this._previousScrollHeight; - } - - private updatePreviousScrollHeight(): void { - this._previousScrollHeight = this._tree.scrollHeight; - } - - private wasLastElementVisible(): boolean { - // Due to rounding, the scrollTop + renderHeight will not exactly match the scrollHeight. - // Consider the tree to be scrolled all the way down if it is within 2px of the bottom. - return this._tree.scrollTop + this._tree.renderHeight >= this._previousScrollHeight - 2; + private _withPersistedAutoScroll(fn: () => void): void { + const wasScrolledToBottom = this.isScrolledToBottom; + fn(); + if (wasScrolledToBottom) { + this.scrollToEnd(); + } } /** @@ -798,13 +772,6 @@ export class ChatListWidget extends Disposable { return this._renderer.getTemplateDataForRequestId(requestId); } - /** - * Update item height after rendering. - */ - updateItemHeightOnRender(element: ChatTreeItem, template: IChatListItemTemplate): void { - this._renderer.updateItemHeightOnRender(element, template); - } - /** * Update renderer options. */ @@ -850,7 +817,7 @@ export class ChatListWidget extends Disposable { this._previousLastItemMinHeight = lastItemMinHeight; const lastItem = this._viewModel?.getItems().at(-1); if (lastItem && this._visible && this._tree.hasElement(lastItem)) { - this._tree.updateElementHeight(lastItem, undefined); + this._updateElementHeight(lastItem, undefined); } } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 482687545f5..261bcb27333 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -1508,7 +1508,6 @@ export class ChatWidget extends Disposable implements IChatWidget { this.inputPart.dnd.setDisabledOverlay(!isInput); this.input.renderAttachedContext(); this.input.setValue(currentElement.messageText, false); - this.listWidget.updateItemHeightOnRender(currentElement, item); this.onDidChangeItems(); this.input.inputEditor.focus(); @@ -1589,9 +1588,6 @@ export class ChatWidget extends Disposable implements IChatWidget { this.inputPart?.setEditing(!!this.viewModel?.editing && isInput); this.onDidChangeItems(); - if (editedRequest?.currentElement) { - this.listWidget.updateItemHeightOnRender(editedRequest.currentElement, editedRequest); - } type CancelRequestEditEvent = { editRequestType: string; @@ -1652,18 +1648,6 @@ export class ChatWidget extends Disposable implements IChatWidget { this.styles, true ); - this._register(autorun(reader => { - this.inlineInputPart.height.read(reader); - if (!this.listWidget) { - // This is set up before the list/renderer are created - return; - } - - const editedRequest = this.listWidget.getTemplateDataForRequestId(this.viewModel?.editing?.id); - if (isRequestVM(editedRequest?.currentElement) && this.viewModel?.editing) { - this.listWidget.updateItemHeightOnRender(editedRequest?.currentElement, editedRequest); - } - })); } else { this.inputPartDisposable.value = this.instantiationService.createInstance(ChatInputPart, this.location, From 995b6269960fadb953825f1fcdde274f4ddf0aa7 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 20 Jan 2026 19:27:36 -0800 Subject: [PATCH 323/387] Remove chat response model onDidChange listener (#289297) --- .../contrib/chat/common/model/chatModel.ts | 28 ++++--------------- .../chat/common/model/chatViewModel.ts | 6 +--- 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index 875f3abf283..ff97b0a75b4 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -1054,13 +1054,6 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._register(this._response.onDidChangeValue(() => this._onDidChange.fire(defaultChatResponseModelChangeReason))); this.id = params.restoredId ?? 'response_' + generateUuid(); - this._register(this._session.onDidChange((e) => { - if (e.kind === 'setCheckpoint') { - const isDisabled = e.disabledResponseIds.has(this.id); - this._shouldBeBlocked.set(isDisabled, undefined); - } - })); - let lastStartedWaitingAt: number | undefined = undefined; this.confirmationAdjustedTimestamp = derived(reader => { const pending = this.isPendingConfirmation.read(reader); @@ -1089,6 +1082,10 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._codeBlockInfos = [...codeBlockInfo]; } + setBlockedState(isBlocked: boolean): void { + this._shouldBeBlocked.set(isBlocked, undefined); + } + /** * Apply a progress update to the actual response content. */ @@ -1615,7 +1612,6 @@ export type IChatChangeEvent = | IChatMoveEvent | IChatSetHiddenEvent | IChatCompletedRequestEvent - | IChatSetCheckpointEvent | IChatSetCustomTitleEvent ; @@ -1624,12 +1620,6 @@ export interface IChatAddRequestEvent { request: IChatRequestModel; } -export interface IChatSetCheckpointEvent { - kind: 'setCheckpoint'; - disabledRequestIds: Set; - disabledResponseIds: Set; -} - export interface IChatChangedRequestEvent { kind: 'changedRequest'; request: IChatRequestModel; @@ -2153,17 +2143,14 @@ export class ChatModel extends Disposable implements IChatModel { } } - const disabledRequestIds = new Set(); - const disabledResponseIds = new Set(); for (let i = this._requests.length - 1; i >= 0; i -= 1) { const request = this._requests[i]; if (this._checkpoint && !checkpoint) { request.setShouldBeBlocked(false); } else if (checkpoint && i >= checkpointIndex) { request.setShouldBeBlocked(true); - disabledRequestIds.add(request.id); if (request.response) { - disabledResponseIds.add(request.response.id); + request.response.setBlockedState(true); } } else if (checkpoint && i < checkpointIndex) { request.setShouldBeBlocked(false); @@ -2171,11 +2158,6 @@ export class ChatModel extends Disposable implements IChatModel { } this._checkpoint = checkpoint; - this._onDidChange.fire({ - kind: 'setCheckpoint', - disabledRequestIds, - disabledResponseIds - }); } private _checkpoint: ChatRequestModel | undefined = undefined; diff --git a/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts index d36593cb3bf..2e75fe9d44e 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts @@ -38,7 +38,7 @@ export function assertIsResponseVM(item: unknown): asserts item is IChatResponse } } -export type IChatViewModelChangeEvent = IChatAddRequestEvent | IChangePlaceholderEvent | IChatSessionInitEvent | IChatSetHiddenEvent | IChatSetCheckpointEvent | null; +export type IChatViewModelChangeEvent = IChatAddRequestEvent | IChangePlaceholderEvent | IChatSessionInitEvent | IChatSetHiddenEvent | null; export interface IChatAddRequestEvent { kind: 'addRequest'; @@ -56,10 +56,6 @@ export interface IChatSetHiddenEvent { kind: 'setHidden'; } -export interface IChatSetCheckpointEvent { - kind: 'setCheckpoint'; -} - export interface IChatViewModel { readonly model: IChatModel; readonly sessionResource: URI; From f60f946b1736ae6f26b944f8f5b6ce6270d05405 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Tue, 20 Jan 2026 20:33:43 -0800 Subject: [PATCH 324/387] Add optional chatSessionResource parameter for read operations in ManageTodoListTool (#289290) * Add optional chatSessionResource parameter for read operations in ManageTodoListTool * Update src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../common/tools/builtinTools/manageTodoListTool.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts index 895a6c537f5..e1ba741425a 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts @@ -80,6 +80,8 @@ interface IManageTodoListToolInputParams { title: string; status: 'not-started' | 'in-progress' | 'completed'; }>; + // used for todo read only + chatSessionResource?: string; } export class ManageTodoListTool extends Disposable implements IToolImpl { @@ -95,7 +97,14 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { // eslint-disable-next-line @typescript-eslint/no-explicit-any async invoke(invocation: IToolInvocation, _countTokens: any, _progress: any, _token: CancellationToken): Promise { const args = invocation.parameters as IManageTodoListToolInputParams; - const chatSessionResource = invocation.context?.sessionResource; + let chatSessionResource = invocation.context?.sessionResource; + if (!chatSessionResource && args.operation === 'read' && args.chatSessionResource) { + try { + chatSessionResource = URI.parse(args.chatSessionResource); + } catch (error) { + this.logService.error('ManageTodoListTool: Invalid chatSessionResource URI', error); + } + } if (!chatSessionResource) { return { content: [{ From c9cd85055091d3a8245657d587d2ee327a58d6df Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 20 Jan 2026 22:53:48 -0800 Subject: [PATCH 325/387] Fix input shifting when editing request (#289301) Caused by losing the flex gap that applied to an empty chat-input-overlay --- src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 055cd427deb..a4010246d04 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1570,7 +1570,7 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .interactive-input-part { margin: 0px 12px; - padding: 4px 0 8px 0px; + padding: 4px 0 12px 0px; display: flex; flex-direction: column; gap: 4px; @@ -2736,7 +2736,8 @@ have to be updated for changes to the rules above, or to support more deeply nes } .chat-row-disabled-overlay, -.interactive-item-container .chat-edit-input-container .chat-editing-session { +.interactive-item-container .chat-edit-input-container .chat-editing-session, +.chat-input-overlay { display: none; } From 83a8d44fcf7dbdb6fdb0699da949e5deb39dad82 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Tue, 20 Jan 2026 23:01:06 -0800 Subject: [PATCH 326/387] Remove experimental tags from todo list widget configuration (#289304) --- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index d0ad59d06c1..96798d993db 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -811,10 +811,6 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: true, description: nls.localize('chat.tools.todos.showWidget', "Controls whether to show the todo list widget above the chat input. When enabled, the widget displays todo items created by the agent and updates as progress is made."), - tags: ['experimental'], - experiment: { - mode: 'auto' - } }, [ChatConfiguration.ThinkingStyle]: { type: 'string', From 1d742f0e0010e6625aebce3e867021d04711b4ba Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 21 Jan 2026 08:23:27 +0100 Subject: [PATCH 327/387] agent sessions - fix logging (#289316) --- .../agentSessions/agentSessionsModel.ts | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index 23970c73a7a..7023219910e 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -233,12 +233,20 @@ class AgentSessionsLogger extends Disposable { private registerListeners(): void { this._register(this.logService.onDidChangeLogLevel(level => { if (level === LogLevel.Trace) { - this.logIfTrace('Log level changed to trace'); + this.logAllStatsIfTrace('Log level changed to trace'); } })); } - logIfTrace(reason: string): void { + logIfTrace(msg: string): void { + if (this.logService.getLevel() !== LogLevel.Trace) { + return; + } + + this.trace(`[Agent Sessions] ${msg}`); + } + + logAllStatsIfTrace(reason: string): void { if (this.logService.getLevel() !== LogLevel.Trace) { return; } @@ -249,11 +257,6 @@ class AgentSessionsLogger extends Disposable { } private logAllSessions(reason: string): void { - const channel = this.outputService.getChannel(agentSessionsOutputChannelId); - if (!channel) { - return; - } - const { sessions, sessionStates } = this.getSessionsData(); const lines: string[] = []; @@ -316,15 +319,10 @@ class AgentSessionsLogger extends Disposable { lines.push(`=== End Agent Sessions ===`); - channel.append(lines.join('\n') + '\n'); + this.trace(lines.join('\n')); } private logSessionStates(): void { - const channel = this.outputService.getChannel(agentSessionsOutputChannelId); - if (!channel) { - return; - } - const { sessionStates } = this.getSessionsData(); const lines: string[] = []; @@ -341,15 +339,10 @@ class AgentSessionsLogger extends Disposable { lines.push(`=== End Session States ===`); - channel.append(lines.join('\n') + '\n'); + this.trace(lines.join('\n')); } private logMapSessionToState(): void { - const channel = this.outputService.getChannel(agentSessionsOutputChannelId); - if (!channel) { - return; - } - const { mapSessionToState } = this.getSessionsData(); const lines: string[] = []; @@ -367,7 +360,16 @@ class AgentSessionsLogger extends Disposable { lines.push(`=== End Map Session To State ===`); - channel.append(lines.join('\n') + '\n'); + this.trace(lines.join('\n')); + } + + private trace(msg: string): void { + const channel = this.outputService.getChannel(agentSessionsOutputChannelId); + if (!channel) { + return; + } + + channel.append(`${msg}\n`); } } @@ -425,7 +427,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode mapSessionToState: this.mapSessionToState }) )); - this.logger.logIfTrace('Loaded cached sessions'); + this.logger.logAllStatsIfTrace('Loaded cached sessions'); this.registerListeners(); @@ -618,7 +620,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } } - this.logger.logIfTrace('Sessions resolved from providers'); + this.logger.logAllStatsIfTrace('Sessions resolved from providers'); this._onDidChangeSessions.fire(); } From 8022faa42461227757d3e6cbd2764153867e303a Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Tue, 20 Jan 2026 23:26:15 -0800 Subject: [PATCH 328/387] Update chatStatusWidget to show for some users (#289302) --- .../browser/widget/input/chatStatusWidget.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatStatusWidget.ts index 3ddd5d98959..7f2bf8bce90 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatStatusWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatStatusWidget.ts @@ -52,18 +52,16 @@ export class ChatStatusWidget extends Disposable implements IChatInputPartWidget } private initializeIfEnabled(): void { - const enabledSku = this.configurationService.getValue('chat.statusWidget.sku'); - if (enabledSku !== 'free' && enabledSku !== 'anonymous') { - return; - } - const entitlement = this.chatEntitlementService.entitlement; const isAnonymous = this.chatEntitlementService.anonymous; - if (enabledSku === 'anonymous' && isAnonymous) { - this.createWidgetContent(enabledSku); - } else if (enabledSku === 'free' && entitlement === ChatEntitlement.Free) { - this.createWidgetContent(enabledSku); + // Free tier is always enabled, anonymous is controlled by experiment via chat.statusWidget.sku + const enabledSku = this.configurationService.getValue('chat.statusWidget.sku'); + + if (isAnonymous && enabledSku === 'anonymous') { + this.createWidgetContent('anonymous'); + } else if (entitlement === ChatEntitlement.Free) { + this.createWidgetContent('free'); } else { return; } From 3c27a03b3a3aded8a8dce12e52e4b919ec413745 Mon Sep 17 00:00:00 2001 From: dileepyavan <52841896+dileepyavan@users.noreply.github.com> Date: Tue, 20 Jan 2026 23:26:50 -0800 Subject: [PATCH 329/387] Appending node exec path for running srt (#289150) * correcting the srt path * Fixing path for srt command executable --- .../common/terminalSandboxService.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts index 5eef10bff66..4de957f07a4 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts @@ -6,7 +6,7 @@ import { VSBuffer } from '../../../../../base/common/buffer.js'; import { FileAccess } from '../../../../../base/common/network.js'; import { dirname, join } from '../../../../../base/common/path.js'; -import { isNative, OperatingSystem, OS } from '../../../../../base/common/platform.js'; +import { OperatingSystem, OS } from '../../../../../base/common/platform.js'; import { joinPath } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; @@ -33,6 +33,7 @@ export interface ITerminalSandboxService { export class TerminalSandboxService implements ITerminalSandboxService { readonly _serviceBrand: undefined; private _srtPath: string; + private _execPath?: string; private _sandboxConfigPath: string | undefined; private _needsForceUpdateConfigFile = true; private _tempDir: URI | undefined; @@ -49,6 +50,9 @@ export class TerminalSandboxService implements ITerminalSandboxService { const appRoot = dirname(FileAccess.asFileUri('').fsPath); // srt path is dist/cli.js inside the sandbox-runtime package. this._srtPath = join(appRoot, 'node_modules', '@anthropic-ai', 'sandbox-runtime', 'dist', 'cli.js'); + // Get the node executable path from native environment service if available (Electron's execPath with ELECTRON_RUN_AS_NODE) + const nativeEnv = this._environmentService as IEnvironmentService & { execPath?: string }; + this._execPath = nativeEnv.execPath; this._sandboxSettingsId = generateUuid(); this._initTempDir(); this._remoteAgentService.getEnvironment().then(remoteEnv => this._os = remoteEnv?.os ?? OS); @@ -65,7 +69,14 @@ export class TerminalSandboxService implements ITerminalSandboxService { if (!this._sandboxConfigPath || !this._tempDir) { throw new Error('Sandbox config path or temp dir not initialized'); } - return `"${this._srtPath}" TMPDIR=${this._tempDir.fsPath} --settings "${this._sandboxConfigPath}" "${command}"`; + if (!this._execPath) { + throw new Error('Executable path not set to run sandbox commands'); + } + // Use ELECTRON_RUN_AS_NODE=1 to make Electron executable behave as Node.js + // TMPDIR must be set as environment variable before the command + // Use -c to pass the command string directly (like sh -c), avoiding argument parsing issues + const wrappedCommand = `"${this._execPath}" "${this._srtPath}" TMPDIR=${this._tempDir.fsPath} --settings "${this._sandboxConfigPath}" -c "${command}"`; + return `ELECTRON_RUN_AS_NODE=1 ${wrappedCommand}`; } public getTempDir(): URI | undefined { @@ -117,7 +128,7 @@ export class TerminalSandboxService implements ITerminalSandboxService { } private _initTempDir(): void { - if (this.isEnabled() && isNative) { + if (this.isEnabled()) { this._needsForceUpdateConfigFile = true; const environmentService = this._environmentService as IEnvironmentService & { tmpDir?: URI }; this._tempDir = environmentService.tmpDir; From 41afb6c853aea3fab20e2ef37c0f76f1755ab8f2 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 21 Jan 2026 08:27:06 +0100 Subject: [PATCH 330/387] Workbench - floating menu cleanup (#289312) --- .../floatingMenu/browser/floatingMenu.ts | 29 ++++--------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts index a8af1c1b93d..1ae190279d2 100644 --- a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts @@ -6,7 +6,7 @@ import { h } from '../../../../base/browser/dom.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { autorun, constObservable, derived, observableFromEvent } from '../../../../base/common/observable.js'; -import { MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { getActionBarActions, MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { IMenuService, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -29,30 +29,13 @@ export class FloatingEditorToolbar extends Disposable implements IEditorContribu const editorObs = this._register(observableCodeEditor(editor)); const menu = this._register(menuService.createMenu(MenuId.EditorContent, editor.contextKeyService)); - const menuActionsObs = observableFromEvent(this, menu.onDidChange, () => menu.getActions()); + const menuGroupsObs = observableFromEvent(this, menu.onDidChange, () => menu.getActions()); + const menuPrimaryActionIdObs = derived(reader => { - const menuActions = menuActionsObs.read(reader); - if (menuActions.length === 0) { - return undefined; - } + const menuGroups = menuGroupsObs.read(reader); - // Navigation group - const navigationGroup = menuActions - .find((group) => group[0] === 'navigation'); - - // First action in navigation group - if (navigationGroup && navigationGroup[1].length > 0) { - return navigationGroup[1][0].id; - } - - // Fallback to first group/action - for (const [, actions] of menuActions) { - if (actions.length > 0) { - return actions[0].id; - } - } - - return undefined; + const { primary } = getActionBarActions(menuGroups, () => true); + return primary.length > 0 ? primary[0].id : undefined; }); this._register(autorun(reader => { From 4384ba85624579692363adac676744ad76ed16e4 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 21 Jan 2026 08:27:29 +0100 Subject: [PATCH 331/387] Git - do not provide original resource for files that are in the `.git` folder (#289313) --- extensions/git/src/repository.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index fbd06340e57..04b310e5024 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1114,6 +1114,12 @@ export class Repository implements Disposable { return undefined; } + // Ignore path that is inside the .git directory (ex: COMMIT_EDITMSG) + if (isDescendant(this.dotGit.commonPath ?? this.dotGit.path, uri.fsPath)) { + this.logger.trace(`[Repository][provideOriginalResource] Resource is inside .git directory: ${uri.toString()}`); + return undefined; + } + // Ignore symbolic links const stat = await workspace.fs.stat(uri); if ((stat.type & FileType.SymbolicLink) !== 0) { From 90bae3ca474a434962a83d617cabb43daab6c7fe Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 21 Jan 2026 08:57:05 +0100 Subject: [PATCH 332/387] feat(inlineChat): refactor inline chat affordance and rendering modes (#289159) * feat(inlineChat): refactor inline chat affordance and rendering modes - Replaced InlineChatSelectionIndicator with InlineChatAffordance for improved affordance handling. - Introduced InlineChatGutterAffordance and InlineChatEditorAffordance for gutter and editor affordances respectively. - Added InlineChatOverlayWidget for rendering inline chat as a hover overlay. - Updated configuration keys to support new affordance options and rendering modes. - Removed deprecated ShowGutterMenu configuration in favor of more flexible affordance settings. - Enhanced inline chat behavior to support both zone and hover rendering modes. * thanks copilot --- .../chatEditing/chatEditingEditorOverlay.ts | 2 +- .../browser/inlineChatAffordance.ts | 151 ++++++ .../browser/inlineChatController.ts | 28 +- .../browser/inlineChatEditorAffordance.ts | 135 ++++++ .../browser/inlineChatGutterAffordance.ts | 106 ++++ .../browser/inlineChatOverlayWidget.ts | 264 ++++++++++ .../inlineChatSelectionGutterIndicator.ts | 456 ------------------ .../media/inlineChatEditorAffordance.css | 28 ++ .../contrib/inlineChat/common/inlineChat.ts | 29 +- 9 files changed, 719 insertions(+), 480 deletions(-) create mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts create mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts create mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts create mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts delete mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatSelectionGutterIndicator.ts create mode 100644 src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts index d7f0000b99b..48c6be29680 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts @@ -394,7 +394,7 @@ class ChatEditingOverlayController { const { session, entry } = data; - if (!session.isGlobalEditingSession && !configurationService.getValue(InlineChatConfigKeys.ShowGutterMenu)) { + if (!session.isGlobalEditingSession && configurationService.getValue(InlineChatConfigKeys.RenderMode) === 'zone') { // inline chat with zone UI - no need for chat overlay hide(); return; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts new file mode 100644 index 00000000000..5b8a4bba6f7 --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun, debouncedObservable, derived, observableValue } from '../../../../base/common/observable.js'; +import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { InlineChatConfigKeys } from '../common/inlineChat.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; +import { InlineChatEditorAffordance } from './inlineChatEditorAffordance.js'; +import { InlineChatOverlayWidget } from './inlineChatOverlayWidget.js'; +import { InlineChatGutterAffordance } from './inlineChatGutterAffordance.js'; +import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; +import { assertType } from '../../../../base/common/types.js'; +import { Event } from '../../../../base/common/event.js'; + +export class InlineChatAffordance extends Disposable { + + private readonly _overlayWidget: InlineChatOverlayWidget; + + private _menuData = observableValue<{ rect: DOMRect; above: boolean } | undefined>(this, undefined); + + + constructor( + private readonly _editor: ICodeEditor, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService, + @IChatEntitlementService chatEntiteldService: IChatEntitlementService, + ) { + super(); + + // Create the overlay widget once, owned by this class + this._overlayWidget = this._store.add(this._instantiationService.createInstance(InlineChatOverlayWidget, this._editor)); + + const affordance = observableConfigValue<'off' | 'gutter' | 'editor'>(InlineChatConfigKeys.Affordance, 'off', configurationService); + + const editorObs = observableCodeEditor(this._editor); + + const suppressGutter = observableValue(this, false); + + const debouncedSelection = debouncedObservable(editorObs.cursorSelection, 500); + + const selection = observableValue(this, undefined); + + + this._store.add(autorun(r => { + const value = editorObs.cursorSelection.read(r); + if (!value || value.isEmpty()) { + selection.set(undefined, undefined); + } + })); + + + this._store.add(autorun(r => { + if (chatEntiteldService.sentimentObs.read(r).hidden) { + selection.set(undefined, undefined); + return; + } + if (suppressGutter.read(r)) { + selection.set(undefined, undefined); + return; + } + const value = debouncedSelection.read(r); + if (!value || value.isEmpty()) { + selection.set(undefined, undefined); + return; + } + selection.set(value, undefined); + })); + + + + // Instantiate the gutter indicator + this._store.add(this._instantiationService.createInstance( + InlineChatGutterAffordance, + editorObs, + derived(r => affordance.read(r) === 'gutter' ? selection.read(r) : undefined), + suppressGutter, + this._menuData + )); + + // Create content widget (alternative to gutter indicator) + this._store.add(this._instantiationService.createInstance( + InlineChatEditorAffordance, + this._editor, + derived(r => affordance.read(r) === 'editor' ? selection.read(r) : undefined), + suppressGutter, + this._menuData + )); + + // Reset suppressGutter when the selection changes + this._store.add(autorun(reader => { + editorObs.cursorSelection.read(reader); + suppressGutter.set(false, undefined); + })); + + this._store.add(autorun(r => { + const data = this._menuData.read(r); + if (!data) { + return; + } + + const editorDomNode = this._editor.getDomNode()!; + const editorRect = editorDomNode.getBoundingClientRect(); + const padding = 1; + + let top: number; + if (data.above) { + // Pass the top of the gutter indicator minus padding + top = data.rect.top - editorRect.top - padding; + } else { + // Menu appears below - position at bottom of gutter indicator + top = data.rect.bottom - editorRect.top + padding; + } + const left = data.rect.left - editorRect.left; + + // Show the overlay widget + this._overlayWidget.show(top, left, data.above); + })); + + this._store.add(this._overlayWidget.onDidHide(() => { + suppressGutter.set(true, undefined); + this._menuData.set(undefined, undefined); + this._editor.focus(); + })); + } + + async showMenuAtSelection() { + assertType(this._editor.hasModel()); + + const direction = this._editor.getSelection().getDirection(); + const position = this._editor.getPosition(); + const editorDomNode = this._editor.getDomNode(); + const scrolledPosition = this._editor.getScrolledVisiblePosition(position); + const editorRect = editorDomNode.getBoundingClientRect(); + const x = editorRect.left + scrolledPosition.left; + const y = editorRect.top + scrolledPosition.top; + + this._menuData.set({ + rect: new DOMRect(x, y, 0, scrolledPosition.height), + above: direction === SelectionDirection.RTL + }, undefined); + + await Event.toPromise(this._overlayWidget.onDidHide); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 5b164f22167..09df9d10aa2 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -51,7 +51,7 @@ import { INotebookEditorService } from '../../notebook/browser/services/notebook import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommon.js'; import { INotebookService } from '../../notebook/common/notebookService.js'; import { CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; -import { InlineChatSelectionIndicator } from './inlineChatSelectionGutterIndicator.js'; +import { InlineChatAffordance } from './inlineChatAffordance.js'; import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js'; import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; @@ -109,9 +109,9 @@ export class InlineChatController implements IEditorContribution { private readonly _store = new DisposableStore(); private readonly _isActiveController = observableValue(this, false); - private readonly _showGutterMenu: IObservable; + private readonly _renderMode: IObservable<'zone' | 'hover'>; private readonly _zone: Lazy; - private readonly _gutterIndicator: InlineChatSelectionIndicator; + private readonly _gutterIndicator: InlineChatAffordance; private readonly _currentSession: IObservable; @@ -141,9 +141,9 @@ export class InlineChatController implements IEditorContribution { const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this._configurationService); - this._showGutterMenu = observableConfigValue(InlineChatConfigKeys.ShowGutterMenu, false, this._configurationService); + this._renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this._configurationService); - this._gutterIndicator = this._store.add(this._instaService.createInstance(InlineChatSelectionIndicator, this._editor)); + this._gutterIndicator = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor)); this._zone = new Lazy(() => { @@ -285,14 +285,14 @@ export class InlineChatController implements IEditorContribution { // HIDE/SHOW const session = visibleSessionObs.read(r); - const showGutterMenu = this._showGutterMenu.read(r); + const renderMode = this._renderMode.read(r); if (!session) { this._zone.rawValue?.hide(); this._zone.rawValue?.widget.chatWidget.setModel(undefined); _editor.focus(); ctxInlineChatVisible.reset(); - } else if (showGutterMenu) { - // showGutterMenu mode: set model but don't show zone, keep focus in editor + } else if (renderMode === 'hover') { + // hover mode: set model but don't show zone, keep focus in editor this._zone.value.widget.chatWidget.setModel(session.chatModel); this._zone.rawValue?.hide(); ctxInlineChatVisible.set(true); @@ -438,16 +438,10 @@ export class InlineChatController implements IEditorContribution { existingSession.dispose(); } - // use gutter menu to ask for input - if (!arg?.message && this._configurationService.getValue(InlineChatConfigKeys.ShowGutterMenu)) { - const position = this._editor.getPosition(); - const editorDomNode = this._editor.getDomNode(); - const scrolledPosition = this._editor.getScrolledVisiblePosition(position); - const editorRect = editorDomNode.getBoundingClientRect(); - const x = editorRect.left + scrolledPosition.left; - const y = editorRect.top + scrolledPosition.top; + // use hover overlay to ask for input + if (!arg?.message && this._configurationService.getValue(InlineChatConfigKeys.RenderMode) === 'hover') { // show menu and RETURN because the menu is re-entrant - await this._gutterIndicator.showMenuAt(x, y, scrolledPosition.height); + await this._gutterIndicator.showMenuAtSelection(); return true; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts new file mode 100644 index 00000000000..9fa3043f0fb --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/inlineChatEditorAffordance.css'; +import * as dom from '../../../../base/browser/dom.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; +import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; +import { autorun, IObservable, ISettableObservable } from '../../../../base/common/observable.js'; +import { assertType } from '../../../../base/common/types.js'; + +/** + * Content widget that shows a small sparkle icon at the cursor position. + * When clicked, it shows the overlay widget for inline chat. + */ +export class InlineChatEditorAffordance extends Disposable implements IContentWidget { + + private static _idPool = 0; + + private readonly _id = `inline-chat-content-widget-${InlineChatEditorAffordance._idPool++}`; + private readonly _domNode: HTMLElement; + private _position: IContentWidgetPosition | null = null; + private _isVisible = false; + + readonly allowEditorOverflow = true; + readonly suppressMouseDown = false; + + constructor( + private readonly _editor: ICodeEditor, + selection: IObservable, + suppressAffordance: ISettableObservable, + private readonly _hover: ISettableObservable<{ rect: DOMRect; above: boolean } | undefined> + ) { + super(); + + // Create the widget DOM + this._domNode = dom.$('.inline-chat-content-widget'); + + // Add sparkle icon + const icon = dom.append(this._domNode, dom.$('.icon')); + icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.sparkle)); + + // Handle click to show overlay widget + this._store.add(dom.addDisposableListener(this._domNode, dom.EventType.CLICK, (e) => { + e.preventDefault(); + e.stopPropagation(); + this._showOverlayWidget(); + })); + + this._store.add(autorun(r => { + const sel = selection.read(r); + const suppressed = suppressAffordance.read(r); + if (sel && !suppressed) { + this._show(sel); + } else { + this._hide(); + } + })); + } + + private _show(selection: Selection): void { + + // Position at the cursor (active end of selection) + const cursorPosition = selection.getPosition(); + const direction = selection.getDirection(); + + // Show above for RTL (selection going up), below for LTR (selection going down) + const preference = direction === SelectionDirection.RTL + ? ContentWidgetPositionPreference.ABOVE + : ContentWidgetPositionPreference.BELOW; + + this._position = { + position: cursorPosition, + preference: [preference], + }; + + if (this._isVisible) { + this._editor.layoutContentWidget(this); + } else { + this._editor.addContentWidget(this); + this._isVisible = true; + } + } + + private _hide(): void { + if (this._isVisible) { + this._isVisible = false; + this._editor.removeContentWidget(this); + } + } + + private _showOverlayWidget(): void { + assertType(this._editor.hasModel()); + + if (!this._position || !this._position.position) { + return; + } + + const position = this._position.position; + const editorDomNode = this._editor.getDomNode(); + const scrolledPosition = this._editor.getScrolledVisiblePosition(position); + const editorRect = editorDomNode.getBoundingClientRect(); + const x = editorRect.left + scrolledPosition.left; + const y = editorRect.top + scrolledPosition.top; + + this._hide(); + this._hover.set({ + rect: new DOMRect(x, y, 0, scrolledPosition.height), + above: this._position.preference[0] === ContentWidgetPositionPreference.ABOVE + }, undefined); + } + + getId(): string { + return this._id; + } + + getDomNode(): HTMLElement { + return this._domNode; + } + + getPosition(): IContentWidgetPosition | null { + return this._position; + } + + override dispose(): void { + if (this._isVisible) { + this._editor.removeContentWidget(this); + } + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts new file mode 100644 index 00000000000..b1bff7c10d5 --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from '../../../../base/common/codicons.js'; +import { autorun, constObservable, derived, IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; +import { ObservableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; +import { LineRange } from '../../../../editor/common/core/ranges/lineRange.js'; +import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; +import { InlineEditsGutterIndicator, InlineEditsGutterIndicatorData, InlineSuggestionGutterMenuData, SimpleInlineSuggestModel } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.js'; +import { InlineEditTabAction } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.js'; +import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { HoverService } from '../../../../platform/hover/browser/hoverService.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { CTX_INLINE_CHAT_GUTTER_VISIBLE } from '../common/inlineChat.js'; + +export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { + + + constructor( + private readonly _myEditorObs: ObservableCodeEditor, + selection: IObservable, + suppressAffordance: ISettableObservable, + private readonly _hover: ISettableObservable<{ rect: DOMRect; above: boolean } | undefined>, + @IHoverService hoverService: HoverService, + @IInstantiationService instantiationService: IInstantiationService, + @IAccessibilityService accessibilityService: IAccessibilityService, + @IThemeService themeService: IThemeService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + const data = derived(r => { + const value = selection.read(r); + if (!value || suppressAffordance.read(r)) { + return undefined; + } + + // Use the cursor position (active end of selection) to determine the line + const cursorPosition = value.getPosition(); + const lineRange = new LineRange(cursorPosition.lineNumber, cursorPosition.lineNumber + 1); + + // Create minimal gutter menu data (empty for prototype) + const gutterMenuData = new InlineSuggestionGutterMenuData( + undefined, // action + '', // displayName + [], // extensionCommands + undefined, // alternativeAction + undefined, // modelInfo + undefined, // setModelId + ); + + return new InlineEditsGutterIndicatorData( + gutterMenuData, + lineRange, + new SimpleInlineSuggestModel(() => { }, () => { }), + undefined, // altAction + { + icon: Codicon.sparkle, + } + ); + }); + + const focusIsInMenu = observableValue({}, false); + + super( + _myEditorObs, data, constObservable(InlineEditTabAction.Jump), constObservable(0), constObservable(false), focusIsInMenu, + hoverService, instantiationService, accessibilityService, themeService + ); + + this._store.add(autorun(r => { + const element = _hover.read(r); + this._hoverVisible.set(!!element, undefined); + })); + + // Update context key when gutter visibility changes + const gutterVisibleCtxKey = CTX_INLINE_CHAT_GUTTER_VISIBLE.bindTo(contextKeyService); + this._store.add({ dispose: () => gutterVisibleCtxKey.reset() }); + this._store.add(autorun(reader => { + const isVisible = data.read(reader) !== undefined; + gutterVisibleCtxKey.set(isVisible); + })); + } + + protected override _showHover(): void { + + if (this._hoverVisible.get()) { + return; + } + + // Use the icon element from the base class as anchor + const iconElement = this._iconRef.element; + if (!iconElement) { + this._hover.set(undefined, undefined); + return; + } + + const selection = this._myEditorObs.cursorSelection.get(); + const direction = selection?.getDirection() ?? SelectionDirection.LTR; + this._hover.set({ rect: iconElement.getBoundingClientRect(), above: direction === SelectionDirection.RTL }, undefined); + } + + +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts new file mode 100644 index 00000000000..120a6d8a26a --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -0,0 +1,264 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { IAction } from '../../../../base/common/actions.js'; +import { ActionBar, ActionsOrientation } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { KeyCode } from '../../../../base/common/keyCodes.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { URI } from '../../../../base/common/uri.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { IActiveCodeEditor, ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; +import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; +import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { localize } from '../../../../nls.js'; +import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; +import { ACTION_START } from '../common/inlineChat.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOptions.js'; +import { PlaceholderTextContribution } from '../../../../editor/contrib/placeholderText/browser/placeholderTextContribution.js'; +import { InlineChatRunOptions } from './inlineChatController.js'; +import { Position } from '../../../../editor/common/core/position.js'; + +/** + * Overlay widget that displays a vertical action bar menu. + */ +export class InlineChatOverlayWidget extends Disposable implements IOverlayWidget { + + private static _idPool = 0; + + private readonly _id = `inline-chat-gutter-menu-${InlineChatOverlayWidget._idPool++}`; + private readonly _domNode: HTMLElement; + private readonly _inputContainer: HTMLElement; + private readonly _actionBar: ActionBar; + private readonly _input: IActiveCodeEditor; + private _position: IOverlayWidgetPosition | null = null; + private readonly _onDidHide = this._store.add(new Emitter()); + readonly onDidHide = this._onDidHide.event; + + private _isVisible = false; + private _inlineStartAction: IAction | undefined; + + readonly allowEditorOverflow = true; + + constructor( + private readonly _editor: ICodeEditor, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IMenuService private readonly _menuService: IMenuService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IInstantiationService instantiationService: IInstantiationService, + @IModelService modelService: IModelService, + @IConfigurationService configurationService: IConfigurationService, + ) { + super(); + + // Create container + this._domNode = dom.$('.inline-chat-gutter-menu'); + + // Create input editor container + this._inputContainer = dom.append(this._domNode, dom.$('.input')); + this._inputContainer.style.width = '200px'; + this._inputContainer.style.height = '26px'; + this._inputContainer.style.display = 'flex'; + this._inputContainer.style.alignItems = 'center'; + this._inputContainer.style.justifyContent = 'center'; + + // Create editor options + const options = getSimpleEditorOptions(configurationService); + options.wordWrap = 'off'; + options.lineNumbers = 'off'; + options.glyphMargin = false; + options.lineDecorationsWidth = 0; + options.lineNumbersMinChars = 0; + options.folding = false; + options.minimap = { enabled: false }; + options.scrollbar = { vertical: 'auto', horizontal: 'hidden', alwaysConsumeMouseWheel: true, verticalSliderSize: 6 }; + options.renderLineHighlight = 'none'; + options.placeholder = this._keybindingService.appendKeybinding(localize('placeholderWithSelection', "Edit selection"), ACTION_START); + + const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { + isSimpleWidget: true, + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + PlaceholderTextContribution.ID, + ]) + }; + + this._input = this._store.add(instantiationService.createInstance(CodeEditorWidget, this._inputContainer, options, codeEditorWidgetOptions)) as IActiveCodeEditor; + + const model = this._store.add(modelService.createModel('', null, URI.parse(`gutter-input:${Date.now()}`), true)); + this._input.setModel(model); + this._input.layout({ width: 200, height: 18 }); + + // Listen to content size changes and resize the input editor (max 3 lines) + this._store.add(this._input.onDidContentSizeChange(e => { + if (e.contentHeightChanged) { + this._updateInputHeight(e.contentHeight); + } + })); + + // Handle Enter key to submit and ArrowDown to focus action bar + this._store.add(this._input.onKeyDown(e => { + if (e.keyCode === KeyCode.Enter && !e.shiftKey) { + const value = this._input.getModel().getValue() ?? ''; + // TODO@jrieken this isn't nice + if (this._inlineStartAction && value) { + e.preventDefault(); + e.stopPropagation(); + this._actionBar.actionRunner.run( + this._inlineStartAction, + { message: value, autoSend: true } satisfies InlineChatRunOptions + ); + } + } else if (e.keyCode === KeyCode.Escape) { + // Hide overlay if input is empty + const value = this._input.getModel().getValue() ?? ''; + if (!value) { + e.preventDefault(); + e.stopPropagation(); + this.hide(); + } + } else if (e.keyCode === KeyCode.DownArrow) { + // Focus first action bar item when at the end of the input + const inputModel = this._input.getModel(); + const position = this._input.getPosition(); + const lastLineNumber = inputModel.getLineCount(); + const lastLineMaxColumn = inputModel.getLineMaxColumn(lastLineNumber); + if (Position.equals(position, new Position(lastLineNumber, lastLineMaxColumn))) { + e.preventDefault(); + e.stopPropagation(); + this._actionBar.focus(); + } + } + })); + + // Create vertical action bar + this._actionBar = this._store.add(new ActionBar(this._domNode, { + orientation: ActionsOrientation.VERTICAL, + preventLoopNavigation: true, + })); + + // Track focus - hide when focus leaves + const focusTracker = this._store.add(dom.trackFocus(this._domNode)); + this._store.add(focusTracker.onDidBlur(() => this.hide())); + + // Handle action bar cancel (Escape key) + this._store.add(this._actionBar.onDidCancel(() => this.hide())); + this._store.add(this._actionBar.onWillRun(() => this.hide())); + } + + /** + * Show the widget at the specified position. + * @param top Top offset relative to editor + * @param left Left offset relative to editor + * @param anchorAbove Whether to anchor above the position (widget grows upward) + */ + show(top: number, left: number, anchorAbove: boolean): void { + + // Clear input state + this._input.getModel().setValue(''); + this._inputContainer.style.height = '26px'; + this._input.layout({ width: 200, height: 18 }); + + // Refresh actions from menu + this._refreshActions(); + + // Set initial position + this._position = { + preference: { top, left }, + stackOrdinal: 10000, + }; + + // Add widget to editor + if (!this._isVisible) { + this._editor.addOverlayWidget(this); + this._isVisible = true; + + } else if (!anchorAbove) { + this._editor.layoutOverlayWidget(this); + } + + // If anchoring above, adjust position after render to account for widget height + if (anchorAbove) { + const widgetHeight = this._domNode.offsetHeight; + this._position = { + preference: { top: top - widgetHeight, left }, + stackOrdinal: 10000, + }; + this._editor.layoutOverlayWidget(this); + } + + // Focus the input editor + setTimeout(() => this._input.focus(), 0); + } + + /** + * Hide the widget (removes from editor but does not dispose). + */ + hide(): void { + if (!this._isVisible) { + return; + } + this._isVisible = false; + this._editor.removeOverlayWidget(this); + this._onDidHide.fire(); + } + + private _refreshActions(): void { + // Clear existing actions + this._actionBar.clear(); + this._inlineStartAction = undefined; + + // Get fresh actions from menu + const actions = getFlatActionBarActions(this._menuService.getMenuActions(MenuId.ChatEditorInlineGutter, this._contextKeyService, { shouldForwardArgs: true })); + + // Set actions with keybindings (skip ACTION_START since we have the input editor) + for (const action of actions) { + if (action.id === ACTION_START) { + this._inlineStartAction = action; + continue; + } + const keybinding = this._keybindingService.lookupKeybinding(action.id)?.getLabel(); + this._actionBar.push(action, { icon: false, label: true, keybinding }); + } + } + + private _updateInputHeight(contentHeight: number): void { + const lineHeight = this._input.getOption(EditorOption.lineHeight); + const maxHeight = 3 * lineHeight; + const clampedHeight = Math.min(contentHeight, maxHeight); + const containerPadding = 8; + + this._inputContainer.style.height = `${clampedHeight + containerPadding}px`; + this._input.layout({ width: 200, height: clampedHeight }); + if (this._isVisible) { + this._editor.layoutOverlayWidget(this); + } + } + + getId(): string { + return this._id; + } + + getDomNode(): HTMLElement { + return this._domNode; + } + + getPosition(): IOverlayWidgetPosition | null { + return this._position; + } + + override dispose(): void { + if (this._isVisible) { + this._editor.removeOverlayWidget(this); + } + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSelectionGutterIndicator.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSelectionGutterIndicator.ts deleted file mode 100644 index eebb0032be9..00000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSelectionGutterIndicator.ts +++ /dev/null @@ -1,456 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../base/browser/dom.js'; -import { IAction } from '../../../../base/common/actions.js'; -import { ActionBar, ActionsOrientation } from '../../../../base/browser/ui/actionbar/actionbar.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { KeyCode } from '../../../../base/common/keyCodes.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { autorun, constObservable, debouncedObservable, derived, IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; -import { URI } from '../../../../base/common/uri.js'; -import { Emitter } from '../../../../base/common/event.js'; -import { IActiveCodeEditor, ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; -import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; -import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js'; -import { observableCodeEditor, ObservableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; -import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; -import { LineRange } from '../../../../editor/common/core/ranges/lineRange.js'; -import { SelectionDirection } from '../../../../editor/common/core/selection.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; -import { InlineEditTabAction } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.js'; -import { InlineEditsGutterIndicator, InlineEditsGutterIndicatorData, InlineSuggestionGutterMenuData, SimpleInlineSuggestModel } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { HoverService } from '../../../../platform/hover/browser/hoverService.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { localize } from '../../../../nls.js'; -import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; -import { ACTION_START, CTX_INLINE_CHAT_GUTTER_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; -import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOptions.js'; -import { PlaceholderTextContribution } from '../../../../editor/contrib/placeholderText/browser/placeholderTextContribution.js'; -import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; -import { InlineChatRunOptions } from './inlineChatController.js'; -import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; -import { Position } from '../../../../editor/common/core/position.js'; - - -export class InlineChatSelectionIndicator extends Disposable { - - private readonly _gutterIndicator: InlineChatGutterIndicator; - - constructor( - private readonly _editor: ICodeEditor, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IConfigurationService configurationService: IConfigurationService, - @IChatEntitlementService chatEntiteldService: IChatEntitlementService, - @IContextKeyService contextKeyService: IContextKeyService - ) { - super(); - - const enabled = observableConfigValue(InlineChatConfigKeys.ShowGutterMenu, false, configurationService); - - const editorObs = observableCodeEditor(this._editor); - const focusIsInMenu = observableValue(this, false); - - // Observable to suppress the gutter when an action is selected - const suppressGutter = observableValue(this, false); - - // Debounce the selection to add a delay before showing the indicator - const debouncedSelection = debouncedObservable(editorObs.cursorSelection, 500); - - // Context key for gutter visibility - const gutterVisibleCtxKey = CTX_INLINE_CHAT_GUTTER_VISIBLE.bindTo(contextKeyService); - this._store.add({ dispose: () => gutterVisibleCtxKey.reset() }); - - // Create data observable based on the primary selection - // Use raw selection for immediate hide, debounced for delayed show - const data = derived(reader => { - // Check if feature is enabled or if AI features are disabled - if (!enabled.read(reader) || chatEntiteldService.sentiment.hidden) { - return undefined; - } - - // Hide when suppressed (e.g., after an action is selected) - if (suppressGutter.read(reader)) { - return undefined; - } - - // Read raw selection - if empty, immediately hide - const rawSelection = editorObs.cursorSelection.read(reader); - if (!rawSelection || rawSelection.isEmpty()) { - return undefined; - } - - // Read debounced selection for showing - this adds delay - const selection = debouncedSelection.read(reader); - if (!selection || selection.isEmpty()) { - return undefined; - } - - // Use the cursor position (active end of selection) to determine the line - const cursorPosition = selection.getPosition(); - const lineRange = new LineRange(cursorPosition.lineNumber, cursorPosition.lineNumber + 1); - - // Create minimal gutter menu data (empty for prototype) - const gutterMenuData = new InlineSuggestionGutterMenuData( - undefined, // action - '', // displayName - [], // extensionCommands - undefined, // alternativeAction - undefined, // modelInfo - undefined, // setModelId - ); - - // Create model with console.log actions for prototyping - const model = new SimpleInlineSuggestModel(() => { }, () => { }); - - return new InlineEditsGutterIndicatorData( - gutterMenuData, - lineRange, - model, - undefined, // altAction - { - icon: Codicon.sparkle, - } - ); - }); - - // Instantiate the gutter indicator - this._gutterIndicator = this._store.add(this._instantiationService.createInstance( - InlineChatGutterIndicator, - editorObs, - data, - constObservable(InlineEditTabAction.Jump), // tabAction - not used with custom styles - constObservable(0), // verticalOffset - constObservable(false), // isHoveringOverInlineEdit - focusIsInMenu, - suppressGutter, - )); - - // Reset suppressGutter when the selection changes - this._store.add(autorun(reader => { - editorObs.cursorSelection.read(reader); - suppressGutter.set(false, undefined); - })); - - // Update context key when gutter visibility changes - this._store.add(autorun(reader => { - const isVisible = data.read(reader) !== undefined; - gutterVisibleCtxKey.set(isVisible); - })); - } - - /** - * Show the gutter menu at the specified coordinates. - * @returns Promise that resolves when menu closes - */ - showMenuAt(x: number, y: number, height: number = 0): Promise { - return this._gutterIndicator.showMenuAt(x, y, height); - } -} - -/** - * Overlay widget that displays a vertical action bar menu. - */ -class InlineChatGutterMenuWidget extends Disposable implements IOverlayWidget { - - private static _idPool = 0; - - private readonly _id = `inline-chat-gutter-menu-${InlineChatGutterMenuWidget._idPool++}`; - private readonly _domNode: HTMLElement; - private readonly _inputContainer: HTMLElement; - private readonly _actionBar: ActionBar; - private readonly _input: IActiveCodeEditor; - private _position: IOverlayWidgetPosition | null = null; - private readonly _onDidHide = this._register(new Emitter()); - readonly onDidHide = this._onDidHide.event; - - readonly allowEditorOverflow = true; - - constructor( - private readonly _editor: ICodeEditor, - top: number, - left: number, - anchorAbove: boolean, - @IKeybindingService keybindingService: IKeybindingService, - @IMenuService menuService: IMenuService, - @IContextKeyService contextKeyService: IContextKeyService, - @IInstantiationService instantiationService: IInstantiationService, - @IModelService modelService: IModelService, - @IConfigurationService configurationService: IConfigurationService, - ) { - super(); - - // Create container - this._domNode = dom.$('.inline-chat-gutter-menu'); - - // Create input editor container - this._inputContainer = dom.append(this._domNode, dom.$('.input')); - this._inputContainer.style.width = '200px'; - this._inputContainer.style.height = '26px'; - this._inputContainer.style.display = 'flex'; - this._inputContainer.style.alignItems = 'center'; - this._inputContainer.style.justifyContent = 'center'; - - // Create editor options - const options = getSimpleEditorOptions(configurationService); - options.wordWrap = 'off'; - options.lineNumbers = 'off'; - options.glyphMargin = false; - options.lineDecorationsWidth = 0; - options.lineNumbersMinChars = 0; - options.folding = false; - options.minimap = { enabled: false }; - options.scrollbar = { vertical: 'auto', horizontal: 'hidden', alwaysConsumeMouseWheel: true, verticalSliderSize: 6 }; - options.renderLineHighlight = 'none'; - options.placeholder = keybindingService.appendKeybinding(localize('placeholderWithSelection', "Edit selection"), ACTION_START); - - const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { - isSimpleWidget: true, - contributions: EditorExtensionsRegistry.getSomeEditorContributions([ - PlaceholderTextContribution.ID, - ]) - }; - - this._input = this._store.add(instantiationService.createInstance(CodeEditorWidget, this._inputContainer, options, codeEditorWidgetOptions)) as IActiveCodeEditor; - - const model = this._store.add(modelService.createModel('', null, URI.parse(`gutter-input:${Date.now()}`), true)); - this._input.setModel(model); - this._input.layout({ width: 200, height: 18 }); - - // Listen to content size changes and resize the input editor (max 3 lines) - this._store.add(this._input.onDidContentSizeChange(e => { - if (e.contentHeightChanged) { - this._updateInputHeight(e.contentHeight); - } - })); - - let inlineStartAction: IAction | undefined; - - // Handle Enter key to submit and ArrowDown to focus action bar - this._store.add(this._input.onKeyDown(e => { - if (e.keyCode === KeyCode.Enter && !e.shiftKey) { - const value = this._input.getModel().getValue() ?? ''; - // TODO@jrieken this isn't nice - if (inlineStartAction && value) { - e.preventDefault(); - e.stopPropagation(); - this._actionBar.actionRunner.run( - inlineStartAction, - { message: value, autoSend: true } satisfies InlineChatRunOptions - ); - } - } else if (e.keyCode === KeyCode.DownArrow) { - // Focus first action bar item when at the end of the input - const inputModel = this._input.getModel(); - const position = this._input.getPosition(); - const lastLineNumber = inputModel.getLineCount(); - const lastLineMaxColumn = inputModel.getLineMaxColumn(lastLineNumber); - if (Position.equals(position, new Position(lastLineNumber, lastLineMaxColumn))) { - e.preventDefault(); - e.stopPropagation(); - this._actionBar.focus(); - } - } - })); - - // Get actions from menu - const actions = getFlatActionBarActions(menuService.getMenuActions(MenuId.ChatEditorInlineGutter, contextKeyService, { shouldForwardArgs: true })); - - // Create vertical action bar - this._actionBar = this._store.add(new ActionBar(this._domNode, { - orientation: ActionsOrientation.VERTICAL, - })); - - // Set actions with keybindings (skip ACTION_START since we have the input editor) - for (const action of actions) { - if (action.id === ACTION_START) { - inlineStartAction = action; - continue; - } - const keybinding = keybindingService.lookupKeybinding(action.id)?.getLabel(); - this._actionBar.push(action, { icon: false, label: true, keybinding }); - } - - // Set initial position - this._position = { - preference: { top, left }, - stackOrdinal: 10000, - }; - - // Track focus - hide when focus leaves - const focusTracker = this._store.add(dom.trackFocus(this._domNode)); - this._store.add(focusTracker.onDidBlur(() => this._hide())); - - // Handle action bar cancel (Escape key) - this._store.add(this._actionBar.onDidCancel(() => this._hide())); - this._store.add(this._actionBar.onWillRun(() => this._hide())); - - // Add widget to editor - this._editor.addOverlayWidget(this); - - // If anchoring above, adjust position after render to account for widget height - if (anchorAbove) { - const widgetHeight = this._domNode.offsetHeight; - this._position = { - preference: { top: top - widgetHeight, left }, - stackOrdinal: 10000, - }; - this._editor.layoutOverlayWidget(this); - } - - // Focus the input editor - setTimeout(() => this._input.focus(), 0); - } - - private _hide(): void { - this._onDidHide.fire(); - } - - private _updateInputHeight(contentHeight: number): void { - const lineHeight = this._input.getOption(EditorOption.lineHeight); - const maxHeight = 3 * lineHeight; - const clampedHeight = Math.min(contentHeight, maxHeight); - const containerPadding = 8; - - this._inputContainer.style.height = `${clampedHeight + containerPadding}px`; - this._input.layout({ width: 200, height: clampedHeight }); - this._editor.layoutOverlayWidget(this); - } - - getId(): string { - return this._id; - } - - getDomNode(): HTMLElement { - return this._domNode; - } - - getPosition(): IOverlayWidgetPosition | null { - return this._position; - } - - override dispose(): void { - this._editor.removeOverlayWidget(this); - super.dispose(); - } -} - -/** - * Custom gutter indicator for selection that shows a menu overlay widget. - */ -class InlineChatGutterIndicator extends InlineEditsGutterIndicator { - - private readonly _myInstantiationService: IInstantiationService; - private _currentMenuWidget: InlineChatGutterMenuWidget | undefined; - - constructor( - private readonly _myEditorObs: ObservableCodeEditor, - data: IObservable, - tabAction: IObservable, - verticalOffset: IObservable, - isHoveringOverInlineEdit: IObservable, - focusIsInMenu: ISettableObservable, - private readonly _suppressGutter: ISettableObservable, - @IHoverService hoverService: HoverService, - @IInstantiationService instantiationService: IInstantiationService, - @IAccessibilityService accessibilityService: IAccessibilityService, - @IThemeService themeService: IThemeService, - ) { - super(_myEditorObs, data, tabAction, verticalOffset, isHoveringOverInlineEdit, focusIsInMenu, hoverService, instantiationService, accessibilityService, themeService); - this._myInstantiationService = instantiationService; - } - - protected override _showHover(): void { - - if (this._hoverVisible.get()) { - return; - } - - // Use the icon element from the base class as anchor - const iconElement = this._iconRef.element; - if (!iconElement) { - return; - } - - this._hoverVisible.set(true, undefined); - const rect = iconElement.getBoundingClientRect(); - - this.showMenuAt(rect.left, rect.top, rect.height).finally(() => { - this._hoverVisible.set(false, undefined); - }); - } - - /** - * Show the gutter menu at the specified coordinates. - * @returns Promise that resolves when menu closes - */ - showMenuAt(x: number, y: number, height: number = 0): Promise { - return new Promise(resolve => { - // Clean up existing widget if any - this._currentMenuWidget?.dispose(); - this._currentMenuWidget = undefined; - - // Determine selection direction to position menu above or below - const selection = this._myEditorObs.cursorSelection.get(); - const direction = selection?.getDirection() ?? SelectionDirection.LTR; - - // Convert screen coordinates to editor-relative coordinates - const editor = this._myEditorObs.editor; - const editorDomNode = editor.getDomNode(); - if (!editorDomNode) { - resolve(); - return; - } - - const editorRect = editorDomNode.getBoundingClientRect(); - const padding = 1; - - // Calculate position relative to editor - // For RTL (above), we pass the top of the gutter indicator; widget will adjust after measuring its height - // For LTR (below), we pass the bottom of the gutter indicator - const anchorAbove = direction === SelectionDirection.RTL; - let top: number; - if (anchorAbove) { - // Pass the top of the gutter indicator minus padding - top = y - editorRect.top - padding; - } else { - // Menu appears below - position at bottom of gutter indicator - top = y - editorRect.top + height + padding; - } - const left = x - editorRect.left; - - const store = new DisposableStore(); - - // Create and show overlay widget - this._currentMenuWidget = this._myInstantiationService.createInstance( - InlineChatGutterMenuWidget, - editor, - top, - left, - anchorAbove, - ); - - // Handle widget hide - store.add(this._currentMenuWidget.onDidHide(() => { - this._suppressGutter.set(true, undefined); - store.dispose(); - this._currentMenuWidget?.dispose(); - this._currentMenuWidget = undefined; - - // Focus editor - editor.focus(); - - resolve(); - })); - }); - } -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css new file mode 100644 index 00000000000..81a039d9d5c --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.inline-chat-content-widget { + box-shadow: 0 0 8px 2px var(--vscode-widget-shadow); + border: 1px solid var(--vscode-widget-border, transparent); + border-radius: 4px; + color: var(--vscode-button-foreground); + background-color: var(--vscode-button-background); + display: flex; + height: 16px; + width: 16px; + align-items: center; + justify-content: center; + cursor: pointer; + padding: 2px; +} + +.inline-chat-content-widget:hover { + background-color: var(--vscode-button-hoverBackground); +} + +.inline-chat-content-widget .icon.codicon { + margin: 0; + color: var(--vscode-button-foreground); +} diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index e3a74c6adb6..311cb19ddda 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -15,13 +15,13 @@ import { NOTEBOOK_IS_ACTIVE_EDITOR } from '../../notebook/common/notebookContext export const enum InlineChatConfigKeys { FinishOnType = 'inlineChat.finishOnType', - StartWithOverlayWidget = 'inlineChat.startWithOverlayWidget', HoldToSpeech = 'inlineChat.holdToSpeech', /** @deprecated do not read on client */ EnableV2 = 'inlineChat.enableV2', notebookAgent = 'inlineChat.notebookAgent', PersistModelChoice = 'inlineChat.persistModelChoice', - ShowGutterMenu = 'inlineChat.showGutterMenu', + Affordance = 'inlineChat.affordance', + RenderMode = 'inlineChat.renderMode', } Registry.as(Extensions.Configuration).registerConfiguration({ @@ -63,10 +63,27 @@ Registry.as(Extensions.Configuration).registerConfigurat mode: 'auto' } }, - [InlineChatConfigKeys.ShowGutterMenu]: { - description: localize('showGutterMenu', "Controls whether a gutter indicator is shown when text is selected to quickly access inline chat."), - default: false, - type: 'boolean', + [InlineChatConfigKeys.Affordance]: { + description: localize('affordance', "Controls whether an inline chat affordance is shown when text is selected."), + default: 'off', + type: 'string', + enum: ['off', 'gutter', 'editor'], + enumDescriptions: [ + localize('affordance.off', "No affordance is shown."), + localize('affordance.gutter', "Show an affordance in the gutter."), + localize('affordance.editor', "Show an affordance in the editor at the cursor position."), + ], + tags: ['experimental'] + }, + [InlineChatConfigKeys.RenderMode]: { + description: localize('renderMode', "Controls how inline chat is rendered."), + default: 'zone', + type: 'string', + enum: ['zone', 'hover'], + enumDescriptions: [ + localize('renderMode.zone', "Render inline chat as a zone widget below the current line."), + localize('renderMode.hover', "Render inline chat as a hover overlay."), + ], tags: ['experimental'] } } From e1cad32733f1930096e427710c7dd6814eaaef87 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 21 Jan 2026 09:47:58 +0100 Subject: [PATCH 333/387] skip reporting stats in web (#289156) * skip reporting stats in web * accept suggestion --- .../abstractExtensionManagementService.ts | 10 ++------ .../common/extensionGalleryManifest.ts | 1 - .../common/extensionGalleryManifestService.ts | 4 --- .../common/extensionGalleryService.ts | 25 ++++++++----------- 4 files changed, 12 insertions(+), 28 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index e52ace88933..8bb3e5144d0 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -428,12 +428,6 @@ export abstract class AbstractExtensionManagementService extends CommontExtensio durationSinceUpdate, source: task.options.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] as string | undefined }); - // In web, report extension install statistics explicitly. In Desktop, statistics are automatically updated while downloading the VSIX. - if (isWeb && task.operation !== InstallOperation.Update) { - try { - await this.galleryService.reportStatistic(local.manifest.publisher, local.manifest.name, local.manifest.version, StatisticType.Install); - } catch (error) { /* ignore */ } - } } installExtensionResultsMap.set(key, { local, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.options.profileLocation, applicationScoped: local.isApplicationScoped }); })); @@ -854,8 +848,8 @@ export abstract class AbstractExtensionManagementService extends CommontExtensio await task.run(); await this.joinAllSettled(this.participants.map(participant => participant.postUninstall(task.extension, task.options, CancellationToken.None))); - // only report if extension has a mapped gallery extension. UUID identifies the gallery extension. - if (task.extension.identifier.uuid) { + // only report if extension has a mapped gallery extension and not in web. UUID identifies the gallery extension. + if (task.extension.identifier.uuid && !isWeb) { try { await this.galleryService.reportStatistic(task.extension.manifest.publisher, task.extension.manifest.name, task.extension.manifest.version, StatisticType.Uninstall); } catch (error) { /* ignore */ } diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryManifest.ts b/src/vs/platform/extensionManagement/common/extensionGalleryManifest.ts index a601b7a3c7b..cef5e2af703 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryManifest.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryManifest.ts @@ -10,7 +10,6 @@ export const enum ExtensionGalleryResourceType { ExtensionQueryService = 'ExtensionQueryService', ExtensionLatestVersionUri = 'ExtensionLatestVersionUriTemplate', ExtensionStatisticsUri = 'ExtensionStatisticsUriTemplate', - WebExtensionStatisticsUri = 'WebExtensionStatisticsUriTemplate', PublisherViewUri = 'PublisherViewUriTemplate', ExtensionDetailsViewUri = 'ExtensionDetailsViewUriTemplate', ExtensionRatingViewUri = 'ExtensionRatingViewUriTemplate', diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryManifestService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryManifestService.ts index 658219eab6e..53be55bdffd 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryManifestService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryManifestService.ts @@ -54,10 +54,6 @@ export class ExtensionGalleryManifestService extends Disposable implements IExte id: `${extensionsGallery.serviceUrl}/publishers/{publisher}/extensions/{name}/{version}/stats?statType={statTypeName}`, type: ExtensionGalleryResourceType.ExtensionStatisticsUri }, - { - id: `${extensionsGallery.serviceUrl}/itemName/{publisher}.{name}/version/{version}/statType/{statTypeValue}/vscodewebextension`, - type: ExtensionGalleryResourceType.WebExtensionStatisticsUri - }, ]; if (extensionsGallery.publisherUrl) { diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 594412ff657..e74641e055e 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -1531,28 +1531,23 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle } async reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise { + if (isWeb) { + this.logService.info('ExtensionGalleryService#reportStatistic: Skipped in web'); + return undefined; + } + const manifest = await this.extensionGalleryManifestService.getExtensionGalleryManifest(); if (!manifest) { return undefined; } - let url: string; - - if (isWeb) { - const resource = getExtensionGalleryManifestResourceUri(manifest, ExtensionGalleryResourceType.WebExtensionStatisticsUri); - if (!resource) { - return; - } - url = format2(resource, { publisher, name, version, statTypeValue: type === StatisticType.Install ? '1' : '3' }); - } else { - const resource = getExtensionGalleryManifestResourceUri(manifest, ExtensionGalleryResourceType.ExtensionStatisticsUri); - if (!resource) { - return; - } - url = format2(resource, { publisher, name, version, statTypeName: type }); + const resource = getExtensionGalleryManifestResourceUri(manifest, ExtensionGalleryResourceType.ExtensionStatisticsUri); + if (!resource) { + return; } + const url = format2(resource, { publisher, name, version, statTypeName: type }); - const Accept = isWeb ? 'api-version=6.1-preview.1' : '*/*;api-version=4.0-preview.1'; + const Accept = '*/*;api-version=4.0-preview.1'; const commonHeaders = await this.commonHeadersPromise; const headers = { ...commonHeaders, Accept }; try { From 46e5821fd3cf0998059c5220e19085fde1675429 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 21 Jan 2026 10:52:11 +0100 Subject: [PATCH 334/387] feat(inlineChat): enhance inline chat affordance with new selection handling and styling --- .../lib/stylelint/vscode-known-variables.json | 3 +- .../browser/inlineChatAffordance.ts | 41 ++++++++++--------- .../browser/inlineChatEditorAffordance.ts | 13 +++++- .../media/inlineChatEditorAffordance.css | 26 +++++------- 4 files changed, 45 insertions(+), 38 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 17778581978..a2f1696f372 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -999,7 +999,8 @@ "--comment-thread-state-color", "--comment-thread-state-background-color", "--inline-edit-border-radius", - "--chat-subagent-last-item-height" + "--chat-subagent-last-item-height", + "--vscode-inline-chat-affordance-height" ], "sizes": [ "--vscode-bodyFontSize", diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts index 5b8a4bba6f7..fba6cf4cf6f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../base/common/lifecycle.js'; -import { autorun, debouncedObservable, derived, observableValue } from '../../../../base/common/observable.js'; +import { autorun, debouncedObservable, derived, observableValue, runOnChange } from '../../../../base/common/observable.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -18,6 +18,7 @@ import { InlineChatGutterAffordance } from './inlineChatGutterAffordance.js'; import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; import { assertType } from '../../../../base/common/types.js'; import { Event } from '../../../../base/common/event.js'; +import { CursorChangeReason } from '../../../../editor/common/cursorEvents.js'; export class InlineChatAffordance extends Disposable { @@ -45,41 +46,41 @@ export class InlineChatAffordance extends Disposable { const debouncedSelection = debouncedObservable(editorObs.cursorSelection, 500); - const selection = observableValue(this, undefined); + const selectionData = observableValue(this, undefined); - this._store.add(autorun(r => { - const value = editorObs.cursorSelection.read(r); - if (!value || value.isEmpty()) { - selection.set(undefined, undefined); + let explicitSelection = false; + + this._store.add(runOnChange(editorObs.selections, (value, _prev, events) => { + explicitSelection = events.every(e => e.reason === CursorChangeReason.Explicit); + if (!value || value.length !== 1 || value[0].isEmpty() || !explicitSelection) { + selectionData.set(undefined, undefined); } })); + this._store.add(autorun(r => { + const value = debouncedSelection.read(r); + if (!value || value.isEmpty() || !explicitSelection) { + selectionData.set(undefined, undefined); + return; + } + selectionData.set(value, undefined); + })); this._store.add(autorun(r => { if (chatEntiteldService.sentimentObs.read(r).hidden) { - selection.set(undefined, undefined); - return; + selectionData.set(undefined, undefined); } if (suppressGutter.read(r)) { - selection.set(undefined, undefined); - return; + selectionData.set(undefined, undefined); } - const value = debouncedSelection.read(r); - if (!value || value.isEmpty()) { - selection.set(undefined, undefined); - return; - } - selection.set(value, undefined); })); - - // Instantiate the gutter indicator this._store.add(this._instantiationService.createInstance( InlineChatGutterAffordance, editorObs, - derived(r => affordance.read(r) === 'gutter' ? selection.read(r) : undefined), + derived(r => affordance.read(r) === 'gutter' ? selectionData.read(r) : undefined), suppressGutter, this._menuData )); @@ -88,7 +89,7 @@ export class InlineChatAffordance extends Disposable { this._store.add(this._instantiationService.createInstance( InlineChatEditorAffordance, this._editor, - derived(r => affordance.read(r) === 'editor' ? selection.read(r) : undefined), + derived(r => affordance.read(r) === 'editor' ? selectionData.read(r) : undefined), suppressGutter, this._menuData )); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts index 9fa3043f0fb..3ed19bbfa6b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts @@ -4,11 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import './media/inlineChatEditorAffordance.css'; +import { IDimension } from '../../../../base/browser/dom.js'; import * as dom from '../../../../base/browser/dom.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; +import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; import { autorun, IObservable, ISettableObservable } from '../../../../base/common/observable.js'; import { assertType } from '../../../../base/common/types.js'; @@ -42,7 +44,7 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi // Add sparkle icon const icon = dom.append(this._domNode, dom.$('.icon')); - icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.sparkle)); + icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.sparkleFilled)); // Handle click to show overlay widget this._store.add(dom.addDisposableListener(this._domNode, dom.EventType.CLICK, (e) => { @@ -126,6 +128,15 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi return this._position; } + beforeRender(): IDimension | null { + const position = this._editor.getPosition(); + const lineHeight = position ? this._editor.getLineHeightForPosition(position) : this._editor.getOption(EditorOption.lineHeight); + + this._domNode.style.setProperty('--vscode-inline-chat-affordance-height', `${lineHeight}px`); + + return null; + } + override dispose(): void { if (this._isVisible) { this._editor.removeContentWidget(this); diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css index 81a039d9d5c..5eaa356dcfa 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css @@ -4,25 +4,19 @@ *--------------------------------------------------------------------------------------------*/ .inline-chat-content-widget { - box-shadow: 0 0 8px 2px var(--vscode-widget-shadow); - border: 1px solid var(--vscode-widget-border, transparent); - border-radius: 4px; - color: var(--vscode-button-foreground); - background-color: var(--vscode-button-background); - display: flex; - height: 16px; - width: 16px; - align-items: center; - justify-content: center; - cursor: pointer; + background-color: var(--vscode-editor-background); padding: 2px; -} - -.inline-chat-content-widget:hover { - background-color: var(--vscode-button-hoverBackground); + border-radius: 8px; + display: flex; + align-items: center; + box-shadow: 0 4px 8px var(--vscode-widget-shadow); + cursor: pointer; + min-width: var(--vscode-inline-chat-affordance-height); + min-height: var(--vscode-inline-chat-affordance-height); + line-height: var(--vscode-inline-chat-affordance-height); } .inline-chat-content-widget .icon.codicon { margin: 0; - color: var(--vscode-button-foreground); + color: var(--vscode-editorLightBulb-foreground); } From 6f765aed7468a2daed29be9d36aafe821843ac77 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 21 Jan 2026 10:55:52 +0100 Subject: [PATCH 335/387] Agent sessions in Chat integration feedback (fix #289188) (#289325) * Agent sessions in Chat integration feedback (fix #289188) * . * . * . --- .../agentSessions.contribution.ts | 7 +- .../agentSessions/agentSessionsActions.ts | 82 +------ .../agentSessions/agentSessionsViewer.ts | 4 + .../contrib/chat/browser/chat.contribution.ts | 5 - .../widgetHosts/viewPane/chatViewPane.ts | 203 ++++++++---------- .../chat/common/actions/chatContextKeys.ts | 1 - .../contrib/chat/common/constants.ts | 1 - 7 files changed, 102 insertions(+), 201 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts index 1016c2afb42..e329879e8f1 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts @@ -16,7 +16,7 @@ import { IAgentSessionsService, AgentSessionsService } from './agentSessionsServ import { LocalAgentsSessionsProvider } from './localAgentSessionsProvider.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js'; import { ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { ArchiveAgentSessionAction, ArchiveAgentSessionSectionAction, UnarchiveAgentSessionAction, OpenAgentSessionInEditorGroupAction, OpenAgentSessionInNewEditorGroupAction, OpenAgentSessionInNewWindowAction, ShowAgentSessionsSidebar, HideAgentSessionsSidebar, ToggleAgentSessionsSidebar, RefreshAgentSessionsViewerAction, FindAgentSessionInViewerAction, MarkAgentSessionUnreadAction, MarkAgentSessionReadAction, FocusAgentSessionsAction, SetAgentSessionsOrientationStackedAction, SetAgentSessionsOrientationSideBySideAction, PickAgentSessionAction, ArchiveAllAgentSessionsAction, RenameAgentSessionAction, DeleteAgentSessionAction, DeleteAllLocalSessionsAction, HideAgentSessionsAction, MarkAgentSessionSectionReadAction, ShowAllAgentSessionsAction, ShowRecentAgentSessionsAction, UnarchiveAgentSessionSectionAction } from './agentSessionsActions.js'; +import { ArchiveAgentSessionAction, ArchiveAgentSessionSectionAction, UnarchiveAgentSessionAction, OpenAgentSessionInEditorGroupAction, OpenAgentSessionInNewEditorGroupAction, OpenAgentSessionInNewWindowAction, ShowAgentSessionsSidebar, HideAgentSessionsSidebar, ToggleAgentSessionsSidebar, RefreshAgentSessionsViewerAction, FindAgentSessionInViewerAction, MarkAgentSessionUnreadAction, MarkAgentSessionReadAction, FocusAgentSessionsAction, SetAgentSessionsOrientationStackedAction, SetAgentSessionsOrientationSideBySideAction, PickAgentSessionAction, ArchiveAllAgentSessionsAction, RenameAgentSessionAction, DeleteAgentSessionAction, DeleteAllLocalSessionsAction, MarkAgentSessionSectionReadAction, UnarchiveAgentSessionSectionAction, ToggleChatViewSessionsAction } from './agentSessionsActions.js'; import { AgentSessionsQuickAccessProvider, AGENT_SESSIONS_QUICK_ACCESS_PREFIX } from './agentSessionsQuickAccess.js'; import { AuxiliaryBarMaximizedContext } from '../../../../common/contextkeys.js'; @@ -43,9 +43,7 @@ registerAction2(FindAgentSessionInViewerAction); registerAction2(ShowAgentSessionsSidebar); registerAction2(HideAgentSessionsSidebar); registerAction2(ToggleAgentSessionsSidebar); -registerAction2(ShowAllAgentSessionsAction); -registerAction2(ShowRecentAgentSessionsAction); -registerAction2(HideAgentSessionsAction); +registerAction2(ToggleChatViewSessionsAction); registerAction2(SetAgentSessionsOrientationStackedAction); registerAction2(SetAgentSessionsOrientationSideBySideAction); @@ -57,7 +55,6 @@ MenuRegistry.appendMenuItem(MenuId.AgentSessionsToolbar, { group: 'navigation', order: 3, icon: Codicon.filter, - when: ChatContextKeys.agentSessionsViewerLimited.negate() } satisfies ISubmenuItem); MenuRegistry.appendMenuItem(MenuId.AgentSessionsToolbar, { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts index 8543fc206bd..c8277d3bf63 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts @@ -38,29 +38,17 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../../pla //#region Chat View -const showSessionsSubmenu = new MenuId('chatShowSessionsSubmenu'); -MenuRegistry.appendMenuItem(MenuId.ChatWelcomeContext, { - submenu: showSessionsSubmenu, - title: localize2('chat.showSessions', "Show Sessions"), - group: '0_sessions', - order: 1, - when: ChatContextKeys.inChatEditor.negate() -}); - -export class ShowAllAgentSessionsAction extends Action2 { - +export class ToggleChatViewSessionsAction extends Action2 { constructor() { super({ - id: 'workbench.action.chat.showAllAgentSessions', - title: localize2('chat.showSessions.all', "All"), - toggled: ContextKeyExpr.and( - ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsEnabled}`, true), - ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsShowRecentOnly}`, false) - ), + id: 'workbench.action.chat.toggleChatViewSessions', + title: localize2('chat.toggleChatViewSessions.label', "Show Sessions"), + toggled: ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsEnabled}`, true), menu: { - id: showSessionsSubmenu, - group: 'navigation', - order: 1 + id: MenuId.ChatWelcomeContext, + group: '0_sessions', + order: 1, + when: ChatContextKeys.inChatEditor.negate() } }); } @@ -68,56 +56,8 @@ export class ShowAllAgentSessionsAction extends Action2 { async run(accessor: ServicesAccessor): Promise { const configurationService = accessor.get(IConfigurationService); - await configurationService.updateValue(ChatConfiguration.ChatViewSessionsEnabled, true); - await configurationService.updateValue(ChatConfiguration.ChatViewSessionsShowRecentOnly, false); - } -} - -export class ShowRecentAgentSessionsAction extends Action2 { - - constructor() { - super({ - id: 'workbench.action.chat.showRecentAgentSessions', - title: localize2('chat.showSessions.recent', "Recent"), - toggled: ContextKeyExpr.and( - ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsEnabled}`, true), - ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsShowRecentOnly}`, true) - ), - menu: { - id: showSessionsSubmenu, - group: 'navigation', - order: 2 - } - }); - } - - async run(accessor: ServicesAccessor): Promise { - const configurationService = accessor.get(IConfigurationService); - - await configurationService.updateValue(ChatConfiguration.ChatViewSessionsEnabled, true); - await configurationService.updateValue(ChatConfiguration.ChatViewSessionsShowRecentOnly, true); - } -} - -export class HideAgentSessionsAction extends Action2 { - - constructor() { - super({ - id: 'workbench.action.chat.hideAgentSessions', - title: localize2('chat.showSessions.none', "None"), - toggled: ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsEnabled}`, false), - menu: { - id: showSessionsSubmenu, - group: 'navigation', - order: 3 - } - }); - } - - async run(accessor: ServicesAccessor): Promise { - const configurationService = accessor.get(IConfigurationService); - - await configurationService.updateValue(ChatConfiguration.ChatViewSessionsEnabled, false); + const chatViewSessionsEnabled = configurationService.getValue(ChatConfiguration.ChatViewSessionsEnabled); + await configurationService.updateValue(ChatConfiguration.ChatViewSessionsEnabled, !chatViewSessionsEnabled); } } @@ -845,7 +785,6 @@ export class RefreshAgentSessionsViewerAction extends Action2 { id: MenuId.AgentSessionsToolbar, group: 'navigation', order: 1, - when: ChatContextKeys.agentSessionsViewerLimited.negate() }, }); } @@ -866,7 +805,6 @@ export class FindAgentSessionInViewerAction extends Action2 { id: MenuId.AgentSessionsToolbar, group: 'navigation', order: 2, - when: ChatContextKeys.agentSessionsViewerLimited.negate() } }); } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 481f4bed2f1..c911f796803 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -349,6 +349,10 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre } private renderHover(session: ITreeNode, template: IAgentSessionItemTemplate): void { + if (!isSessionInProgressStatus(session.element.status)) { + return; // the hover is complex and large, for now limit it to in-progress sessions only + } + template.elementDisposable.add( this.hoverService.setupDelayedHover(template.element, () => this.buildHoverContent(session.element), { groupId: 'agent.sessions' }) ); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 96798d993db..9794f9c58b1 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -397,11 +397,6 @@ configurationRegistry.registerConfiguration({ default: true, description: nls.localize('chat.viewSessions.enabled', "Show chat agent sessions when chat is empty or to the side when chat view is wide enough."), }, - [ChatConfiguration.ChatViewSessionsShowRecentOnly]: { - type: 'boolean', - default: false, - description: nls.localize('chat.viewSessions.showRecentOnly', "When enabled, only show recent sessions in the stacked sessions view. When disabled, show all sessions."), - }, [ChatConfiguration.ChatViewSessionsOrientation]: { type: 'string', enum: ['stacked', 'sideBySide'], diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 0e334221dc7..a9fe036efb9 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -68,6 +68,7 @@ interface IChatViewPaneState extends Partial { sessionId?: string; sessionsSidebarWidth?: number; + sessionsStackedHeight?: number; } type ChatViewPaneOpenedClassification = { @@ -130,13 +131,13 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { ) { this.viewState.sessionId = undefined; // clear persisted session on fresh start } - this.sessionsViewerLimited = this.configurationService.getValue(ChatConfiguration.ChatViewSessionsShowRecentOnly) ?? false; this.sessionsViewerVisible = false; // will be updated from layout code + this.sessionsViewerSidebarWidth = Math.max(ChatViewPane.SESSIONS_SIDEBAR_MIN_WIDTH, this.viewState.sessionsSidebarWidth ?? ChatViewPane.SESSIONS_SIDEBAR_DEFAULT_WIDTH); + this.sessionsViewerStackedHeight = this.viewState.sessionsStackedHeight ?? ChatViewPane.SESSIONS_STACKED_DEFAULT_HEIGHT; // Contextkeys this.chatViewLocationContext = ChatContextKeys.panelLocation.bindTo(contextKeyService); - this.sessionsViewerLimitedContext = ChatContextKeys.agentSessionsViewerLimited.bindTo(contextKeyService); this.sessionsViewerOrientationContext = ChatContextKeys.agentSessionsViewerOrientation.bindTo(contextKeyService); this.sessionsViewerPositionContext = ChatContextKeys.agentSessionsViewerPosition.bindTo(contextKeyService); this.sessionsViewerVisibilityContext = ChatContextKeys.agentSessionsViewerVisible.bindTo(contextKeyService); @@ -149,7 +150,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private updateContextKeys(): void { const { position, location } = this.getViewPositionAndLocation(); - this.sessionsViewerLimitedContext.set(this.sessionsViewerLimited); this.chatViewLocationContext.set(location ?? ViewContainerLocation.AuxiliaryBar); this.sessionsViewerOrientationContext.set(this.sessionsViewerOrientation); this.sessionsViewerPositionContext.set(position === Position.RIGHT ? AgentSessionsViewerPosition.Right : AgentSessionsViewerPosition.Left); @@ -221,22 +221,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { return e.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION); })(() => this.updateViewPaneClasses(true))); - // Sessions viewer limited setting changes - this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => { - return e.affectsConfiguration(ChatConfiguration.ChatViewSessionsShowRecentOnly); - })(() => { - const oldSessionsViewerLimited = this.sessionsViewerLimited; - if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.SideBySide) { - this.sessionsViewerLimited = false; // side by side always shows all - } else { - this.sessionsViewerLimited = this.configurationService.getValue(ChatConfiguration.ChatViewSessionsShowRecentOnly) ?? false; - } - - if (oldSessionsViewerLimited !== this.sessionsViewerLimited) { - this.notifySessionsControlLimitedChanged(true /* layout */, true /* update */); - } - })); - // Entitlement changes this._register(this.chatEntitlementService.onDidChangeSentiment(() => { this.updateViewPaneClasses(true); @@ -317,9 +301,10 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { //#region Sessions Control - private static readonly SESSIONS_LIMIT = 5; private static readonly SESSIONS_SIDEBAR_MIN_WIDTH = 200; private static readonly SESSIONS_SIDEBAR_DEFAULT_WIDTH = 300; + private static readonly SESSIONS_STACKED_MIN_HEIGHT = AgentSessionsListDelegate.ITEM_HEIGHT; + private static readonly SESSIONS_STACKED_DEFAULT_HEIGHT = 3 * AgentSessionsListDelegate.ITEM_HEIGHT; private static readonly CHAT_WIDGET_DEFAULT_WIDTH = 300; private static readonly SESSIONS_SIDEBAR_VIEW_MIN_WIDTH = this.CHAT_WIDGET_DEFAULT_WIDTH + this.SESSIONS_SIDEBAR_DEFAULT_WIDTH; @@ -330,15 +315,14 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private sessionsControlContainer: HTMLElement | undefined; private sessionsControl: AgentSessionsControl | undefined; private sessionsCount = 0; - private sessionsViewerLimited: boolean; private sessionsViewerVisible: boolean; private sessionsViewerOrientation = AgentSessionsViewerOrientation.Stacked; private sessionsViewerOrientationConfiguration: 'stacked' | 'sideBySide' = 'sideBySide'; private sessionsViewerOrientationContext: IContextKey; - private sessionsViewerLimitedContext: IContextKey; private sessionsViewerVisibilityContext: IContextKey; private sessionsViewerPositionContext: IContextKey; private sessionsViewerSidebarWidth: number; + private sessionsViewerStackedHeight: number; private sessionsViewerSash: Sash | undefined; private readonly sessionsViewerSashDisposables = this._register(new MutableDisposable()); @@ -349,7 +333,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { // Sessions Title const sessionsTitleContainer = this.sessionsTitleContainer = append(sessionsContainer, $('.agent-sessions-title-container')); const sessionsTitle = this.sessionsTitle = append(sessionsTitleContainer, $('span.agent-sessions-title')); - this.updateSessionsControlTitle(); + sessionsTitle.textContent = localize('sessions', "Sessions"); this._register(addDisposableListener(sessionsTitle, EventType.CLICK, () => { this.sessionsControl?.scrollToTop(); this.sessionsControl?.focus(); @@ -364,23 +348,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { // Sessions Filter const sessionsFilter = this._register(this.instantiationService.createInstance(AgentSessionsFilter, { filterMenuId: MenuId.AgentSessionsViewerFilterSubMenu, - limitResults: () => { - return that.sessionsViewerLimited ? ChatViewPane.SESSIONS_LIMIT : undefined; - }, - groupResults: () => { - return !that.sessionsViewerLimited; - }, - overrideExclude(session) { - if (that.sessionsViewerLimited) { - if (session.isArchived()) { - return true; // exclude archived sessions when limited - } - - return false; - } - - return undefined; // leave up to the filter settings - }, + groupResults() { return true; }, notifyResults(count: number) { that.notifySessionsControlCountChanged(count); }, @@ -414,23 +382,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { } return openEvent; }, - overrideCompare(sessionA: IAgentSession, sessionB: IAgentSession): number | undefined { - - // When limited where only few sessions show, sort unread sessions to the top - if (that.sessionsViewerLimited) { - const aIsUnread = !sessionA.isRead(); - const bIsUnread = !sessionB.isRead(); - - if (aIsUnread && !bIsUnread) { - return -1; // a (unread) comes before b (read) - } - if (!aIsUnread && bIsUnread) { - return 1; // a (read) comes after b (unread) - } - } - - return undefined; - } })); this._register(this.onDidChangeBodyVisibility(visible => sessionsControl.setVisible(visible))); @@ -477,20 +428,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { } } - private notifySessionsControlLimitedChanged(triggerLayout: boolean, triggerUpdate: boolean): Promise { - this.sessionsViewerLimitedContext.set(this.sessionsViewerLimited); - - this.updateSessionsControlTitle(); - - const updatePromise = triggerUpdate ? this.sessionsControl?.update() : undefined; - - if (triggerLayout) { - this.relayout(); - } - - return updatePromise ?? Promise.resolve(); - } - private notifySessionsControlCountChanged(newSessionsCount?: number): void { const countChanged = typeof newSessionsCount === 'number' && newSessionsCount !== this.sessionsCount; this.sessionsCount = newSessionsCount ?? this.sessionsCount; @@ -502,14 +439,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { } } - private updateSessionsControlTitle(): void { - if (!this.sessionsTitle) { - return; - } - - this.sessionsTitle.textContent = this.sessionsViewerLimited ? localize('recentSessions', "Recent Sessions") : localize('allSessions', "Sessions"); - } - private updateSessionsControlVisibility(): { changed: boolean; visible: boolean } { if (!this.sessionsContainer || !this.viewPaneContainer) { return { changed: false, visible: false }; @@ -523,10 +452,9 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { // Sessions control: stacked if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { newSessionsContainerVisible = - !!this.chatEntitlementService.sentiment.installed && // chat is installed (otherwise make room for terms and welcome) - (!this._widget || this._widget?.isEmpty()) && // chat widget empty - !this.welcomeController?.isShowingWelcome.get() && // welcome not showing - (this.sessionsCount > 0 || !this.sessionsViewerLimited); // has sessions or is showing all sessions + !!this.chatEntitlementService.sentiment.installed && // chat is installed (otherwise make room for terms and welcome) + (!this._widget || this._widget?.isEmpty()) && // chat widget empty + !this.welcomeController?.isShowingWelcome.get(); // welcome not showing } // Sessions control: sidebar @@ -920,42 +848,29 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this.sessionsViewerOrientationContext.set(AgentSessionsViewerOrientation.Stacked); } - // Update limited state based on orientation change + // Update based on orientation change if (oldSessionsViewerOrientation !== this.sessionsViewerOrientation) { - const oldSessionsViewerLimited = this.sessionsViewerLimited; - if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.SideBySide) { - this.sessionsViewerLimited = false; // side by side always shows all - } else { - this.sessionsViewerLimited = this.configurationService.getValue(ChatConfiguration.ChatViewSessionsShowRecentOnly) ?? false; - } - - let updatePromise: Promise; - if (oldSessionsViewerLimited !== this.sessionsViewerLimited) { - updatePromise = this.notifySessionsControlLimitedChanged(false /* already in layout */, true /* update */); - } else { - updatePromise = this.sessionsControl?.update(); // still need to update for section visibility - } // Switching to side-by-side, reveal the current session after elements have loaded if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.SideBySide) { - updatePromise.then(() => { - const sessionResource = this._widget?.viewModel?.sessionResource; - if (sessionResource) { - this.sessionsControl?.reveal(sessionResource); - } - }); + const sessionResource = this._widget?.viewModel?.sessionResource; + if (sessionResource) { + this.sessionsControl?.reveal(sessionResource); + } } } // Ensure visibility is in sync before we layout const { visible: sessionsContainerVisible } = this.updateSessionsControlVisibility(); - // Handle Sash (only visible in side-by-side) - if (!sessionsContainerVisible || this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { + // Handle Sash + if (!sessionsContainerVisible || oldSessionsViewerOrientation !== this.sessionsViewerOrientation /* re-create on orientation change */) { this.sessionsViewerSashDisposables.clear(); this.sessionsViewerSash = undefined; - } else if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.SideBySide) { - if (!this.sessionsViewerSashDisposables.value && this.viewPaneContainer) { + } + if (sessionsContainerVisible) { + const needsSashRecreation = !this.sessionsViewerSashDisposables.value || oldSessionsViewerOrientation !== this.sessionsViewerOrientation; + if (needsSashRecreation && this.viewPaneContainer) { this.createSessionsViewerSash(this.viewPaneContainer, height, width); } } @@ -984,21 +899,14 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { widthReduction = this.sessionsContainer.offsetWidth; } - // Show stacked (grows with the number of items displayed) + // Show stacked else { - let sessionsHeight: number; - if (this.sessionsViewerLimited) { - sessionsHeight = this.sessionsCount * AgentSessionsListDelegate.ITEM_HEIGHT; - } else { - sessionsHeight = availableSessionsHeight; - } - - const borderBottom = 1; - sessionsHeight = Math.min(availableSessionsHeight, sessionsHeight) - borderBottom; + const sessionsHeight = this.computeEffectiveStackedSessionsHeight(availableSessionsHeight) - 1 /* border bottom */; this.sessionsControlContainer.style.height = `${sessionsHeight}px`; this.sessionsControlContainer.style.width = ``; this.sessionsControl.layout(sessionsHeight, width); + this.sessionsViewerSash?.layout(); heightReduction = this.sessionsContainer.offsetHeight; widthReduction = 0; // stacked on top of the chat widget @@ -1017,6 +925,16 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { ); } + private computeEffectiveStackedSessionsHeight(availableHeight: number, sessionsViewerStackedHeight = this.sessionsViewerStackedHeight): number { + return Math.max( + ChatViewPane.SESSIONS_STACKED_MIN_HEIGHT, // never smaller than min height for stacked sessions + Math.min( + sessionsViewerStackedHeight, + availableHeight // never taller than available height + ) + ); + } + getLastDimensions(orientation: AgentSessionsViewerOrientation): { height: number; width: number } | undefined { return this.lastDimensionsPerOrientation.get(orientation); } @@ -1024,6 +942,14 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private createSessionsViewerSash(container: HTMLElement, height: number, width: number): void { const disposables = this.sessionsViewerSashDisposables.value = new DisposableStore(); + if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.SideBySide) { + this.createSideBySideSash(container, height, width, disposables); + } else { + this.createStackedSash(container, height, width, disposables); + } + } + + private createSideBySideSash(container: HTMLElement, height: number, width: number, disposables: DisposableStore): void { const sash = this.sessionsViewerSash = disposables.add(new Sash(container, { getVerticalSashLeft: () => { const sessionsViewerSidebarWidth = this.computeEffectiveSideBySideSessionsSidebarWidth(this.lastDimensions?.width ?? width); @@ -1063,6 +989,49 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { })); } + private createStackedSash(container: HTMLElement, height: number, width: number, disposables: DisposableStore): void { + const sash = this.sessionsViewerSash = disposables.add(new Sash(container, { + getHorizontalSashTop: () => { + if (!this.sessionsContainer || !this.sessionsTitleContainer) { + return 0; + } + + const titleHeight = this.sessionsTitleContainer.offsetHeight; + const availableHeight = (this.lastDimensions?.height ?? height) - titleHeight - Math.max(ChatViewPane.MIN_CHAT_WIDGET_HEIGHT, this._widget?.input?.height.get() ?? 0); + const sessionsHeight = this.computeEffectiveStackedSessionsHeight(availableHeight); + + return titleHeight + sessionsHeight; + } + }, { orientation: Orientation.HORIZONTAL })); + + let sashStartHeight: number | undefined; + disposables.add(sash.onDidStart(() => sashStartHeight = this.sessionsViewerStackedHeight)); + disposables.add(sash.onDidEnd(() => sashStartHeight = undefined)); + + disposables.add(sash.onDidChange(e => { + if (sashStartHeight === undefined || !this.lastDimensions || !this.sessionsTitleContainer) { + return; + } + + const titleHeight = this.sessionsTitleContainer.offsetHeight; + const delta = e.currentY - e.startY; + const newHeight = sashStartHeight + delta; + + const availableHeight = this.lastDimensions.height - titleHeight - Math.max(ChatViewPane.MIN_CHAT_WIDGET_HEIGHT, this._widget?.input?.height.get() ?? 0); + this.sessionsViewerStackedHeight = this.computeEffectiveStackedSessionsHeight(availableHeight, newHeight); + this.viewState.sessionsStackedHeight = this.sessionsViewerStackedHeight; + + this.layoutBody(this.lastDimensions.height, this.lastDimensions.width); + })); + + disposables.add(sash.onDidReset(() => { + this.sessionsViewerStackedHeight = ChatViewPane.SESSIONS_STACKED_DEFAULT_HEIGHT; + this.viewState.sessionsStackedHeight = this.sessionsViewerStackedHeight; + + this.relayout(); + })); + } + //#endregion override saveState(): void { diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index ed0de643267..e7e17675dff 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -96,7 +96,6 @@ export namespace ChatContextKeys { export const panelLocation = new RawContextKey('chatPanelLocation', undefined, { type: 'number', description: localize('chatPanelLocation', "The location of the chat panel.") }); export const agentSessionsViewerFocused = new RawContextKey('agentSessionsViewerFocused', true, { type: 'boolean', description: localize('agentSessionsViewerFocused', "If the agent sessions view in the chat view is focused.") }); - export const agentSessionsViewerLimited = new RawContextKey('agentSessionsViewerLimited', undefined, { type: 'boolean', description: localize('agentSessionsViewerLimited', "If the agent sessions view in the chat view is limited to show recent sessions only.") }); export const agentSessionsViewerOrientation = new RawContextKey('agentSessionsViewerOrientation', undefined, { type: 'number', description: localize('agentSessionsViewerOrientation', "Orientation of the agent sessions view in the chat view.") }); export const agentSessionsViewerPosition = new RawContextKey('agentSessionsViewerPosition', undefined, { type: 'number', description: localize('agentSessionsViewerPosition', "Position of the agent sessions view in the chat view.") }); export const agentSessionsViewerVisible = new RawContextKey('agentSessionsViewerVisible', undefined, { type: 'boolean', description: localize('agentSessionsViewerVisible', "Visibility of the agent sessions view in the chat view.") }); diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 93219c0793e..454da88af9c 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -31,7 +31,6 @@ export enum ChatConfiguration { TodosShowWidget = 'chat.tools.todos.showWidget', NotifyWindowOnResponseReceived = 'chat.notifyWindowOnResponseReceived', ChatViewSessionsEnabled = 'chat.viewSessions.enabled', - ChatViewSessionsShowRecentOnly = 'chat.viewSessions.showRecentOnly', ChatViewSessionsOrientation = 'chat.viewSessions.orientation', ChatViewTitleEnabled = 'chat.viewTitle.enabled', SubagentToolCustomAgents = 'chat.customAgentInSubagent.enabled', From 406f3f031d6b8783861194764a78423a4e97a0b4 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 21 Jan 2026 10:59:04 +0100 Subject: [PATCH 336/387] Workbench - extract floating menu into a widget (#289333) --- .../floatingMenu/browser/floatingMenu.ts | 84 ++++++++++++++----- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts index 1ae190279d2..1302d757b10 100644 --- a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts @@ -4,11 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { h } from '../../../../base/browser/dom.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { autorun, constObservable, derived, observableFromEvent } from '../../../../base/common/observable.js'; +import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { getActionBarActions, MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { autorun, constObservable, derived, IObservable, observableFromEvent } from '../../../../base/common/observable.js'; +import { URI } from '../../../../base/common/uri.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { IMenuService, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { ICodeEditor, OverlayWidgetPositionPreference } from '../../../browser/editorBrowser.js'; @@ -27,8 +29,50 @@ export class FloatingEditorToolbar extends Disposable implements IEditorContribu super(); const editorObs = this._register(observableCodeEditor(editor)); + const editorUriObs = derived(reader => editorObs.model.read(reader)?.uri); - const menu = this._register(menuService.createMenu(MenuId.EditorContent, editor.contextKeyService)); + // Widget + const widget = this._register(instantiationService.createInstance( + FloatingEditorToolbarWidget, + MenuId.EditorContent, + editor.contextKeyService, + editorUriObs)); + + // Render widget + this._register(autorun(reader => { + const hasActions = widget.hasActions.read(reader); + if (!hasActions) { + return; + } + + // Overlay widget + reader.store.add(editorObs.createOverlayWidget({ + allowEditorOverflow: false, + domNode: widget.element, + minContentWidthInPx: constObservable(0), + position: constObservable({ + preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER + }) + })); + })); + } +} + +export class FloatingEditorToolbarWidget extends Disposable { + readonly element: HTMLElement; + readonly hasActions: IObservable; + + constructor( + _menuId: MenuId, + _scopedContextKeyService: IContextKeyService, + _toolbarContext: IObservable, + @IInstantiationService instantiationService: IInstantiationService, + @IKeybindingService keybindingService: IKeybindingService, + @IMenuService menuService: IMenuService + ) { + super(); + + const menu = this._register(menuService.createMenu(_menuId, _scopedContextKeyService)); const menuGroupsObs = observableFromEvent(this, menu.onDidChange, () => menu.getActions()); const menuPrimaryActionIdObs = derived(reader => { @@ -38,20 +82,24 @@ export class FloatingEditorToolbar extends Disposable implements IEditorContribu return primary.length > 0 ? primary[0].id : undefined; }); + this.hasActions = derived(reader => menuGroupsObs.read(reader).length > 0); + + this.element = h('div.floating-menu-overlay-widget').root; + this._register(toDisposable(() => this.element.remove())); + + // Set height explicitly to ensure that the floating menu element + // is rendered in the lower right corner at the correct position. + this.element.style.height = '26px'; + this._register(autorun(reader => { + const hasActions = this.hasActions.read(reader); const menuPrimaryActionId = menuPrimaryActionIdObs.read(reader); - if (!menuPrimaryActionId) { + if (!hasActions || !menuPrimaryActionId) { return; } - const container = h('div.floating-menu-overlay-widget'); - - // Set height explicitly to ensure that the floating menu element - // is rendered in the lower right corner at the correct position. - container.root.style.height = '26px'; - // Toolbar - const toolbar = instantiationService.createInstance(MenuWorkbenchToolBar, container.root, MenuId.EditorContent, { + const toolbar = instantiationService.createInstance(MenuWorkbenchToolBar, this.element, MenuId.EditorContent, { actionViewItemProvider: (action, options) => { if (!(action instanceof MenuItemAction)) { return undefined; @@ -92,18 +140,8 @@ export class FloatingEditorToolbar extends Disposable implements IEditorContribu reader.store.add(toolbar); reader.store.add(autorun(reader => { - const model = editorObs.model.read(reader); - toolbar.context = model?.uri; - })); - - // Overlay widget - reader.store.add(editorObs.createOverlayWidget({ - allowEditorOverflow: false, - domNode: container.root, - minContentWidthInPx: constObservable(0), - position: constObservable({ - preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER - }) + const context = _toolbarContext.read(reader); + toolbar.context = context; })); })); } From 07796e20b387499d635514d8323ab9aceb2153fd Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 21 Jan 2026 11:19:26 +0100 Subject: [PATCH 337/387] feat(inlineChat): add keyboard navigation for action bar to focus input editor --- src/vs/base/browser/ui/actionbar/actionbar.ts | 6 ++++++ .../inlineChat/browser/inlineChatOverlayWidget.ts | 13 ++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index 50f60127c44..da20e27e040 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -473,6 +473,12 @@ export class ActionBar extends Disposable implements IActionRunner { return this.viewItems.length === 0; } + isFocused(index?: number): boolean { + return index === undefined + ? DOM.isAncestor(DOM.getActiveElement(), this.domNode) + : DOM.isAncestor(DOM.getActiveElement(), this.actionsList.children[index]); + } + focus(index?: number): void; focus(selectFirst?: boolean): void; focus(arg?: number | boolean): void { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 120a6d8a26a..61eaa9a3c6d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../base/browser/dom.js'; -import { IAction } from '../../../../base/common/actions.js'; +import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; +import { IAction, Separator } from '../../../../base/common/actions.js'; import { ActionBar, ActionsOrientation } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; @@ -145,6 +146,16 @@ export class InlineChatOverlayWidget extends Disposable implements IOverlayWidge preventLoopNavigation: true, })); + // Handle ArrowUp on first action bar item to focus input editor + this._store.add(dom.addDisposableListener(this._actionBar.domNode, 'keydown', e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.UpArrow) && this._actionBar.isFocused(this._actionBar.viewItems.findIndex(item => item.action.id !== Separator.ID))) { + event.preventDefault(); + event.stopPropagation(); + this._input.focus(); + } + }, true)); + // Track focus - hide when focus leaves const focusTracker = this._store.add(dom.trackFocus(this._domNode)); this._store.add(focusTracker.onDidBlur(() => this.hide())); From 4101fbafeb95b13d321767f7941499f7cdea8369 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 21 Jan 2026 11:29:38 +0100 Subject: [PATCH 338/387] fix(inlineChat): change hide method visibility to private --- .../contrib/inlineChat/browser/inlineChatOverlayWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 61eaa9a3c6d..acb3b19c8c3 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -213,7 +213,7 @@ export class InlineChatOverlayWidget extends Disposable implements IOverlayWidge /** * Hide the widget (removes from editor but does not dispose). */ - hide(): void { + private hide(): void { if (!this._isVisible) { return; } From 74c3dc70ea8f7f8da2a3f9905806d1644c8edca7 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 21 Jan 2026 11:34:34 +0100 Subject: [PATCH 339/387] refactor(inlineChat): rename hide method to _hide and update references --- .../inlineChat/browser/inlineChatOverlayWidget.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index acb3b19c8c3..28abc336994 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -124,7 +124,7 @@ export class InlineChatOverlayWidget extends Disposable implements IOverlayWidge if (!value) { e.preventDefault(); e.stopPropagation(); - this.hide(); + this._hide(); } } else if (e.keyCode === KeyCode.DownArrow) { // Focus first action bar item when at the end of the input @@ -158,11 +158,11 @@ export class InlineChatOverlayWidget extends Disposable implements IOverlayWidge // Track focus - hide when focus leaves const focusTracker = this._store.add(dom.trackFocus(this._domNode)); - this._store.add(focusTracker.onDidBlur(() => this.hide())); + this._store.add(focusTracker.onDidBlur(() => this._hide())); // Handle action bar cancel (Escape key) - this._store.add(this._actionBar.onDidCancel(() => this.hide())); - this._store.add(this._actionBar.onWillRun(() => this.hide())); + this._store.add(this._actionBar.onDidCancel(() => this._hide())); + this._store.add(this._actionBar.onWillRun(() => this._hide())); } /** @@ -213,7 +213,7 @@ export class InlineChatOverlayWidget extends Disposable implements IOverlayWidge /** * Hide the widget (removes from editor but does not dispose). */ - private hide(): void { + private _hide(): void { if (!this._isVisible) { return; } From a831cc9f3d97d525e4381517615f7f798bd6b381 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 21 Jan 2026 11:54:09 +0100 Subject: [PATCH 340/387] Fix #275134 (#289347) --- .../workbench/api/common/extHostLanguageModels.ts | 1 + .../browser/chatManagement/chatModelsWidget.ts | 14 +++++++------- .../browser/widget/input/modelPickerActionItem.ts | 2 +- .../contrib/chat/common/languageModels.ts | 1 + src/vscode-dts/vscode.proposed.chatProvider.d.ts | 6 ++++++ 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index b82b1bd992a..090ac1a2a95 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -216,6 +216,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { detail: m.detail, tooltip: m.tooltip, version: m.version, + multiplier: m.multiplier, maxInputTokens: m.maxInputTokens, maxOutputTokens: m.maxOutputTokens, auth, diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts index 23d1cd4a26c..7e711f72181 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts @@ -64,9 +64,9 @@ export function getModelHoverContent(model: ILanguageModel): MarkdownString { markdown.appendText(`\n`); } - if (model.metadata.detail) { + if (model.metadata.multiplier) { markdown.appendMarkdown(`${localize('models.cost', 'Multiplier')}: `); - markdown.appendMarkdown(model.metadata.detail); + markdown.appendMarkdown(model.metadata.multiplier); markdown.appendText(`\n`); } @@ -538,7 +538,7 @@ class MultiplierColumnRenderer extends ModelsTableColumnRenderer Date: Wed, 21 Jan 2026 11:55:19 +0100 Subject: [PATCH 341/387] refactor(inlineChat): rename InlineChatOverlayWidget to InlineChatInputOverlayWidget and update references --- .../browser/inlineChatAffordance.ts | 25 ++++++++----------- .../browser/inlineChatController.ts | 4 ++- .../browser/inlineChatOverlayWidget.ts | 23 ++++++++--------- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts index fba6cf4cf6f..b66467d4e34 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../base/common/lifecycle.js'; -import { autorun, debouncedObservable, derived, observableValue, runOnChange } from '../../../../base/common/observable.js'; +import { autorun, debouncedObservable, derived, observableValue, runOnChange, waitForState } from '../../../../base/common/observable.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -13,31 +13,25 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; import { InlineChatEditorAffordance } from './inlineChatEditorAffordance.js'; -import { InlineChatOverlayWidget } from './inlineChatOverlayWidget.js'; +import { InlineChatInputOverlayWidget } from './inlineChatOverlayWidget.js'; import { InlineChatGutterAffordance } from './inlineChatGutterAffordance.js'; import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; import { assertType } from '../../../../base/common/types.js'; -import { Event } from '../../../../base/common/event.js'; import { CursorChangeReason } from '../../../../editor/common/cursorEvents.js'; export class InlineChatAffordance extends Disposable { - private readonly _overlayWidget: InlineChatOverlayWidget; - private _menuData = observableValue<{ rect: DOMRect; above: boolean } | undefined>(this, undefined); - constructor( private readonly _editor: ICodeEditor, + private readonly _overlayWidget: InlineChatInputOverlayWidget, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @IChatEntitlementService chatEntiteldService: IChatEntitlementService, ) { super(); - // Create the overlay widget once, owned by this class - this._overlayWidget = this._store.add(this._instantiationService.createInstance(InlineChatOverlayWidget, this._editor)); - const affordance = observableConfigValue<'off' | 'gutter' | 'editor'>(InlineChatConfigKeys.Affordance, 'off', configurationService); const editorObs = observableCodeEditor(this._editor); @@ -124,10 +118,13 @@ export class InlineChatAffordance extends Disposable { this._overlayWidget.show(top, left, data.above); })); - this._store.add(this._overlayWidget.onDidHide(() => { - suppressGutter.set(true, undefined); - this._menuData.set(undefined, undefined); - this._editor.focus(); + this._store.add(autorun(r => { + const pos = this._overlayWidget.position.read(r); + if (pos === null) { + suppressGutter.set(true, undefined); + this._menuData.set(undefined, undefined); + this._editor.focus(); + } })); } @@ -147,6 +144,6 @@ export class InlineChatAffordance extends Disposable { above: direction === SelectionDirection.RTL }, undefined); - await Event.toPromise(this._overlayWidget.onDidHide); + await waitForState(this._overlayWidget.position, pos => pos === null); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 09df9d10aa2..136a5e70fca 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -52,6 +52,7 @@ import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommo import { INotebookService } from '../../notebook/common/notebookService.js'; import { CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; import { InlineChatAffordance } from './inlineChatAffordance.js'; +import { InlineChatInputOverlayWidget } from './inlineChatOverlayWidget.js'; import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js'; import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; @@ -143,7 +144,8 @@ export class InlineChatController implements IEditorContribution { const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this._configurationService); this._renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this._configurationService); - this._gutterIndicator = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor)); + const overlayWidget = this._store.add(this._instaService.createInstance(InlineChatInputOverlayWidget, this._editor)); + this._gutterIndicator = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor, overlayWidget)); this._zone = new Lazy(() => { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 28abc336994..32b9436c75b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -10,7 +10,7 @@ import { ActionBar, ActionsOrientation } from '../../../../base/browser/ui/actio import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; -import { Emitter } from '../../../../base/common/event.js'; +import { IObservable, observableValue } from '../../../../base/common/observable.js'; import { IActiveCodeEditor, ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js'; @@ -32,18 +32,17 @@ import { Position } from '../../../../editor/common/core/position.js'; /** * Overlay widget that displays a vertical action bar menu. */ -export class InlineChatOverlayWidget extends Disposable implements IOverlayWidget { +export class InlineChatInputOverlayWidget extends Disposable implements IOverlayWidget { private static _idPool = 0; - private readonly _id = `inline-chat-gutter-menu-${InlineChatOverlayWidget._idPool++}`; + private readonly _id = `inline-chat-gutter-menu-${InlineChatInputOverlayWidget._idPool++}`; private readonly _domNode: HTMLElement; private readonly _inputContainer: HTMLElement; private readonly _actionBar: ActionBar; private readonly _input: IActiveCodeEditor; - private _position: IOverlayWidgetPosition | null = null; - private readonly _onDidHide = this._store.add(new Emitter()); - readonly onDidHide = this._onDidHide.event; + private readonly _position = observableValue(this, null); + readonly position: IObservable = this._position; private _isVisible = false; private _inlineStartAction: IAction | undefined; @@ -182,10 +181,10 @@ export class InlineChatOverlayWidget extends Disposable implements IOverlayWidge this._refreshActions(); // Set initial position - this._position = { + this._position.set({ preference: { top, left }, stackOrdinal: 10000, - }; + }, undefined); // Add widget to editor if (!this._isVisible) { @@ -199,10 +198,10 @@ export class InlineChatOverlayWidget extends Disposable implements IOverlayWidge // If anchoring above, adjust position after render to account for widget height if (anchorAbove) { const widgetHeight = this._domNode.offsetHeight; - this._position = { + this._position.set({ preference: { top: top - widgetHeight, left }, stackOrdinal: 10000, - }; + }, undefined); this._editor.layoutOverlayWidget(this); } @@ -218,8 +217,8 @@ export class InlineChatOverlayWidget extends Disposable implements IOverlayWidge return; } this._isVisible = false; + this._position.set(null, undefined); this._editor.removeOverlayWidget(this); - this._onDidHide.fire(); } private _refreshActions(): void { @@ -263,7 +262,7 @@ export class InlineChatOverlayWidget extends Disposable implements IOverlayWidge } getPosition(): IOverlayWidgetPosition | null { - return this._position; + return this._position.get(); } override dispose(): void { From 452ea78218105d3176dfa3e146e66943a354f900 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 21 Jan 2026 11:55:29 +0100 Subject: [PATCH 342/387] Fixes worker error message (#289348) --- .../standalone/browser/services/standaloneWebWorkerService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/standalone/browser/services/standaloneWebWorkerService.ts b/src/vs/editor/standalone/browser/services/standaloneWebWorkerService.ts index b5a676fd870..1ff3fb838d9 100644 --- a/src/vs/editor/standalone/browser/services/standaloneWebWorkerService.ts +++ b/src/vs/editor/standalone/browser/services/standaloneWebWorkerService.ts @@ -23,8 +23,9 @@ export class StandaloneWebWorkerService extends WebWorkerService { } protected override _getWorkerLoadingFailedErrorMessage(descriptor: WebWorkerDescriptor): string | undefined { + const examplePath = '\'...?esm\''; // Broken up to avoid detection by bundler plugin return `Failed to load worker script for label: ${descriptor.label}. -Ensure your bundler properly bundles modules referenced by "new URL("...?esm", import.meta.url)".`; +Ensure your bundler properly bundles modules referenced by "new URL(${examplePath}, import.meta.url)".`; } override getWorkerUrl(descriptor: WebWorkerDescriptor): string { From 08412c1258b568a41f627f6462c88d79b245aa9c Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 21 Jan 2026 11:03:41 +0000 Subject: [PATCH 343/387] feat(codicons): add new codicons for screen cut and ask --- .../browser/ui/codicons/codicon/codicon.ttf | Bin 124072 -> 124884 bytes src/vs/base/common/codiconsLibrary.ts | 2 ++ 2 files changed, 2 insertions(+) diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index e7e46096e129d10563f6df06e20698d4a3411f3c..0c9d65f81c2f321cb91672b9002411e0ed2c1d5e 100644 GIT binary patch delta 9377 zcmY+~37k!J{|E5T=gw+|F~%6OjD2ilpN7UZ$y)Y=WDP_1og{Y}OOuc#M3M?2Nytu< zWLMH&k|gPzlRWaYs7L?L&$s{U|NNil^**0-?_Bqu^ZWho`TfrKw&8fp#~;KjtQYRP zV&*C!t|?GHVd%(5b2_(Ow-%^96=bp~su~vHS z_pxIp6q(rXKb;=~Pv`*3J-29osl34LH$%OmmfQ@LU2=)n7XCJ{x# zALbwq<`53$Fb?Mkj^b!e;6zU1WKQ9ue2mjMgSni^S$q;N@F~va)11foT*_r!&J|qA z)m+Qp}w#oc%skE^$z!hAf5 ziKvBiHpOOa!!`a}BeM*`EYI#-$g>=P@AwK|4zm|`b0EiI0{?@KY>wCAvpzd>JnFMQ zd!jj7pa(zZAH2?M{1-mw0xn`F%n20<6%8fuOMb<#d7j_stMUbkV+4ESU?>{1@E7A5 z$3p0je&~x*_zynAml%&cF2NDx;tm7||HQtij4G^)lDx_Ln99N|!s1M15=$_Jcd#tW zu_zPxDSqczY(y*y>Dv|0GECN&yFC8IM$AA3IE=HEujd*?Q{{*|3ak!Z-7Cha-d;l<->+^l!J^eIq(J>e61W}I7XRc z_=$3;(WMFAFoUzoM@*P4wKLr4$^~zPK_BHv6N*ueGJH`v+VC}{`#{mP4&GR!s~)^@ z2F;Y?jjn|7CKz24;Y~!?ZGfA4Fv;jb32(ANd*u|PizmFPMwd}|j~cj^n`U%rh4+|o zvyC_1V7>BjLzgoQ?+Ul4KL5=S`BZe(N!DXECWqv-fY8fm9DEq z*K~MK8n_n9H~dgJSHjwGpLU)$bZ0Qn(0#x&hVE<@8lF%tGCZSPY6F!*`U+jHVSVnVK{|oT|sK7*tTYt`W^F@YWg4G4Na~ie?*luNuue@HQCDK=3vi z%|-Cs35#YWcy6eO<|lZYjb5qp1&`8`Gjm5Z+Fs=@8y7qlr;taJL?a=16#ZjAl!CdyVEzc&?X3GblXQ z8=|=s-T{MbjBa13(pOYLmGdYh~d3!G$q43Y&1E;b6qK# zrr{kiny}#=HJZBN9XFc9;kjWTn$F>!Hk#Psska2 zYqS=C_mR=60N%$&>jQY77_AiGeQLC3fcKfv>H*&8M(YT8Ul^?@;C*SrT3o>U%4nGZ z@0`(s1K!t0OAmPGjTRyBzA;*kz`J0yFoE~2(GmsTMWe+EyzdOwDK8nVU*LW3`u}^K zy&IvIjTSZVelS|zz`J6!(1G`((UJ$AXSDc%=Nm19-~~ntB6vR;Esfw^HCiOW``KWh z`ro@|v~q&?FQYXSyz54*DR{pat*hYOFj`^3`?t|r3*N6rt1fuI8LhwI{cf}pgZGEg znhf4e32UR}8N5G@7HaVRGFr01`;XD$4c;xIWgNV}jTUs!$={}>ofaWRi#+%tqvam_ z2&07`{79oEAi{o>9*9{vcHmss7VOUj}Vpv^S(y*4YlwrE^4#V2Y z(uQ@EWz_$E8C|j)D9ai)RF*SrtV}h`QkFMtqI3r(yjxk(@E&C)!&b`5hOL$Ez=ds< z?!blN4%$gG?5M0}*hT5iNZ3tT!|(xRO~W2acc#LAN_VEhhn4P3g@cstOofA$bq$9o z>$(2dhC}tBzTq&XYYpLWrE3l02xW%hDCM1oqm`M46O@e%Cn{a*2`4G3ovcgmLyUs3*p?zy#wy_D+=!@IS!-f*Du zRinxR{0#uVQ1x5*Z8}Q#UDs#Yh7Z*VfoYFO`s6GV$6QfEI{7;Q)M({r~svg02Eh(xa!FLBMswlyCEg)eP zm*Be=5S5wWpED{r!FR18$Wyx35EY@|yVekuqu^gK$W?x8RHA}^(WqDj-+e0tKPfL6 z)vw^Y^}jYyDGUB(gDT1&jLKW^uNah6{%Ckp=^5Uq^bJ#$?gNE|mF@$DMU+Aa1HNzyO8-&6VN;gu4Dav0A-I?7mEUR=E7hyT&uZC{W{buL}-S0+~IQV}UxaZtS z6w^lKIrx7XY+Ukdr6%DrKtRJ}WyrXyd=Oz&!b1>gphFI#3>zsmv>X-q5X2aj`Vhn# z75xxs1UV}IA<)QjR0u?%5f^qF)dUf!5glk$1ZqS_bwUI>F9*GqI$wt`DHDyVhzN=q z{HaviI?Pt8101U319x(wY9j)*xr4466@ePvQ3(=(dfvearE39ESrUOd!$D(ZX~RCs zGKOuG?(BpulDuQq=Z3LZ^>4uAywT&vR2sJ4v2^}pZ?Wm}_qGlF&oTa@h$-C(`fu%!AwxX-9Cji7^3i5fviqhd9J zPDW*G1g>QSuKBwdm9i0ZH7aT&=w>iPdB5QrWp|^JH-ZNwtc|MQ2znUR!4dQ{a4&W* z6uL8UeJHAuBj{~ZFGui@QRN(g`&Ni*=m`26RnrmlGjM}PuD`V9cV1YMxqMn4B2m}sy@Imy6{iphq*D5n_x zY=B^@f$OM8jebf%;F?5m-;(pyOSX5fdfbE(m9AkVR7~lHk?2PU1i3~(Kj@bigHMzp z!@rlL)uthmPhR1Bx|AW|;vEyT}6v``fEpBAon)uT3FBeWJ zys1djA}=RQEjl={Y+~=kX^DRoD_g8-u|CD7Csj}Cm$V>hebSZU3B@ytFD!nj_?O9{ zw9o=w1;OzpON`a<3}sRZdi$lh!Hia<#?PKCPZreR}ov)eqL_R%2Ps zHZ{-I>Q`%RtqbW%=~?N$(#NN-Oy8Y;wRTROq&h9@jIVR3ZnwJI>V8?TUA?{aW9w(t z&#S+?{<#Jf8uVx|x51ePHySR>h|CCg$=H!`{mxc*&bqT8GdnXk^Gu_xMk^bgZJgM6 z`dtmOkkvG6b=HX{^_om>a;Rxa)AP+*HJje-O7rf`3tE(Fu{b*+drtPXyI0+Pv*qxX zAK#OG&(3?Uw`$XBbgM0`zHHsD^^Dd#T6=AJx4F=^dE30UJKLqTk8VG{{rP))+?ROY zrVbrC?CubRJC5$Svr}59ZJj%GKHsHFmy2CHbv@CoWw$lmZrwlN{#)J0ci;9vw+GJj z=-y**&q_Tz^(^T5SFd5ct~{9a;Dz3az5Ddu{ZPU~c@JIe)4b2vKIi&Y>ASe^rG7{H z*Xuv5|Kk2v2J{$^H{klf%z@Je?teJ_;mw1R2aO)|+@OL%HwJebJa6!)Lt=-7GlvWx zvU14boP?Z~Im2^yjf-z;s^cnNqn1f?3jV(L2$JkBd8jPDU?%eoN<2#Jc8-HX%=?P;eT$cXma<-ho&^1GJMLhsj*Y9Jlf;YkEf+gn-hMl=3`r@SD2nZegETlyu;%s zW~9x?opC0d9PSrh6F#3?IJaBwrYF*$=>5ckC(h)}&AUD`d*-28<7bCvubh2qPTZW9 zPvXgTPp-|cp5HHja{hw+D^LACckbN%Pq%ye*u19mHqTF+KWqMl1x**^FSz(j&cZqi zw=62XXx5@5i<1}6TYP0nt0m!8&qhAGaB1Aq{H4LNtIPW=KlWVub2%$gS4>~AV`cKn z?3G7W^?3m=U_ zi~CCLo4P-F|MdMU_wU?)cK@{lNe4zBIB~G)!GeP~-`-phDp>r^^h2qKmc5(v?y19F z4sUrc>Ah+1ZF}$cBe_QoAB{e``e<;h-?3%KvyZPi(d)#vlXXsRIF)fK>fq_Jr*pP< zs{KoBu}EEFz|xw3Vs-hy8{L=Bsn5GgRg0-yDXj(5GMckwr35CIXH27#DGf_3KUtsY zCEJ#77!eg4N=&TWv`T75YLUo<+Oefm%P)^;!1{L-D;5)zS*}gf%EePNn%ognw`gQh zmT%ZBBXwqW=C15a{!uYp`1XgUYpPCFm7$(~W9gwgijgV;Jfj~C-*mSxjWScpGqYiX zl;r42X&H^G#n_IXDcPu6T4sq#(a9;z_1_FMw4IE!Y7J7#F*#MgdDqg@&6tr{@%A&( zI+HqV9&*oAYp!3gi!xl7_NPi|?%9S_|KB6MQF3%GR*Kf!>9gG{N;YitUoT11AJZ~2 zo3L7{Uh%&V8vXZ!ha+Mly2ivLrlgcli7itmR*{kz6O&pqg0b=O;i82Z|42-He9`#$ z(~;3Jg(5;x5fSkTQA~}DN{Ek$hzdm%iiy^n#4w_K{EEov=-8;JNS2O_ii(Ynj=Z6a zEEF5BXHyajeO@$0&&J0Va?i%i(u*VOy4|`D+}_@Pd`kbp#h3_p6y@ToRt?9Ma|aet zGbmwruP8hLk#3>F;ijSKxX#9{NqiYYEG;!Cd^ZX*}e*ib4ORfL_ delta 8691 zcmXBZ37k#k9|rK}xyCm3A^R4x@7c2N#w29RmMmio27|FL*G|YbX#NsnEZIYL(lmCV zk|arz>YS4#NhCuB|2fm zfPN4Dp0^*UP#%aVJFMUM#0-@`wne;w)op49jGIB~@297zrGF8KsJ zT@olZd+E{AIif)QtPCHVkb7xRgHd~n^A?kbXDujQ_72bRV}zc}N`A^(%4o?a-xf>z z?-vnq6dC?|R#_PdWK2Uu#FFHM$U>Z&JSVcr(CYfJ1-sFZ1F->rvKpfK36`Ny2sna`oXk#)W?TM<2RMy8{Dvp+E-&#A$x}ScGrY)i z{0Kkr171k3o~=N*7)qcdo<$PIU@XRAA|{~>oFaxtN7YpzP7GW`#U@4NZ94oLAZz2V&@D|?2 z8l+-V7+bLuyRZj)u@CQKKMvp^4r>OF;{-lL8a_fgPT?HR;{ra$r}zw?Yren5_xK4v z;~sv+Z+M8`@d*FmDgGsNsv%}zW|m-SmSH(oWF=N+71m%)*5wPV&!%k77HrAZ48O>B z?7)}Vg{UVCCR4bY>zT@p+{|s{-z~Y7*koCow=Bo`5JEX9j@am4DV-OCSnx-LOV9Xc6hAL z_8f`o?89zogvRK~Px%M$^B#Y}7kqoDC6m*^8~%m{T#B&vGtzVm1E9@Ih|EH~1PiLm5ILyu_tkgrWG3wOI>4vOAu^ zUA%+#h-Wdvk>~BF9xC--@11vj7z%USMho%E6!joTJu+KKqvl- z7-ZnMP|i>;T;V^u31!;LTp@;$UAGJrayJhB7t+1V>uV69>}Oa?+23%sa)8lo241Ye z4dp~A) z+bO(BMmJV?lMOa1rx?1gP1RD?1E_7t3mdvW=7v?+Ryp11t_*L6(ft|TOrtwByjcc! zl(P-LQO+^CkHed5;6`hn;U)J+=NleUE-<8Wq2VdzV#CAAC5C5|OARk7-Dn8UDU*%v zDDajW{GeQ6_#s=>_qb&~J z9;3|;o?E)2Z4cf)qYV(=`$k(Ky!}R-BD@1e+oM*(LA?;|lD zMEfYbqeeR_ykkatE4<@IyDU66(4zeo-U*|f7v6_Pdoa8-C#)Z8t0!*8L>n`_bfYaA z-btg)8lGDbqHP=AX`>As-dUrq9Nq<^O HM%z0)w^BqKJ-kZ>)0E-MdJygY@IEoB z1K@pXR1v`Y%&0bicg3hmfcLpky#Vj3QRM*d3!@qW-Zi6Y0^XNKbp^cZCae+z-d9G& z2D}?aWe2>kjS3KWH;qaWc;6TmCGc(;l_&7NH7Zo#-8L#&;C*LQyuiC-;1=}vZvMa5 z*}K(x*Qm6C_k&T91JB(7MCA^=pNtA0c%D%S1kX1rhTsK8Wf8oejS3`q_l!ydt_95!TZCg3WN99s2+p&r%`1F z?}>!=dhkO=MIZbOM&%#; z2%|y}VLzi@h-yOknT)DK_?eCBMEF^ZDn|HOjcP~u*^H`4_}Pu>N%%PobdY{dqvL{~ z%eecjpZh;y&>dwSLwA%>hVB6J8kSJzGc2uq#;}YszhOCL0mF*Qf`*lpg$yez3maBZ z7Sa6sMRYT+p)6`xQ(4TguClnHZc4s85Mg~~NyDbfXAPSxOBuFMx`P$ARJwx|wpKo8 zR7>kgS;KZpcNW49%JPOUD=QdwQM$7ec2&Bw6TYf+XD95ZbY~~*udHe~Kv~T&R$1N6 zzaAK;7j6WEgOqLrgoBmO8xB#{GE7j`HXNp`V>n9bMoc(bS83&Segl3BqtX?=n+9=jVEtB( z{t>>eCvHxJ^ObE37bv3*7b@EtE?34Fu28xu6s}aZGkjC|l3|Lny`ejZj)v=%VK=wJ zRAnc_jmpl3o0V?Ph1-;F&V@Ua-3)gtUom`7+1+rDvWMY5WlzKRmAy=uNA#q(;Zdbq zNWx>vK8DAYuNl1+gWuP3%Bru zpDW`HuPO%{-cq_nD7>v4YIsNK7Nqd5(k)2g56a<&KPg8TddfsY-z}n%#&tY=w}OQ~ zE8PkfK2RnZK2(k|{9QTL@R4$y;h##knB~9geuAD0pD5kJ7XGbt3p=a_KGl=SCX`9( zR=I>SE2kR0h1CxmSEBjTOej)0-Gp*0XP8hPrMnV@?q)N~(EaXg!}9L>pQ9I|_r&n$ z8ofJ)KhNlWGW^$#-Z8_UZ=lQ0Utshu8va59cOq{XR|orxjNW6zUu^Vl8~&32Xoa`~ zSZeeZ9R4z+H{RQ9g%5{eBGI+cN$h#?lNreuCv{GA?j1WcVi`R zKeWfNk8-b37X!Y#V~F}0@ZUG;Y`}LXA?T_+VEC!>py40NLx%U2haD|l-qVvKMr{%J zM-9GE9y4?+=eXe#XZM4%<=xSBE05_RB4 zqKx4|<#R^8Py}U-x}pfm8TCgIlsCAktYAWJN-G+5PZ5MG=|S+4va;b)Wfi05DuSv; zZB_);4BU`bH>|C!Vbp|0P}4xyQ{dKxs4L4BXox!12s#+Jspx3by+-h|Q6C#YCxcjJXQSRWf-Xi~ZUkK=tOqr`5p*+Z zeIsz+6u4h>^Com>)x)SUj-aPeiyT2Oqh>jR-bQV61g{!3&=K@8IHYtZB-DBe2kuOT z6O?W~MV)p8ZmtAVlwj=`Ur*_b@&mCFs_FlBpR+#jx@Zk9A)%L0D{p*p9vsHGWv7? z!59O#X2!btAFHGLRXNV+!vh544ct7skr92MfMBB0M+*oh8GSB^V6vh6H{6IxD5r9& z(MJ&Yl?GRoAw&L?98;lM_+Z47|0iWWm-$JS#H^87$7g*Q zSw3=owu;%-WUrY$Jx9fy$Qk5H&2>9>DEF+~skzg0-^mk^XLp{vQTd~)MkPkAj5?aP zVBVO#{qxSxyE|Xgd^?}%_Do9tQ2y%qQ}W*|5M5wi!N`IW3tlLcROn3MoQ201UKcJ> ztH`pVbBaDHw!GN2;!(wy6n|Kvb%`4#=RMoMRK8NTO7APv>A9TGT`fDjT;Xzk%B7W$ zC||XF>+;tt6t8fxV$X_qD^;!(UujjPv`V)sJ*ix(a?{FVE8nZKs>-=4PpZaLyU6J@R<~5$o^?~|Mbta|!n_yK>zA$Hss6$GPaDKESko}B;e|#KjS?H(XdK;mU6aC1 zRyNJqbbPZi&1N)v(7bo^Gc6jonAqZCi$^W%wCvyVa;r?O8nznM>UQh+)~DJ;wrSjE zXY``zCvD@}-i%3kvF3|c+BI#L)b3IErN%GqY+t|ql@4(ou5}#J@#@Q+UOv~Ue5W~` ze(XG_^OG+1yIk+uz3Yu`&AV;wcJ-C$S7yC(zk6c$v>x$2QhMC!*|g`%UXi_q^g7k+ zaqrmP+g~mGYC@m(eGc}y|61YK5?{O7H?r@h?aYB(q@zIFVX!4n5x9#Uq=oFQpL zBZuBd$e++LVf(Q1!#WPzH0iLXzaA2q+_{L2eEE=XTkYvIl}vcD1k zM*5=Mi-s(^w|M`OI!o3s&Azn#($&lIElXT>A-PO)c;WJb%a^Uly&`GF?Un6U?pYbU znVJ$xsh_ec<<$S$t-AfzoYk3D-+jB?+h^8PUo&w{`r3%Kacdu~OIY{to#XF3SYLKS z&J7(m%-C=yHA`y$)bx!xHzsVnwkc`Tsm(Pvr)>#s8MS5KyUpJ{u{C<@rfr$Fb>DV- zd+Y66w%^|ovt!lH>O1%CDiz)}e%FcJ1$VD{FXwyf_f*_hao?K#G5dG!|MWoP11k^Q zJh=Se zSoFgnty9|7k7|9Cl8*Fx=~GW;IvI0v;i)pGMxDBIy6EXsXUd+rdA8o!edns4JAA(7 z`Ss@+>>XR>w=6Sz)IHFnE+0R8pldH?8NsLnea^8=#DO^%nUMz;Ut+(g1Al$XFEi?n GgZ~4vmGo-> diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index 29037850602..90cd1bab10f 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -650,4 +650,6 @@ export const codiconsLibrary = { removeSmall: register('remove-small', 0xec7c), worktreeSmall: register('worktree-small', 0xec7d), worktree: register('worktree', 0xec7e), + screenCut: register('screen-cut', 0xec7f), + ask: register('ask', 0xec80), } as const; From 70dd1421d47b60101c583d217529c2bbc8fc96b8 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 21 Jan 2026 11:27:23 +0000 Subject: [PATCH 344/387] Enhance sticky widget styles for dark theme in the editor --- extensions/theme-2026/themes/styles.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 91f1e05826f..dd3b7d9c8f9 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -200,7 +200,8 @@ .monaco-workbench .monaco-editor .minimap-shadow-visible { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10); } /* Sticky Scroll */ -.monaco-workbench .monaco-editor .sticky-widget { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10) !important; border-bottom: none !important; background: rgba(252, 252, 253, 0.65) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; border-radius: 0 0 8px 8px !important; } +.monaco-workbench .monaco-editor .sticky-widget { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10) !important; border-bottom: none !important; background: rgba(252, 252, 253, 0.65) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; } +.monaco-workbench.vs-dark .monaco-editor .sticky-widget { border-bottom: none !important; background: rgba(0, 0, 0, 0.3) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; } .monaco-workbench .monaco-editor .sticky-widget *, .monaco-workbench .monaco-editor .sticky-widget > *, .monaco-workbench .monaco-editor .sticky-widget .sticky-widget-line-numbers, From 40d9a48e6df09ebb4514c0c962867738e474d698 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 21 Jan 2026 11:31:38 +0000 Subject: [PATCH 345/387] Refactor quick input widget styles for improved dark theme support --- extensions/theme-2026/themes/styles.css | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index dd3b7d9c8f9..680293515b5 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -70,7 +70,6 @@ .monaco-workbench .quick-input-widget .monaco-list, .monaco-workbench .quick-input-widget .monaco-list-row { border-color: transparent !important; outline: none !important; } .monaco-workbench .quick-input-widget .monaco-inputbox { box-shadow: none !important; background: transparent !important; } -.monaco-workbench .quick-input-widget .quick-input-filter .monaco-inputbox { background: rgba(249, 250, 251, 0.4) !important; border-radius: 6px; } .monaco-workbench.vs-dark .quick-input-widget .quick-input-filter .monaco-inputbox, .monaco-workbench .quick-input-widget .monaco-list.list_id_6:not(.drop-target):not(.dragging) .monaco-list-row:hover:not(.selected):not(.focused) { background-color: color-mix(in srgb, var(--vscode-list-hoverBackground) 40%, transparent); } @@ -214,11 +213,8 @@ .monaco-workbench .monaco-editor .sticky-widget-focus-preview, .monaco-workbench .monaco-editor .sticky-scroll-focus-line, .monaco-workbench .monaco-editor .focused .sticky-widget, -.monaco-workbench .monaco-editor:has(.sticky-widget:focus-within) .sticky-widget { background: rgba(252, 252, 253, 0.75) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; box-shadow: 0 0 8px rgba(0, 0, 0, 0.12) !important; border-radius: 0 0 8px 8px !important; } -.monaco-workbench.vs-dark .monaco-editor .sticky-widget-focus-preview, -.monaco-workbench.vs-dark .monaco-editor .sticky-scroll-focus-line, -.monaco-workbench.vs-dark .monaco-editor .focused .sticky-widget, -.monaco-workbench.vs-dark .monaco-editor:has(.sticky-widget:focus-within) .sticky-widget, +.monaco-workbench .monaco-editor:has(.sticky-widget:focus-within) .sticky-widget { background: rgba(252, 252, 253, 0.75) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; box-shadow: 0 0 8px rgba(0, 0, 0, 0.12) !important; } +.monaco-workbench.vs-dark .monaco-editor:has(.sticky-widget:focus-within) .sticky-widget { background: rgba(0, 0, 0, 0.4) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; box-shadow: 0 0 8px rgba(0, 0, 0, 0.12) !important; } /* Notebook */ .monaco-workbench .notebookOverlay .monaco-list .cell-focus-indicator { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); border-radius: 6px; } From 0826995426227a6f55410c87a8f9500c776afa43 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 21 Jan 2026 11:58:41 +0000 Subject: [PATCH 346/387] Remove background color from quick input widget for improved transparency --- extensions/theme-2026/themes/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 680293515b5..ecbb3f20c0a 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -52,7 +52,7 @@ .monaco-workbench .part.statusbar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); z-index: 55; position: relative; } /* Quick Input (Command Palette) */ -.monaco-workbench .quick-input-widget { box-shadow: 0 0 20px rgba(0, 0, 0, 0.15) !important; border-radius: 12px; overflow: hidden; background: rgba(252, 252, 253, 0.5) !important; backdrop-filter: blur(20px) saturate(100%); -webkit-backdrop-filter: blur(40px) saturate(180%); } +.monaco-workbench .quick-input-widget { box-shadow: 0 0 20px rgba(0, 0, 0, 0.15) !important; border-radius: 12px; overflow: hidden; backdrop-filter: blur(20px) saturate(100%); -webkit-backdrop-filter: blur(40px) saturate(180%); } .monaco-workbench.vs-dark .quick-input-widget, .monaco-workbench.hc-black .quick-input-widget { background: rgba(10, 10, 11, 0.5) !important; backdrop-filter: blur(40px) saturate(180%); -webkit-backdrop-filter: blur(40px) saturate(180%); box-shadow: 0 0 20px rgba(0, 0, 0, 0.15) !important; } .monaco-workbench .quick-input-widget .monaco-list-rows { background-color: transparent !important; } From 04617215cf03c92e4695c21673244c32374e54b4 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:06:55 +0100 Subject: [PATCH 347/387] Agent sessions - polish toolbar (#289350) --- .../chat/browser/agentSessions/media/agentsessionsviewer.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css index 846c2ff8fd9..38aa85e3d78 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css @@ -47,10 +47,11 @@ .monaco-list-row .agent-session-title-toolbar { /* for the absolute positioning of the toolbar below */ position: relative; + height: 16px; .monaco-toolbar { /* this is required because the overal height (including the padding needed for hover feedback) would push down the title otherwise */ - position: absolute; + position: relative; right: 0; top: 0; display: none; @@ -59,7 +60,6 @@ .monaco-list-row:hover .agent-session-title-toolbar, .monaco-list-row.focused .agent-session-title-toolbar { - width: 44px; .monaco-toolbar { display: block; From f227bb971bd56392e90b9f96f246fba36f6f6de5 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 21 Jan 2026 12:21:15 +0000 Subject: [PATCH 348/387] Update 2026 Dark theme colors and styles for improved consistency and readability --- extensions/theme-2026/themes/2026-dark.json | 210 ++++++++++---------- extensions/theme-2026/themes/styles.css | 10 +- 2 files changed, 112 insertions(+), 108 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 4cfdf873343..565a6f81968 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -8,217 +8,217 @@ "errorForeground": "#f48771", "descriptionForeground": "#888888", "icon.foreground": "#888888", - "focusBorder": "#498FADB3", - "textBlockQuote.background": "#232627", + "focusBorder": "#49A4CCB3", + "textBlockQuote.background": "#242526", "textBlockQuote.border": "#2A2B2CFF", - "textCodeBlock.background": "#232627", - "textLink.foreground": "#589BB8", - "textLink.activeForeground": "#61A0BC", + "textCodeBlock.background": "#242526", + "textLink.foreground": "#60AFD2", + "textLink.activeForeground": "#6CB5D5", "textPreformat.foreground": "#888888", "textSeparator.foreground": "#2a2a2aFF", - "button.background": "#498FAE", + "button.background": "#49A4CC", "button.foreground": "#FFFFFF", - "button.hoverBackground": "#4D94B4", + "button.hoverBackground": "#54A9CF", "button.border": "#2A2B2CFF", - "button.secondaryBackground": "#232627", + "button.secondaryBackground": "#242526", "button.secondaryForeground": "#bfbfbf", - "button.secondaryHoverBackground": "#303234", - "checkbox.background": "#232627", + "button.secondaryHoverBackground": "#313233", + "checkbox.background": "#242526", "checkbox.border": "#2A2B2CFF", "checkbox.foreground": "#bfbfbf", - "dropdown.background": "#191B1D", + "dropdown.background": "#191A1B", "dropdown.border": "#333536", "dropdown.foreground": "#bfbfbf", - "dropdown.listBackground": "#1F2223", - "input.background": "#191B1D", + "dropdown.listBackground": "#202122", + "input.background": "#191A1B", "input.border": "#333536FF", "input.foreground": "#bfbfbf", "input.placeholderForeground": "#777777", - "inputOption.activeBackground": "#498FAE33", + "inputOption.activeBackground": "#49A4CC33", "inputOption.activeForeground": "#bfbfbf", "inputOption.activeBorder": "#2A2B2CFF", - "inputValidation.errorBackground": "#191B1D", + "inputValidation.errorBackground": "#191A1B", "inputValidation.errorBorder": "#2A2B2CFF", "inputValidation.errorForeground": "#bfbfbf", - "inputValidation.infoBackground": "#191B1D", + "inputValidation.infoBackground": "#191A1B", "inputValidation.infoBorder": "#2A2B2CFF", "inputValidation.infoForeground": "#bfbfbf", - "inputValidation.warningBackground": "#191B1D", + "inputValidation.warningBackground": "#191A1B", "inputValidation.warningBorder": "#2A2B2CFF", "inputValidation.warningForeground": "#bfbfbf", "scrollbar.shadow": "#191B1D4D", - "scrollbarSlider.background": "#81848533", - "scrollbarSlider.hoverBackground": "#81848566", - "scrollbarSlider.activeBackground": "#81848599", - "badge.background": "#498FAE", + "scrollbarSlider.background": "#83848533", + "scrollbarSlider.hoverBackground": "#83848566", + "scrollbarSlider.activeBackground": "#83848599", + "badge.background": "#49A4CC", "badge.foreground": "#FFFFFF", - "progressBar.background": "#858889", - "list.activeSelectionBackground": "#498FAE26", + "progressBar.background": "#878889", + "list.activeSelectionBackground": "#49A4CC26", "list.activeSelectionForeground": "#bfbfbf", - "list.inactiveSelectionBackground": "#232627", + "list.inactiveSelectionBackground": "#242526", "list.inactiveSelectionForeground": "#bfbfbf", - "list.hoverBackground": "#252829", + "list.hoverBackground": "#262728", "list.hoverForeground": "#bfbfbf", - "list.dropBackground": "#498FAE1A", - "list.focusBackground": "#498FAE26", + "list.dropBackground": "#49A4CC1A", + "list.focusBackground": "#49A4CC26", "list.focusForeground": "#bfbfbf", - "list.focusOutline": "#498FADB3", + "list.focusOutline": "#49A4CCB3", "list.highlightForeground": "#bfbfbf", "list.invalidItemForeground": "#444444", "list.errorForeground": "#f48771", "list.warningForeground": "#e5ba7d", - "activityBar.background": "#191B1D", + "activityBar.background": "#191A1B", "activityBar.foreground": "#bfbfbf", "activityBar.inactiveForeground": "#888888", "activityBar.border": "#2A2B2CFF", "activityBar.activeBorder": "#2A2B2CFF", - "activityBar.activeFocusBorder": "#498FADB3", - "activityBarBadge.background": "#498FAE", + "activityBar.activeFocusBorder": "#49A4CCB3", + "activityBarBadge.background": "#49A4CC", "activityBarBadge.foreground": "#FFFFFF", - "sideBar.background": "#191B1D", + "sideBar.background": "#191A1B", "sideBar.foreground": "#bfbfbf", "sideBar.border": "#2A2B2CFF", "sideBarTitle.foreground": "#bfbfbf", - "sideBarSectionHeader.background": "#191B1D", + "sideBarSectionHeader.background": "#191A1B", "sideBarSectionHeader.foreground": "#bfbfbf", "sideBarSectionHeader.border": "#2A2B2CFF", - "titleBar.activeBackground": "#191B1D", + "titleBar.activeBackground": "#191A1B", "titleBar.activeForeground": "#bfbfbf", - "titleBar.inactiveBackground": "#191B1D", + "titleBar.inactiveBackground": "#191A1B", "titleBar.inactiveForeground": "#888888", "titleBar.border": "#2A2B2CFF", - "menubar.selectionBackground": "#232627", + "menubar.selectionBackground": "#242526", "menubar.selectionForeground": "#bfbfbf", - "menu.background": "#1F2223", + "menu.background": "#202122", "menu.foreground": "#bfbfbf", - "menu.selectionBackground": "#498FAE26", + "menu.selectionBackground": "#49A4CC26", "menu.selectionForeground": "#bfbfbf", - "menu.separatorBackground": "#818485", + "menu.separatorBackground": "#838485", "menu.border": "#2A2B2CFF", "commandCenter.foreground": "#bfbfbf", "commandCenter.activeForeground": "#bfbfbf", - "commandCenter.background": "#191B1D", - "commandCenter.activeBackground": "#252829", + "commandCenter.background": "#191A1B", + "commandCenter.activeBackground": "#262728", "commandCenter.border": "#333536", - "editor.background": "#121416", + "editor.background": "#121314", "editor.foreground": "#BBBEBF", "editorLineNumber.foreground": "#858889", "editorLineNumber.activeForeground": "#BBBEBF", "editorCursor.foreground": "#BBBEBF", - "editor.selectionBackground": "#498FAE33", - "editor.inactiveSelectionBackground": "#498FAE80", - "editor.selectionHighlightBackground": "#498FAE1A", - "editor.wordHighlightBackground": "#498FAE33", - "editor.wordHighlightStrongBackground": "#498FAE33", - "editor.findMatchBackground": "#498FAE4D", - "editor.findMatchHighlightBackground": "#498FAE26", - "editor.findRangeHighlightBackground": "#232627", - "editor.hoverHighlightBackground": "#232627", - "editor.lineHighlightBackground": "#232627", - "editor.rangeHighlightBackground": "#232627", - "editorLink.activeForeground": "#4a8fad", + "editor.selectionBackground": "#49A4CC33", + "editor.inactiveSelectionBackground": "#49A4CC80", + "editor.selectionHighlightBackground": "#49A4CC1A", + "editor.wordHighlightBackground": "#49A4CC33", + "editor.wordHighlightStrongBackground": "#49A4CC33", + "editor.findMatchBackground": "#49A4CC4D", + "editor.findMatchHighlightBackground": "#49A4CC26", + "editor.findRangeHighlightBackground": "#242526", + "editor.hoverHighlightBackground": "#242526", + "editor.lineHighlightBackground": "#242526", + "editor.rangeHighlightBackground": "#242526", + "editorLink.activeForeground": "#4aa4cc", "editorWhitespace.foreground": "#8888884D", - "editorIndentGuide.background": "#8184854D", - "editorIndentGuide.activeBackground": "#818485", + "editorIndentGuide.background": "#8384854D", + "editorIndentGuide.activeBackground": "#838485", "editorRuler.foreground": "#848484", "editorCodeLens.foreground": "#888888", - "editorBracketMatch.background": "#498FAE55", + "editorBracketMatch.background": "#49A4CC55", "editorBracketMatch.border": "#2A2B2CFF", - "editorWidget.background": "#1F2223", + "editorWidget.background": "#202122", "editorWidget.border": "#2A2B2CFF", "editorWidget.foreground": "#bfbfbf", - "editorSuggestWidget.background": "#1F2223", + "editorSuggestWidget.background": "#202122", "editorSuggestWidget.border": "#2A2B2CFF", "editorSuggestWidget.foreground": "#bfbfbf", "editorSuggestWidget.highlightForeground": "#bfbfbf", - "editorSuggestWidget.selectedBackground": "#498FAE26", - "editorHoverWidget.background": "#1F2223", + "editorSuggestWidget.selectedBackground": "#49A4CC26", + "editorHoverWidget.background": "#202122", "editorHoverWidget.border": "#2A2B2CFF", "peekView.border": "#2A2B2CFF", - "peekViewEditor.background": "#191B1D", - "peekViewEditor.matchHighlightBackground": "#498FAE33", - "peekViewResult.background": "#232627", + "peekViewEditor.background": "#191A1B", + "peekViewEditor.matchHighlightBackground": "#49A4CC33", + "peekViewResult.background": "#242526", "peekViewResult.fileForeground": "#bfbfbf", "peekViewResult.lineForeground": "#888888", - "peekViewResult.matchHighlightBackground": "#498FAE33", - "peekViewResult.selectionBackground": "#498FAE26", + "peekViewResult.matchHighlightBackground": "#49A4CC33", + "peekViewResult.selectionBackground": "#49A4CC26", "peekViewResult.selectionForeground": "#bfbfbf", - "peekViewTitle.background": "#232627", + "peekViewTitle.background": "#242526", "peekViewTitleDescription.foreground": "#888888", "peekViewTitleLabel.foreground": "#bfbfbf", - "editorGutter.background": "#121416", - "editorGutter.addedBackground": "#71C792", - "editorGutter.deletedBackground": "#EF8773", - "diffEditor.insertedTextBackground": "#71C79233", - "diffEditor.removedTextBackground": "#EF877333", + "editorGutter.background": "#121314", + "editorGutter.addedBackground": "#72C892", + "editorGutter.deletedBackground": "#F28772", + "diffEditor.insertedTextBackground": "#72C89233", + "diffEditor.removedTextBackground": "#F2877233", "editorOverviewRuler.border": "#2A2B2CFF", - "editorOverviewRuler.findMatchForeground": "#4a8fad99", + "editorOverviewRuler.findMatchForeground": "#4aa4cc99", "editorOverviewRuler.modifiedForeground": "#6ab890", "editorOverviewRuler.addedForeground": "#73c991", "editorOverviewRuler.deletedForeground": "#f48771", "editorOverviewRuler.errorForeground": "#f48771", "editorOverviewRuler.warningForeground": "#e5ba7d", - "panel.background": "#191B1D", + "panel.background": "#191A1B", "panel.border": "#2A2B2CFF", - "panelTitle.activeBorder": "#498FAD", + "panelTitle.activeBorder": "#49A4CC", "panelTitle.activeForeground": "#bfbfbf", "panelTitle.inactiveForeground": "#888888", - "statusBar.background": "#191B1D", + "statusBar.background": "#191A1B", "statusBar.foreground": "#bfbfbf", "statusBar.border": "#2A2B2CFF", - "statusBar.focusBorder": "#498FADB3", - "statusBar.debuggingBackground": "#498FAE", + "statusBar.focusBorder": "#49A4CCB3", + "statusBar.debuggingBackground": "#49A4CC", "statusBar.debuggingForeground": "#FFFFFF", - "statusBar.noFolderBackground": "#191B1D", + "statusBar.noFolderBackground": "#191A1B", "statusBar.noFolderForeground": "#bfbfbf", - "statusBarItem.activeBackground": "#4A4D4F", - "statusBarItem.hoverBackground": "#252829", - "statusBarItem.focusBorder": "#498FADB3", - "statusBarItem.prominentBackground": "#498FAE", + "statusBarItem.activeBackground": "#4B4C4D", + "statusBarItem.hoverBackground": "#262728", + "statusBarItem.focusBorder": "#49A4CCB3", + "statusBarItem.prominentBackground": "#49A4CC", "statusBarItem.prominentForeground": "#FFFFFF", - "statusBarItem.prominentHoverBackground": "#498FAE", - "tab.activeBackground": "#121416", + "statusBarItem.prominentHoverBackground": "#49A4CC", + "tab.activeBackground": "#121314", "tab.activeForeground": "#bfbfbf", - "tab.inactiveBackground": "#191B1D", + "tab.inactiveBackground": "#191A1B", "tab.inactiveForeground": "#888888", "tab.border": "#2A2B2CFF", "tab.lastPinnedBorder": "#2A2B2CFF", "tab.activeBorder": "#121314", - "tab.hoverBackground": "#252829", + "tab.hoverBackground": "#262728", "tab.hoverForeground": "#bfbfbf", - "tab.unfocusedActiveBackground": "#121416", + "tab.unfocusedActiveBackground": "#121314", "tab.unfocusedActiveForeground": "#888888", - "tab.unfocusedInactiveBackground": "#191B1D", + "tab.unfocusedInactiveBackground": "#191A1B", "tab.unfocusedInactiveForeground": "#444444", - "editorGroupHeader.tabsBackground": "#191B1D", + "editorGroupHeader.tabsBackground": "#191A1B", "editorGroupHeader.tabsBorder": "#2A2B2CFF", "breadcrumb.foreground": "#888888", - "breadcrumb.background": "#121416", + "breadcrumb.background": "#121314", "breadcrumb.focusForeground": "#bfbfbf", "breadcrumb.activeSelectionForeground": "#bfbfbf", - "breadcrumbPicker.background": "#1F2223", + "breadcrumbPicker.background": "#202122", "notificationCenter.border": "#2A2B2CFF", "notificationCenterHeader.foreground": "#bfbfbf", - "notificationCenterHeader.background": "#232627", + "notificationCenterHeader.background": "#242526", "notificationToast.border": "#2A2B2CFF", "notifications.foreground": "#bfbfbf", - "notifications.background": "#1F2223", + "notifications.background": "#202122", "notifications.border": "#2A2B2CFF", - "notificationLink.foreground": "#4a8fad", - "extensionButton.prominentBackground": "#498FAE", + "notificationLink.foreground": "#4aa4cc", + "extensionButton.prominentBackground": "#49A4CC", "extensionButton.prominentForeground": "#FFFFFF", - "extensionButton.prominentHoverBackground": "#4D94B4", + "extensionButton.prominentHoverBackground": "#54A9CF", "pickerGroup.border": "#2A2B2CFF", "pickerGroup.foreground": "#bfbfbf", - "quickInput.background": "#1F2223", + "quickInput.background": "#202122", "quickInput.foreground": "#bfbfbf", - "quickInputList.focusBackground": "#498FAE26", + "quickInputList.focusBackground": "#49A4CC26", "quickInputList.focusForeground": "#bfbfbf", "quickInputList.focusIconForeground": "#bfbfbf", - "quickInputList.hoverBackground": "#505354", - "terminal.selectionBackground": "#498FAE33", + "quickInputList.hoverBackground": "#515253", + "terminal.selectionBackground": "#49A4CC33", "terminalCursor.foreground": "#bfbfbf", - "terminalCursor.background": "#191B1D", + "terminalCursor.background": "#191A1B", "gitDecoration.addedResourceForeground": "#73c991", "gitDecoration.modifiedResourceForeground": "#e5ba7d", "gitDecoration.deletedResourceForeground": "#f48771", @@ -227,10 +227,10 @@ "gitDecoration.conflictingResourceForeground": "#f48771", "gitDecoration.stageModifiedResourceForeground": "#e5ba7d", "gitDecoration.stageDeletedResourceForeground": "#f48771", - "quickInputTitle.background": "#1F2223", + "quickInputTitle.background": "#202122", "quickInput.border": "#333536", - "chat.requestBubbleBackground": "#498FAE26", - "chat.requestBubbleHoverBackground": "#498FAE46" + "chat.requestBubbleBackground": "#49A4CC26", + "chat.requestBubbleHoverBackground": "#49A4CC46" }, "tokenColors": [ { diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index ecbb3f20c0a..93562d0ed73 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -24,8 +24,8 @@ .monaco-workbench .monaco-sash.vertical { z-index: 45; } .monaco-workbench .monaco-sash.horizontal { z-index: 45; } -.monaco-workbench .activitybar.left.bordered::before, -.monaco-workbench .activitybar.right.bordered::before { +.monaco-workbench.vs .activitybar.left.bordered::before, +.monaco-workbench.vs .activitybar.right.bordered::before { border: none; } @@ -98,8 +98,12 @@ .monaco-workbench .notifications-center { backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); +} +.monaco-workbench.vs .notifications-center { background-color: rgba(255, 255, 255, 0.6) !important; - +} +.monaco-workbench.vs-dark .notifications-center { + background-color: rgba(10, 10, 11, 0.6) !important; } .monaco-workbench .notifications-list-container, .monaco-workbench > .notifications-center > .notifications-center-header, From 94d39180f4a84d4fd0d307b4537fa2bbadb90f8c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 12:49:25 +0000 Subject: [PATCH 349/387] Archive session on /clear instead of backgrounding (#289317) * Initial plan * Make /clear command archive current session before creating new one Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> * polish * . --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> Co-authored-by: Benjamin Pasero --- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 9794f9c58b1..87ad9d92a2c 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -86,6 +86,7 @@ import { registerChatElicitationActions } from './actions/chatElicitationActions import { registerChatToolActions } from './actions/chatToolActions.js'; import { ChatTransferContribution } from './actions/chatTransfer.js'; import './agentSessions/agentSessions.contribution.js'; +import { IAgentSessionsService } from './agentSessions/agentSessionsService.js'; import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService, IQuickChatService } from './chat.js'; import { ChatAccessibilityService } from './accessibility/chatAccessibilityService.js'; import './attachments/chatAttachmentModel.js'; @@ -1143,15 +1144,17 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { @IChatAgentService chatAgentService: IChatAgentService, @IChatWidgetService chatWidgetService: IChatWidgetService, @IInstantiationService instantiationService: IInstantiationService, + @IAgentSessionsService agentSessionsService: IAgentSessionsService, ) { super(); this._store.add(slashCommandService.registerSlashCommand({ command: 'clear', - detail: nls.localize('clear', "Start a new chat"), + detail: nls.localize('clear', "Start a new chat and archive the current one"), sortText: 'z2_clear', executeImmediately: true, locations: [ChatAgentLocation.Chat] - }, async () => { + }, async (_prompt, _progress, _history, _location, sessionResource) => { + agentSessionsService.getSession(sessionResource)?.setArchived(true); commandService.executeCommand(ACTION_ID_NEW_CHAT); })); this._store.add(slashCommandService.registerSlashCommand({ From 6e16763b87bfadaf71eb556ba3e1b7cdd1ba3106 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 12:49:39 +0000 Subject: [PATCH 350/387] Add experimental tri-state chat toggle for command center (#289336) * Initial plan * Add experimental tri-state chat toggle from command center Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> * Refactor: Extract tri-state condition to variable for readability Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> * . * . --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> Co-authored-by: Benjamin Pasero --- .../contrib/chat/browser/actions/chatActions.ts | 11 ++++++++++- .../contrib/chat/browser/chat.contribution.ts | 6 ++++++ src/vs/workbench/contrib/chat/common/constants.ts | 1 + 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 8b6a6f5f946..316ce529b61 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -463,11 +463,20 @@ export function registerChatActions() { const viewsService = accessor.get(IViewsService); const viewDescriptorService = accessor.get(IViewDescriptorService); const widgetService = accessor.get(IChatWidgetService); + const configurationService = accessor.get(IConfigurationService); const chatLocation = viewDescriptorService.getViewLocationById(ChatViewId); if (viewsService.isViewVisible(ChatViewId)) { - this.updatePartVisibility(layoutService, chatLocation, false); + if ( + chatLocation === ViewContainerLocation.AuxiliaryBar && + configurationService.getValue(ChatConfiguration.CommandCenterTriStateToggle) && + !layoutService.isAuxiliaryBarMaximized() + ) { + layoutService.setAuxiliaryBarMaximized(true); + } else { + this.updatePartVisibility(layoutService, chatLocation, false); + } } else { this.updatePartVisibility(layoutService, chatLocation, true); (await widgetService.revealWidget())?.focusInput(); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 87ad9d92a2c..89f0d678288 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -194,6 +194,12 @@ configurationRegistry.registerConfiguration({ markdownDescription: nls.localize('chat.commandCenter.enabled', "Controls whether the command center shows a menu for actions to control chat (requires {0}).", '`#window.commandCenter#`'), default: true }, + [ChatConfiguration.CommandCenterTriStateToggle]: { // TODO@bpasero settle this + type: 'boolean', + markdownDescription: nls.localize('chat.commandCenter.triStateToggle', "When enabled, clicking the chat icon in the command center cycles through: show chat, maximize chat, hide chat. This requires chat to be contained in the secondary sidebar."), + default: product.quality !== 'stable', + tags: ['experimental'] + }, [ChatConfiguration.AgentStatusEnabled]: { type: 'boolean', markdownDescription: nls.localize('chat.agentsControl.enabled', "Controls whether the Agent Status indicator is shown in the title bar command center. Enabling this setting will automatically enable {0}.", '`#window.commandCenter#`'), diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 454da88af9c..1fe1601342e 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -37,6 +37,7 @@ export enum ChatConfiguration { ShowCodeBlockProgressAnimation = 'chat.agent.codeBlockProgress', RestoreLastPanelSession = 'chat.restoreLastPanelSession', ExitAfterDelegation = 'chat.exitAfterDelegation', + CommandCenterTriStateToggle = 'chat.commandCenter.triStateToggle', } /** From 4b251f19ca3040b228e4c73b614952ffbf6cc00e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 21 Jan 2026 13:59:17 +0100 Subject: [PATCH 351/387] agent sessions - fix use of time label (#289371) --- .../agentSessions/agentSessionsViewer.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index c911f796803..a1217866701 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -298,11 +298,11 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre session.element.timing.inProgressTime && session.element.timing.finishedOrFailedTime > session.element.timing.inProgressTime ) { - const duration = this.toDuration(session.element.timing.inProgressTime, session.element.timing.finishedOrFailedTime, false); + const duration = this.toDuration(session.element.timing.inProgressTime, session.element.timing.finishedOrFailedTime, false, true); template.description.textContent = session.element.status === AgentSessionStatus.Failed ? - localize('chat.session.status.failedAfter', "Failed after {0}.", duration ?? '1s') : - localize('chat.session.status.completedAfter', "Completed in {0}.", duration ?? '1s'); + localize('chat.session.status.failedAfter', "Failed after {0}.", duration) : + localize('chat.session.status.completedAfter', "Completed in {0}.", duration); } else { template.description.textContent = session.element.status === AgentSessionStatus.Failed ? localize('chat.session.status.failed', "Failed") : @@ -311,13 +311,9 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre } } - private toDuration(startTime: number, endTime: number, useFullTimeWords: boolean): string | undefined { - const elapsed = Math.round((endTime - startTime) / 1000) * 1000; - if (elapsed < 1000) { - return undefined; - } - - if (elapsed < 30000) { + private toDuration(startTime: number, endTime: number, useFullTimeWords: boolean, disallowNow: boolean): string { + const elapsed = Math.max(Math.round((endTime - startTime) / 1000) * 1000, 1000 /* clamp to 1s */); + if (!disallowNow && elapsed < 30000) { return localize('secondsDuration', "now"); } @@ -329,7 +325,7 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre const getTimeLabel = (session: IAgentSession) => { let timeLabel: string | undefined; if (session.status === AgentSessionStatus.InProgress && session.timing.inProgressTime) { - timeLabel = this.toDuration(session.timing.inProgressTime, Date.now(), false); + timeLabel = this.toDuration(session.timing.inProgressTime, Date.now(), false, false); } if (!timeLabel) { From a126becb4a76877678bdced1278d31d7ba5e3fd6 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:54:57 +0100 Subject: [PATCH 352/387] Multi-diff - cleanup editor content toolbar (#289375) * Workbench - polish multi-diff editor floating menu * More cleanup * More cleanup --- .../browser/widget/multiDiffEditor/style.css | 43 +-------- .../floatingMenu/browser/floatingMenu.css | 3 +- .../floatingMenu/browser/floatingMenu.ts | 5 +- .../browser/multiDiffEditor.ts | 87 +++++++------------ 4 files changed, 39 insertions(+), 99 deletions(-) diff --git a/src/vs/editor/browser/widget/multiDiffEditor/style.css b/src/vs/editor/browser/widget/multiDiffEditor/style.css index edc93b85ce9..57e2ab568cd 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/style.css +++ b/src/vs/editor/browser/widget/multiDiffEditor/style.css @@ -36,52 +36,13 @@ > .multi-diff-root-floating-menu { position: absolute; - right: 32px; - bottom: 32px; top: auto; + right: 28px; + bottom: 24px; left: auto; - height: auto; width: auto; - padding: 4px 6px; - color: var(--vscode-button-foreground); - background-color: var(--vscode-button-background); - border-radius: 4px; - border: 1px solid var(--vscode-contrastBorder); - display: flex; - align-items: center; - z-index: 10; - box-shadow: 0 3px 12px var(--vscode-widget-shadow); - overflow: hidden; } - .multi-diff-root-floating-menu .action-item > .action-label { - padding: 7px 8px; - font-size: 15px; - border-radius: 2px; - } - - .multi-diff-root-floating-menu .action-item > .action-label.codicon { - color: var(--vscode-button-foreground); - } - - .multi-diff-root-floating-menu .action-item > .action-label.codicon:not(.separator) { - padding-top: 6px; - padding-bottom: 6px; - } - - .multi-diff-root-floating-menu .action-item:first-child > .action-label { - padding-left: 7px; - } - - .multi-diff-root-floating-menu .action-item:last-child > .action-label { - padding-right: 7px; - } - - .multi-diff-root-floating-menu .action-item .action-label.separator { - background-color: var(--vscode-button-separator); - } - - .active { --vscode-multiDiffEditor-border: var(--vscode-focusBorder); } diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css index 5221366ed32..422e073e5e7 100644 --- a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css @@ -40,7 +40,8 @@ justify-content: center; } - .action-item.primary > .action-label { + .action-item.primary > .action-label, + .action-item.primary > .action-label.action-label.codicon:not(.separator) { background-color: var(--vscode-button-background); color: var(--vscode-button-foreground); } diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts index 1302d757b10..1a530186e66 100644 --- a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts @@ -94,12 +94,13 @@ export class FloatingEditorToolbarWidget extends Disposable { this._register(autorun(reader => { const hasActions = this.hasActions.read(reader); const menuPrimaryActionId = menuPrimaryActionIdObs.read(reader); - if (!hasActions || !menuPrimaryActionId) { + + if (!hasActions) { return; } // Toolbar - const toolbar = instantiationService.createInstance(MenuWorkbenchToolBar, this.element, MenuId.EditorContent, { + const toolbar = instantiationService.createInstance(MenuWorkbenchToolBar, this.element, _menuId, { actionViewItemProvider: (action, options) => { if (!(action instanceof MenuItemAction)) { return undefined; diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts index ce7befb765f..5747d3131b0 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts @@ -5,12 +5,11 @@ import * as DOM from '../../../../base/browser/dom.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { MultiDiffEditorWidget } from '../../../../editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.js'; import { IResourceLabel, IWorkbenchUIElementFactory } from '../../../../editor/browser/widget/multiDiffEditor/workbenchUIElementFactory.js'; import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js'; -import { FloatingClickMenu } from '../../../../platform/actions/browser/floatingMenu.js'; -import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; +import { MenuId } from '../../../../platform/actions/common/actions.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { InstantiationService } from '../../../../platform/instantiation/common/instantiationService.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -33,14 +32,14 @@ import { IDiffEditor } from '../../../../editor/common/editorCommon.js'; import { Range } from '../../../../editor/common/core/range.js'; import { MultiDiffEditorItem } from './multiDiffSourceResolverService.js'; import { IEditorProgressService } from '../../../../platform/progress/common/progress.js'; -import { ResourceContextKey } from '../../../common/contextkeys.js'; +import { autorun, derived, observableValue } from '../../../../base/common/observable.js'; +import { FloatingEditorToolbarWidget } from '../../../../editor/contrib/floatingMenu/browser/floatingMenu.js'; export class MultiDiffEditor extends AbstractEditorWithViewState { static readonly ID = 'multiDiffEditor'; private _multiDiffEditorWidget: MultiDiffEditorWidget | undefined = undefined; private _viewModel: MultiDiffEditorViewModel | undefined; - private _sessionResourceContextKey: ResourceContextKey | undefined; private _contentOverlay: MultiDiffEditorContentMenuOverlay | undefined; public get viewModel(): MultiDiffEditorViewModel | undefined { @@ -56,8 +55,7 @@ export class MultiDiffEditor extends AbstractEditorWithViewState { await super.setInput(input, options, context, token); this._viewModel = await input.getViewModel(); - this._sessionResourceContextKey?.set(input.resource); this._contentOverlay?.updateResource(input.resource); this._multiDiffEditorWidget!.setViewModel(this._viewModel); @@ -127,7 +119,6 @@ export class MultiDiffEditor extends AbstractEditorWithViewState { await super.clearInput(); - this._sessionResourceContextKey?.set(null); this._contentOverlay?.updateResource(undefined); this._multiDiffEditorWidget!.setViewModel(undefined); } @@ -187,64 +178,50 @@ export class MultiDiffEditor extends AbstractEditorWithViewState()); - private readonly resourceContextKey: ResourceContextKey; - private currentResource: URI | undefined; - private readonly rebuild: () => void; + private readonly resourceObs = observableValue(this, undefined); constructor( root: HTMLElement, - resourceContextKey: ResourceContextKey, contextKeyService: IContextKeyService, - menuService: IMenuService, - instantiationService: IInstantiationService, + instantiationService: IInstantiationService ) { super(); - this.resourceContextKey = resourceContextKey; - const menu = this._register(menuService.createMenu(MenuId.MultiDiffEditorContent, contextKeyService)); + // Widget + const widget = instantiationService.createInstance( + FloatingEditorToolbarWidget, + MenuId.MultiDiffEditorContent, + contextKeyService, + this.resourceObs); + widget.element.classList.add('multi-diff-root-floating-menu'); + this._register(widget); - this.rebuild = () => { - this.overlayStore.clear(); + // Derived to show/hide + const showToolbarObs = derived(reader => { + const resource = this.resourceObs.read(reader); + const hasActions = widget.hasActions.read(reader); - const hasActions = menu.getActions().length > 0; - if (!hasActions) { + return resource !== undefined && hasActions; + }); + + this._register(autorun(reader => { + const showToolbar = showToolbarObs.read(reader); + if (!showToolbar) { return; } - const container = DOM.h('div.floating-menu-overlay-widget.multi-diff-root-floating-menu'); - root.appendChild(container.root); - const floatingMenu = instantiationService.createInstance(FloatingClickMenu, { - container: container.root, - menuId: MenuId.MultiDiffEditorContent, - getActionArg: () => this.currentResource, - }); - - const store = new DisposableStore(); - store.add(floatingMenu); - store.add(toDisposable(() => container.root.remove())); - this.overlayStore.value = store; - }; - - this.rebuild(); - this._register(menu.onDidChange(() => { - this.overlayStore.clear(); - this.rebuild(); + root.appendChild(widget.element); + reader.store.add(toDisposable(() => { + widget.element.remove(); + })); })); - - this._register(resourceContextKey); } public updateResource(resource: URI | undefined): void { - this.currentResource = resource; - // Update context key and rebuild so menu arg matches - this.resourceContextKey.set(resource ?? null); - this.overlayStore.clear(); - this.rebuild(); + this.resourceObs.set(resource, undefined); } } - class WorkbenchUIElementFactory implements IWorkbenchUIElementFactory { constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, From e66c02674a6bf1ded57ba03e44aaa99f2712cd13 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 21 Jan 2026 14:58:46 +0100 Subject: [PATCH 353/387] fix #287511 (#289380) * fix #287511 * fix feedback --- .../chatManagement/chatModelsViewModel.ts | 73 +++++++++--- .../chatManagement/chatModelsWidget.ts | 109 +++++++++++++++--- .../contrib/chat/common/languageModels.ts | 33 +++--- .../chatModelsViewModel.test.ts | 10 +- .../chat/test/common/languageModels.ts | 4 +- 5 files changed, 171 insertions(+), 58 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsViewModel.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsViewModel.ts index 614e2b2f281..4b25cf272dd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsViewModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsViewModel.ts @@ -6,7 +6,7 @@ import { distinct } from '../../../../../base/common/arrays.js'; import { IMatch, IFilter, or, matchesCamelCase, matchesWords, matchesBaseContiguousSubString } from '../../../../../base/common/filters.js'; import { Emitter } from '../../../../../base/common/event.js'; -import { ILanguageModelsService, IUserFriendlyLanguageModel, ILanguageModelChatMetadataAndIdentifier } from '../../../chat/common/languageModels.js'; +import { ILanguageModelsService, ILanguageModelProviderDescriptor, ILanguageModelChatMetadataAndIdentifier } from '../../../chat/common/languageModels.js'; import { localize } from '../../../../../nls.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ILanguageModelsProviderGroup } from '../../common/languageModelsConfiguration.js'; @@ -39,12 +39,13 @@ export const SEARCH_SUGGESTIONS = { }; export interface ILanguageModelProvider { - vendor: IUserFriendlyLanguageModel; + vendor: ILanguageModelProviderDescriptor; group: ILanguageModelsProviderGroup; } export interface ILanguageModel extends ILanguageModelChatMetadataAndIdentifier { provider: ILanguageModelProvider; + visible: boolean; } export interface ILanguageModelEntry { @@ -258,7 +259,7 @@ export class ChatModelsViewModel extends Disposable { for (const modelEntry of modelEntries) { if (visible !== undefined) { - if ((modelEntry.metadata.isUserSelectable ?? false) !== visible) { + if (modelEntry.visible !== visible) { continue; } } @@ -361,7 +362,7 @@ export class ChatModelsViewModel extends Disposable { if (this.groupBy === ChatModelGroup.Visibility) { const visible = [], hidden = []; for (const model of languageModels) { - if (model.metadata.isUserSelectable) { + if (model.visible) { visible.push(model); } else { hidden.push(model); @@ -418,18 +419,18 @@ export class ChatModelsViewModel extends Disposable { }; } result.sort((a, b) => { - if (a.models[0]?.provider.vendor.vendor === 'copilot') { return -1; } - if (b.models[0]?.provider.vendor.vendor === 'copilot') { return 1; } + if (a.models[0]?.provider.vendor.isDefault) { return -1; } + if (b.models[0]?.provider.vendor.isDefault) { return 1; } return a.group.label.localeCompare(b.group.label); }); } for (const group of result) { group.models.sort((a, b) => { - if (a.provider.vendor.vendor === 'copilot' && b.provider.vendor.vendor === 'copilot') { + if (a.provider.vendor.isDefault && b.provider.vendor.isDefault) { return a.metadata.name.localeCompare(b.metadata.name); } - if (a.provider.vendor.vendor === 'copilot') { return -1; } - if (b.provider.vendor.vendor === 'copilot') { return 1; } + if (a.provider.vendor.isDefault) { return -1; } + if (b.provider.vendor.isDefault) { return 1; } if (a.provider.group.name === b.provider.group.name) { return a.metadata.name.localeCompare(b.metadata.name); } @@ -455,10 +456,10 @@ export class ChatModelsViewModel extends Disposable { }; } - getVendors(): IUserFriendlyLanguageModel[] { + getVendors(): ILanguageModelProviderDescriptor[] { return [...this.languageModelsService.getVendors()].sort((a, b) => { - if (a.vendor === 'copilot') { return -1; } - if (b.vendor === 'copilot') { return 1; } + if (a.isDefault) { return -1; } + if (b.isDefault) { return 1; } return a.displayName.localeCompare(b.displayName); }); } @@ -494,7 +495,7 @@ export class ChatModelsViewModel extends Disposable { this.doFilter(); } - private addVendorModels(vendor: IUserFriendlyLanguageModel): void { + private addVendorModels(vendor: ILanguageModelProviderDescriptor): void { const models: ILanguageModel[] = []; const languageModelsGroups = this.languageModelsService.getLanguageModelGroups(vendor.vendor); for (const group of languageModelsGroups) { @@ -519,13 +520,14 @@ export class ChatModelsViewModel extends Disposable { if (!metadata) { continue; } - if (vendor.vendor === 'copilot' && metadata.id === 'auto') { + if (vendor.isDefault && metadata.id === 'auto') { continue; } models.push({ identifier, metadata, provider, + visible: metadata.isUserSelectable ?? false, }); } } @@ -533,14 +535,14 @@ export class ChatModelsViewModel extends Disposable { } toggleVisibility(model: ILanguageModelEntry): void { - const isVisible = model.model.metadata.isUserSelectable ?? false; - const newVisibility = !isVisible; + const newVisibility = !model.model.visible; this.languageModelsService.updateModelPickerPreference(model.model.identifier, newVisibility); const metadata = this.languageModelsService.lookupLanguageModel(model.model.identifier); const index = this.viewModelEntries.indexOf(model); if (metadata && index !== -1) { - model.id = this.getModelId(model.model); + model.model.visible = newVisibility; model.model.metadata = metadata; + model.id = this.getModelId(model.model); if (this.groupBy === ChatModelGroup.Visibility) { this.modelsSorted = false; } @@ -548,8 +550,43 @@ export class ChatModelsViewModel extends Disposable { } } + setModelsVisibility(models: ILanguageModelEntry[], visible: boolean): void { + for (const model of models) { + this.languageModelsService.updateModelPickerPreference(model.model.identifier, visible); + model.model.visible = visible; + } + // Refresh to update the UI + this.languageModelGroups = this.groupModels(this.languageModels); + this.doFilter(); + } + + setGroupVisibility(group: ILanguageModelProviderEntry | ILanguageModelGroupEntry, visible: boolean): void { + const models = this.getModelsForGroup(group); + for (const model of models) { + this.languageModelsService.updateModelPickerPreference(model.identifier, visible); + model.visible = visible; + } + // Refresh to update the UI + this.languageModelGroups = this.groupModels(this.languageModels); + this.doFilter(); + } + + getModelsForGroup(group: ILanguageModelProviderEntry | ILanguageModelGroupEntry): ILanguageModel[] { + if (isLanguageModelProviderEntry(group)) { + return this.languageModels.filter(m => + this.getProviderGroupId(m.provider.group) === group.id + ); + } else { + // Group by visibility + return this.languageModels.filter(m => + (group.id === 'visible' && m.visible) || + (group.id === 'hidden' && !m.visible) + ); + } + } + private getModelId(modelEntry: ILanguageModel): string { - return `${modelEntry.provider.group.name}.${modelEntry.identifier}.${modelEntry.metadata.version}-visible:${modelEntry.metadata.isUserSelectable}`; + return `${modelEntry.provider.group.name}.${modelEntry.identifier}.${modelEntry.metadata.version}-visible:${modelEntry.visible}`; } private getProviderGroupId(group: ILanguageModelsProviderGroup): string { diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts index 7e711f72181..a363006e3c3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts @@ -9,7 +9,7 @@ import { Emitter } from '../../../../../base/common/event.js'; import * as DOM from '../../../../../base/browser/dom.js'; import { Button, IButtonOptions } from '../../../../../base/browser/ui/button/button.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { ILanguageModelsService, IUserFriendlyLanguageModel } from '../../../chat/common/languageModels.js'; +import { ILanguageModelsService, ILanguageModelProviderDescriptor } from '../../../chat/common/languageModels.js'; import { localize } from '../../../../../nls.js'; import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; @@ -280,7 +280,7 @@ abstract class ModelsTableColumnRenderer